Skip to main content

tank_core/
interval.rs

1use crate::Result;
2use anyhow::anyhow;
3use std::{
4    hash::{Hash, Hasher},
5    ops::{Add, AddAssign, Neg, Sub, SubAssign},
6};
7
8/// Time interval units.
9///
10/// Used for conversion and formatting.
11#[derive(PartialEq, Eq, Clone, Copy, Debug)]
12pub enum IntervalUnit {
13    Year,
14    Month,
15    Day,
16    Hour,
17    Minute,
18    Second,
19    Microsecond,
20    Nanosecond,
21}
22
23impl IntervalUnit {
24    pub fn from_bitmask(mask: u8) -> Result<IntervalUnit> {
25        Ok(match mask {
26            1 => IntervalUnit::Nanosecond,
27            2 => IntervalUnit::Microsecond,
28            4 => IntervalUnit::Second,
29            8 => IntervalUnit::Minute,
30            16 => IntervalUnit::Hour,
31            32 => IntervalUnit::Day,
32            64 => IntervalUnit::Month,
33            128 => IntervalUnit::Year,
34            _ => return Err(anyhow!("Invalid mask, it must be a single bit on")),
35        })
36    }
37}
38
39/// Calendar-aware time interval.
40///
41/// Stores months, days, and nanoseconds separately to preserve semantics.
42#[derive(Default, Debug, Clone, Copy)]
43pub struct Interval {
44    pub months: i64,
45    pub days: i64,
46    pub nanos: i128,
47}
48
49impl Interval {
50    pub const ZERO: Interval = Interval {
51        months: 0,
52        days: 0,
53        nanos: 0,
54    };
55
56    pub const DAYS_IN_MONTH: i64 = 30;
57    pub const DAYS_IN_MONTH_AVG: f64 = 30.436875;
58    pub const SECS_IN_DAY: i64 = 60 * 60 * 24;
59    pub const NANOS_IN_SEC: i128 = 1_000_000_000;
60    pub const NANOS_IN_HOUR: i128 = Self::NANOS_IN_SEC * 3600;
61    pub const NANOS_IN_DAY: i128 = Self::SECS_IN_DAY as i128 * Self::NANOS_IN_SEC;
62    pub const MICROS_IN_DAY: i128 = Self::SECS_IN_DAY as i128 * 1_000_000;
63
64    pub const fn new(months: i64, days: i64, nanos: i128) -> Self {
65        Self {
66            months,
67            days,
68            nanos,
69        }
70    }
71
72    pub const fn from_duration(duration: &std::time::Duration) -> Self {
73        Self {
74            months: 0,
75            days: 0,
76            nanos: duration.as_nanos() as i128,
77        }
78    }
79
80    const fn saturating_days(days: i128) -> i64 {
81        if days > i64::MAX as i128 {
82            i64::MAX
83        } else if days < i64::MIN as i128 {
84            i64::MIN
85        } else {
86            days as i64
87        }
88    }
89
90    pub const fn from_nanos(value: i128) -> Self {
91        Self {
92            months: 0,
93            days: Self::saturating_days(value / Self::NANOS_IN_DAY),
94            nanos: (value % Self::NANOS_IN_DAY),
95        }
96    }
97
98    pub const fn from_micros(value: i128) -> Self {
99        const MICROS_IN_DAY: i128 = (Interval::SECS_IN_DAY * 1_000_000) as _;
100        Self {
101            months: 0,
102            days: Self::saturating_days(value / MICROS_IN_DAY),
103            nanos: (value % MICROS_IN_DAY) * 1_000,
104        }
105    }
106
107    pub const fn from_millis(value: i128) -> Self {
108        const MILLIS_IN_DAY: i128 = (Interval::SECS_IN_DAY * 1_000) as _;
109        Self {
110            months: 0,
111            days: Self::saturating_days(value / MILLIS_IN_DAY),
112            nanos: ((value % MILLIS_IN_DAY) * 1_000_000),
113        }
114    }
115
116    pub const fn from_secs(value: i64) -> Self {
117        Self {
118            months: 0,
119            days: (value / Self::SECS_IN_DAY) as _,
120            nanos: ((value % Self::SECS_IN_DAY) * 1_000_000_000) as _,
121        }
122    }
123
124    pub const fn from_mins(value: i64) -> Self {
125        const MINS_IN_DAYS: i64 = 60 * 24;
126        let days = value / MINS_IN_DAYS;
127        let nanos = (value % MINS_IN_DAYS) * Interval::NANOS_IN_SEC as i64 * 60;
128        Self {
129            months: 0,
130            days,
131            nanos: nanos as _,
132        }
133    }
134
135    pub const fn from_hours(value: i64) -> Self {
136        Self {
137            months: 0,
138            days: (value / 24),
139            nanos: ((value % 24) * Interval::NANOS_IN_HOUR as i64) as _,
140        }
141    }
142
143    pub const fn from_days(value: i64) -> Self {
144        Self {
145            months: 0,
146            days: value,
147            nanos: 0,
148        }
149    }
150
151    pub const fn from_weeks(value: i64) -> Self {
152        Self {
153            months: 0,
154            days: value.saturating_mul(7),
155            nanos: 0,
156        }
157    }
158
159    pub const fn from_months(value: i64) -> Self {
160        Self {
161            months: value,
162            days: 0,
163            nanos: 0,
164        }
165    }
166
167    pub const fn from_years(value: i64) -> Self {
168        Self {
169            months: value.saturating_mul(12),
170            days: 0,
171            nanos: 0,
172        }
173    }
174
175    pub const fn as_ns(&self) -> i128 {
176        (self.months as i128)
177            .saturating_mul(Self::DAYS_IN_MONTH as i128)
178            .saturating_mul(Self::NANOS_IN_DAY)
179            .saturating_add(self.days_nanos())
180    }
181
182    /// Total nanoseconds contributed by `days` and `nanos`, saturating on overflow.
183    pub const fn days_nanos(&self) -> i128 {
184        (self.days as i128)
185            .saturating_mul(Self::NANOS_IN_DAY)
186            .saturating_add(self.nanos)
187    }
188
189    /// Decompose into `(hours, minutes, seconds, nanoseconds)`.
190    ///
191    /// When the interval is negative, only the first non-zero component carries the sign.
192    pub const fn as_hmsns(&self) -> (i128, i8, i8, i32) {
193        let total = self.as_ns();
194        let abs = total.unsigned_abs();
195        let ns = (abs % Self::NANOS_IN_SEC as u128) as i32;
196        let sec = abs / Self::NANOS_IN_SEC as u128;
197        let s = (sec % 60) as i8;
198        let min = sec / 60;
199        let m = (min % 60) as i8;
200        let h = (min / 60) as i128;
201        match total < 0 {
202            true if h > 0 => (-h, m, s, ns),
203            true if m > 0 => (0, -m, s, ns),
204            true if s > 0 => (0, 0, -s, ns),
205            true => (0, 0, 0, -ns),
206            false => (h, m, s, ns),
207        }
208    }
209
210    pub const fn is_zero(&self) -> bool {
211        self.months == 0 && self.days == 0 && self.nanos == 0
212    }
213
214    /// Convert to `std::time::Duration` (approximate).
215    ///
216    /// Uses `days_in_month` for conversion. Negative intervals saturate to zero.
217    pub fn as_duration(&self, days_in_month: f64) -> std::time::Duration {
218        let months_nanos = (self.months as f64) * days_in_month * (Interval::NANOS_IN_DAY as f64);
219        let nanos = (months_nanos as i128).saturating_add(self.days_nanos());
220        if nanos < 0 {
221            log::error!(
222                "Negative inverval `{self:?}` cannot be converted to `std::time::Duration`"
223            );
224            return std::time::Duration::ZERO;
225        }
226        let secs = (nanos / Interval::NANOS_IN_SEC) as u64;
227        let nanos = (nanos % Interval::NANOS_IN_SEC) as u32;
228        std::time::Duration::new(secs, nanos)
229    }
230
231    pub const fn units_and_factors(&self) -> &[(IntervalUnit, i128)] {
232        static UNITS: &[(IntervalUnit, i128)] = &[
233            (IntervalUnit::Year, Interval::NANOS_IN_DAY * 30 * 12),
234            (IntervalUnit::Month, Interval::NANOS_IN_DAY * 30),
235            (IntervalUnit::Day, Interval::NANOS_IN_DAY),
236            (IntervalUnit::Hour, Interval::NANOS_IN_SEC * 3600),
237            (IntervalUnit::Minute, Interval::NANOS_IN_SEC * 60),
238            (IntervalUnit::Second, Interval::NANOS_IN_SEC),
239            (IntervalUnit::Microsecond, 1_000),
240            (IntervalUnit::Nanosecond, 1),
241        ];
242        UNITS
243    }
244
245    /// Identify non-zero components (year, month, etc.).
246    pub fn units_mask(&self) -> u8 {
247        let mut mask = 0_u8;
248        if self.months != 0 {
249            if self.months % 12 == 0 {
250                mask |= 1 << 7;
251            } else if self.months != 0 {
252                mask |= 1 << 6;
253            }
254        }
255        let nanos = self.days_nanos();
256        if nanos != 0 {
257            for (i, &(_, factor)) in self.units_and_factors().iter().skip(2).enumerate() {
258                if nanos % factor == 0 {
259                    let shift = 5 - i; // i:0..5
260                    mask |= 1 << shift;
261                    break;
262                }
263            }
264        }
265        mask
266    }
267
268    /// Extract value for a specific unit (e.g. number of full years).
269    pub fn unit_value(&self, unit: IntervalUnit) -> i128 {
270        if unit == IntervalUnit::Year {
271            self.months as i128 / 12
272        } else if unit == IntervalUnit::Month {
273            self.months as i128
274        } else {
275            let factor = *self
276                .units_and_factors()
277                .iter()
278                .find_map(|(u, k)| if *u == unit { Some(k) } else { None })
279                .expect("The unit must be present");
280            self.days_nanos() / factor
281        }
282    }
283}
284
285impl PartialEq for Interval {
286    fn eq(&self, other: &Self) -> bool {
287        self.months == other.months && self.days_nanos() == other.days_nanos()
288    }
289}
290
291impl Eq for Interval {}
292
293impl Hash for Interval {
294    fn hash<H: Hasher>(&self, state: &mut H) {
295        self.months.hash(state);
296        self.days_nanos().hash(state);
297    }
298}
299
300macro_rules! sum_intervals {
301    ($lhs:ident $op:tt $rhs:ident) => {{
302        let days_total = $lhs.days as i128 $op $rhs.days as i128
303            + $lhs.nanos / Interval::NANOS_IN_DAY $op $rhs.nanos / Interval::NANOS_IN_DAY;
304        let days = days_total.clamp(i64::MIN as _, i64::MAX as _);
305        let mut nanos = $lhs.nanos % Interval::NANOS_IN_DAY $op $rhs.nanos % Interval::NANOS_IN_DAY;
306        if days != days_total {
307            nanos = nanos.saturating_add(
308                days_total.saturating_sub(days).saturating_mul(Interval::NANOS_IN_DAY),
309            );
310        }
311        Interval {
312            months: (($lhs.months as i128) $op ($rhs.months as i128))
313                .clamp(i64::MIN as i128, i64::MAX as i128) as _,
314            days: days as _,
315            nanos,
316        }
317    }};
318}
319
320impl Add for Interval {
321    type Output = Interval;
322    fn add(self, rhs: Self) -> Self {
323        sum_intervals!(self + rhs)
324    }
325}
326
327impl AddAssign for Interval {
328    fn add_assign(&mut self, rhs: Self) {
329        *self = sum_intervals!(self + rhs);
330    }
331}
332
333impl Sub for Interval {
334    type Output = Interval;
335    fn sub(self, rhs: Self) -> Self::Output {
336        sum_intervals!(self - rhs)
337    }
338}
339
340impl SubAssign for Interval {
341    fn sub_assign(&mut self, rhs: Self) {
342        *self = sum_intervals!(self - rhs);
343    }
344}
345
346impl Neg for Interval {
347    type Output = Interval;
348    fn neg(self) -> Self::Output {
349        Self::default() - self
350    }
351}
352
353impl From<std::time::Duration> for Interval {
354    fn from(value: std::time::Duration) -> Self {
355        Self {
356            months: 0,
357            days: value.as_secs() as i64 / Interval::SECS_IN_DAY,
358            nanos: (value.as_secs() as i64 % Interval::SECS_IN_DAY) as i128
359                * Interval::NANOS_IN_SEC
360                + value.subsec_nanos() as i128,
361        }
362    }
363}
364
365impl From<Interval> for std::time::Duration {
366    fn from(value: Interval) -> Self {
367        value.as_duration(Interval::DAYS_IN_MONTH as _)
368    }
369}
370
371impl From<time::Duration> for Interval {
372    fn from(value: time::Duration) -> Self {
373        let seconds = value.whole_seconds();
374        Self {
375            months: 0,
376            days: seconds / Interval::SECS_IN_DAY,
377            nanos: ((seconds % Interval::SECS_IN_DAY) * Interval::NANOS_IN_SEC as i64
378                + value.subsec_nanoseconds() as i64) as i128,
379        }
380    }
381}
382
383impl From<Interval> for time::Duration {
384    fn from(value: Interval) -> Self {
385        let seconds = (value.days as i128)
386            .saturating_add((value.months as i128).saturating_mul(Interval::DAYS_IN_MONTH as i128))
387            .saturating_mul(Interval::SECS_IN_DAY as i128)
388            .saturating_add(value.nanos / Interval::NANOS_IN_SEC);
389        let seconds = seconds.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
390        let nanos = (value.nanos % Interval::NANOS_IN_SEC) as i32;
391        time::Duration::new(seconds, nanos)
392    }
393}