Skip to main content

kyu_types/
interval.rs

1/// Calendar interval: months + days + microseconds.
2/// 16 bytes total, matching the C++ `interval_t` layout.
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
4#[repr(C)]
5pub struct Interval {
6    pub months: i32,
7    pub days: i32,
8    pub micros: i64,
9}
10
11impl Interval {
12    pub const ZERO: Self = Self {
13        months: 0,
14        days: 0,
15        micros: 0,
16    };
17
18    pub const fn new(months: i32, days: i32, micros: i64) -> Self {
19        Self {
20            months,
21            days,
22            micros,
23        }
24    }
25
26    pub const fn from_months(months: i32) -> Self {
27        Self::new(months, 0, 0)
28    }
29
30    pub const fn from_days(days: i32) -> Self {
31        Self::new(0, days, 0)
32    }
33
34    pub const fn from_micros(micros: i64) -> Self {
35        Self::new(0, 0, micros)
36    }
37}
38
39impl std::fmt::Display for Interval {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        let mut parts = Vec::new();
42        if self.months != 0 {
43            let years = self.months / 12;
44            let months = self.months % 12;
45            if years != 0 {
46                parts.push(format!(
47                    "{years} year{}",
48                    if years.abs() != 1 { "s" } else { "" }
49                ));
50            }
51            if months != 0 {
52                parts.push(format!(
53                    "{months} month{}",
54                    if months.abs() != 1 { "s" } else { "" }
55                ));
56            }
57        }
58        if self.days != 0 {
59            parts.push(format!(
60                "{} day{}",
61                self.days,
62                if self.days.abs() != 1 { "s" } else { "" }
63            ));
64        }
65        if self.micros != 0 || parts.is_empty() {
66            let total_secs = self.micros / 1_000_000;
67            let remaining_micros = self.micros % 1_000_000;
68            let hours = total_secs / 3600;
69            let mins = (total_secs % 3600) / 60;
70            let secs = total_secs % 60;
71            if remaining_micros != 0 {
72                parts.push(format!(
73                    "{hours:02}:{mins:02}:{secs:02}.{remaining_micros:06}"
74                ));
75            } else {
76                parts.push(format!("{hours:02}:{mins:02}:{secs:02}"));
77            }
78        }
79        write!(f, "{}", parts.join(" "))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn size_is_16_bytes() {
89        assert_eq!(std::mem::size_of::<Interval>(), 16);
90    }
91
92    #[test]
93    fn zero() {
94        let z = Interval::ZERO;
95        assert_eq!(z.months, 0);
96        assert_eq!(z.days, 0);
97        assert_eq!(z.micros, 0);
98    }
99
100    #[test]
101    fn constructors() {
102        let m = Interval::from_months(14);
103        assert_eq!(m.months, 14);
104        assert_eq!(m.days, 0);
105
106        let d = Interval::from_days(30);
107        assert_eq!(d.days, 30);
108
109        let u = Interval::from_micros(1_000_000);
110        assert_eq!(u.micros, 1_000_000);
111    }
112
113    #[test]
114    fn display_zero() {
115        assert_eq!(Interval::ZERO.to_string(), "00:00:00");
116    }
117
118    #[test]
119    fn display_complex() {
120        let iv = Interval::new(14, 5, 3_723_000_000);
121        let s = iv.to_string();
122        assert!(s.contains("1 year"));
123        assert!(s.contains("2 months"));
124        assert!(s.contains("5 days"));
125        assert!(s.contains("01:02:03"));
126    }
127
128    #[test]
129    fn equality_and_hash() {
130        let a = Interval::new(1, 2, 3);
131        let b = Interval::new(1, 2, 3);
132        let c = Interval::new(1, 2, 4);
133        assert_eq!(a, b);
134        assert_ne!(a, c);
135    }
136}