jiff/util/
common.rs

1/*!
2A collection of datetime related utility functions.
3
4These routines are written on Rust's native primitive integer types instead of
5Jiff's internal ranged integer types. In some sense, we should do the latter,
6but in practice there are two problems.
7
8Firstly, some of these routines need to be `const` so that we can use them in
9`const` constructors like `Date::constant`. But Jiff's ranged integers are
10nowhere near able to be `const`.
11
12Secondly, some of these routines are difficult to write using Jiff's
13ranged integers. Particularly the more complicated ones like the conversions
14between Unix epoch days and Gregorian calendar dates. I was able to do it,
15but when benchmarking, I noticed that the codegen was not as good as when
16normal primitive integers are used. This generally shouldn't happen, because
17Jiff's ranged integers are _supposed_ to compile away completely when debug
18assertions aren't enabled. Alas, it's just simpler to write it out this way.
19
20Note that these routines assume that their inputs are within Jiff's defined
21ranges. For example, `year` must be in the range `-9999..=9999`.
22*/
23
24use crate::util::t;
25
26/// Returns true if and only if the given year is a leap year.
27///
28/// A leap year is a year with 366 days. Typical years have 365 days.
29#[inline]
30pub(crate) const fn is_leap_year(year: i16) -> bool {
31    // From: https://github.com/BurntSushi/jiff/pull/23
32    let d = if year % 25 != 0 { 4 } else { 16 };
33    (year % d) == 0
34}
35
36/// Return the number of days in the given month.
37#[inline]
38pub(crate) const fn days_in_month(year: i16, month: i8) -> i8 {
39    // From: https://github.com/BurntSushi/jiff/pull/23
40    if month == 2 {
41        if is_leap_year(year) {
42            29
43        } else {
44            28
45        }
46    } else {
47        30 | (month ^ month >> 3)
48    }
49}
50
51/// Converts a Gregorian date to days since the Unix epoch.
52///
53/// This is Neri-Schneider. There's no branching or divisions.
54///
55/// Ref: https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L83
56#[inline(always)]
57#[allow(non_upper_case_globals, non_snake_case)]
58pub(crate) fn to_unix_epoch_day(year: i16, month: i8, day: i8) -> i32 {
59    const s: u32 = 82;
60    const K: u32 = 719468 + 146097 * s;
61    const L: u32 = 400 * s;
62
63    let year = year as u32;
64    let month = month as u32;
65    let day = day as u32;
66
67    let J = month <= 2;
68    let Y = year.wrapping_add(L).wrapping_sub(J as u32);
69    let M = if J { month + 12 } else { month };
70    let D = day - 1;
71    let C = Y / 100;
72
73    let y_star = 1461 * Y / 4 - C + C / 4;
74    let m_star = (979 * M - 2919) / 32;
75    let N = y_star + m_star + D;
76
77    let N_U = N.wrapping_sub(K);
78    N_U as i32
79}
80
81/// Converts days since the Unix epoch to a Gregorian date.
82///
83/// This is Neri-Schneider. There's no branching or divisions.
84///
85/// Ref: <https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L40C3-L40C34>
86#[inline(always)]
87#[allow(non_upper_case_globals, non_snake_case)]
88pub(crate) fn from_unix_epoch_day(days: i32) -> (i16, i8, i8) {
89    const s: u32 = 82;
90    const K: u32 = 719468 + 146097 * s;
91    const L: u32 = 400 * s;
92
93    let N_U = days as u32;
94    let N = N_U.wrapping_add(K);
95
96    let N_1 = 4 * N + 3;
97    let C = N_1 / 146097;
98    let N_C = (N_1 % 146097) / 4;
99
100    let N_2 = 4 * N_C + 3;
101    let P_2 = 2939745 * u64::from(N_2);
102    let Z = (P_2 / 4294967296) as u32;
103    let N_Y = (P_2 % 4294967296) as u32 / 2939745 / 4;
104    let Y = 100 * C + Z;
105
106    let N_3 = 2141 * N_Y + 197913;
107    let M = N_3 / 65536;
108    let D = (N_3 % 65536) / 2141;
109
110    let J = N_Y >= 306;
111    let year = Y.wrapping_sub(L).wrapping_add(J as u32) as i16;
112    let month = (if J { M - 12 } else { M }) as i8;
113    let day = (D + 1) as i8;
114    (year, month, day)
115}
116
117/// Converts `HH:MM:SS.nnnnnnnnn` to a nanosecond in a single civil day.
118#[inline(always)]
119pub(crate) fn to_day_nanosecond(
120    hour: i8,
121    minute: i8,
122    second: i8,
123    subsec: i32,
124) -> i64 {
125    let mut nanos: i64 = 0;
126    nanos += i64::from(hour) * t::NANOS_PER_HOUR.value();
127    nanos += i64::from(minute) * t::NANOS_PER_MINUTE.value();
128    nanos += i64::from(second) * t::NANOS_PER_SECOND.value();
129    nanos += i64::from(subsec);
130    nanos
131}
132
133/// Converts a nanosecond in a single civil day to `HH:MM::SS.nnnnnnnnn`.
134#[inline(always)]
135pub(crate) fn from_day_nanosecond(mut nanos: i64) -> (i8, i8, i8, i32) {
136    let (mut hour, mut minute, mut second, mut subsec) = (0, 0, 0, 0);
137    if nanos != 0 {
138        hour = (nanos / t::NANOS_PER_HOUR.value()) as i8;
139        nanos %= t::NANOS_PER_HOUR.value();
140        if nanos != 0 {
141            minute = (nanos / t::NANOS_PER_MINUTE.value()) as i8;
142            nanos %= t::NANOS_PER_MINUTE.value();
143            if nanos != 0 {
144                second = (nanos / t::NANOS_PER_SECOND.value()) as i8;
145                subsec = (nanos % t::NANOS_PER_SECOND.value()) as i32;
146            }
147        }
148    }
149    (hour, minute, second, subsec)
150}
151
152/// Converts a Unix timestamp with an offset to a Gregorian datetime.
153#[inline(always)]
154pub(crate) fn timestamp_to_datetime_zulu(
155    mut secs: i64,
156    mut subsec: i32,
157    offset: i32,
158) -> (i16, i8, i8, i8, i8, i8, i32) {
159    secs += i64::from(offset);
160    let mut days = secs.div_euclid(86_400) as i32;
161    secs = secs.rem_euclid(86_400);
162    if subsec < 0 {
163        if secs > 0 {
164            secs -= 1;
165            subsec += t::NANOS_PER_SECOND.value() as i32;
166        } else {
167            days -= 1;
168            secs += 86_399;
169            subsec += 1_000_000_000;
170        }
171    }
172
173    let (year, month, day) = from_unix_epoch_day(days);
174    let hour = (secs / t::SECONDS_PER_HOUR.value()) as i8;
175    secs %= t::SECONDS_PER_HOUR.value();
176    let minute = (secs / t::SECONDS_PER_MINUTE.value()) as i8;
177    let second = (secs % t::SECONDS_PER_MINUTE.value()) as i8;
178    (year, month, day, hour, minute, second, subsec)
179}
180
181/// Converts a Gregorian datetime and its offset to a Unix timestamp.
182#[inline(always)]
183pub(crate) fn datetime_zulu_to_timestamp(
184    year: i16,
185    month: i8,
186    day: i8,
187    hour: i8,
188    minute: i8,
189    second: i8,
190    mut subsec: i32,
191    offset: i32,
192) -> (i64, i32) {
193    let day = to_unix_epoch_day(year, month, day);
194    let mut secs = i64::from(day) * t::SECONDS_PER_CIVIL_DAY.value();
195    secs += i64::from(hour) * t::SECONDS_PER_HOUR.value();
196    secs += i64::from(minute) * t::SECONDS_PER_MINUTE.value();
197    secs += i64::from(second);
198    secs -= i64::from(offset);
199    if day < 0 && subsec != 0 {
200        secs += 1;
201        subsec -= 1_000_000_000;
202    }
203    (secs, subsec)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn t_is_leap_year() {
212        assert!(is_leap_year(2024));
213        assert!(!is_leap_year(2023));
214        assert!(!is_leap_year(2025));
215        assert!(is_leap_year(2000));
216        assert!(!is_leap_year(1900));
217        assert!(!is_leap_year(1800));
218        assert!(!is_leap_year(1700));
219        assert!(is_leap_year(1600));
220        assert!(is_leap_year(0));
221        assert!(!is_leap_year(-1));
222        assert!(!is_leap_year(-2));
223        assert!(!is_leap_year(-3));
224        assert!(is_leap_year(-4));
225        assert!(!is_leap_year(-100));
226        assert!(!is_leap_year(-200));
227        assert!(!is_leap_year(-300));
228        assert!(is_leap_year(400));
229        assert!(!is_leap_year(9999));
230        assert!(!is_leap_year(-9999));
231    }
232
233    #[test]
234    fn t_days_in_month() {
235        assert_eq!(28, days_in_month(-9999, 2));
236    }
237
238    // N.B. The other routines are generally covered by tests in
239    // `src/civil/date.rs`. But adding tests here too probably makes sense.
240}