Start learning free

LESSON 568 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Playlist in Scala: 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 Scala, 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

    SCALA · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Playlist in Scala, explained

    Problem overview

    A music app keeps playlists: songs in the order they were added. The lesson builds them from two classes. 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 title and a length in seconds. In Scala it is one line, because the header declares both values.
    • add(song) adds 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": 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.

    Approach

    The playlist keeps its songs in a private val holding an ArrayBuffer[Song], a growable list. Private means that only the methods of Playlist change it, so it can't get out of step with what they report. The val only means that the name always refers to the same buffer. The contents still change.

    Each method then has a small job:

    • add appends with +=.
    • remove goes through the positions with songs.indices, and on the first title that matches it removes that position and returns true. Returning at once is what keeps the second song with the same title in place.
    • totalSeconds adds the lengths in a loop.
    • duration reuses totalSeconds() and formats it. Whole minutes are total / 60, and the seconds left over are total % 60.

    Step by step

    1. Declare songs as a private empty ArrayBuffer[Song].
    2. In add, append the song.
    3. In remove, loop over songs.indices. When songs(i).title == title, call songs.remove(i) and return true.
    4. After the loop, return false.
    5. In totalSeconds, start total at 0 and add each song.seconds. Return it.
    6. In duration, store totalSeconds(), split it into minutes and seconds.
    7. When seconds is below 10, return the string with a 0 before the seconds. Otherwise return it as it is.

    The solution in Scala

    class Song(val title: String, val seconds: Int)
    
    class Playlist {
        private val songs = scala.collection.mutable.ArrayBuffer[Song]()
    
        def add(song: Song): Unit = {
            songs += song
        }
    
        def remove(title: String): Boolean = {
            for (i <- songs.indices) {
                if (songs(i).title == title) {
                    songs.remove(i)
                    return true
                }
            }
            return false
        }
    
        def totalSeconds(): Int = {
            var total = 0
            for (song <- songs) {
                total += song.seconds
            }
            return total
        }
    
        def duration(): String = {
            val total = totalSeconds()
            val minutes = total / 60
            val seconds = total % 60
            if (seconds < 10) {
                return s"$minutes:0$seconds"
            }
            return s"$minutes:$seconds"
        }
    }
    

    Details worth knowing:

    • String interpolation. s"$minutes:0$seconds" puts the values of the variables into the text. For 265 seconds, minutes is 4 and seconds is 25, and the text is 4:25.
    • == on strings. In Scala, == compares the contents, so songs(i).title == title is a plain text comparison. "Rain" and "rain" are different titles.
    • remove(i) shifts. ArrayBuffer.remove(i) takes out the element at position i, and the later elements move up one place. The method returns right after removing, so the loop never continues over the shifted positions.
    • A compiler warning. The return sits inside a for loop, which Scala turns into a function call, and Scala 3.5 reports a warning for that: Non local returns are no longer supported; use `boundary` and `boundary.break` in `scala.util` instead. The program still compiles and returns the right values.

    Another way to find the song

    indexWhere returns the position of the first match, or -1, so remove needs no loop and no return:

    def remove(title: String): Boolean = {
        val i = songs.indexWhere(_.title == title)
        if (i == -1) false else { songs.remove(i); true }
    }
    

    Trying it

    val playlist = new Playlist()
    playlist.add(new Song("Intro", 65))
    playlist.add(new Song("Sunrise", 200))
    println(playlist.totalSeconds())
    println(playlist.duration())
    println(playlist.remove("Intro"))
    println(playlist.duration())
    

    This prints:

    265
    4:25
    true
    3:20
    

    Scala prints booleans as true and false; the lesson's tests print them as True and False. More cases:

    • An empty playlist has duration() of 0:00, remove("Rain") gives false and totalSeconds() gives 0.
    • With two songs called Echo (30 and 45 seconds) and a Rain of 5 seconds, the first remove("Echo") gives true and leaves 50 seconds. The second gives true again, the third gives false, and the duration is 0:05.
    • A single song of 3600 seconds gives 60:00, and one of 750 seconds gives 12:30.

    Why keep the list private

    If the songs were public, any code could add to the list or change it without going through add and remove. Keeping the buffer private means every change goes through the methods of Playlist, which makes the class easier to trust: totalSeconds() and duration() always describe the songs the playlist really holds.

    Following the formatting

    For 265 seconds, total / 60 is 4 and total % 60 is 25, so the text is 4:25. For 5 seconds the minutes are 0 and the seconds are 5. Since 5 is below 10, the first branch runs and the text is 0:05. For 3600 seconds the minutes are 60 and the seconds are 0, so the text is 60:00. The minutes can pass 59 because there are no hours in the format.

    Following the removal

    With Echo (30), Echo (45) and Rain (5), the first call to remove("Echo") matches at position 0, removes it and returns. The buffer is now Echo (45) and Rain (5), so the total is 50.

    Common mistakes

    • Removing every song with the title. Only the first one goes.
    • Forgetting the padding. Without the extra 0, 5 seconds read 0:5.
    • Padding the minutes. They are never padded: 12:30 and 60:00 are correct.
    • Ignoring case. Titles are compared exactly, so remove("rain") doesn't remove "Rain" and returns false.
    • Repeating the adding in duration. Calling totalSeconds() keeps that logic in one place.
    Keep going with Scala500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free