* trait ClockDuration, CalendarDuration, tests * make clock rendering more generic * remove `should_blink` from `RenderClockState` * pass less down: `mode` -> `editable_time` * simplify `event` duration states * remove deprecated `DirectedDuration` * fix comments
849 lines
27 KiB
Rust
849 lines
27 KiB
Rust
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<CalendarDuration> 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<Duration> for DurationEx {
|
|
fn from(inner: Duration) -> Self {
|
|
Self { inner }
|
|
}
|
|
}
|
|
|
|
impl From<DurationEx> 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<u8, Report> {
|
|
let secs = s.parse::<u8>().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<u8, Report> {
|
|
let mins = m.parse::<u8>().map_err(|_| eyre!("Invalid minutes"))?;
|
|
ensure!(mins < 60, "Minutes must be less than 60.");
|
|
Ok(mins)
|
|
}
|
|
|
|
/// Parse hours
|
|
fn parse_hours(h: &str) -> Result<u8, Report> {
|
|
let hours = h.parse::<u8>().map_err(|_| eyre!("Invalid hours"))?;
|
|
Ok(hours)
|
|
}
|
|
|
|
/// Parses `Duration` from `hh:mm:ss`, `mm:ss` or `ss`
|
|
pub fn parse_duration(arg: &str) -> Result<Duration, Report> {
|
|
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<Duration, Report> {
|
|
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::<u64>()
|
|
.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::<u64>()
|
|
.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");
|
|
}
|
|
}
|