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 returnstrue. If no song has the 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, 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
- Give
Songthe fieldstitleandseconds.NewSongreturns aSongwith both set. Go has no constructors; a function whose name starts withNewplays that role. - Give
Playlista fieldsongsof type[]Song.NewPlaylistreturnsPlaylist{}. Its slice isnil, Go's empty slice, andappendandlenalready work on it. addappends the song and stores the result back:this.songs = append(this.songs, song). Storing it back matters becauseappendcan return a new slice.removeloops with an indexi, since removing goes by position. Whenthis.songs[i].title == title, it cuts songiout and returnstrueright away, so only the first match goes. If the loop ends, nothing matched, and it returnsfalse.- The cut is
append(this.songs[:i], this.songs[i+1:]...): the songs beforei, followed by the songs after it. The...passes the second slice's elements one by one, becauseappendtakes single elements. totalSecondsstartstotalat 0, adds eachsong.secondswithrange, and returns it.durationtakestotal := this.totalSeconds(), thenminutes := total / 60andseconds := total % 60. Integer division keeps the whole minutes, and%gives what is left.- If
secondsis below 10, the result needs an extra"0"before it:":0". Otherwise the separator is just":".strconv.Itoaturns 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 variablep := NewPlaylist()work althoughpis a value: Go takes&pfor you when the variable can be addressed. - The loop in
removeuses the index form becauserangewould 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")returnstrueand takes only the 30-second song, leaving 50 seconds. The second removes the other Echo and returnstrue; the third finds none and returnsfalse. Then onlyRainis left, andduration()returns0:05. - Empty playlist.
duration()returns0:00,remove("Rain")returnsfalseandtotalSeconds()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 make60: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.
