Problem overview
A music app keeps playlists: songs in the order they were added. You build it from two classes. A Song holds one song's title and its length in seconds. A Playlist starts empty, keeps a list of Song objects and has four methods:
add(song)puts a song at the end;remove(title)removes the first song with that title and returnstrue, or changes nothing and returnsfalse;totalSeconds()returns the sum of the lengths,0for an empty playlist;duration()returns the same total as"m:ss": minutes (never padded), a colon and two digits of seconds. 5 seconds are"0:05", 750 are"12:30"and 3600 are"60:00".
Two songs of 65 and 200 seconds make 265 seconds, which is 4 minutes (240 seconds) and 25 seconds: "4:25".
Approach: objects that hold objects
The playlist keeps whole Song objects in an array, so each song's title and length stay together. The array is private: only the playlist's own methods change it, so what totalSeconds and duration report can't get out of step with the list.
remove works by position, because the array's splice removes by index. A for loop with an index finds the first matching title, removes that song and returns true right away. Returning at once stops the loop, so only the first match goes. If the loop ends without a match, the method returns false. duration doesn't add anything itself; it calls totalSeconds() and only formats the result. Adding up the lengths takes O(n) time for n songs, and so does remove, which may walk the whole list. add takes constant time.
Step by step
Songfields and constructor. Declaretitle: stringandseconds: number, and store both parameters inthis. They stay public because the playlist reads them.- The list. In
Playlist, declareprivate songs: Song[] = []. Every new playlist gets its own empty array. add. Push the song ontothis.songs.remove. Loop withlet ifrom 0 whilei < this.songs.length. Inside, comparethis.songs[i].titlewithtitleusing===. On a match,this.songs.splice(i, 1)removes that one element, the later songs move up one place, and the method returnstrue. After the loop, returnfalse.totalSeconds. Starttotalat 0, add eachsong.secondsin afor...ofloop and return it.duration, part one. Storethis.totalSeconds()in aconst total. Minutes areMath.floor(total / 60)and seconds aretotal % 60.duration, part two. When the seconds are below 10, return the minutes,":0"and the seconds. Otherwise return the minutes,":"and the seconds.
The solution in TypeScript
class Song {
title: string;
seconds: number;
constructor(title: string, seconds: number) {
this.title = title;
this.seconds = seconds;
}
}
class Playlist {
private songs: Song[] = [];
add(song: Song): void {
this.songs.push(song);
}
remove(title: string): boolean {
for (let i = 0; i < this.songs.length; i++) {
if (this.songs[i].title === title) {
this.songs.splice(i, 1);
return true;
}
}
return false;
}
totalSeconds(): number {
let total = 0;
for (const song of this.songs) {
total += song.seconds;
}
return total;
}
duration(): string {
const total = this.totalSeconds();
const minutes = Math.floor(total / 60);
const seconds = total % 60;
if (seconds < 10) {
return minutes + ":0" + seconds;
}
return minutes + ":" + seconds;
}
}
TypeScript details worth knowing:
- A field with an initial value.
private songs: Song[] = [];runs for eachnew Playlist(), so two playlists never share a list. splice(i, 1)changes the array. It removes one element starting at indexiand shifts the later ones down by one place. That is whyremovereturns right after calling it: the indexes have just changed, and the loop shouldn't continue./keeps the fraction.265 / 60is about 4.4167, soMath.floorkeeps the whole minutes.%gives what's left over.- Numbers join strings with
+.minutes + ":0" + secondsstarts with a number, but once":0"is in the sum, the rest is joined as text. The result is a string, asduration()promises. ===compares exactly."Rain"and"rain"are different titles.
Seeing it run
const p = new Playlist();
p.add(new Song("Intro", 65));
p.add(new Song("Sunrise", 200));
console.log(p.totalSeconds(), p.duration(), p.remove("Intro"), p.duration());
const q = new Playlist();
console.log(q.duration(), q.remove("Rain"), q.totalSeconds());
const r = new Playlist();
r.add(new Song("Echo", 30));
r.add(new Song("Echo", 45));
r.add(new Song("Rain", 5));
console.log(r.remove("Echo"), r.totalSeconds(), r.remove("Echo"), r.remove("Echo"), r.duration());
const t = new Playlist();
t.add(new Song("A", 750));
console.log(t.duration());
t.add(new Song("B", 2850));
console.log(t.duration(), t.totalSeconds());
console.log(r.remove("rain"));
265 4:25 true 3:20
0:00 false 0
true 50 true false 0:05
12:30
60:00 3600
false
The empty playlist lasts 0:00, and there is no Rain to remove. In the third playlist two songs are called Echo. The first remove takes only the first one (30 seconds), which leaves 45 + 5 = 50 seconds. The second takes the other Echo, and the third finds none, so only Rain is left: 5 seconds, written 0:05. A 750-second song is 12:30, and adding 2850 seconds more makes exactly 3600, which is 60:00: the minutes aren't capped at 59 and never padded. The last line shows that "rain" doesn't match "Rain".
Edge cases and common mistakes
- Removing every match. Without the
return trueinside the loop, the loop would keep going afterspliceand skip the song that just moved into indexi. Return right away to remove only the first. - Forgetting the padding. Without the
":0"branch, 5 seconds read0:5. The branch is for seconds below 10 only. - Padding the minutes. Don't:
"60:00"and"0:05"keep the minutes as they are. - Empty playlist.
totalSeconds()starts at 0, so it returns 0 andduration()returns"0:00".removeon an empty list returnsfalse. - Adding in
duration. Callingthis.totalSeconds()keeps the adding in one place; copy it and the two methods can drift apart. - Making
songspublic. Outside code could then change the list without going throughaddandremove.privateis a compile-time check in TypeScript, and the compiler rejects access from outside the class. - Wrong comparison. A single
=assigns instead of comparing. Use===.
