Problem overview
A music app keeps playlists: songs in the order they were added. You build it from two structs. A Song holds a title and a length in seconds. A Playlist keeps a vector of songs and answers questions about them:
add(song)puts a song at the end;remove(title)removes the first song with that title and returnstrue, or changes nothing and returnsfalse;total_seconds()adds up the lengths (0 for an empty playlist);duration()writes the total as"m:ss": minutes, a colon, and the seconds with two digits.
With "Intro" (65 seconds) and "Sunrise" (200 seconds), the total is 265 seconds, which duration() writes as 4:25. After removing "Intro", the playlist lasts 200 seconds, written 3:20.
Approach: the playlist owns its songs
Playlist has one field, songs: Vec<Song>. Because add and remove change the playlist, they take &mut self. total_seconds and duration only read it, so they take &self.
duration doesn't add anything itself. It calls total_seconds and only formats the result, so the adding lives in one place. The format uses integer division and remainder: the minutes are total / 60 and the seconds are total % 60. For 265, that is 4 and 25.
Step by step
- Declare
Song's fields:titleas aStringandsecondsas ani32. - Write
Song::new, turning the title into aString. - Declare
Playlist's field:songs, aVec<Song>. - Write
Playlist::new, which starts with an emptyVec::new(). - Write
add: push the song ontoself.songs. - Write
remove. Removing goes by position, so loop over the indices0..self.songs.len(). When the song at indexihas the title, callself.songs.remove(i)and returntrueat once. That stops the loop, so only the first match goes. After the loop, returnfalse. - Write
total_seconds: alet mut total = 0and a loop over&self.songsthat addssong.seconds. - Write
duration. Get the total, split it into minutes and seconds, and pad the seconds: below 10 they get an extra0in front. The minutes are never padded.
The solution in Rust
struct Song {
title: String,
seconds: i32,
}
impl Song {
fn new(title: &str, seconds: i32) -> Song {
Song { title: title.to_string(), seconds }
}
}
struct Playlist {
songs: Vec<Song>,
}
impl Playlist {
fn new() -> Playlist {
Playlist { songs: Vec::new() }
}
fn add(&mut self, song: Song) {
self.songs.push(song);
}
fn remove(&mut self, title: &str) -> bool {
for i in 0..self.songs.len() {
if self.songs[i].title == title {
self.songs.remove(i);
return true;
}
}
false
}
fn total_seconds(&self) -> i32 {
let mut total = 0;
for song in &self.songs {
total += song.seconds;
}
total
}
fn duration(&self) -> String {
let total = self.total_seconds();
let minutes = total / 60;
let seconds = total % 60;
if seconds < 10 {
return format!("{}:0{}", minutes, seconds);
}
format!("{}:{}", minutes, seconds)
}
}
Using it:
fn main() {
let mut playlist = Playlist::new();
playlist.add(Song::new("Intro", 65));
playlist.add(Song::new("Sunrise", 200));
println!("{} {}", playlist.total_seconds(), playlist.duration());
println!("{} {}", playlist.remove("Intro"), playlist.duration());
}
265 4:25
true 3:20
Rust details worth knowing
&mut selfagainst&self. A method that changes the struct needs a mutable borrow, and the variable that holds the playlist must be declared withlet mut.- Looping by index. Removing goes by position, so the loop counts indices and calls
self.songs.remove(i)on the one that matches. Returning right after the removal means the loop never continues over a vector that has shifted. Vec::removeshifts. It takes out the element at the index, and the elements after it move up one place.- Comparing strings.
self.songs[i].title == titlecompares aStringwith a&strby content, and it is case-sensitive, so "Rain" and "rain" are different titles. - Formatting.
format!("{}:0{}", minutes, seconds)builds the padded text by hand. Rust can also pad with{:02}:format!("{}:{:02}", 4, 5)gives4:05. The lesson's two-branch version does the same thing.
Edge cases and common mistakes
- An empty playlist.
duration()gives0:00,remove("Rain")givesfalseandtotal_seconds()gives0. - Duplicate titles. With two songs called "Echo" (30 and 45 seconds) and "Rain" (5), the first
remove("Echo")takes the 30-second one, leaving 50 seconds. The secondremove("Echo")takes the other, and a third finds none. - Padding. 5 seconds must read
0:05, not0:5. A length of 750 seconds is12:30, and 3600 seconds is60:00, because the minutes never wrap into hours. - Dividing first. Integer division keeps only the whole part, so
total / 60is the whole minutes andtotal % 60is what is left over.
Complexity
For a playlist of n songs, add takes constant time on average, because pushing onto a vector's end is cheap. remove takes O(n): the search can visit every song, and Vec::remove then shifts the songs after the removed one. total_seconds and duration visit every song once, which is O(n) as well. With at most 200 operations, none of this matters in practice.
Why add and remove need a mutable playlist
Because add and remove take &mut self, calling one on a variable declared with plain let doesn't compile:
let playlist = Playlist::new();
playlist.add(Song::new("Intro", 65));
The compiler reports cannot borrow `playlist` as mutable, as it is not declared as mutable and suggests let mut playlist. The two reading methods work on either kind of variable. The signatures tell the caller which methods can change the playlist and which can't.
The same idea explains the structure of the code. Playlist owns its Song values: add takes a Song by value, so the song moves into the vector, and the caller can't use that song again afterward. Song's fields are only read by Playlist's methods, which sit in the same module and can see them.
Finally, duration is a good example of reuse. It gets its number from total_seconds and does nothing but format it, so if the way of adding up ever changes, one method changes and the other follows.
Testing
Running this sequence on a new playlist, with the calls and results as they happened:
add Echo 30, add Echo 45, add Rain 5
remove("Echo") -> true
total_seconds() -> 50
remove("Echo") -> true
remove("Echo") -> false
duration() -> 0:05
