Start learning free

LESSON 568 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Playlist in Kotlin: Two Classes Working Together

Practice O(n) Read the full explanation

A music app keeps playlists: songs in the order they were added. Build it from two classes that work together. A Song holds one song's data. A Playlist keeps a list of Song objects and answers questions about them.

Implement:

  • Song(title, seconds) holds a song's title and its length in seconds;
  • Playlist() starts with no songs. Its methods:
    • add(song) adds a Song at the end of the playlist;
    • remove(title) removes the first song with that title and returns true; if no song has that title, it changes nothing and returns false;
    • totalSeconds() returns the sum of the songs' lengths in seconds (0 for an empty playlist);
    • duration() returns the same total as a string "m:ss": the minutes, a colon, then the seconds with two digits. 5 seconds are "0:05", 750 seconds are "12:30", and 3600 seconds are "60:00": the minutes are never padded.

In Kotlin, Song is one line: its header declares title and seconds, and it needs nothing else.

The input is a list of operations, applied in order to one new playlist. The output lists what the remove, totalSeconds and duration calls return, in order, with booleans printed as True and False.

 

Example 1:

Input:
    add(Song("Intro", 65))
    add(Song("Sunrise", 200))
    totalSeconds()
    duration()
    remove("Intro")
    duration()
Output: 265 4:25 True 3:20
Explanation: The two songs last 65 + 200 = 265 seconds: 4 minutes (240 seconds) and 25 seconds.
After "Intro" is removed, only the 200 seconds of "Sunrise" are left: 3 minutes and 20 seconds.

Example 2:

Input:
    duration()
    remove("Rain")
    totalSeconds()
Output: 0:00 False 0
Explanation: The playlist is empty: it lasts 0 seconds, and there is no "Rain" to remove.

Example 3:

Input:
    add(Song("Echo", 30))
    add(Song("Echo", 45))
    add(Song("Rain", 5))
    remove("Echo")
    totalSeconds()
    remove("Echo")
    remove("Echo")
    duration()
Output: True 50 True False 0:05
Explanation: Two songs are called "Echo". The first remove takes only the first one, the 30 seconds,
so 45 + 5 = 50 seconds are left. The second remove takes the other "Echo", and the third finds none.
Only "Rain" is left: 5 seconds, written 0:05.

 

Constraints:

  • 1 <= seconds <= 3600
  • A title is one word of letters and digits. Titles are compared exactly: "Rain" and "rain" are different titles.
  • At most 200 operations.
STEP 1 OF 14
    Ctrl + Enter to check

    KOTLIN · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Playlist in Kotlin, explained

    Problem overview

    A music app keeps playlists: songs in the order they were added. You build it from two classes that cooperate. A Song holds one song's data. A Playlist keeps a list of Song objects and answers questions about them.

    • Song(title, seconds) holds a song's title and length in seconds. In Kotlin it is one line, and the starter code gives it to you complete.
    • Playlist() starts with no songs.
    • add(song) puts a song at the end.
    • remove(title) removes the first song with that title and returns true. If no song has that title it changes nothing and returns false.
    • totalSeconds() returns the sum of the lengths, 0 for an empty playlist.
    • duration() returns the same total as "m:ss": minutes, a colon, then two digits of seconds. 5 seconds are 0:05, 750 seconds are 12:30, and 3600 seconds are 60:00. The minutes are never padded and never roll over into hours.

    Approach

    The playlist needs to remember its songs, so it keeps a private list. Keeping it private means only the playlist's own methods can change it, so what totalSeconds() reports always matches the songs that are really there.

    Three of the methods are plain list work:

    • add appends to the end.
    • remove walks the list from the front, so the first match is the one that goes. It needs each position, because removeAt removes by index.
    • totalSeconds adds up every song's seconds.

    duration only formats. It calls totalSeconds() instead of repeating the addition, so the adding lives in one place. Then total / 60 gives the whole minutes (integer division drops the remainder) and total % 60 the seconds left over.

    Step by step

    1. Inside Playlist, declare private val songs = mutableListOf<Song>(). The variable never points to another list, but the list can change.
    2. In add, call songs.add(song).
    3. In remove, loop with for (i in songs.indices) and test songs[i].title == title. In Kotlin == compares the text of two strings, so titles are compared exactly: Rain and rain differ.
    4. On a match, call songs.removeAt(i) and return true at once. Returning stops the loop, so only the first song with that title goes.
    5. After the loop, return false: nothing matched and the list is unchanged.
    6. In totalSeconds, start var total = 0, add song.seconds for each song, and return total.
    7. In duration, store totalSeconds() in total, then minutes = total / 60 and seconds = total % 60.
    8. If seconds < 10, return "$minutes:0$seconds"; the extra 0 fills the first digit. Otherwise return "$minutes:$seconds".

    The solution in Kotlin

    class Song(val title: String, val seconds: Int)
    class Playlist {
        private val songs = mutableListOf<Song>()
        fun add(song: Song) {
            songs.add(song)
        }
        fun remove(title: String): Boolean {
            for (i in songs.indices) {
                if (songs[i].title == title) {
                    songs.removeAt(i)
                    return true
                }
            }
            return false
        }
        fun totalSeconds(): Int {
            var total = 0
            for (song in songs) {
                total += song.seconds
            }
            return total
        }
        fun duration(): String {
            val total = totalSeconds()
            val minutes = total / 60
            val seconds = total % 60
            if (seconds < 10) {
                return "$minutes:0$seconds"
            }
            return "$minutes:$seconds"
        }
    }
    

    Kotlin details:

    • A string template such as "$minutes:0$seconds" inserts the variables' values. The text after $minutes starts with a colon, which cannot be part of a name, so the template reads as intended.
    • songs.indices is the range 0 up to the last index, so the loop never goes out of bounds, even for an empty list.
    • It is safe to remove inside this loop because the method returns immediately afterwards and never touches the list again.

    A run

    val p = Playlist()
    p.add(Song("Intro", 65))
    p.add(Song("Sunrise", 200))
    println(p.totalSeconds())
    println(p.duration())
    println(p.remove("Intro"))
    println(p.duration())
    
    265
    4:25
    true
    3:20

    Two songs of 65 and 200 seconds make 265: 4 minutes (240 seconds) and 25 seconds. After Intro goes, only the 200 seconds of Sunrise remain, which is 3 minutes 20 seconds. In Kotlin, println shows booleans as true and false; the lesson's tests print them as True and False.

    An empty playlist gives 0:00 for duration(), false for remove("Rain") and 0 for totalSeconds(). Duplicate titles show the "first only" rule. With Echo (30 s), Echo (45 s) and Rain (5 s), the first remove("Echo") returns true and leaves 50 seconds; the second returns true; the third returns false; the playlist then lasts 0:05. A single song of 750 seconds shows 12:30, one of 3600 seconds shows 60:00, and remove("rain") on a playlist holding Rain returns false.

    Another way to pad the seconds

    Instead of the if, padStart fills a string to a given width. Turning the seconds into text and padding it with zeros to two characters gives the same result, and the two return lines become one:

    return "$minutes:${seconds.toString().padStart(2, '0')}"
    

    For 5 seconds this gives 0:05, for 750 it gives 12:30 and for 3600 it gives 60:00. The lesson's steps use the if, which is easier to follow while you are learning, and the result is identical.

    Edge cases and common mistakes

    • Removing every match. Returning after the first removeAt is what limits the removal to one song.
    • Removing inside a for (song in songs) loop and carrying on. A list changed during that kind of loop throws a ConcurrentModificationException. Looping over indices and returning right away avoids it.
    • Forgetting the leading zero. Without it, 5 seconds read 0:5.
    • Padding the minutes. The minutes stay as they are: 60:00, not 01:00:00.
    • Duplicating the adding in duration. Calling totalSeconds() keeps one copy of that logic.
    • Case. Titles are compared exactly, so do not lowercase them.
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free