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 returnstrue. If no song has that title, it changes nothing and returnsfalse.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 are0:05, 750 seconds are12:30and 3600 seconds are60: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:
addappends with+=.removegoes through the positions withsongs.indices, and on the first title that matches it removes that position and returnstrue. Returning at once is what keeps the second song with the same title in place.totalSecondsadds the lengths in a loop.durationreusestotalSeconds()and formats it. Whole minutes aretotal / 60, and the seconds left over aretotal % 60.
Step by step
- Declare
songsas a private emptyArrayBuffer[Song]. - In
add, append the song. - In
remove, loop oversongs.indices. Whensongs(i).title == title, callsongs.remove(i)and returntrue. - After the loop, return
false. - In
totalSeconds, starttotalat 0 and add eachsong.seconds. Return it. - In
duration, storetotalSeconds(), split it intominutesandseconds. - When
secondsis below 10, return the string with a0before 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,minutesis 4 andsecondsis 25, and the text is4:25. ==on strings. In Scala,==compares the contents, sosongs(i).title == titleis a plain text comparison."Rain"and"rain"are different titles.remove(i)shifts.ArrayBuffer.remove(i)takes out the element at positioni, 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
returnsits inside aforloop, 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()of0:00,remove("Rain")givesfalseandtotalSeconds()gives 0. - With two songs called
Echo(30 and 45 seconds) and aRainof 5 seconds, the firstremove("Echo")givestrueand leaves 50 seconds. The second givestrueagain, the third givesfalse, and the duration is0:05. - A single song of 3600 seconds gives
60:00, and one of 750 seconds gives12: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 read0:5. - Padding the minutes. They are never padded:
12:30and60:00are correct. - Ignoring case. Titles are compared exactly, so
remove("rain")doesn't remove"Rain"and returnsfalse. - Repeating the adding in
duration. CallingtotalSeconds()keeps that logic in one place.
