diff --git a/flake.nix b/flake.nix index 50aa40a..dc94030 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,11 @@ cargoHash = "sha256-9sA6eHkJAFB4PRX7SG5RQXAGi5RRd8m3cPien/H/1fU="; + cargoCheckFlags = ["--show-output"]; + + TZ = "CET"; + RUST_BACKTRACE = 1; + src = ./.; }; } diff --git a/src/main.rs b/src/main.rs index 8cf9ff9..168661b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,53 +1,240 @@ /// GPLv3 2024 KLM banana bread© use chrono::prelude::*; use std::{env, io}; +use utils::print_time_delta; + +use crate::utils::parse_timestamp; + +#[derive(Debug)] +struct SharedState { + now: DateTime, + tz: FixedOffset, +} + +impl SharedState { + fn new() -> Self { + Self { + now: Local::now(), + tz: *Local::now().offset(), + } + } + + fn new_from(now: DateTime, tz: FixedOffset) -> Self { + Self { now, tz } + } +} + +mod utils { + use chrono::{prelude::*, TimeDelta}; + + use crate::SharedState; + + #[inline] + pub fn naive_time_to_local_datetime(ctx: &SharedState, t: NaiveTime) -> DateTime { + NaiveDateTime::new(ctx.now.date_naive(), t) + .and_local_timezone(ctx.tz) + .single() + .unwrap() + .into() + } + + #[inline] + pub fn parse_timestamp(ctx: &SharedState, input: &String) -> DateTime { + let tz = ctx.tz; + + if let Some(t) = input.parse::>().ok() { + t + } else if let Some(t) = NaiveDateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S").ok() { + t.and_local_timezone(tz).single().unwrap().into() + } else if let Some(t) = NaiveTime::parse_from_str(&[&input, ":00"].concat(), "%H:%M").ok() { + naive_time_to_local_datetime(ctx, t) + } else if let Some(t) = NaiveTime::parse_from_str(input, "%H:%M").ok() { + naive_time_to_local_datetime(ctx, t) + } else if let Some(t) = NaiveTime::parse_from_str(input, "%H:%M:%S").ok() { + naive_time_to_local_datetime(ctx, t) + } else { + panic!("Couldn't parse timestamp."); + } + } + + #[inline] + pub fn print_time_delta(delta: TimeDelta) { + let days = ((delta.num_seconds() / 60) / 60) / 24; + let hours = ((delta.num_seconds() / 60) / 60) % 24; + let minutes = (delta.num_seconds() / 60) % 60; + let seconds = delta.num_seconds() % 60; + + println!( + "{}d {}h {:0>2}m {:0>2}s", + days, &hours, minutes as f32, seconds + ); + } + + #[cfg(test)] + mod tests { + use super::*; + + const DATE: &str = "1998-01-20T16:00:00+01:00"; + + #[inline] + fn get_ctx() -> SharedState { + let test_date = DATE.parse::>().unwrap(); + dbg!(test_date); + SharedState::new_from(test_date, *test_date.offset()) + } + + #[test] + fn parse_timestamp_h() { + assert_eq!( + parse_timestamp(&get_ctx(), &"10".to_string()), + "1998-01-20T10:00:00+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_h_high() { + assert_eq!( + parse_timestamp(&get_ctx(), &"24".to_string()), + "1998-01-20T10:00:00+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + fn parse_timestamp_hm() { + assert_eq!( + parse_timestamp(&get_ctx(), &"23:59".to_string()), + "1998-01-20T23:59:00+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_hm_highh() { + assert_eq!( + parse_timestamp(&get_ctx(), &"24:59".to_string()), + "1998-01-20T23:59:00+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_hm_highm() { + assert_eq!( + parse_timestamp(&get_ctx(), &"23:60".to_string()), + "1998-01-20T23:59:00+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_hm_highhm() { + assert_eq!( + parse_timestamp(&get_ctx(), &"24:60".to_string()), + "1998-01-20T23:59:00+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + fn parse_timestamp_hms() { + assert_eq!( + parse_timestamp(&get_ctx(), &"23:59:59".to_string()), + "1998-01-20T23:59:59+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_hms_highh() { + assert_eq!( + parse_timestamp(&get_ctx(), &"24:59:59".to_string()), + "1998-01-20T24:59:59+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_hms_highm() { + assert_eq!( + parse_timestamp(&get_ctx(), &"23:60:59".to_string()), + "1998-01-20T23:60:59+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_hms_highs() { + assert_eq!( + parse_timestamp(&get_ctx(), &"23:59:61".to_string()), + "1998-01-20T23:59:61+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "Couldn't parse timestamp.")] + fn parse_timestamp_hms_highhms() { + assert_eq!( + parse_timestamp(&get_ctx(), &"24:60:61".to_string()), + "1998-01-20T24:60:61+01:00" + .parse::>() + .unwrap() + ); + } + + #[test] + fn parse_timestamp_default() { + assert_eq!( + parse_timestamp(&get_ctx(), &"1998-01-20T23:59:59+01:00".to_string()), + "1998-01-20T23:59:59+01:00" + .parse::>() + .unwrap() + ); + assert_eq!( + parse_timestamp(&get_ctx(), &"1998-01-20T23:59:59Z".to_string()), + "1998-01-21T00:59:59+01:00" + .parse::>() + .unwrap() + ); + // FIXME + // assert_eq!( + // parse_timestamp(&get_ctx(), &"1998-01-20T23:59:59Z+02:00".to_string()), + // "1998-01-21T23:59:59+01:00" + // .parse::>() + // .unwrap() + // ); + } + } +} fn main() -> io::Result<()> { let args: Vec = env::args().collect(); - if args.len() == 1 - || !(args[1].len() == 8 as usize - || args[1].len() == 19 as usize - || args[1].len() >= 21 as usize) - { - println!("usage: since hh:mm:ss"); //8 - println!("usage: since yyyy-mm-dd hh:mm:ss"); //19 - println!("usage: since "); //21 - std::process::abort(); - } + let ctx = SharedState::new(); - let buffer = &args[1]; + print_time_delta(DateTime::signed_duration_since( + ctx.now, + parse_timestamp(&ctx, &args[1]), + )); - let tz = *Local::now().offset(); - - let timestamp: DateTime; - if let Some(t) = buffer.parse::>().ok() { - timestamp = t; - } else if let Some(t) = NaiveDateTime::parse_from_str(buffer, "%Y-%m-%d %H:%M:%S").ok() { - timestamp = t.and_local_timezone(tz).single().unwrap().into(); - } else if let Some(t) = NaiveTime::parse_from_str(buffer, "%H:%M:%S").ok() { - timestamp = NaiveDateTime::new(Local::now().date_naive(), t) - .and_local_timezone(tz) - .single() - .unwrap() - .into(); - } else { - println!("Couldn't parse timestamp!"); - std::process::abort(); - } - - let now = Local::now(); - - let delta = DateTime::signed_duration_since(now, timestamp); - - let days = ((delta.num_seconds() / 60) / 60) / 24; - let hours = ((delta.num_seconds() / 60) / 60) % 24; - let minutes = (delta.num_seconds() / 60) % 60; - let seconds = delta.num_seconds() % 60; - - println!( - "{}d {}h {:0>2}m {:0>2}s", - days, &hours, minutes as f32, seconds - ); Ok(()) }