Start learning free

LESSON 568 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Playlist in Go: Structs, Pointer Receivers and Slices

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, created by NewSong(title, seconds), holds a song's title and its length in seconds;
  • Playlist, created by NewPlaylist(), 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.

Go has no classes: Song and Playlist are structs, created by NewSong(title, seconds) and NewPlaylist(), and the playlist's methods have a pointer receiver, *Playlist, because they change it.

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(NewSong("Intro", 65))
    add(NewSong("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(NewSong("Echo", 30))
    add(NewSong("Echo", 45))
    add(NewSong("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 17
    Ctrl + Enter to check

    GO · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Playlist in Go, explained

    Problem overview

    A music app keeps playlists: songs in the order they were added. You build it from two structs that work together. A Song holds one song's title and its length in seconds, created by NewSong(title, seconds). A Playlist, created by NewPlaylist(), starts empty and has four methods:

    • add(song) puts a song at the end.
    • remove(title) removes the first song with that title and returns true. If no song has the 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": minutes, a colon, and two digits of seconds. 5 seconds are "0:05", 750 are "12:30" and 3600 are "60:00", so the minutes are never padded.

    Add Intro (65 seconds) and Sunrise (200) and the total is 265, which duration() shows as 4:25.

    Approach

    Playlist keeps its songs in a field, a []Song, and only its own methods change it. Its methods have a pointer receiver, *Playlist. That matters: Go passes a receiver by value, so a method with a plain Playlist receiver would work on a copy, and the playlist you called it on would never change. With *Playlist, the method gets the address of the original and its changes stick.

    For the rest, remove is a search for the first match and a cut, totalSeconds is a sum, and duration only formats totalSeconds(), so the adding is written once.

    Step by step

    1. Give Song the fields title and seconds. NewSong returns a Song with both set. Go has no constructors; a function whose name starts with New plays that role.
    2. Give Playlist a field songs of type []Song. NewPlaylist returns Playlist{}. Its slice is nil, Go's empty slice, and append and len already work on it.
    3. add appends the song and stores the result back: this.songs = append(this.songs, song). Storing it back matters because append can return a new slice.
    4. remove loops with an index i, since removing goes by position. When this.songs[i].title == title, it cuts song i out and returns true right away, so only the first match goes. If the loop ends, nothing matched, and it returns false.
    5. The cut is append(this.songs[:i], this.songs[i+1:]...): the songs before i, followed by the songs after it. The ... passes the second slice's elements one by one, because append takes single elements.
    6. totalSeconds starts total at 0, adds each song.seconds with range, and returns it.
    7. duration takes total := this.totalSeconds(), then minutes := total / 60 and seconds := total % 60. Integer division keeps the whole minutes, and % gives what is left.
    8. If seconds is below 10, the result needs an extra "0" before it: ":0". Otherwise the separator is just ":". strconv.Itoa turns each number into text for the +.

    The solution in Go

    type Song struct {
        title   string
        seconds int
    }
    
    func NewSong(title string, seconds int) Song {
        return Song{title: title, seconds: seconds}
    }
    
    type Playlist struct {
        songs []Song
    }
    
    func NewPlaylist() Playlist {
        return Playlist{}
    }
    
    func (this *Playlist) add(song Song) {
        this.songs = append(this.songs, song)
    }
    
    func (this *Playlist) remove(title string) bool {
        for i := 0; i < len(this.songs); i++ {
            if this.songs[i].title == title {
                this.songs = append(this.songs[:i], this.songs[i+1:]...)
                return true
            }
        }
        return false
    }
    
    func (this *Playlist) totalSeconds() int {
        total := 0
        for _, song := range this.songs {
            total += song.seconds
        }
        return total
    }
    
    func (this *Playlist) duration() string {
        total := this.totalSeconds()
        minutes := total / 60
        seconds := total % 60
        if seconds < 10 {
            return strconv.Itoa(minutes) + ":0" + strconv.Itoa(seconds)
        }
        return strconv.Itoa(minutes) + ":" + strconv.Itoa(seconds)
    }

    Notes on the Go details:

    • The calls p.add(...) on a variable p := NewPlaylist() work although p is a value: Go takes &p for you when the variable can be addressed.
    • The loop in remove uses the index form because range would give a copy of each song and no position to cut at.
    • The slice cut moves the later songs up one place in the same backing array, which is fine here because the playlist owns its slice.

    Using the playlist

    p := NewPlaylist()
    p.add(NewSong("Intro", 65))
    p.add(NewSong("Sunrise", 200))
    fmt.Println(p.totalSeconds())
    fmt.Println(p.duration())
    fmt.Println(p.remove("Intro"))
    fmt.Println(p.duration())
    265
    4:25
    true
    3:20
    

    After Intro goes, only the 200 seconds of Sunrise remain: 3 minutes and 20 seconds.

    Complexity

    add is O(1) on average. remove is O(n) for n songs: it may scan the whole list and then shift the later songs. totalSeconds is O(n), and so is duration, since it calls totalSeconds and then does a few constant-time steps.

    Edge cases and common mistakes

    • Duplicate titles. After adding Echo (30), Echo (45) and Rain (5), the first remove("Echo") returns true and takes only the 30-second song, leaving 50 seconds. The second removes the other Echo and returns true; the third finds none and returns false. Then only Rain is left, and duration() returns 0:05.
    • Empty playlist. duration() returns 0:00, remove("Rain") returns false and totalSeconds() returns 0.
    • Value receiver. With func (this Playlist) add(song Song), the append changes a copy, and the playlist stays empty.
    • Padding. Only the seconds are padded: 5 seconds make 0:05, and 3600 make 60:00, with no hours.
    • Case. Titles are compared exactly, so "Rain" and "rain" are different.

    Another way

    The two return statements in duration can be one call to fmt.Sprintf, where %02d means "at least two digits, padded with zeros". In a file of your own, with fmt imported, this gives 4:25, 0:05, 12:30, 60:00 and 0:00 for 265, 5, 750, 3600 and 0 seconds:

    return fmt.Sprintf("%d:%02d", total/60, total%60)

    Testing

    Start with an empty playlist and check duration(), remove("Rain") and totalSeconds(): 0:00, false, 0. Then try the boundary lengths one song at a time. A single song of 5 seconds gives 0:05, 750 gives 12:30 and 3600 gives 60:00. Finally, run the duplicate-title sequence above and watch the total fall from 80 to 50 after the first removal, because only one Echo goes.

    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free