Problem overview
A music app keeps playlists: songs in the order they were added. You build it from two classes. A Song holds a title and its length in seconds. A Playlist 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, 0 for an empty playlist;Duration()returns the same total as"m:ss": minutes, a colon, and seconds with two digits.
After adding Intro (65 seconds) and Sunrise (200 seconds), TotalSeconds() is 265 and Duration() is "4:25".
Approach: one class holds the data, the other uses it
Song is plain data: two public fields and a constructor. Playlist keeps its songs in a private List<Song>. Because the list is private, only the playlist's own methods can change it, so what TotalSeconds and Duration report always matches what Add and Remove did.
Every method is a small loop over that list. Duration reuses TotalSeconds, so the adding is written only once.
Step by step
- In
Song, declarepublic string titleandpublic int secondsand store the constructor's parameters withthis.. - In
Playlist, declareprivate List<Song> songs = new List<Song>();. A new playlist starts empty. Addcallssongs.Add(song).Removeneeds positions, so it uses aforloop overifrom 0 tosongs.Count. Whensongs[i].title == title, it callssongs.RemoveAt(i)and returnstrueat once. Returning stops the loop, so only the first match goes. After the loop, it returnsfalse.TotalSecondsstartstotalat 0 and addssong.secondsfor each song in aforeach.Durationsplits the total:total / 60whole minutes andtotal % 60leftover seconds. If the seconds are below 10 it inserts a0after the colon; otherwise it does not.
The solution in C#
class Song {
public string title;
public int seconds;
public Song(string title, int seconds) {
this.title = title;
this.seconds = seconds;
}
}
class Playlist {
private List<Song> songs = new List<Song>();
public void Add(Song song) {
songs.Add(song);
}
public bool Remove(string title) {
for (int i = 0; i < songs.Count; i++) {
if (songs[i].title == title) {
songs.RemoveAt(i);
return true;
}
}
return false;
}
public int TotalSeconds() {
int total = 0;
foreach (Song song in songs) {
total += song.seconds;
}
return total;
}
public string Duration() {
int total = TotalSeconds();
int minutes = total / 60;
int seconds = total % 60;
if (seconds < 10) {
return minutes + ":0" + seconds;
}
return minutes + ":" + seconds;
}
}
Details worth knowing
The two-digit seconds. 5 seconds must read 0:05, not 0:5, so a seconds value under 10 gets a ":0". The minutes are never padded: 750 seconds print as 12:30 and 3600 seconds as 60:00. A playlist of 55 + 5 seconds prints 1:00.
Strings and numbers with +. In minutes + ":0" + seconds, the + between the number and the text joins them as text, so the integers appear as digits.
Removing while looping. RemoveAt(i) shifts the later songs one place up. That is safe here because the method returns immediately after removing, so the loop never continues with shifted positions.
Exact titles. The comparison with == on strings compares their contents, and it is case-sensitive. On a playlist holding one song titled "Rain", Remove("rain") returns False and changes nothing, while Remove("Rain") returns True. Titles that differ only in case are different titles, which is why a title is compared exactly and never converted to lower case first.
Testing
Example 3 as code:
var playlist = new Playlist();
playlist.Add(new Song("Echo", 30));
playlist.Add(new Song("Echo", 45));
playlist.Add(new Song("Rain", 5));
Console.WriteLine(playlist.Remove("Echo"));
Console.WriteLine(playlist.TotalSeconds());
Console.WriteLine(playlist.Remove("Echo"));
Console.WriteLine(playlist.Remove("Echo"));
Console.WriteLine(playlist.Duration());
True
50
True
False
0:05
An empty playlist gives Duration() 0:00, Remove("Rain") False and TotalSeconds() 0. (When C# prints a bool it writes True and False.)
Complexity
Add is O(1) on average. Remove, TotalSeconds and Duration are O(n) for n songs, since each may look at every song once.
How Duration turns seconds into text
Integer division gives the whole minutes and % gives what is left. These came out of the code:
| TotalSeconds() | total / 60 | total % 60 | Duration() |
|---|---|---|---|
| 0 | 0 | 0 | 0:00 |
| 5 | 0 | 5 | 0:05 |
| 59 | 0 | 59 | 0:59 |
| 60 | 1 | 0 | 1:00 |
| 65 | 1 | 5 | 1:05 |
| 265 | 4 | 25 | 4:25 |
| 750 | 12 | 30 | 12:30 |
| 3600 | 60 | 0 | 60:00 |
The padding matters in the rows where the leftover seconds are below 10: 0, 5 and 9 must print as 00, 05 and 09. The branch for values below 10 supplies the extra zero. Seconds from 10 to 59 already have two digits, and the minutes are printed as they are.
Duplicates
With songs A (10 seconds), B (20) and A (30), the first Remove("A") returns True and leaves 50 seconds, the second returns True and leaves 20, and the third returns False and still leaves 20. Each call removes at most one song.
Common mistakes
- Removing every matching song instead of only the first.
- Writing
minutes + ":" + secondswithout the zero, giving0:5. - Padding the minutes.
- Making
songspublic, so code outside can change the list behind the playlist's back.
Two fields named seconds
Inside Duration the local variable seconds is the leftover seconds from the split, while Song has a field of the same name for a song's length. They live in different classes, so they never clash: song.seconds always means the song's length, and a bare seconds in Duration is the local variable.
Also, TotalSeconds() is called from Duration() without any object in front of it: inside a class, a method name alone means "this object's method".
Key takeaways
- Two classes can divide the work: one holds a record's data, the other holds a collection of them and answers questions.
- Keeping the collection private means only the class's own methods change it.
- Searching by index with a
forloop is the right tool when you need the position, for example to callRemoveAt. - Build one method on another (
DurationonTotalSeconds) so each rule exists in one place.
