Start learning free

LESSON 571 OF 576 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Playlist in Rust: Two Structs Working Together

Practice O(n) Read the full explanation

A music app keeps playlists: songs in the order they were added. Build it from two classes that work together. A Song holds one song's data. A Playlist keeps a list of Song objects and answers questions about them.

Implement:

  • Song, created by Song::new(title, seconds), holds a song's title and its length in seconds;
  • Playlist, created by Playlist::new(), starts with no songs. Its methods:
    • add(song) adds a Song at the end of the playlist;
    • remove(title) removes the first song with that title and returns true; if no song has that title, it changes nothing and returns false;
    • total_seconds() returns the sum of the songs' lengths in seconds (0 for an empty playlist);
    • duration() returns the same total as a string "m:ss": the minutes, a colon, then the seconds with two digits. 5 seconds are "0:05", 750 seconds are "12:30", and 3600 seconds are "60:00": the minutes are never padded.

Rust has no classes: Song and Playlist are structs with a new function and an impl block. add and remove take &mut self because they change the playlist; the other two only read it.

The input is a list of operations, applied in order to one new playlist. The output lists what the remove, total_seconds and duration calls return, in order, with booleans printed as True and False.

 

Example 1:

Input:
    add(Song::new("Intro", 65))
    add(Song::new("Sunrise", 200))
    total_seconds()
    duration()
    remove("Intro")
    duration()
Output: 265 4:25 True 3:20
Explanation: The two songs last 65 + 200 = 265 seconds: 4 minutes (240 seconds) and 25 seconds.
After "Intro" is removed, only the 200 seconds of "Sunrise" are left: 3 minutes and 20 seconds.

Example 2:

Input:
    duration()
    remove("Rain")
    total_seconds()
Output: 0:00 False 0
Explanation: The playlist is empty: it lasts 0 seconds, and there is no "Rain" to remove.

Example 3:

Input:
    add(Song::new("Echo", 30))
    add(Song::new("Echo", 45))
    add(Song::new("Rain", 5))
    remove("Echo")
    total_seconds()
    remove("Echo")
    remove("Echo")
    duration()
Output: True 50 True False 0:05
Explanation: Two songs are called "Echo". The first remove takes only the first one, the 30 seconds,
so 45 + 5 = 50 seconds are left. The second remove takes the other "Echo", and the third finds none.
Only "Rain" is left: 5 seconds, written 0:05.

 

Constraints:

  • 1 <= seconds <= 3600
  • A title is one word of letters and digits. Titles are compared exactly: "Rain" and "rain" are different titles.
  • At most 200 operations.
STEP 1 OF 17
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Playlist in Rust, explained

    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 returns true, or changes nothing and returns false;
    • 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

    1. Declare Song's fields: title as a String and seconds as an i32.
    2. Write Song::new, turning the title into a String.
    3. Declare Playlist's field: songs, a Vec<Song>.
    4. Write Playlist::new, which starts with an empty Vec::new().
    5. Write add: push the song onto self.songs.
    6. Write remove. Removing goes by position, so loop over the indices 0..self.songs.len(). When the song at index i has the title, call self.songs.remove(i) and return true at once. That stops the loop, so only the first match goes. After the loop, return false.
    7. Write total_seconds: a let mut total = 0 and a loop over &self.songs that adds song.seconds.
    8. Write duration. Get the total, split it into minutes and seconds, and pad the seconds: below 10 they get an extra 0 in 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 self against &self. A method that changes the struct needs a mutable borrow, and the variable that holds the playlist must be declared with let 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::remove shifts. It takes out the element at the index, and the elements after it move up one place.
    • Comparing strings. self.songs[i].title == title compares a String with a &str by 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) gives 4:05. The lesson's two-branch version does the same thing.

    Edge cases and common mistakes

    • An empty playlist. duration() gives 0:00, remove("Rain") gives false and total_seconds() gives 0.
    • 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 second remove("Echo") takes the other, and a third finds none.
    • Padding. 5 seconds must read 0:05, not 0:5. A length of 750 seconds is 12:30, and 3600 seconds is 60:00, because the minutes never wrap into hours.
    • Dividing first. Integer division keeps only the whole part, so total / 60 is the whole minutes and total % 60 is 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
    
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free