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 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": minutes, a colon, then two digits of seconds. 5 seconds are0:05, 750 seconds are12:30, and 3600 seconds are60: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:
addappends to the end.removewalks the list from the front, so the first match is the one that goes. It needs each position, becauseremoveAtremoves by index.totalSecondsadds up every song'sseconds.
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
- Inside
Playlist, declareprivate val songs = mutableListOf<Song>(). The variable never points to another list, but the list can change. - In
add, callsongs.add(song). - In
remove, loop withfor (i in songs.indices)and testsongs[i].title == title. In Kotlin==compares the text of two strings, so titles are compared exactly:Rainandraindiffer. - On a match, call
songs.removeAt(i)andreturn trueat once. Returning stops the loop, so only the first song with that title goes. - After the loop,
return false: nothing matched and the list is unchanged. - In
totalSeconds, startvar total = 0, addsong.secondsfor each song, and returntotal. - In
duration, storetotalSeconds()intotal, thenminutes = total / 60andseconds = total % 60. - 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$minutesstarts with a colon, which cannot be part of a name, so the template reads as intended. songs.indicesis the range0up 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
removeAtis 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 aConcurrentModificationException. 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, not01:00:00. - Duplicating the adding in
duration. CallingtotalSeconds()keeps one copy of that logic. - Case. Titles are compared exactly, so do not lowercase them.
