packedtime_rs/
kernels.rs

1use crate::{EpochDays, MILLIS_PER_DAY};
2
3#[inline]
4fn truncate_millis(ts: i64, truncate: i64) -> i64 {
5    ts / truncate * truncate
6}
7
8#[inline]
9fn truncate_millis_float(ts: f64, truncate: i64) -> f64 {
10    let truncate = truncate as f64;
11    (ts / truncate).floor() * truncate
12}
13
14#[inline]
15pub fn date_trunc_day_timestamp_millis(ts: i64) -> i64 {
16    truncate_millis(ts, MILLIS_PER_DAY)
17}
18
19#[inline]
20pub fn date_trunc_day_timestamp_millis_float(ts: f64) -> f64 {
21    truncate_millis_float(ts, MILLIS_PER_DAY)
22}
23
24#[inline]
25pub fn date_trunc_week_timestamp_millis(ts: i64) -> i64 {
26    // unix epoch starts on a thursday
27    let offset = 4 * MILLIS_PER_DAY;
28    truncate_millis(ts - offset, 7 * MILLIS_PER_DAY) + offset
29}
30
31#[inline]
32pub fn date_trunc_week_timestamp_millis_float(ts: f64) -> f64 {
33    let offset = (4 * MILLIS_PER_DAY) as f64;
34    truncate_millis_float(ts - offset, 7 * MILLIS_PER_DAY) + offset
35}
36
37#[inline]
38pub fn date_trunc_month_timestamp_millis(ts: i64) -> i64 {
39    let epoch_days = EpochDays::from_timestamp_millis(ts);
40    let truncated = epoch_days.date_trunc_month();
41    truncated.to_timestamp_millis()
42}
43
44#[inline]
45pub fn date_trunc_month_timestamp_millis_float(ts: f64) -> f64 {
46    let epoch_days = EpochDays::from_timestamp_millis_float(ts);
47    let truncated = epoch_days.date_trunc_month();
48    truncated.to_timestamp_millis_float()
49}
50
51#[inline]
52pub fn date_trunc_year_timestamp_millis(ts: i64) -> i64 {
53    let epoch_days = EpochDays::from_timestamp_millis(ts);
54    let truncated = epoch_days.date_trunc_year();
55    truncated.to_timestamp_millis()
56}
57
58#[inline]
59pub fn date_trunc_year_timestamp_millis_float(ts: f64) -> f64 {
60    let epoch_days = EpochDays::from_timestamp_millis_float(ts);
61    let truncated = epoch_days.date_trunc_year();
62    truncated.to_timestamp_millis_float()
63}
64
65#[inline]
66pub fn date_trunc_quarter_timestamp_millis(ts: i64) -> i64 {
67    let epoch_days = EpochDays::from_timestamp_millis(ts);
68    let truncated = epoch_days.date_trunc_quarter();
69    truncated.to_timestamp_millis()
70}
71
72#[inline]
73pub fn date_trunc_quarter_timestamp_millis_float(ts: f64) -> f64 {
74    let epoch_days = EpochDays::from_timestamp_millis_float(ts);
75    let truncated = epoch_days.date_trunc_quarter();
76    truncated.to_timestamp_millis_float()
77}
78
79#[inline]
80pub fn date_part_year_timestamp_millis(ts: i64) -> i32 {
81    let epoch_days = EpochDays::from_timestamp_millis(ts);
82    epoch_days.extract_year()
83}
84
85#[inline]
86pub fn date_part_month_timestamp_millis(ts: i64) -> i32 {
87    let epoch_days = EpochDays::from_timestamp_millis(ts);
88    epoch_days.extract_month()
89}
90
91#[inline]
92fn timestamp_to_epoch_days_and_remainder(ts: i64) -> (EpochDays, i64) {
93    let (days, millis) = (ts.div_euclid(MILLIS_PER_DAY), ts.rem_euclid(MILLIS_PER_DAY));
94    (EpochDays::new(days as i32), millis)
95}
96
97#[inline]
98fn timestamp_to_epoch_days_and_remainder_float(ts: f64) -> (EpochDays, f64) {
99    let days = (ts * (1.0 / MILLIS_PER_DAY as f64)).floor();
100    let millis = ts - days * (MILLIS_PER_DAY as f64);
101    (EpochDays::new(unsafe { days.to_int_unchecked() }), millis)
102}
103
104#[inline]
105pub fn date_add_month_timestamp_millis(ts: i64, months: i32) -> i64 {
106    let (epoch_days, millis) = timestamp_to_epoch_days_and_remainder(ts);
107    let new_epoch_days = epoch_days.add_months(months);
108    new_epoch_days.to_timestamp_millis() + millis
109}
110
111#[inline]
112pub fn date_add_month_timestamp_millis_float(ts: f64, months: i32) -> f64 {
113    let (epoch_days, millis) = timestamp_to_epoch_days_and_remainder_float(ts);
114    let new_epoch_days = epoch_days.add_months(months);
115    new_epoch_days.to_timestamp_millis_float() + millis
116}
117
118#[inline]
119fn timestamp_to_year_month_millis_of_month(ts: i64) -> (i32, i32, i64) {
120    let (ed, millis) = timestamp_to_epoch_days_and_remainder(ts);
121    let (year, month, day) = ed.to_ymd();
122    let millis_of_month = (day as i64) * MILLIS_PER_DAY + millis;
123    (year, month, millis_of_month)
124}
125
126#[inline]
127fn timestamp_to_year_month_millis_of_month_float(ts: f64) -> (i32, i32, f64) {
128    let (ed, millis) = timestamp_to_epoch_days_and_remainder_float(ts);
129    let (year, month, day) = ed.to_ymd();
130    let millis_of_month = (day as f64) * (MILLIS_PER_DAY as f64) + millis;
131    (year, month, millis_of_month)
132}
133
134#[inline]
135pub fn date_diff_month_timestamp_millis(t0: i64, t1: i64) -> i32 {
136    let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month(t0);
137    let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month(t1);
138    (y1 * 12 + m1) - (y0 * 12 + m0) - ((ms1 < ms0) as i32)
139}
140
141#[inline]
142pub fn date_diff_month_timestamp_millis_float(t0: f64, t1: f64) -> i32 {
143    let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month_float(t0);
144    let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month_float(t1);
145    (y1 * 12 + m1) - (y0 * 12 + m0) - ((ms1 < ms0) as i32)
146}
147
148#[inline]
149pub fn date_diff_year_timestamp_millis(t0: i64, t1: i64) -> i32 {
150    let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month(t0);
151    let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month(t1);
152    y1 - y0 - (((m1, ms1) < (m0, ms0)) as i32)
153}
154
155#[inline]
156pub fn date_diff_year_timestamp_millis_float(t0: f64, t1: f64) -> i32 {
157    let (y0, m0, ms0) = timestamp_to_year_month_millis_of_month_float(t0);
158    let (y1, m1, ms1) = timestamp_to_year_month_millis_of_month_float(t1);
159    y1 - y0 - (((m1, ms1) < (m0, ms0)) as i32)
160}
161
162#[inline]
163pub fn days_in_month_timestamp_millis(ts: i64) -> i32 {
164    let epoch_days = EpochDays::from_timestamp_millis(ts);
165    epoch_days.days_in_month()
166}
167
168#[inline]
169pub fn days_in_month_timestamp_millis_float(ts: f64) -> i32 {
170    let epoch_days = EpochDays::from_timestamp_millis_float(ts);
171    epoch_days.days_in_month()
172}
173
174#[cfg(test)]
175mod tests {
176    use crate::epoch_days::EpochDays;
177    use crate::{
178        date_add_month_timestamp_millis, date_diff_month_timestamp_millis, date_diff_year_timestamp_millis,
179        date_trunc_month_timestamp_millis, date_trunc_quarter_timestamp_millis, date_trunc_year_timestamp_millis,
180    };
181    use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime};
182    use std::ops::Add;
183
184    fn timestamp_to_naive_date_time(ts: i64) -> NaiveDateTime {
185        NaiveDateTime::from_timestamp(ts / 1000, 0).add(chrono::Duration::milliseconds(ts % 1000))
186    }
187
188    fn date_trunc_year_chrono(ts: i64) -> i64 {
189        let ndt = timestamp_to_naive_date_time(ts);
190        let truncated = NaiveDateTime::new(NaiveDate::from_ymd(ndt.year(), 1, 1), NaiveTime::from_hms(0, 0, 0));
191        truncated.timestamp_millis()
192    }
193
194    fn date_trunc_month_chrono(ts: i64) -> i64 {
195        let ndt = timestamp_to_naive_date_time(ts);
196        let truncated = NaiveDateTime::new(NaiveDate::from_ymd(ndt.year(), ndt.month(), 1), NaiveTime::from_hms(0, 0, 0));
197        truncated.timestamp_millis()
198    }
199
200    #[test]
201    fn test_date_trunc_year_millis() {
202        assert_eq!(1640995200_000, date_trunc_year_timestamp_millis(1640995200_000));
203        assert_eq!(1640995200_000, date_trunc_year_timestamp_millis(1658765238_000));
204    }
205
206    #[test]
207    fn test_date_trunc_quarter_millis() {
208        assert_eq!(1640995200_000, date_trunc_quarter_timestamp_millis(1640995200_000));
209        assert_eq!(1656633600_000, date_trunc_quarter_timestamp_millis(1658766592_000));
210    }
211
212    #[test]
213    fn test_date_trunc_month_millis() {
214        assert_eq!(1640995200_000, date_trunc_month_timestamp_millis(1640995200_000));
215        assert_eq!(1656633600_000, date_trunc_month_timestamp_millis(1658765238_000));
216    }
217
218    #[test]
219    fn test_date_add_months() {
220        let epoch_day = EpochDays::from_ymd(2022, 7, 31);
221        assert_eq!(epoch_day.add_months(1), EpochDays::from_ymd(2022, 8, 31));
222        assert_eq!(epoch_day.add_months(2), EpochDays::from_ymd(2022, 9, 30));
223        assert_eq!(epoch_day.add_months(3), EpochDays::from_ymd(2022, 10, 31));
224        assert_eq!(epoch_day.add_months(4), EpochDays::from_ymd(2022, 11, 30));
225        assert_eq!(epoch_day.add_months(5), EpochDays::from_ymd(2022, 12, 31));
226    }
227
228    #[test]
229    fn test_date_add_months_year_boundary() {
230        let epoch_day = EpochDays::from_ymd(2022, 7, 31);
231        assert_eq!(epoch_day.add_months(6), EpochDays::from_ymd(2023, 1, 31));
232        assert_eq!(epoch_day.add_months(7), EpochDays::from_ymd(2023, 2, 28));
233        assert_eq!(epoch_day.add_months(8), EpochDays::from_ymd(2023, 3, 31));
234    }
235
236    #[test]
237    fn test_date_add_months_leap_year() {
238        let epoch_day = EpochDays::from_ymd(2022, 7, 31);
239        assert_eq!(epoch_day.add_months(19), EpochDays::from_ymd(2024, 2, 29));
240
241        let epoch_day = EpochDays::from_ymd(2022, 2, 28);
242        assert_eq!(epoch_day.add_months(24), EpochDays::from_ymd(2024, 2, 28));
243
244        let epoch_day = EpochDays::from_ymd(2024, 2, 29);
245        assert_eq!(epoch_day.add_months(12), EpochDays::from_ymd(2025, 2, 28));
246    }
247
248    #[test]
249    fn test_date_add_months_negative() {
250        let epoch_day = EpochDays::from_ymd(2022, 7, 31);
251        assert_eq!(epoch_day.add_months(-1), EpochDays::from_ymd(2022, 6, 30));
252        assert_eq!(epoch_day.add_months(-2), EpochDays::from_ymd(2022, 5, 31));
253        assert_eq!(epoch_day.add_months(-3), EpochDays::from_ymd(2022, 4, 30));
254        assert_eq!(epoch_day.add_months(-4), EpochDays::from_ymd(2022, 3, 31));
255        assert_eq!(epoch_day.add_months(-5), EpochDays::from_ymd(2022, 2, 28));
256        assert_eq!(epoch_day.add_months(-6), EpochDays::from_ymd(2022, 1, 31));
257        assert_eq!(epoch_day.add_months(-7), EpochDays::from_ymd(2021, 12, 31));
258    }
259
260    #[test]
261    fn test_date_add_months_negative_year_boundary() {
262        let epoch_day = EpochDays::from_ymd(2022, 7, 31);
263        assert_eq!(epoch_day.add_months(-7), EpochDays::from_ymd(2021, 12, 31));
264    }
265
266    #[test]
267    fn test_date_add_months_timestamp_millis() {
268        assert_eq!(date_add_month_timestamp_millis(1661102969_000, 1), 1663781369000);
269        assert_eq!(date_add_month_timestamp_millis(1661102969_000, 12), 1692638969000);
270    }
271
272    #[test]
273    fn test_date_diff_months() {
274        assert_eq!(
275            date_diff_month_timestamp_millis(
276                EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
277                EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis()
278            ),
279            0
280        );
281        assert_eq!(
282            date_diff_month_timestamp_millis(
283                EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
284                EpochDays::from_ymd(2023, 11, 1).to_timestamp_millis()
285            ),
286            1
287        );
288        assert_eq!(
289            date_diff_month_timestamp_millis(
290                EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
291                EpochDays::from_ymd(2023, 11, 14).to_timestamp_millis()
292            ),
293            0
294        );
295        assert_eq!(
296            date_diff_month_timestamp_millis(
297                EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
298                EpochDays::from_ymd(2023, 11, 15).to_timestamp_millis()
299            ),
300            1
301        );
302        assert_eq!(
303            date_diff_month_timestamp_millis(
304                EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
305                EpochDays::from_ymd(2023, 11, 16).to_timestamp_millis()
306            ),
307            1
308        );
309    }
310
311    #[test]
312    fn test_date_diff_years() {
313        assert_eq!(
314            date_diff_year_timestamp_millis(
315                EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
316                EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis()
317            ),
318            0
319        );
320        assert_eq!(
321            date_diff_year_timestamp_millis(
322                EpochDays::from_ymd(2023, 10, 1).to_timestamp_millis(),
323                EpochDays::from_ymd(2023, 11, 1).to_timestamp_millis()
324            ),
325            0
326        );
327        assert_eq!(
328            date_diff_year_timestamp_millis(
329                EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
330                EpochDays::from_ymd(2024, 10, 14).to_timestamp_millis()
331            ),
332            0
333        );
334        assert_eq!(
335            date_diff_year_timestamp_millis(
336                EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
337                EpochDays::from_ymd(2024, 10, 15).to_timestamp_millis()
338            ),
339            1
340        );
341        assert_eq!(
342            date_diff_year_timestamp_millis(
343                EpochDays::from_ymd(2023, 10, 15).to_timestamp_millis(),
344                EpochDays::from_ymd(2024, 10, 16).to_timestamp_millis()
345            ),
346            1
347        );
348        assert_eq!(
349            date_diff_year_timestamp_millis(
350                EpochDays::from_ymd(2024, 2, 29).to_timestamp_millis(),
351                EpochDays::from_ymd(2025, 2, 28).to_timestamp_millis()
352            ),
353            0
354        );
355        assert_eq!(
356            date_diff_year_timestamp_millis(
357                EpochDays::from_ymd(2024, 2, 29).to_timestamp_millis(),
358                EpochDays::from_ymd(2025, 3, 1).to_timestamp_millis()
359            ),
360            1
361        );
362    }
363
364    #[test]
365    #[cfg_attr(any(miri, not(feature = "expensive_tests")), ignore)]
366    fn test_date_trunc_year_exhaustive() {
367        let start = chrono::NaiveDate::from_ymd(1700, 1, 1).and_hms(0, 0, 0).timestamp_millis();
368        let end = chrono::NaiveDate::from_ymd(2500, 1, 1).and_hms(0, 0, 0).timestamp_millis();
369
370        for ts in (start..end).step_by(60_000) {
371            let trunc_chrono = date_trunc_year_chrono(ts);
372            let trunc_packed = date_trunc_year_timestamp_millis(ts);
373            assert_eq!(trunc_chrono, trunc_packed, "{} != {} for {}", trunc_chrono, trunc_packed, ts);
374
375            let ts = ts + 59_999;
376            let trunc_chrono = date_trunc_year_chrono(ts);
377            let trunc_packed = date_trunc_year_timestamp_millis(ts);
378            assert_eq!(trunc_chrono, trunc_packed, "{} != {} for {}", trunc_chrono, trunc_packed, ts);
379        }
380    }
381
382    #[test]
383    #[cfg_attr(any(miri, not(feature = "expensive_tests")), ignore)]
384    fn test_date_trunc_month_exhaustive() {
385        let start = chrono::NaiveDate::from_ymd(1700, 1, 1).and_hms(0, 0, 0).timestamp_millis();
386        let end = chrono::NaiveDate::from_ymd(2500, 1, 1).and_hms(0, 0, 0).timestamp_millis();
387
388        for ts in (start..end).step_by(60_000) {
389            let trunc_chrono = date_trunc_month_chrono(ts);
390            let trunc_packed = date_trunc_month_timestamp_millis(ts);
391            assert_eq!(trunc_packed, trunc_chrono, "{} != {} for {}", trunc_packed, trunc_chrono, ts);
392
393            let ts = ts + 59_999;
394            let trunc_chrono = date_trunc_month_chrono(ts);
395            let trunc_packed = date_trunc_month_timestamp_millis(ts);
396            assert_eq!(trunc_chrono, trunc_packed, "{} != {} for {}", trunc_chrono, trunc_packed, ts);
397        }
398    }
399}