From ad12c3ca671cb61210c6096fe6159538ed6e78c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Toma=C5=9Bko?= Date: Sun, 8 Sep 2024 05:01:25 +0200 Subject: [PATCH] #45 implement printing all listens for a day --- endsong_ui/src/plot.rs | 4 +- endsong_ui/src/print.rs | 83 +++++++++++++++++++++++++++++++++++++++ endsong_ui/src/ui/help.rs | 5 +++ endsong_ui/src/ui/mod.rs | 20 ++++++++++ src/entry.rs | 7 +--- 5 files changed, 111 insertions(+), 8 deletions(-) diff --git a/endsong_ui/src/plot.rs b/endsong_ui/src/plot.rs index 3d4f625..2b998ba 100644 --- a/endsong_ui/src/plot.rs +++ b/endsong_ui/src/plot.rs @@ -110,10 +110,10 @@ fn write_and_open_plot(plot: &Plot, title: &str) { }; } -/// Replaces Windows forbidden symbols in path with a '_' +/// Replaces Windows forbidden symbols in path with an '_' /// /// Also removes whitespace and replaces empty -/// strings with "_" +/// strings with '_' fn normalize_path(path: &str) -> String { // https://stackoverflow.com/a/31976060 // Array > HashSet bc of overhead diff --git a/endsong_ui/src/print.rs b/endsong_ui/src/print.rs index e48c703..8909274 100644 --- a/endsong_ui/src/print.rs +++ b/endsong_ui/src/print.rs @@ -404,3 +404,86 @@ fn normalize_dates<'a>( (start, end) } + +/// Prints each [`SongEntry`] on a specific day +pub fn day(entries: &[SongEntry], date: DateTime) { + let day = date.date_naive(); + let day_entries = entries.iter().filter(|e| e.timestamp.date_naive() == day); + + let length = day_entries.clone().count(); + if length == 0 { + println!("You haven't listened to any songs on {day}!"); + return; + } + let time_played = pretty_duration(day_entries.clone().map(|e| e.time_played).sum()); + let sng = if length == 1 { "song" } else { "songs" }; + println!("You've listened to {length} {sng} on {day} for {time_played}!"); + + for entry in day_entries { + println!( + "{}: {} - {} ({}) for {}s", + entry.timestamp.time(), + entry.artist, + entry.track, + entry.album, + entry.time_played.num_seconds() + ); + } +} + +/// Formats [`TimeDelta`] in a convenient way +/// +/// I.e. not include hours when only minutes necessary etc. +fn pretty_duration(duration: TimeDelta) -> String { + let hours = duration.num_hours(); + let minutes = duration.num_minutes(); + + if minutes == 0 || minutes == 1 { + return format!("{} seconds", duration.num_seconds()); + } + + if hours > 0 { + let hrs = if hours == 1 { "hour" } else { "hours" }; + let remaining_minutes = minutes % (60 * hours); + // yes, I know ignoring 1min :) + if remaining_minutes == 0 || remaining_minutes == 1 { + return format!("{hours} {hrs}"); + } + return format!("{hours} {hrs} and {remaining_minutes} minutes"); + } + + format!("{minutes} minutes") +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests [`pretty_duration`] + #[test] + fn pretty_dur() { + let thirty_secs = TimeDelta::seconds(30); + assert_eq!(pretty_duration(thirty_secs), "30 seconds"); + + let one_half_min = TimeDelta::seconds(90); + assert_eq!(pretty_duration(one_half_min), "90 seconds"); + + let two_mins = TimeDelta::minutes(2); + assert_eq!(pretty_duration(two_mins), "2 minutes"); + + let fifty_nine_mins = TimeDelta::minutes(59); + assert_eq!(pretty_duration(fifty_nine_mins), "59 minutes"); + + let one_hour = TimeDelta::hours(1); + assert_eq!(pretty_duration(one_hour), "1 hour"); + + let one_half_hours = TimeDelta::minutes(90); + assert_eq!(pretty_duration(one_half_hours), "1 hour and 30 minutes"); + + let three_half_hours = TimeDelta::minutes(3 * 60 + 30); + assert_eq!(pretty_duration(three_half_hours), "3 hours and 30 minutes"); + + let five_days = TimeDelta::days(5); + assert_eq!(pretty_duration(five_days), "120 hours"); + } +} diff --git a/endsong_ui/src/ui/help.rs b/endsong_ui/src/ui/help.rs index 0317e8f..e30bf56 100644 --- a/endsong_ui/src/ui/help.rs +++ b/endsong_ui/src/ui/help.rs @@ -128,6 +128,11 @@ const fn print_commands() -> &'static [Command] { "psonsd", "prints a song with all the albums it may be from within a date range", ), + Command( + "print day", + "pd", + "prints all song entries on a specific day", + ), ] } diff --git a/endsong_ui/src/ui/mod.rs b/endsong_ui/src/ui/mod.rs index c6fe735..404a8a1 100644 --- a/endsong_ui/src/ui/mod.rs +++ b/endsong_ui/src/ui/mod.rs @@ -106,6 +106,7 @@ impl ShellHelper { "print songs date", "print top artists", "print top songs", + "print day", "plot", "plot rel", "plot compare", @@ -314,6 +315,7 @@ fn match_input( "print top artists" | "ptarts" => match_print_top(entries, rl, Aspect::Artists)?, "print top albums" | "ptalbs" => match_print_top(entries, rl, Aspect::Albums)?, "print top songs" | "ptsons" => match_print_top(entries, rl, Aspect::Songs(false))?, + "print day" | "pd" => match_print_day(entries, rl)?, "plot" | "g" => match_plot(entries, rl)?, "plot rel" | "gr" => match_plot_relative(entries, rl)?, "plot compare" | "gc" => match_plot_compare(entries, rl)?, @@ -577,6 +579,24 @@ fn match_print_top( Ok(()) } +/// Used by [`match_input()`] for `print day` command +fn match_print_day( + entries: &SongEntries, + rl: &mut Editor, +) -> Result<(), UiError> { + // make sure no wrong autocompletes appear + rl.helper_mut().unwrap().reset(); + + // 1st prompt: start date + println!("What day's data do you want to see? YYYY-MM-DD"); + let usr_input_date = rl.readline(PROMPT_SECONDARY)?; + let date = parse_date(&usr_input_date)?; + + print::day(entries, date); + + Ok(()) +} + /// Used by [`match_input()`] for `plot` command fn match_plot( entries: &SongEntries, diff --git a/src/entry.rs b/src/entry.rs index 8da740a..1a411b4 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -371,11 +371,6 @@ impl SongEntries { /// /// Minimum duration is 1 day and maximum duration is the whole dataset, so /// a check is performed and the timespan is adjusted accordingly - /// - /// # Panics - /// - /// Unwraps used on [`TimeDelta::try_days`], but won't panic since - /// only duration of 1 day created #[must_use] pub fn max_listening_time( &self, @@ -384,7 +379,7 @@ impl SongEntries { let first = self.first_date(); let last = self.last_date(); - let one_day = TimeDelta::try_days(1).unwrap(); + let one_day = TimeDelta::days(1); let actual_time_span = match time_span { // maximum duration is whole dataset?