Skip to main content

rosetta_date/
delta.rs

1//! Time delta calculation between two `RosettaDateTime` values.
2
3use crate::datetime::RosettaDateTime;
4
5/// Represents the difference between two date-times, broken down into
6/// human-readable components.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct RosettaDelta {
9    /// Positive if `end > start`.
10    pub total_seconds: i64,
11    /// Approximate years component.
12    pub years: i64,
13    /// Approximate months component (remainder after years).
14    pub months: i64,
15    /// Days component (remainder after months).
16    pub days: i64,
17    /// Hours component.
18    pub hours: i64,
19    /// Minutes component.
20    pub minutes: i64,
21    /// Seconds component.
22    pub seconds: i64,
23}
24
25impl RosettaDelta {
26    /// Compute the delta from `start` to `end`.
27    ///
28    /// The breakdown into years/months is approximate (assumes 365-day years
29    /// and 30-day months).  Total seconds is exact.
30    pub fn between(start: &RosettaDateTime, end: &RosettaDateTime) -> Self {
31        let total = end.timestamp() - start.timestamp();
32        let abs = total.unsigned_abs();
33
34        let years = abs / (365 * 86400);
35        let rem = abs % (365 * 86400);
36        let months = rem / (30 * 86400);
37        let rem = rem % (30 * 86400);
38        let days = rem / 86400;
39        let rem = rem % 86400;
40        let hours = rem / 3600;
41        let rem = rem % 3600;
42        let minutes = rem / 60;
43        let seconds = rem % 60;
44
45        let sign = if total >= 0 { 1i64 } else { -1i64 };
46
47        Self {
48            total_seconds: total,
49            years: sign * years as i64,
50            months: sign * months as i64,
51            days: sign * days as i64,
52            hours: sign * hours as i64,
53            minutes: sign * minutes as i64,
54            seconds: sign * seconds as i64,
55        }
56    }
57
58    /// Total absolute seconds.
59    pub fn abs_seconds(&self) -> u64 {
60        self.total_seconds.unsigned_abs()
61    }
62
63    /// Is the delta representing a positive (future) direction?
64    pub fn is_positive(&self) -> bool {
65        self.total_seconds >= 0
66    }
67
68    /// Total complete days (truncated).
69    pub fn total_days(&self) -> i64 {
70        self.total_seconds / 86400
71    }
72
73    /// Total complete hours.
74    pub fn total_hours(&self) -> i64 {
75        self.total_seconds / 3600
76    }
77
78    /// Total complete minutes.
79    pub fn total_minutes(&self) -> i64 {
80        self.total_seconds / 60
81    }
82}
83
84impl std::fmt::Display for RosettaDelta {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        let abs = self.total_seconds.unsigned_abs();
87        let sign = if self.total_seconds < 0 { "-" } else { "" };
88
89        let d = abs / 86400;
90        let h = (abs % 86400) / 3600;
91        let m = (abs % 3600) / 60;
92        let s = abs % 60;
93
94        if d > 0 {
95            write!(f, "{}{}d {:02}:{:02}:{:02}", sign, d, h, m, s)
96        } else {
97            write!(f, "{}{:02}:{:02}:{:02}", sign, h, m, s)
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::timezone::TzOffset;
106
107    #[test]
108    fn test_delta_basic() {
109        let a = RosettaDateTime::from_components(2023, 10, 1, 12, 0, 0, TzOffset::UTC).unwrap();
110        let b = RosettaDateTime::from_components(2023, 10, 1, 14, 30, 15, TzOffset::UTC).unwrap();
111        let delta = RosettaDelta::between(&a, &b);
112        assert_eq!(delta.hours, 2);
113        assert_eq!(delta.minutes, 30);
114        assert_eq!(delta.seconds, 15);
115        assert!(delta.is_positive());
116    }
117
118    #[test]
119    fn test_delta_negative() {
120        let a = RosettaDateTime::from_components(2023, 10, 5, 12, 0, 0, TzOffset::UTC).unwrap();
121        let b = RosettaDateTime::from_components(2023, 10, 1, 12, 0, 0, TzOffset::UTC).unwrap();
122        let delta = RosettaDelta::between(&a, &b);
123        assert!(!delta.is_positive());
124        assert_eq!(delta.total_days(), -4);
125    }
126
127    #[test]
128    fn test_delta_display() {
129        let a = RosettaDateTime::from_components(2023, 10, 1, 0, 0, 0, TzOffset::UTC).unwrap();
130        let b = RosettaDateTime::from_components(2023, 10, 3, 5, 30, 45, TzOffset::UTC).unwrap();
131        let delta = RosettaDelta::between(&a, &b);
132        assert_eq!(format!("{}", delta), "2d 05:30:45");
133    }
134}