use color_eyre::{ Report, eyre::{ensure, eyre}, }; use std::cmp::min; use std::fmt; use std::time::Duration; use time::OffsetDateTime; // unstable // https://doc.rust-lang.org/src/core/time.rs.html#32 pub const SECS_PER_MINUTE: u64 = 60; // unstable // https://doc.rust-lang.org/src/core/time.rs.html#34 pub const MINS_PER_HOUR: u64 = 60; // unstable // https://doc.rust-lang.org/src/core/time.rs.html#36 const HOURS_PER_DAY: u64 = 24; pub const ONE_DECI_SECOND: Duration = Duration::from_millis(100); pub const ONE_SECOND: Duration = Duration::from_secs(1); pub const ONE_MINUTE: Duration = Duration::from_secs(SECS_PER_MINUTE); pub const ONE_HOUR: Duration = Duration::from_secs(MINS_PER_HOUR * SECS_PER_MINUTE); pub const ONE_DAY: Duration = Duration::from_secs(HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE); pub const ONE_YEAR: Duration = Duration::from_secs(DAYS_PER_YEAR * HOURS_PER_DAY * MINS_PER_HOUR * SECS_PER_MINUTE); // Days per year // "There are 365 days in a year in a common year of the Gregorian calendar and 366 days in a leap year. // Leap years occur every four years. The average number of days in a year is 365.2425 days." // ^ https://www.math.net/days-in-a-year const DAYS_PER_YEAR: u64 = 365; // ignore leap year of 366 days // max. 999y 364d 23:59:59.9 (1000 years - 1 decisecond) pub const MAX_DURATION: Duration = ONE_YEAR .saturating_mul(1000) .saturating_sub(ONE_DECI_SECOND); /// Trait for duration types that can be displayed in clock widgets. /// /// This trait abstracts over different duration calculation strategies: /// - `DurationEx`: Uses fixed 365-day years (fast, simple) /// - `CalendarDuration`: Uses actual calendar dates (accounts for leap years) pub trait ClockDuration { /// Total years fn years(&self) -> u64; /// Total days fn days(&self) -> u64; /// Days within the current year (0-364 or 0-365 for leap years) fn days_mod(&self) -> u64; /// Total hours fn hours(&self) -> u64; /// Hours within the current day (0-23) fn hours_mod(&self) -> u64; /// Hours as 12-hour clock (1-12) fn hours_mod_12(&self) -> u64; /// Total minutes fn minutes(&self) -> u64; /// Minutes within the current hour (0-59) fn minutes_mod(&self) -> u64; /// Total seconds fn seconds(&self) -> u64; /// Seconds within the current minute (0-59) fn seconds_mod(&self) -> u64; /// Deciseconds (tenths of a second, 0-9) fn decis(&self) -> u64; /// Total milliseconds fn millis(&self) -> u128; } /// Calendar-aware duration that accounts for leap years. /// /// Unlike `DurationEx` which uses fixed 365-day years, this calculates /// years and days based on actual calendar dates, properly handling leap years. /// /// All calculations are performed on-demand from the stored dates. #[derive(Debug, Clone)] pub struct CalendarDuration { earlier: OffsetDateTime, later: OffsetDateTime, direction: CalendarDurationDirection, } #[derive(PartialEq, Debug, Clone)] pub enum CalendarDurationDirection { Since, Until, } impl CalendarDuration { /// Create a new CalendarDuration by given two `OffsetDateTime`. /// /// The order of arguments matters: /// First: `start_time` - `OffsetDateTime` to start from /// Second: `end_time` - `OffsetDateTime` for expected end pub fn from_start_end_times(start_time: OffsetDateTime, end_time: OffsetDateTime) -> Self { // To avoid negative values by calculating differences of `start` and `end` times, // we might switch those values internally by storing it as `earlier` and `later` values // It simplifies all calculations in `ClockDuration` trait later. // And `direction` will still help to still get original `start` and `end` times later. if start_time <= end_time { Self { earlier: start_time, later: end_time, direction: CalendarDurationDirection::Since, } } else { Self { earlier: end_time, later: start_time, direction: CalendarDurationDirection::Until, } } } pub fn direction(&self) -> &CalendarDurationDirection { &self.direction } pub fn start_time(&self) -> &OffsetDateTime { match self.direction { CalendarDurationDirection::Since => &self.earlier, CalendarDurationDirection::Until => &self.later, } } pub fn end_time(&self) -> &OffsetDateTime { match self.direction { CalendarDurationDirection::Since => &self.later, CalendarDurationDirection::Until => &self.earlier, } } } impl From for Duration { fn from(cal_duration: CalendarDuration) -> Self { let diff = cal_duration.later - cal_duration.earlier; Duration::from_millis(diff.whole_milliseconds().max(0) as u64) } } impl ClockDuration for CalendarDuration { fn years(&self) -> u64 { let mut years = (self.later.year() - self.earlier.year()) as i64; // Check if we've completed a full year by comparing month/day/time let intermediate = self .earlier .replace_year(self.later.year()) .unwrap_or(self.earlier); if intermediate > self.later { years -= 1; } years.max(0) as u64 } fn days_mod(&self) -> u64 { let year_count = self.years(); // Calculate intermediate date after adding complete years let target_year = self.earlier.year() + year_count as i32; let intermediate = self .earlier .replace_year(target_year) .unwrap_or(self.earlier); let remaining = self.later - intermediate; remaining.whole_days().max(0) as u64 } fn days(&self) -> u64 { (self.later - self.earlier).whole_days().max(0) as u64 } fn hours_mod(&self) -> u64 { let total_hours = (self.later - self.earlier).whole_hours(); (total_hours % 24).max(0) as u64 } fn hours(&self) -> u64 { (self.later - self.earlier).whole_hours().max(0) as u64 } fn hours_mod_12(&self) -> u64 { let hours = self.hours_mod(); (hours + 11) % 12 + 1 } fn minutes_mod(&self) -> u64 { let total_minutes = (self.later - self.earlier).whole_minutes(); (total_minutes % 60).max(0) as u64 } fn minutes(&self) -> u64 { (self.later - self.earlier).whole_minutes().max(0) as u64 } fn seconds_mod(&self) -> u64 { let total_seconds = (self.later - self.earlier).whole_seconds(); (total_seconds % 60).max(0) as u64 } fn seconds(&self) -> u64 { (self.later - self.earlier).whole_seconds().max(0) as u64 } fn decis(&self) -> u64 { let total_millis = (self.later - self.earlier).whole_milliseconds(); ((total_millis % 1000) / 100).max(0) as u64 } fn millis(&self) -> u128 { (self.later - self.earlier).whole_milliseconds().max(0) as u128 } } #[derive(Debug, Clone, Copy, PartialOrd)] pub struct DurationEx { inner: Duration, } impl PartialEq for DurationEx { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl From for DurationEx { fn from(inner: Duration) -> Self { Self { inner } } } impl From for Duration { fn from(ex: DurationEx) -> Self { ex.inner } } impl ClockDuration for DurationEx { fn years(&self) -> u64 { self.days() / DAYS_PER_YEAR } fn days(&self) -> u64 { self.hours() / HOURS_PER_DAY } fn days_mod(&self) -> u64 { self.days() % DAYS_PER_YEAR } fn hours(&self) -> u64 { self.seconds() / (SECS_PER_MINUTE * MINS_PER_HOUR) } fn hours_mod(&self) -> u64 { self.hours() % HOURS_PER_DAY } fn hours_mod_12(&self) -> u64 { // 0 => 12, // 1..=12 => hours, // 13..=23 => hours - 12, (self.hours_mod() + 11) % 12 + 1 } fn minutes(&self) -> u64 { self.seconds() / MINS_PER_HOUR } fn minutes_mod(&self) -> u64 { self.minutes() % SECS_PER_MINUTE } fn seconds(&self) -> u64 { self.inner.as_secs() } fn seconds_mod(&self) -> u64 { self.seconds() % SECS_PER_MINUTE } fn decis(&self) -> u64 { (self.inner.subsec_millis() / 100) as u64 } fn millis(&self) -> u128 { self.inner.as_millis() } } impl DurationEx { pub fn saturating_add(&self, ex: DurationEx) -> Self { let inner = self.inner.saturating_add(ex.inner); Self { inner } } pub fn saturating_sub(&self, ex: DurationEx) -> Self { let inner = self.inner.saturating_sub(ex.inner); Self { inner } } pub fn to_string_with_decis(self) -> String { format!("{}.{}", self, self.decis()) } } impl fmt::Display for DurationEx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use ClockDuration as _; // Import trait methods if self.years() >= 1 { write!( f, "{}y {}d {:02}:{:02}:{:02}", self.years(), self.days_mod(), self.hours_mod(), self.minutes_mod(), self.seconds_mod(), ) } else if self.hours() >= HOURS_PER_DAY { write!( f, "{}d {:02}:{:02}:{:02}", self.days_mod(), self.hours_mod(), self.minutes_mod(), self.seconds_mod(), ) } else if self.hours() >= 10 { write!( f, "{:02}:{:02}:{:02}", self.hours_mod(), self.minutes_mod(), self.seconds_mod(), ) } else if self.hours() >= 1 { write!( f, "{}:{:02}:{:02}", self.hours(), self.minutes_mod(), self.seconds_mod() ) } else if self.minutes() >= 10 { write!(f, "{:02}:{:02}", self.minutes_mod(), self.seconds_mod()) } else if self.minutes() >= 1 { write!(f, "{}:{:02}", self.minutes(), self.seconds_mod()) } else if self.seconds() >= 10 { write!(f, "{:02}", self.seconds_mod()) } else { write!(f, "{}", self.seconds()) } } } /// Parse seconds (must be < 60) fn parse_seconds(s: &str) -> Result { let secs = s.parse::().map_err(|_| eyre!("Invalid seconds"))?; ensure!(secs < 60, "Seconds must be less than 60."); Ok(secs) } /// Parse minutes (must be < 60) fn parse_minutes(m: &str) -> Result { let mins = m.parse::().map_err(|_| eyre!("Invalid minutes"))?; ensure!(mins < 60, "Minutes must be less than 60."); Ok(mins) } /// Parse hours fn parse_hours(h: &str) -> Result { let hours = h.parse::().map_err(|_| eyre!("Invalid hours"))?; Ok(hours) } /// Parses `Duration` from `hh:mm:ss`, `mm:ss` or `ss` pub fn parse_duration(arg: &str) -> Result { let parts: Vec<&str> = arg.split(':').collect(); let (hours, minutes, seconds) = match parts.as_slice() { [ss] => { // Single part: seconds only let s = parse_seconds(ss)?; (0u64, 0u64, s as u64) } [mm, ss] => { // Two parts: MM:SS let m = parse_minutes(mm)?; let s = parse_seconds(ss)?; (0u64, m as u64, s as u64) } [hh, mm, ss] => { // Three parts: HH:MM:SS let h = parse_hours(hh)?; let m = parse_minutes(mm)?; let s = parse_seconds(ss)?; (h as u64, m as u64, s as u64) } _ => { return Err(eyre!( "Invalid time format. Use 'ss', 'mm:ss', or 'hh:mm:ss'" )); } }; let total_seconds = hours * 3600 + minutes * 60 + seconds; Ok(Duration::from_secs(total_seconds)) } /// Similar to `parse_duration`, but it parses `years` and `days` in addition /// Formats: `Yy Dd`, `Yy` or `Dd` in any combination to other time formats /// Examples: `10y 3d 12:10:03`, `2d 10:00`, `101y 33`, `5:30` pub fn parse_long_duration(arg: &str) -> Result { let arg = arg.trim(); // parts are separated by whitespaces: // 3 parts: years, days, time let parts: Vec<&str> = arg.split_whitespace().collect(); ensure!(parts.len() <= 3, "Invalid format. Too many parts."); let mut total_duration = Duration::ZERO; let mut time_part: Option<&str> = None; for part in parts { // years if let Some(years_str) = part.strip_suffix('y') { let years = years_str .parse::() .map_err(|_| eyre!("Invalid years value: '{}'", years_str))?; total_duration = total_duration.saturating_add(ONE_YEAR.saturating_mul(years as u32)); } // days else if let Some(days_str) = part.strip_suffix('d') { let days = days_str .parse::() .map_err(|_| eyre!("Invalid days value: '{}'", days_str))?; total_duration = total_duration.saturating_add(ONE_DAY.saturating_mul(days as u32)); } // possible time format else { time_part = Some(part); } } // time format if let Some(time) = time_part { let time_duration = parse_duration(time)?; total_duration = total_duration.saturating_add(time_duration); } // avoid overflow total_duration = min(MAX_DURATION, total_duration); Ok(total_duration) } #[cfg(test)] mod tests { use super::ClockDuration; use super::*; use std::time::Duration; const MINUTE_IN_SECONDS: u64 = ONE_MINUTE.as_secs(); const HOUR_IN_SECONDS: u64 = ONE_HOUR.as_secs(); const DAY_IN_SECONDS: u64 = ONE_DAY.as_secs(); const YEAR_IN_SECONDS: u64 = ONE_YEAR.as_secs(); #[test] fn test_fmt() { // 1y Dd hh:mm:ss (single year) let ex: DurationEx = Duration::from_secs(YEAR_IN_SECONDS + 10 * DAY_IN_SECONDS + 36001).into(); assert_eq!(format!("{ex}"), "1y 10d 10:00:01"); // 5y Dd hh:mm:ss (multiple years) let ex: DurationEx = Duration::from_secs( 5 * YEAR_IN_SECONDS + 100 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1, ) .into(); assert_eq!(format!("{ex}"), "5y 100d 10:00:01"); // 150y Dd hh:mm:ss (more than 100 years) let ex: DurationEx = Duration::from_secs( 150 * YEAR_IN_SECONDS + 200 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1, ) .into(); assert_eq!(format!("{ex}"), "150y 200d 10:00:01"); // 366d hh:mm:ss (days more than a year) let ex: DurationEx = Duration::from_secs(366 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1).into(); assert_eq!(format!("{ex}"), "1y 1d 10:00:01"); // 1d hh:mm:ss (single day) let ex: DurationEx = Duration::from_secs(DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1).into(); assert_eq!(format!("{ex}"), "1d 10:00:01"); // 2d hh:mm:ss (multiple days) let ex: DurationEx = Duration::from_secs(2 * DAY_IN_SECONDS + 10 * HOUR_IN_SECONDS + 1).into(); assert_eq!(format!("{ex}"), "2d 10:00:01"); // hh:mm:ss let ex: DurationEx = Duration::from_secs(10 * HOUR_IN_SECONDS + 1).into(); assert_eq!(format!("{ex}"), "10:00:01"); // h:mm:ss let ex: DurationEx = Duration::from_secs(HOUR_IN_SECONDS + 1).into(); assert_eq!(format!("{ex}"), "1:00:01"); // mm:ss let ex: DurationEx = Duration::from_secs(MINUTE_IN_SECONDS + 11).into(); assert_eq!(format!("{ex}"), "1:11"); // m:ss let ex: DurationEx = Duration::from_secs(MINUTE_IN_SECONDS + 1).into(); assert_eq!(format!("{ex}"), "1:01"); // ss let ex: DurationEx = Duration::from_secs(11).into(); assert_eq!(format!("{ex}"), "11"); // s let ex: DurationEx = Duration::from_secs(1).into(); assert_eq!(format!("{ex}"), "1"); } #[test] fn test_saturating_sub() { let ex: DurationEx = Duration::from_secs(10).into(); let ex2: DurationEx = Duration::from_secs(1).into(); let ex3 = ex.saturating_sub(ex2); assert_eq!(format!("{ex3}"), "9"); } #[test] fn test_saturating_add() { let ex: DurationEx = Duration::from_secs(10).into(); let ex2: DurationEx = Duration::from_secs(1).into(); let ex3 = ex.saturating_add(ex2); assert_eq!(format!("{ex3}"), "11"); } #[test] fn test_hours_mod_12() { // 24 -> 12 let ex: DurationEx = ONE_HOUR.saturating_mul(24).into(); let result = ex.hours_mod_12(); assert_eq!(result, 12); // 12 -> 12 let ex: DurationEx = ONE_HOUR.saturating_mul(12).into(); let result = ex.hours_mod_12(); assert_eq!(result, 12); // 0 -> 12 let ex: DurationEx = ONE_SECOND.into(); let result = ex.hours_mod_12(); assert_eq!(result, 12); // 13 -> 1 let ex: DurationEx = ONE_HOUR.saturating_mul(13).into(); let result = ex.hours_mod_12(); assert_eq!(result, 1); // 1 -> 1 let ex: DurationEx = ONE_HOUR.saturating_mul(1).into(); let result = ex.hours_mod_12(); assert_eq!(result, 1); } #[test] fn test_parse_duration() { // ss assert_eq!(parse_duration("50").unwrap(), Duration::from_secs(50)); // mm:ss assert_eq!( parse_duration("01:30").unwrap(), Duration::from_secs(60 + 30) ); // hh:mm:ss assert_eq!( parse_duration("01:30:00").unwrap(), Duration::from_secs(60 * 60 + 30 * 60) ); // errors assert!(parse_duration("1:60").is_err()); // invalid seconds assert!(parse_duration("60:00").is_err()); // invalid minutes assert!(parse_duration("abc").is_err()); // invalid input assert!(parse_duration("01:02:03:04").is_err()); // too many parts } #[test] fn test_parse_long_duration() { // `Yy` assert_eq!( parse_long_duration("10y").unwrap(), Duration::from_secs(10 * YEAR_IN_SECONDS) ); assert_eq!( parse_long_duration("101y").unwrap(), Duration::from_secs(101 * YEAR_IN_SECONDS) ); // `Dd` assert_eq!( parse_long_duration("2d").unwrap(), Duration::from_secs(2 * DAY_IN_SECONDS) ); // `Yy Dd` assert_eq!( parse_long_duration("10y 3d").unwrap(), Duration::from_secs(10 * YEAR_IN_SECONDS + 3 * DAY_IN_SECONDS) ); // `Yy Dd hh:mm:ss` assert_eq!( parse_long_duration("10y 3d 12:10:03").unwrap(), Duration::from_secs( 10 * YEAR_IN_SECONDS + 3 * DAY_IN_SECONDS + 12 * HOUR_IN_SECONDS + 10 * MINUTE_IN_SECONDS + 3 ) ); // `Dd hh:mm` assert_eq!( parse_long_duration("2d 10:00").unwrap(), Duration::from_secs(2 * DAY_IN_SECONDS + 10 * 60) ); // `Yy ss` assert_eq!( parse_long_duration("101y 33").unwrap(), Duration::from_secs(101 * YEAR_IN_SECONDS + 33) ); // time formats (backward compatibility with `parse_duration`) assert_eq!( parse_long_duration("5:30").unwrap(), Duration::from_secs(5 * MINUTE_IN_SECONDS + 30) ); assert_eq!( parse_long_duration("01:30:45").unwrap(), Duration::from_secs(HOUR_IN_SECONDS + 30 * MINUTE_IN_SECONDS + 45) ); assert_eq!(parse_long_duration("42").unwrap(), Duration::from_secs(42)); // `Dd ss` assert_eq!( parse_long_duration("5d 30").unwrap(), Duration::from_secs(5 * DAY_IN_SECONDS + 30) ); // `Yy hh:mm:ss` assert_eq!( parse_long_duration("1y 01:30:00").unwrap(), Duration::from_secs(YEAR_IN_SECONDS + HOUR_IN_SECONDS + 30 * MINUTE_IN_SECONDS) ); // Whitespace handling assert_eq!( parse_long_duration(" 2d 10:00 ").unwrap(), Duration::from_secs(2 * DAY_IN_SECONDS + 10 * MINUTE_IN_SECONDS) ); // MAX_DURATION clamping assert_eq!(parse_long_duration("1000y").unwrap(), MAX_DURATION); assert_eq!( parse_long_duration("999y 364d 23:59:59").unwrap(), Duration::from_secs( 999 * YEAR_IN_SECONDS + 364 * DAY_IN_SECONDS + 23 * HOUR_IN_SECONDS + 59 * MINUTE_IN_SECONDS + 59 ) ); // errors assert!(parse_long_duration("10x").is_err()); // invalid unit assert!(parse_long_duration("abc").is_err()); // invalid input assert!(parse_long_duration("10y 60:00").is_err()); // invalid minutes in time part assert!(parse_long_duration("5d 1:60").is_err()); // invalid seconds in time part assert!(parse_long_duration("1y 2d 3d 4:00").is_err()); // too many parts (4 parts) assert!(parse_long_duration("1y 2d 3h 4m 5s").is_err()); // too many parts (5 parts) } #[test] fn test_calendar_duration_leap_year() { use time::macros::datetime; // 2024 is a leap year (366 days) let start = datetime!(2024-01-01 00:00:00 UTC); let end = datetime!(2025-01-01 00:00:00 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!(cal_dur.years(), 1, "Should be exactly 1 year"); assert_eq!(cal_dur.days_mod(), 0, "Should be 0 remaining days"); assert_eq!(cal_dur.days(), 366, "2024 has 366 days (leap year)"); } #[test] fn test_calendar_duration_non_leap_year() { use time::macros::datetime; // 2023 is not a leap year (365 days) let start = datetime!(2023-01-01 00:00:00 UTC); let end = datetime!(2024-01-01 00:00:00 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!(cal_dur.years(), 1, "Should be exactly 1 year"); assert_eq!(cal_dur.days_mod(), 0, "Should be 0 remaining days"); assert_eq!(cal_dur.days(), 365, "2023 has 365 days (non-leap year)"); } #[test] fn test_calendar_duration_partial_year_with_leap_day() { use time::macros::datetime; // Span including Feb 29, 2024 let start = datetime!(2024-02-01 00:00:00 UTC); let end = datetime!(2024-03-15 00:00:00 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!(cal_dur.years(), 0, "Should be 0 years"); // Feb 2024 has 29 days, so: 29 days (rest of Feb) + 15 days (March) = 44 days assert_eq!( cal_dur.days(), 43, "Should be 43 days (29 in Feb + 14 partial March)" ); } #[test] fn test_calendar_duration_partial_year_without_leap_day() { use time::macros::datetime; // Same dates but in 2023 (non-leap year) let start = datetime!(2023-02-01 00:00:00 UTC); let end = datetime!(2023-03-15 00:00:00 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!(cal_dur.years(), 0, "Should be 0 years"); // Feb 2023 has 28 days, so: 28 days (rest of Feb) + 15 days (March) = 43 days assert_eq!( cal_dur.days(), 42, "Should be 42 days (28 in Feb + 14 partial March)" ); } #[test] fn test_calendar_duration_multiple_years_spanning_leap_years() { use time::macros::datetime; // From 2023 (non-leap) through 2024 (leap) to 2025 let start = datetime!(2023-03-01 10:00:00 UTC); let end = datetime!(2025-03-01 10:00:00 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!(cal_dur.years(), 2, "Should be exactly 2 years"); assert_eq!(cal_dur.days_mod(), 0, "Should be 0 remaining days"); // Total days: 365 (2023 partial + 2024 partial) + 366 (full 2024 year conceptually included) // Actually: From 2023-03-01 to 2025-03-01 = 365 + 366 = 731 days assert_eq!(cal_dur.days(), 731, "Should be 731 total days"); } #[test] fn test_calendar_duration_year_boundary() { use time::macros::datetime; // Test incomplete year - just before year boundary let start = datetime!(2024-01-01 00:00:00 UTC); let end = datetime!(2024-12-31 23:59:59 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!(cal_dur.years(), 0, "Should be 0 years (not complete)"); assert_eq!(cal_dur.days(), 365, "Should be 365 days"); } #[test] fn test_calendar_duration_hours_minutes_seconds() { use time::macros::datetime; let start = datetime!(2024-01-01 10:30:45 UTC); let end = datetime!(2024-01-02 14:25:50 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!(cal_dur.years(), 0); assert_eq!(cal_dur.days(), 1); assert_eq!(cal_dur.hours_mod(), 3, "Should be 3 hours past midnight"); assert_eq!(cal_dur.minutes_mod(), 55, "Should be 55 minutes"); assert_eq!(cal_dur.seconds_mod(), 5, "Should be 5 seconds"); } #[test] fn test_calendar_duration_reversed_dates() { use time::macros::datetime; // CalendarDuration::between should handle reversed order let later = datetime!(2025-01-01 00:00:00 UTC); let earlier = datetime!(2024-01-01 00:00:00 UTC); let cal_dur = CalendarDuration::from_start_end_times(later, earlier); assert_eq!(cal_dur.years(), 1, "Should still calculate 1 year"); assert_eq!(cal_dur.days(), 366, "Should still be 366 days"); } #[test] fn test_calendar_duration_same_date() { use time::macros::datetime; let date = datetime!(2024-06-15 12:00:00 UTC); let cal_dur = CalendarDuration::from_start_end_times(date, date); assert_eq!(cal_dur.years(), 0); assert_eq!(cal_dur.days(), 0); assert_eq!(cal_dur.hours(), 0); assert_eq!(cal_dur.minutes(), 0); assert_eq!(cal_dur.seconds(), 0); } #[test] fn test_calendar_duration_deciseconds() { use time::macros::datetime; let start = datetime!(2024-01-01 00:00:00.000 UTC); let end = datetime!(2024-01-01 00:00:00.750 UTC); let cal_dur = CalendarDuration::from_start_end_times(start, end); assert_eq!( cal_dur.decis(), 7, "Should be 7 deciseconds (750ms = 7.5 decis, truncated to 7)" ); assert_eq!(cal_dur.millis(), 750, "Should be 750 milliseconds"); } }