Start learning free

LESSON 569 OF 574 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Playlist in Swift: 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.

The starter code spells every name the way your language does.

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 16
    Ctrl + Enter to check

    SWIFT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Playlist in Swift, explained

    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 a Song at the end.
    • remove(_:) removes the first song with that title and returns true, or returns false and 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

    1. In Song, declare let title: String and let seconds: Int and store the init parameters.
    2. In Playlist, declare private var songs = [Song]().
    3. add appends the song.
    4. remove loops i over 0..<songs.count; when songs[i].title == title it calls songs.remove(at: i) and returns true. After the loop it returns false.
    5. totalSeconds adds each song.seconds to a total that starts at 0.
    6. duration splits the total into minutes and seconds. If the seconds are below 10 it puts a 0 after 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.count stops before songs.count. Returning right after remove(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 a Bool. 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, not 0:5.
    • Minutes are not padded. 750 seconds is 12:30, and 3600 seconds is 60:00.
    • Exact titles. "Rain" and "rain" are different titles.
    • Empty playlist. duration() gives 0:00 and remove gives false.

    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:

    Secondsduration()
    00:00
    90:09
    100:10
    590:59
    601:00
    611:01
    5999:59
    360060: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.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free