packedtime_rs/
epoch_days.rs

1use crate::MILLIS_PER_DAY;
2
3// Conversions from/to number of days since the unix epoch.
4// Ported from <https://github.com/ThreeTen/threetenbp/blob/master/src/main/java/org/threeten/bp/LocalDate.java>
5// Original code has the following license:
6
7/*
8 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
9 *
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions are met:
14 *
15 *  * Redistributions of source code must retain the above copyright notice,
16 *    this list of conditions and the following disclaimer.
17 *
18 *  * Redistributions in binary form must reproduce the above copyright notice,
19 *    this list of conditions and the following disclaimer in the documentation
20 *    and/or other materials provided with the distribution.
21 *
22 *  * Neither the name of JSR-310 nor the names of its contributors
23 *    may be used to endorse or promote products derived from this software
24 *    without specific prior written permission.
25 *
26 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
30 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
31 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
32 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
33 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
34 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
35 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
36 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 */
38
39const SECONDS_PER_DAY: i32 = 86400;
40const DAYS_PER_CYCLE: i32 = 146097;
41const DAYS_0000_TO_1970: i32 = (DAYS_PER_CYCLE * 5) - (30 * 365 + 7);
42
43const SUPPORT_NEGATIVE_YEAR: bool = false;
44
45// stored as i32 instead of smaller types in order to access via vectorized gather instructions
46static DAYS_PER_MONTH: [[i32; 12]; 2] = [
47    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
48    [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
49];
50
51/// branchless calculation of whether the given year is a leap year
52#[inline]
53fn is_leap_year(year: i32) -> bool {
54    ((year % 4) == 0) & ((year % 100) != 0) | ((year % 400) == 0)
55}
56
57#[inline]
58fn days_per_month(year: i32, zero_based_month: i32) -> i32 {
59    let is_leap = is_leap_year(year);
60    let is_feb = zero_based_month == 1;
61    let mut days = 30 + ((zero_based_month % 2) != (zero_based_month <= 6) as i32) as i32;
62    days -= (2 - is_leap as i32) * (is_feb as i32);
63    days
64}
65
66/// A date represented as the number of days since the unix epoch 1970-01-01.
67#[derive(PartialEq, Eq, Clone, Copy, Debug)]
68pub struct EpochDays(i32);
69
70impl EpochDays {
71    #[inline]
72    pub fn new(epoch_days: i32) -> Self {
73        Self(epoch_days)
74    }
75
76    #[inline]
77    pub fn days(&self) -> i32 {
78        self.0
79    }
80
81    /// Convert a date to the number of days since the unix epoch 1970-01-01.
82    /// See https://github.com/ThreeTen/threetenbp/blob/master/src/main/java/org/threeten/bp/LocalDate.java#L1634
83    #[inline]
84    pub fn from_ymd(year: i32, month: i32, day: i32) -> Self {
85        let y = year;
86        let m = month;
87        let mut total = 365 * y;
88
89        total += if y < 0 && SUPPORT_NEGATIVE_YEAR {
90            -(y / -4 - y / -100 + y / -400)
91        } else {
92            (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400
93        };
94
95        total += ((367 * m - 362) / 12);
96        total += day - 1;
97
98        total -= 0_i32.wrapping_sub((m > 2) as i32) & (1 + (!is_leap_year(year) as i32));
99
100        Self(total - DAYS_0000_TO_1970)
101    }
102
103    /// Convert the number of days since the unix epoch into a (year, month, day) tuple.
104    /// See https://github.com/ThreeTen/threetenbp/blob/master/src/main/java/org/threeten/bp/LocalDate.java#L281
105    /// The resulting month and day values are 1-based.
106    #[inline]
107    pub fn to_ymd(&self) -> (i32, i32, i32) {
108        let epoch_days = self.0;
109        let mut zero_day = epoch_days + DAYS_0000_TO_1970;
110        // find the march-based year
111        zero_day -= 60; // adjust to 0000-03-01 so leap day is at end of four year cycle
112        let mut adjust = 0;
113        if zero_day < 0 && SUPPORT_NEGATIVE_YEAR {
114            // adjust negative years to positive for calculation
115            let adjust_cycles = (zero_day + 1) / DAYS_PER_CYCLE - 1;
116            adjust = adjust_cycles * 400;
117            zero_day += -adjust_cycles * DAYS_PER_CYCLE;
118        }
119        let mut year_est = (400 * zero_day + 591) / DAYS_PER_CYCLE;
120
121        if !SUPPORT_NEGATIVE_YEAR {
122            year_est &= i32::MAX;
123        }
124
125        let mut doy_est = zero_day - (365 * year_est + year_est / 4 - year_est / 100 + year_est / 400);
126
127        // fix estimate
128        year_est -= (doy_est < 0) as i32;
129        if !SUPPORT_NEGATIVE_YEAR {
130            year_est &= i32::MAX;
131        }
132
133        doy_est = zero_day - (365 * year_est + year_est / 4 - year_est / 100 + year_est / 400);
134
135        year_est += adjust; // reset any negative year
136        let march_doy0 = doy_est;
137
138        // convert march-based values back to january-based
139        let march_month0 = (march_doy0 * 5 + 2) / 153;
140        let month = (march_month0 + 2) % 12 + 1;
141        let dom = march_doy0 - (march_month0 * 306 + 5) / 10 + 1;
142        year_est += march_month0 / 10;
143
144        (year_est, month, dom)
145    }
146
147    #[inline]
148    pub fn from_timestamp_millis(ts: i64) -> Self {
149        // Converting to f64 is not necessarily faster but allows autovectorization if used in a loop
150        Self::from_timestamp_millis_float(ts as f64)
151    }
152
153    #[inline]
154    pub fn from_timestamp_millis_float(ts: f64) -> Self {
155        let epoch_days = (ts * (1.0 / MILLIS_PER_DAY as f64)).floor();
156        Self(unsafe { epoch_days.to_int_unchecked() })
157    }
158
159    #[inline]
160    pub fn to_timestamp_millis(&self) -> i64 {
161        (self.0 as i64) * MILLIS_PER_DAY
162    }
163
164    #[inline]
165    pub fn to_timestamp_millis_float(&self) -> f64 {
166        (self.0 as f64) * (MILLIS_PER_DAY as f64)
167    }
168
169    /// Adds the given number of `months` to `epoch_days`.
170    /// If the day would be out of range for the resulting month
171    /// then the date will be clamped to the end of the month.
172    ///
173    /// For example: 2022-01-31 + 1month => 2022-02-28
174    #[inline]
175    pub fn add_months(&self, months: i32) -> Self {
176        let (mut y, mut m, mut d) = self.to_ymd();
177        let mut m0 = m - 1;
178        m0 += months;
179        y += m0.div_euclid(12);
180        m0 = m0.rem_euclid(12);
181        d = d.min(days_per_month(y, m0));
182        m = m0 + 1;
183        Self::from_ymd(y, m, d)
184    }
185
186    /// Adds the given number of `years` to `epoch_days`.
187    /// If the day would be out of range for the resulting month
188    /// then the date will be clamped to the end of the month.
189    ///
190    /// For example: 2020-02-29 + 1year => 2021-02-28
191    #[inline]
192    pub fn add_years(&self, years: i32) -> Self {
193        let (mut y, m, mut d) = self.to_ymd();
194        y += years;
195        d = d.min(days_per_month(y, m - 1));
196        Self::from_ymd(y, m, d)
197    }
198
199    #[inline]
200    pub fn diff_months(&self, other: EpochDays) -> i32 {
201        let (y0, m0, d0) = self.to_ymd();
202        let (y1, m1, d1) = other.to_ymd();
203
204        // TODO: Special-case the end of month? IOW, should diff(2023-10-31, 2023-11-30) return 1?
205        (y1 * 12 + m1) - (y0 * 12 + m0) - (d1 < d0) as i32
206    }
207
208    #[inline]
209    pub fn diff_years(&self, other: EpochDays) -> i32 {
210        let (y0, m0, d0) = self.to_ymd();
211        let (y1, m1, d1) = other.to_ymd();
212
213        // y1 - y0 - ((m1 < m0) | ((m1 == m0) & (d1 < d0))) as i32
214        y1 - y0 - ((m1, d1) < (m0, d0)) as i32
215    }
216
217    #[inline]
218    pub fn date_trunc_month(&self) -> Self {
219        let (y, m, d) = self.to_ymd();
220        Self::from_ymd(y, m, 1)
221    }
222
223    #[inline]
224    pub fn date_trunc_year(&self) -> Self {
225        let (y, m, d) = self.to_ymd();
226        Self::from_ymd(y, 1, 1)
227    }
228
229    #[inline]
230    pub fn date_trunc_quarter(&self) -> Self {
231        let (y, m, d) = self.to_ymd();
232        Self::from_ymd(y, (m - 1) / 3 * 3 + 1, 1)
233    }
234
235    #[inline]
236    pub fn extract_year(&self) -> i32 {
237        self.to_ymd().0
238    }
239
240    #[inline]
241    pub fn extract_month(&self) -> i32 {
242        self.to_ymd().1
243    }
244
245    #[inline]
246    pub fn extract_quarter(&self) -> i32 {
247        (self.to_ymd().1 - 1) / 3 + 1
248    }
249
250    #[inline]
251    pub fn extract_day_of_month(&self) -> i32 {
252        self.to_ymd().2
253    }
254
255    #[inline]
256    pub fn days_in_month(&self) -> i32 {
257        let (y, m, _) = self.to_ymd();
258        days_per_month(y, m)
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use crate::epoch_days::{days_per_month, is_leap_year, DAYS_PER_MONTH};
265    use crate::EpochDays;
266
267    #[test]
268    fn test_is_leap_year() {
269        assert!(!is_leap_year(1900));
270        assert!(!is_leap_year(1999));
271
272        assert!(is_leap_year(2000));
273
274        assert!(!is_leap_year(2001));
275        assert!(!is_leap_year(2002));
276        assert!(!is_leap_year(2003));
277
278        assert!(is_leap_year(2004));
279        assert!(is_leap_year(2020));
280    }
281
282    #[test]
283    fn test_days_per_month() {
284        for i in 0..12 {
285            assert_eq!(days_per_month(2023, i as i32), DAYS_PER_MONTH[0][i], "non-leap: {i}");
286        }
287        for i in 0..12 {
288            assert_eq!(days_per_month(1900, i as i32), DAYS_PER_MONTH[0][i], "non-leap (%100): {i}");
289        }
290        for i in 0..12 {
291            assert_eq!(days_per_month(2020, i as i32), DAYS_PER_MONTH[1][i], "leap: {i}");
292        }
293        for i in 0..12 {
294            assert_eq!(days_per_month(2000, i as i32), DAYS_PER_MONTH[1][i], "leap (%400): {i}");
295        }
296    }
297
298    #[test]
299    fn test_to_epoch_day() {
300        assert_eq!(0, EpochDays::from_ymd(1970, 1, 1).0);
301        assert_eq!(1, EpochDays::from_ymd(1970, 1, 2).0);
302        assert_eq!(365, EpochDays::from_ymd(1971, 1, 1).0);
303        assert_eq!(365 * 2, EpochDays::from_ymd(1972, 1, 1).0);
304        assert_eq!(365 * 2 + 366, EpochDays::from_ymd(1973, 1, 1).0);
305
306        assert_eq!(18998, EpochDays::from_ymd(2022, 1, 6).0);
307        assert_eq!(19198, EpochDays::from_ymd(2022, 7, 25).0);
308    }
309
310    #[test]
311    fn test_date_trunc_year_epoch_days() {
312        assert_eq!(18993, EpochDays::new(19198).date_trunc_year().days());
313    }
314
315    #[test]
316    fn test_date_trunc_month_epoch_days() {
317        assert_eq!(19174, EpochDays::new(19198).date_trunc_month().days());
318    }
319
320    #[test]
321    fn test_date_diff_month_epoch_days() {
322        assert_eq!(
323            EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 11, 1)),
324            1
325        );
326        assert_eq!(
327            EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 12, 1)),
328            2
329        );
330        assert_eq!(
331            EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 12, 31)),
332            2
333        );
334        assert_eq!(
335            EpochDays::from_ymd(2023, 10, 22).diff_months(EpochDays::from_ymd(2023, 11, 22)),
336            1
337        );
338        assert_eq!(
339            EpochDays::from_ymd(2023, 10, 22).diff_months(EpochDays::from_ymd(2023, 11, 21)),
340            0
341        );
342        assert_eq!(
343            EpochDays::from_ymd(2023, 10, 31).diff_months(EpochDays::from_ymd(2023, 11, 30)),
344            0
345        );
346    }
347
348    #[test]
349    fn test_date_diff_month_epoch_days_negative() {
350        assert_eq!(
351            EpochDays::from_ymd(2023, 11, 1).diff_months(EpochDays::from_ymd(2023, 10, 1)),
352            -1
353        );
354    }
355
356    #[test]
357    fn test_date_diff_year_epoch_days() {
358        assert_eq!(
359            EpochDays::from_ymd(2023, 10, 1).diff_years(EpochDays::from_ymd(2023, 10, 1)),
360            0
361        );
362        assert_eq!(
363            EpochDays::from_ymd(2023, 10, 1).diff_years(EpochDays::from_ymd(2023, 11, 1)),
364            0
365        );
366        assert_eq!(
367            EpochDays::from_ymd(2023, 1, 1).diff_years(EpochDays::from_ymd(2024, 1, 1)),
368            1
369        );
370        assert_eq!(
371            EpochDays::from_ymd(2023, 2, 28).diff_years(EpochDays::from_ymd(2024, 2, 28)),
372            1
373        );
374        assert_eq!(
375            EpochDays::from_ymd(2023, 2, 28).diff_years(EpochDays::from_ymd(2024, 2, 29)),
376            1
377        );
378        assert_eq!(
379            EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2024, 6, 14)),
380            0
381        );
382        assert_eq!(
383            EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2025, 6, 14)),
384            1
385        );
386        assert_eq!(
387            EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2025, 6, 16)),
388            2
389        );
390    }
391
392    #[test]
393    fn test_extract_year() {
394        assert_eq!(2022, EpochDays::from_ymd(2022, 1, 1).extract_year());
395        assert_eq!(2022, EpochDays::from_ymd(2022, 8, 24).extract_year());
396        assert_eq!(2022, EpochDays::from_ymd(2022, 12, 31).extract_year());
397    }
398
399    #[test]
400    fn test_extract_month() {
401        assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_month());
402        assert_eq!(2, EpochDays::from_ymd(2000, 2, 1).extract_month());
403        assert_eq!(2, EpochDays::from_ymd(2000, 2, 29).extract_month());
404        assert_eq!(1, EpochDays::from_ymd(2022, 1, 1).extract_month());
405        assert_eq!(8, EpochDays::from_ymd(2022, 8, 24).extract_month());
406        assert_eq!(12, EpochDays::from_ymd(2022, 12, 31).extract_month());
407    }
408
409    #[test]
410    fn test_extract_day() {
411        assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_day_of_month());
412        assert_eq!(1, EpochDays::from_ymd(2000, 2, 1).extract_day_of_month());
413        assert_eq!(29, EpochDays::from_ymd(2000, 2, 29).extract_day_of_month());
414        assert_eq!(1, EpochDays::from_ymd(2000, 3, 1).extract_day_of_month());
415    }
416
417    #[test]
418    fn test_extract_quarter() {
419        assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_quarter());
420        assert_eq!(1, EpochDays::from_ymd(2000, 2, 1).extract_quarter());
421        assert_eq!(1, EpochDays::from_ymd(2000, 3, 31).extract_quarter());
422        assert_eq!(2, EpochDays::from_ymd(2000, 4, 1).extract_quarter());
423        assert_eq!(3, EpochDays::from_ymd(2000, 7, 1).extract_quarter());
424        assert_eq!(4, EpochDays::from_ymd(2000, 10, 1).extract_quarter());
425        assert_eq!(4, EpochDays::from_ymd(2000, 12, 31).extract_quarter());
426    }
427}