Problem overview
A music app keeps playlists: songs in the order they were added. Two classes work together. Song(title, seconds) holds one song's data. Playlist() starts empty and has these methods:
add(_:)puts aSongat the end.remove(_:)removes the first song with that title and returnstrue, or returnsfalseand changes nothing.totalSeconds()returns the sum of the lengths, 0 when empty.duration()returns the same total as"m:ss": minutes never padded, seconds always two digits.
With Intro (65 s) and Sunrise (200 s) the total is 265 and the duration is 4:25.
Approach
The playlist keeps an array of Song objects, private so that only its own methods change it. remove must know a position, so it walks the indexes and uses remove(at:) on the first match, then returns at once. That is why only the first of two songs with the same title goes. duration() reuses totalSeconds() and only formats the number, with / 60 for the minutes and % 60 for the rest. The loops visit each song once, so remove and totalSeconds are O(n) for n songs.
Step by step
- In
Song, declarelet title: Stringandlet seconds: Intand store theinitparameters. - In
Playlist, declareprivate var songs = [Song](). addappends the song.removeloopsiover0..<songs.count; whensongs[i].title == titleit callssongs.remove(at: i)and returnstrue. After the loop it returnsfalse.totalSecondsadds eachsong.secondsto atotalthat starts at 0.durationsplits the total intominutesandseconds. If the seconds are below 10 it puts a0after the colon.
The solution in Swift
class Song {
let title: String
let seconds: Int
init(_ title: String, _ seconds: Int) {
self.title = title
self.seconds = seconds
}
}
class Playlist {
private var songs = [Song]()
func add(_ song: Song) {
songs.append(song)
}
func remove(_ title: String) -> Bool {
for i in 0..<songs.count {
if songs[i].title == title {
songs.remove(at: i)
return true
}
}
return false
}
func totalSeconds() -> Int {
var total = 0
for song in songs {
total += song.seconds
}
return total
}
func duration() -> String {
let total = totalSeconds()
let minutes = total / 60
let seconds = total % 60
if seconds < 10 {
return "\(minutes):0\(seconds)"
}
return "\(minutes):\(seconds)"
}
}
The lesson's first example, then the playlist with two songs called Echo:
let p = Playlist()
p.add(Song("Intro",65)); p.add(Song("Sunrise",200))
print(p.totalSeconds(), p.duration(), p.remove("Intro"), p.duration())
let q = Playlist()
print(q.duration(), q.remove("Rain"), q.totalSeconds())
q.add(Song("Echo",30)); q.add(Song("Echo",45)); q.add(Song("Rain",5))
print(q.remove("Echo"), q.totalSeconds(), q.remove("Echo"), q.remove("Echo"), q.duration())
265 4:25 true 3:20
0:00 false 0
true 50 true false 0:05
Swift prints booleans as true and false; the lesson's tests print them as True and False. The first remove("Echo") takes only the 30-second song, the second takes the other, and the third finds none.
Swift details worth knowing
0..<songs.countstops beforesongs.count. Returning right afterremove(at:)means the loop never goes on over an array that has changed size.remove(at:)shifts the later elements up by one place.- String interpolation.
"\(minutes):0\(seconds)"puts the values into the text. - Unused result.
remove(_:)returns aBool. Calling it as a statement and ignoring the value gives a warning only.
Edge cases and common mistakes
- Seconds below 10. 5 seconds must read
0:05, not0:5. - Minutes are not padded. 750 seconds is
12:30, and 3600 seconds is60:00. - Exact titles.
"Rain"and"rain"are different titles. - Empty playlist.
duration()gives0:00andremovegivesfalse.
Tracing the example with two Echo songs
The playlist gets Echo (30), Echo (45) and Rain (5). The first remove("Echo") finds the song at index 0 and removes it, so the array holds Echo (45) and Rain (5) and the call returns true. totalSeconds() is 45 + 5 = 50. The next remove("Echo") now finds the song at index 0 again, which is the other Echo, and removes it. The third finds no Echo, goes through the whole loop and returns false. Only Rain is left, 5 seconds, which duration() writes as 0:05.
Testing
Try these in a file of your own. A new playlist gives duration() as 0:00, remove("Rain") as false and totalSeconds() as 0. A playlist with one song of 750 seconds gives 12:30, and one with a song of 3600 seconds gives 60:00. Adding Echo (59) and Rain (1) gives a total of 60 and the duration 1:00.
Complexity
For n songs, add is O(1) on average, and remove is O(n): it may scan the whole array, and removing a song moves the later ones up. totalSeconds() is O(n) and duration(), which calls it, is O(n) too. The playlist takes O(n) space. With at most 200 operations these costs are tiny.
The minutes and seconds arithmetic
A duration in seconds is split with integer division and remainder. For 60 seconds, 60 / 60 is 1 minute and 60 % 60 is 0 seconds left. The values around the edges of the two-digit rule look like this:
| Seconds | duration() |
|---|---|
| 0 | 0:00 |
| 9 | 0:09 |
| 10 | 0:10 |
| 59 | 0:59 |
| 60 | 1:00 |
| 61 | 1:01 |
| 599 | 9:59 |
| 3600 | 60:00 |
Only the seconds below 10 need the extra 0, and a total of exactly 60 shows as 1:00, not 0:60.
Why Playlist keeps the songs private
If songs were public, other code could add to it or empty it directly, and the answers of totalSeconds() and duration() would depend on who touched the array. With private, every change goes through add and remove. The two classes also have different jobs: Song only stores a title and a length, and Playlist owns the rules for a collection of songs. Because Song uses let, a song's title and length can't change after it is created, so a playlist's total can only change when a song is added or removed.
Key takeaways
- One class can hold objects of another class and answer questions about them.
- Reuse a method you already wrote instead of repeating its loop.
