Problem overview
A music app keeps playlists: songs in the order they were added. The task builds this from two classes. A Song holds one song's data, and a Playlist keeps a list of Song objects and answers questions about them.
Song(title, seconds)holds a title and a length in seconds.Playlist()starts empty.add(song)appends a song.remove(title)removes the first song with that title and returnstrue, or changes nothing and returnsfalse.totalSeconds()returns the sum of the lengths, 0 when empty.duration()returns the same total as"m:ss".
The seconds always have two digits and the minutes are never padded: 5 seconds are "0:05", 750 seconds are "12:30", and 3600 seconds are "60:00".
After adding Intro (65 seconds) and Sunrise (200 seconds), the total is 265, which is 4 minutes and 25 seconds: "4:25". Removing Intro leaves 200 seconds, "3:20".
Approach
The playlist keeps its songs in an array. The array is a private field, #songs, so only the playlist's own methods change it. Code outside can add and remove songs, but it cannot put the list out of step with what totalSeconds and duration report.
remove works by position: it needs the index of the first match so it can delete that element. A plain index loop with an early return does this. totalSeconds only needs each song, so for...of fits. duration does not add anything itself: it calls totalSeconds() and only formats the result, so the adding lives in one place.
Step by step
- Write
Song's constructor. Storetitleandsecondsinthis.titleandthis.seconds. They stay public becausePlaylistreads them. - Declare
#songs = []inPlaylist. Each new playlist gets its own empty array. - Write
add. Push theSongobject ontothis.#songs. The playlist keeps the object itself, with its title and length together. - Write
remove. Loop with an indexi. Ifthis.#songs[i].title === title, callthis.#songs.splice(i, 1), which deletes one element at indexiand moves the later ones up, and returntrueat once. The early return means only the first match goes. After the loop, returnfalse. - Write
totalSeconds. Start a total at 0, addsong.secondsfor every song, and return the total. An empty playlist gives 0. - Write
duration. Taketotal = this.totalSeconds(). The minutes areMath.floor(total / 60)and the remaining seconds aretotal % 60. - Pad the seconds. If the seconds are below 10, return the minutes,
":0"and the seconds. Otherwise return the minutes,":"and the seconds.
The solution in JavaScript
class Song {
constructor(title, seconds) {
this.title = title;
this.seconds = seconds;
}
}
class Playlist {
#songs = [];
add(song) {
this.#songs.push(song);
}
remove(title) {
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() {
let total = 0;
for (let song of this.#songs) {
total += song.seconds;
}
return total;
}
duration() {
let total = this.totalSeconds();
let minutes = Math.floor(total / 60);
let seconds = total % 60;
if (seconds < 10) {
return minutes + ":0" + seconds;
}
return minutes + ":" + seconds;
}
}
Numbers and strings with +
In minutes + ":0" + seconds, the first + has a number on the left and a string on the right, so JavaScript joins them as text. The result is a string, and the second + keeps joining. That is why the method returns "0:05" and not a number. Order matters: 1 + 2 + "m" is "3m", while "m" + 1 + 2 is "m12".
The division also needs care: / keeps the fraction in JavaScript, so Math.floor(total / 60) keeps only the whole minutes.
Private field
A private field is invisible outside the class. For a playlist p, p.songs is undefined and p.#songs is a syntax error. The only way to change the list is through add and remove.
Using the classes
const playlist = new Playlist();
playlist.add(new Song("Echo", 30));
playlist.add(new Song("Echo", 45));
playlist.add(new Song("Rain", 5));
console.log(playlist.remove("Echo"), playlist.totalSeconds());
console.log(playlist.remove("Echo"), playlist.remove("Echo"));
console.log(playlist.duration());
true 50
true false
0:05
Two songs are called Echo. The first remove takes only the first one (30 seconds), leaving 45 + 5 = 50. The second takes the other Echo, and the third finds none, so it returns false. Only Rain is left: 5 seconds, written 0:05. The lesson's tests print the booleans as True and False.
An empty playlist gives duration() equal to "0:00", totalSeconds() equal to 0, and remove("Rain") equal to false.
Tracing duration
For a total of 265 seconds, Math.floor(265 / 60) is 4, because 4 whole minutes make 240 seconds, and 265 % 60 is 25, the seconds left over. Since 25 is not below 10, the method returns "4:25". For a total of 5 seconds, the minutes are 0 and the seconds are 5, which is below 10, so the method returns "0:05". For 3600 seconds, the minutes are 60 and the seconds are 0, which is below 10, so the result is "60:00". The zero seconds still need their padding digit.
Edge cases and common mistakes
- Removing every match. Without the early
return, the loop would go on and could skip the element that slides into the removed slot. - Case. Titles are compared exactly, so
"Rain"and"rain"are different titles. - Unpadded seconds. 65 seconds is
"1:05", not"1:5". - Padded minutes. Minutes are never padded: 750 seconds is
"12:30", and 3600 seconds is"60:00", because the minutes are not wrapped into hours. - Two playlists sharing songs. Because
#songs = []is evaluated for each new object, every playlist has its own array. - Repeating the sum. Writing the adding loop again inside
durationworks, but changes would then be needed in two places.
Key takeaways
- One class can hold objects of another class and answer questions about them.
- A private array keeps the playlist consistent.
splice(i, 1)removes by index, and returning early stops after the first match.
