Skip to main content

linkrs_core/value/
date_time.rs

1//! Date and Time Type Module
2//!
3//! This module defines types for dates, times, date-times, and intervals, as well as the related operations.
4
5use crate::value::interval::IntervalValue;
6use serde::{Deserialize, Serialize};
7use std::hash::Hash;
8
9/// Simple date representation
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash)]
11pub struct DateValue {
12    pub year: i32,
13    pub month: u32,
14    pub day: u32,
15}
16
17impl DateValue {
18    /// Add an interval
19    pub fn add_interval(&mut self, interval: &IntervalValue) {
20        if interval.months != 0 {
21            self.add_months(interval.months);
22        }
23
24        if interval.days != 0 {
25            self.add_days(interval.days as i64);
26        }
27    }
28
29    /// Subtract the interval.
30    pub fn sub_interval(&mut self, interval: &IntervalValue) {
31        if interval.months != 0 {
32            self.add_months(-interval.months);
33        }
34
35        if interval.days != 0 {
36            self.add_days(-(interval.days as i64));
37        }
38    }
39
40    fn add_months(&mut self, months: i32) {
41        let mut new_month = self.month as i32 + months;
42        let mut year_delta = 0;
43
44        while new_month > 12 {
45            new_month -= 12;
46            year_delta += 1;
47        }
48
49        while new_month < 1 {
50            new_month += 12;
51            year_delta -= 1;
52        }
53
54        self.year += year_delta;
55        self.month = new_month as u32;
56
57        self.normalize_day();
58    }
59
60    fn add_days(&mut self, days: i64) {
61        let total_days = Self::to_days(self) + days;
62        *self = Self::from_days(total_days);
63    }
64
65    fn normalize_day(&mut self) {
66        let days_in_month = Self::days_in_month(self.year, self.month);
67        if self.day > days_in_month {
68            self.day = days_in_month;
69        }
70    }
71
72    fn days_in_month(year: i32, month: u32) -> u32 {
73        match month {
74            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
75            4 | 6 | 9 | 11 => 30,
76            2 => {
77                if Self::is_leap_year(year) {
78                    29
79                } else {
80                    28
81                }
82            }
83            _ => panic!("Invalid month"),
84        }
85    }
86
87    fn is_leap_year(year: i32) -> bool {
88        (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
89    }
90
91    pub fn to_days(&self) -> i64 {
92        let year = self.year as i64;
93        let month = self.month as i64;
94        let day = self.day as i64;
95
96        let a = (14 - month) / 12;
97        let y = year + 4800 - a;
98        let m = month + 12 * a - 3;
99
100        day + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - 32045
101    }
102
103    /// Convert a day count (days since 0000-03-01 in the proleptic Gregorian
104    /// calendar, as produced by [`DateValue::to_days`]) back into a date.
105    pub fn from_days(days: i64) -> Self {
106        let a = days + 32044;
107        let b = (4 * a + 3) / 146097;
108        let c = a - (146097 * b) / 4;
109        let d = (4 * c + 3) / 1461;
110        let e = c - (1461 * d) / 4;
111        let m = (5 * e + 2) / 153;
112
113        let day = e - (153 * m + 2) / 5 + 1;
114        let month = m + 3 - 12 * (m / 10);
115        let year = 100 * b + d - 4800 + m / 10;
116
117        DateValue {
118            year: year as i32,
119            month: month as u32,
120            day: day as u32,
121        }
122    }
123}
124
125impl Default for DateValue {
126    fn default() -> Self {
127        DateValue {
128            year: 1970,
129            month: 1,
130            day: 1,
131        }
132    }
133}
134
135impl DateValue {
136    /// Estimate the memory usage of the date value
137    pub fn estimated_size(&self) -> usize {
138        std::mem::size_of::<Self>()
139    }
140}
141
142impl std::fmt::Display for DateValue {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
145    }
146}
147
148/// Simple time representation
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash, Default)]
150pub struct TimeValue {
151    pub hour: u32,
152    pub minute: u32,
153    pub sec: u32,
154    pub microsec: u32,
155}
156
157impl TimeValue {
158    /// Add interval
159    pub fn add_interval(&mut self, interval: &IntervalValue) {
160        let total_microseconds = interval.microseconds;
161        let mut new_microseconds = self.microsec as i64 + total_microseconds;
162
163        while new_microseconds >= 86_400_000_000 {
164            new_microseconds -= 86_400_000_000;
165        }
166        while new_microseconds < 0 {
167            new_microseconds += 86_400_000_000;
168        }
169
170        self.microsec = (new_microseconds % 1_000_000) as u32;
171        let total_seconds = new_microseconds / 1_000_000;
172
173        let mut total_time =
174            self.hour as i64 * 3600 + self.minute as i64 * 60 + self.sec as i64 + total_seconds;
175
176        while total_time >= 86_400 {
177            total_time -= 86_400;
178        }
179        while total_time < 0 {
180            total_time += 86_400;
181        }
182
183        self.hour = (total_time / 3600) as u32;
184        self.minute = ((total_time % 3600) / 60) as u32;
185        self.sec = (total_time % 60) as u32;
186    }
187
188    /// Subtract interval
189    pub fn sub_interval(&mut self, interval: &IntervalValue) {
190        let neg_interval = interval.neg();
191        self.add_interval(&neg_interval);
192    }
193}
194
195impl TimeValue {
196    /// Estimate the memory usage of the time value
197    pub fn estimated_size(&self) -> usize {
198        std::mem::size_of::<Self>()
199    }
200}
201
202impl std::fmt::Display for TimeValue {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.sec)
205    }
206}
207
208/// Simple date and time representation
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash)]
210pub struct DateTimeValue {
211    pub year: i32,
212    pub month: u32,
213    pub day: u32,
214    pub hour: u32,
215    pub minute: u32,
216    pub sec: u32,
217    pub microsec: u32,
218}
219
220impl DateTimeValue {
221    /// Add interval
222    pub fn add_interval(&mut self, interval: &IntervalValue) {
223        let mut date = DateValue {
224            year: self.year,
225            month: self.month,
226            day: self.day,
227        };
228        date.add_interval(interval);
229
230        let mut time = TimeValue {
231            hour: self.hour,
232            minute: self.minute,
233            sec: self.sec,
234            microsec: self.microsec,
235        };
236        time.add_interval(interval);
237
238        self.year = date.year;
239        self.month = date.month;
240        self.day = date.day;
241        self.hour = time.hour;
242        self.minute = time.minute;
243        self.sec = time.sec;
244        self.microsec = time.microsec;
245    }
246
247    /// Subtract interval
248    pub fn sub_interval(&mut self, interval: &IntervalValue) {
249        let mut date = DateValue {
250            year: self.year,
251            month: self.month,
252            day: self.day,
253        };
254        date.sub_interval(interval);
255
256        let mut time = TimeValue {
257            hour: self.hour,
258            minute: self.minute,
259            sec: self.sec,
260            microsec: self.microsec,
261        };
262        time.sub_interval(interval);
263
264        self.year = date.year;
265        self.month = date.month;
266        self.day = date.day;
267        self.hour = time.hour;
268        self.minute = time.minute;
269        self.sec = time.sec;
270        self.microsec = time.microsec;
271    }
272}
273
274impl Default for DateTimeValue {
275    fn default() -> Self {
276        DateTimeValue {
277            year: 1970,
278            month: 1,
279            day: 1,
280            hour: 0,
281            minute: 0,
282            sec: 0,
283            microsec: 0,
284        }
285    }
286}
287
288impl DateTimeValue {
289    /// Estimate the memory usage of the datetime value
290    pub fn estimated_size(&self) -> usize {
291        std::mem::size_of::<Self>()
292    }
293
294    /// Convert to micros since the Unix epoch (1970-01-01T00:00:00.000000),
295    /// mirroring [`DateValue::to_days`] for the date part.
296    ///
297    /// For **normalized** fields the micros timeline is strictly monotonic in
298    /// the field-wise order used by `value_compare::cmp_datetime`; the typed
299    /// fast path relies on that (see `chunk/kind.rs` for the shared
300    /// limitation on non-normalized fields).
301    pub fn to_micros(&self) -> i64 {
302        let date = DateValue {
303            year: self.year,
304            month: self.month,
305            day: self.day,
306        };
307        // `DateValue::to_days` counts from JDN 0 (1970-01-01 is day
308        // 2_440_588), so the epoch offset is subtracted to obtain
309        // micros-since-epoch.
310        (date.to_days() - 2_440_588) * 86_400_000_000
311            + i64::from(self.hour) * 3_600_000_000
312            + i64::from(self.minute) * 60_000_000
313            + i64::from(self.sec) * 1_000_000
314            + i64::from(self.microsec)
315    }
316
317    /// Convert micros since the Unix epoch back into a date-time.
318    ///
319    /// Pre-epoch values (negative micros) are handled with `div_euclid` /
320    /// `rem_euclid`, so the result fields are always normalized.
321    pub fn from_micros(micros: i64) -> Self {
322        let days = micros.div_euclid(86_400_000_000) + 2_440_588;
323        let time_of_day = micros.rem_euclid(86_400_000_000);
324        let date = DateValue::from_days(days);
325        DateTimeValue {
326            year: date.year,
327            month: date.month,
328            day: date.day,
329            hour: (time_of_day / 3_600_000_000) as u32,
330            minute: ((time_of_day % 3_600_000_000) / 60_000_000) as u32,
331            sec: ((time_of_day % 60_000_000) / 1_000_000) as u32,
332            microsec: (time_of_day % 1_000_000) as u32,
333        }
334    }
335}
336
337impl std::fmt::Display for DateTimeValue {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        write!(
340            f,
341            "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
342            self.year, self.month, self.day, self.hour, self.minute, self.sec
343        )
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_to_micros_epoch_is_zero() {
353        assert_eq!(DateTimeValue::default().to_micros(), 0);
354    }
355
356    #[test]
357    fn test_to_micros_before_epoch_is_negative() {
358        let dt = DateTimeValue {
359            year: 1969,
360            month: 12,
361            day: 31,
362            hour: 23,
363            minute: 59,
364            sec: 59,
365            microsec: 999_999,
366        };
367        assert_eq!(dt.to_micros(), -1);
368    }
369
370    #[test]
371    fn test_micros_roundtrip() {
372        let dt = DateTimeValue {
373            year: 2024,
374            month: 2,
375            day: 29,
376            hour: 12,
377            minute: 34,
378            sec: 56,
379            microsec: 789_012,
380        };
381        assert_eq!(DateTimeValue::from_micros(dt.to_micros()), dt);
382        let pre_epoch = DateTimeValue {
383            year: 1900,
384            month: 6,
385            day: 15,
386            hour: 8,
387            minute: 30,
388            sec: 0,
389            microsec: 1,
390        };
391        assert_eq!(DateTimeValue::from_micros(pre_epoch.to_micros()), pre_epoch);
392    }
393
394    #[test]
395    fn test_micros_ordering_matches_field_order() {
396        let a = DateTimeValue {
397            year: 2023,
398            month: 12,
399            day: 31,
400            hour: 23,
401            minute: 59,
402            sec: 59,
403            microsec: 999_999,
404        };
405        let b = DateTimeValue {
406            year: 2024,
407            month: 1,
408            day: 1,
409            hour: 0,
410            minute: 0,
411            sec: 0,
412            microsec: 0,
413        };
414        assert!(a.to_micros() < b.to_micros());
415    }
416}