Skip to main content

jiff/shared/util/
itime.rs

1/*!
2This module defines the internal core time data types.
3
4This includes physical time (i.e., a timestamp) and civil time.
5
6These types exist to provide a home for the core algorithms in a datetime
7crate. For example, converting from a timestamp to a Gregorian calendar date
8and clock time.
9
10These routines are specifically implemented on simple primitive integer types
11and implicitly assume that the inputs are valid (i.e., within Jiff's minimum
12and maximum ranges).
13
14These exist to provide `const` capabilities, and also to provide a small
15reusable core of important algorithms that can be shared between `jiff` and
16`jiff-static`.
17
18# Naming
19
20The types in this module are prefixed with letter `I` to make it clear that
21they are internal types. Specifically, to distinguish them from Jiff's public
22types. For example, `Date` versus `IDate`.
23*/
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
26pub(crate) struct ITimestamp {
27    pub(crate) second: i64,
28    pub(crate) nanosecond: i32,
29}
30
31impl ITimestamp {
32    const MIN: ITimestamp =
33        ITimestamp { second: -377705023201, nanosecond: 0 };
34    const MAX: ITimestamp =
35        ITimestamp { second: 253402207200, nanosecond: 999_999_999 };
36
37    /// Creates an `ITimestamp` from a Unix timestamp in seconds.
38    #[inline]
39    pub(crate) const fn from_second(second: i64) -> ITimestamp {
40        ITimestamp { second, nanosecond: 0 }
41    }
42
43    /// Converts a Unix timestamp with an offset to a Gregorian datetime.
44    ///
45    /// The offset should correspond to the number of seconds required to
46    /// add to this timestamp to get the local time.
47    #[cfg_attr(feature = "perf-inline", inline(always))]
48    pub(crate) const fn to_datetime(&self, offset: IOffset) -> IDateTime {
49        let ITimestamp { mut second, mut nanosecond } = *self;
50
51        // Shift second comfortably into the postive domain
52        // so that division and remainder can use unsigned math
53        // which is much faster.
54        // 30 * 400 years: 12,000 yr range > [-9,999..1970]
55        // (146097 being the number of days per 400 years).
56        const DAY_SHIFT: i32 = 30 * 146097;
57        const SEC_SHIFT: i64 = (DAY_SHIFT as i64) * 86_400;
58
59        let pos_sec = (second + (offset.second as i64) + SEC_SHIFT) as u64;
60        let mut epoch_day = (pos_sec / 86_400) as i32;
61        second = (pos_sec % 86_400) as i64;
62
63        if nanosecond < 0 {
64            if second > 0 {
65                second -= 1;
66                nanosecond += 1_000_000_000;
67            } else {
68                epoch_day -= 1;
69                second += 86_399;
70                nanosecond += 1_000_000_000;
71            }
72        }
73
74        epoch_day -= DAY_SHIFT;
75
76        let date = IEpochDay { epoch_day }.to_date();
77        let mut time = ITimeSecond { second: second as i32 }.to_time();
78        time.subsec_nanosecond = nanosecond;
79        IDateTime { date, time }
80    }
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
84pub(crate) struct IOffset {
85    pub(crate) second: i32,
86}
87
88impl IOffset {
89    pub(crate) const UTC: IOffset = IOffset { second: 0 };
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
93pub(crate) struct IDateTime {
94    pub(crate) date: IDate,
95    pub(crate) time: ITime,
96}
97
98impl IDateTime {
99    const MIN: IDateTime = IDateTime { date: IDate::MIN, time: ITime::MIN };
100    const MAX: IDateTime = IDateTime { date: IDate::MAX, time: ITime::MAX };
101
102    /// Converts a Gregorian datetime and its offset to a Unix timestamp.
103    ///
104    /// The offset should correspond to the number of seconds required to
105    /// subtract from this datetime in order to get to UTC.
106    #[cfg_attr(feature = "perf-inline", inline(always))]
107    pub(crate) fn to_timestamp(&self, offset: IOffset) -> ITimestamp {
108        let epoch_day = self.date.to_epoch_day().epoch_day;
109        let mut second = (epoch_day as i64) * 86_400
110            + (self.time.to_second().second as i64);
111        let mut nanosecond = self.time.subsec_nanosecond;
112        second -= offset.second as i64;
113        if epoch_day < 0 && nanosecond != 0 {
114            second += 1;
115            nanosecond -= 1_000_000_000;
116        }
117        ITimestamp { second, nanosecond }
118    }
119
120    /// Converts a Gregorian datetime and its offset to a Unix timestamp.
121    ///
122    /// If the timestamp would overflow Jiff's timestamp range, then this
123    /// returns `None`.
124    ///
125    /// The offset should correspond to the number of seconds required to
126    /// subtract from this datetime in order to get to UTC.
127    #[cfg_attr(feature = "perf-inline", inline(always))]
128    pub(crate) fn to_timestamp_checked(
129        &self,
130        offset: IOffset,
131    ) -> Option<ITimestamp> {
132        let ts = self.to_timestamp(offset);
133        if !(ITimestamp::MIN <= ts && ts <= ITimestamp::MAX) {
134            return None;
135        }
136        Some(ts)
137    }
138
139    #[cfg_attr(feature = "perf-inline", inline(always))]
140    pub(crate) fn saturating_add_seconds(&self, seconds: i32) -> IDateTime {
141        self.checked_add_seconds(seconds).unwrap_or_else(|_| {
142            if seconds < 0 {
143                IDateTime::MIN
144            } else {
145                IDateTime::MAX
146            }
147        })
148    }
149
150    #[cfg_attr(feature = "perf-inline", inline(always))]
151    pub(crate) fn checked_add_seconds(
152        &self,
153        seconds: i32,
154    ) -> Result<IDateTime, RangeError> {
155        let day_second = self
156            .time
157            .to_second()
158            .second
159            .checked_add(seconds)
160            .ok_or_else(|| RangeError::DateTimeSeconds)?;
161        let days = day_second.div_euclid(86400);
162        let second = day_second.rem_euclid(86400);
163        let date = self.date.checked_add_days(days)?;
164        let time = ITimeSecond { second }.to_time();
165        Ok(IDateTime { date, time })
166    }
167}
168
169#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
170pub(crate) struct IEpochDay {
171    pub(crate) epoch_day: i32,
172}
173
174impl IEpochDay {
175    pub(crate) const MIN: IEpochDay = IEpochDay { epoch_day: -4371587 };
176    pub(crate) const MAX: IEpochDay = IEpochDay { epoch_day: 2932896 };
177
178    /// Converts days since the Unix epoch to a Gregorian date.
179    ///
180    /// This is Neri-Schneider. There's no branching or divisions.
181    ///
182    /// Ref: <https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L40C3-L40C34>
183    #[cfg_attr(feature = "perf-inline", inline(always))]
184    #[allow(non_upper_case_globals, non_snake_case)] // to mimic source
185    pub(crate) const fn to_date(&self) -> IDate {
186        const s: u32 = 82;
187        const K: u32 = 719468 + 146097 * s;
188        const L: u32 = 400 * s;
189
190        let N_U = self.epoch_day as u32;
191        let N = N_U.wrapping_add(K);
192
193        let N_1 = 4 * N + 3;
194        let C = N_1 / 146097;
195        let N_C = (N_1 % 146097) / 4;
196
197        let N_2 = 4 * N_C + 3;
198        let P_2 = 2939745 * (N_2 as u64);
199        let Z = (P_2 / 4294967296) as u32;
200        let N_Y = (P_2 % 4294967296) as u32 / 2939745 / 4;
201        let Y = 100 * C + Z;
202
203        let N_3 = 2141 * N_Y + 197913;
204        let M = N_3 / 65536;
205        let D = (N_3 % 65536) / 2141;
206
207        let J = N_Y >= 306;
208        let year = Y.wrapping_sub(L).wrapping_add(J as u32) as i16;
209        let month = (if J { M - 12 } else { M }) as i8;
210        let day = (D + 1) as i8;
211        IDate { year, month, day }
212    }
213
214    /// Returns the day of the week for this epoch day.
215    #[cfg_attr(feature = "perf-inline", inline(always))]
216    pub(crate) const fn weekday(&self) -> IWeekday {
217        // Based on Hinnant's approach here, although we use ISO weekday
218        // numbering by default. Basically, this works by using the knowledge
219        // that 1970-01-01 was a Thursday.
220        //
221        // Ref: http://howardhinnant.github.io/date_algorithms.html
222        IWeekday::from_monday_zero_offset(
223            (self.epoch_day + 3).rem_euclid(7) as i8
224        )
225    }
226
227    /// Add the given number of days to this epoch day.
228    ///
229    /// If this would overflow an `i32` or result in an out-of-bounds epoch
230    /// day, then this returns an error.
231    #[inline]
232    pub(crate) fn checked_add(
233        &self,
234        amount: i32,
235    ) -> Result<IEpochDay, RangeError> {
236        let epoch_day = self.epoch_day;
237        let sum = epoch_day
238            .checked_add(amount)
239            .ok_or_else(|| RangeError::EpochDayI32)?;
240        let ret = IEpochDay { epoch_day: sum };
241        if !(IEpochDay::MIN <= ret && ret <= IEpochDay::MAX) {
242            return Err(RangeError::EpochDayDays);
243        }
244        Ok(ret)
245    }
246}
247
248#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
249pub(crate) struct IDate {
250    pub(crate) year: i16,
251    pub(crate) month: i8,
252    pub(crate) day: i8,
253}
254
255impl IDate {
256    const MIN: IDate = IDate { year: -9999, month: 1, day: 1 };
257    const MAX: IDate = IDate { year: 9999, month: 12, day: 31 };
258
259    /// Fallibly builds a new date.
260    ///
261    /// This checks that the given day is valid for the given year/month.
262    ///
263    /// No other conditions are checked. This assumes `year` and `month` are
264    /// valid, and that `day >= 1`.
265    #[inline]
266    pub(crate) fn try_new(
267        year: i16,
268        month: i8,
269        day: i8,
270    ) -> Result<IDate, RangeError> {
271        if day > 28 {
272            let max_day = days_in_month(year, month);
273            if day > max_day {
274                return Err(RangeError::DateInvalidDays { year, month });
275            }
276        }
277        Ok(IDate { year, month, day })
278    }
279
280    /// Returns the date corresponding to the day of the given year. The day
281    /// of the year should be a value in `1..=366`, with `366` only being valid
282    /// if `year` is a leap year.
283    ///
284    /// This assumes that `year` is valid, but returns an error if `day` is
285    /// not in the range `1..=366`.
286    #[inline]
287    pub(crate) fn from_day_of_year(
288        year: i16,
289        day: i16,
290    ) -> Result<IDate, RangeError> {
291        if !(1 <= day && day <= 366) {
292            return Err(RangeError::DateInvalidDayOfYear { year });
293        }
294        let start = IDate { year, month: 1, day: 1 }.to_epoch_day();
295        let end = start
296            .checked_add(i32::from(day) - 1)
297            // This can only happen when `year=9999` and `day=366`.
298            .map_err(|_| RangeError::DayOfYear)?
299            .to_date();
300        // If we overflowed into the next year, then `day` is too big.
301        if year != end.year {
302            // Can only happen given day=366 and this is a leap year.
303            debug_assert_eq!(day, 366);
304            debug_assert!(!is_leap_year(year));
305            return Err(RangeError::DateInvalidDayOfYear { year });
306        }
307        Ok(end)
308    }
309
310    /// Returns the date corresponding to the day of the given year. The day
311    /// of the year should be a value in `1..=365`, with February 29 being
312    /// completely ignored. That is, it is guaranteed that February 29 will
313    /// never be returned by this function. It is impossible.
314    ///
315    /// This assumes that `year` is valid, but returns an error if `day` is
316    /// not in the range `1..=365`.
317    #[inline]
318    pub(crate) fn from_day_of_year_no_leap(
319        year: i16,
320        mut day: i16,
321    ) -> Result<IDate, RangeError> {
322        if !(1 <= day && day <= 365) {
323            return Err(RangeError::DateInvalidDayOfYearNoLeap);
324        }
325        if day >= 60 && is_leap_year(year) {
326            day += 1;
327        }
328        // The boundary check above guarantees this always succeeds.
329        Ok(IDate::from_day_of_year(year, day).unwrap())
330    }
331
332    /// Converts a Gregorian date to days since the Unix epoch.
333    ///
334    /// This is Neri-Schneider. There's no branching or divisions.
335    ///
336    /// Ref: https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L83
337    #[cfg_attr(feature = "perf-inline", inline(always))]
338    #[allow(non_upper_case_globals, non_snake_case)] // to mimic source
339    pub(crate) const fn to_epoch_day(&self) -> IEpochDay {
340        const s: u32 = 82;
341        const K: u32 = 719468 + 146097 * s;
342        const L: u32 = 400 * s;
343
344        let year = self.year as u32;
345        let month = self.month as u32;
346        let day = self.day as u32;
347
348        let J = month <= 2;
349        let Y = year.wrapping_add(L).wrapping_sub(J as u32);
350        let M = if J { month + 12 } else { month };
351        let D = day - 1;
352        let C = Y / 100;
353
354        let y_star = 1461 * Y / 4 - C + C / 4;
355        let m_star = (979 * M - 2919) / 32;
356        let N = y_star + m_star + D;
357
358        let N_U = N.wrapping_sub(K);
359        let epoch_day = N_U as i32;
360        IEpochDay { epoch_day }
361    }
362
363    /// Returns the day of the week for this date.
364    #[inline]
365    pub(crate) const fn weekday(&self) -> IWeekday {
366        self.to_epoch_day().weekday()
367    }
368
369    /// Returns the `nth` weekday of the month represented by this date.
370    ///
371    /// `nth` must be non-zero and otherwise in the range `-5..=5`. If it
372    /// isn't, an error is returned.
373    ///
374    /// This also returns an error if `abs(nth)==5` and there is no "5th"
375    /// weekday of this month.
376    #[inline]
377    pub(crate) fn nth_weekday_of_month(
378        &self,
379        nth: i8,
380        weekday: IWeekday,
381    ) -> Result<IDate, RangeError> {
382        if nth == 0 || !(-5 <= nth && nth <= 5) {
383            return Err(RangeError::NthWeekdayOfMonth);
384        }
385        if nth > 0 {
386            let first_weekday = self.first_of_month().weekday();
387            let diff = weekday.since(first_weekday);
388            let day = diff + 1 + (nth - 1) * 7;
389            IDate::try_new(self.year, self.month, day)
390        } else {
391            let last = self.last_of_month();
392            let last_weekday = last.weekday();
393            let diff = last_weekday.since(weekday);
394            let day = last.day - diff - (nth.abs() - 1) * 7;
395            // Our math can go below 1 when nth is -5 and there is no "5th from
396            // last" weekday in this month. Since this is outside the bounds
397            // of `Day`, we can't let this boundary condition escape. So we
398            // check it here.
399            if day < 1 {
400                return Err(RangeError::DateInvalidDays {
401                    year: self.year,
402                    month: self.month,
403                });
404            }
405            IDate::try_new(self.year, self.month, day)
406        }
407    }
408
409    /// Returns the day before this date.
410    #[inline]
411    pub(crate) fn yesterday(self) -> Result<IDate, RangeError> {
412        if self.day == 1 {
413            if self.month == 1 {
414                let year = self.year - 1;
415                if year <= -10000 {
416                    return Err(RangeError::Yesterday);
417                }
418                return Ok(IDate { year, month: 12, day: 31 });
419            }
420            let month = self.month - 1;
421            let day = days_in_month(self.year, month);
422            return Ok(IDate { month, day, ..self });
423        }
424        Ok(IDate { day: self.day - 1, ..self })
425    }
426
427    /// Returns the day after this date.
428    #[inline]
429    pub(crate) fn tomorrow(self) -> Result<IDate, RangeError> {
430        if self.day >= 28 && self.day == days_in_month(self.year, self.month) {
431            if self.month == 12 {
432                let year = self.year + 1;
433                if year >= 10000 {
434                    return Err(RangeError::Tomorrow);
435                }
436                return Ok(IDate { year, month: 1, day: 1 });
437            }
438            let month = self.month + 1;
439            return Ok(IDate { month, day: 1, ..self });
440        }
441        Ok(IDate { day: self.day + 1, ..self })
442    }
443
444    /// Returns the year one year before this date.
445    #[inline]
446    pub(crate) fn prev_year(self) -> Result<i16, RangeError> {
447        let year = self.year - 1;
448        if year <= -10_000 {
449            return Err(RangeError::YearPrevious);
450        }
451        Ok(year)
452    }
453
454    /// Returns the year one year from this date.
455    #[inline]
456    pub(crate) fn next_year(self) -> Result<i16, RangeError> {
457        let year = self.year + 1;
458        if year >= 10_000 {
459            return Err(RangeError::YearNext);
460        }
461        Ok(year)
462    }
463
464    /// Add the number of days to this date.
465    #[inline]
466    pub(crate) fn checked_add_days(
467        &self,
468        amount: i32,
469    ) -> Result<IDate, RangeError> {
470        match amount {
471            0 => Ok(*self),
472            -1 => self.yesterday(),
473            1 => self.tomorrow(),
474            n => self.to_epoch_day().checked_add(n).map(|d| d.to_date()),
475        }
476    }
477
478    #[inline]
479    fn first_of_month(&self) -> IDate {
480        IDate { day: 1, ..*self }
481    }
482
483    #[inline]
484    fn last_of_month(&self) -> IDate {
485        IDate { day: days_in_month(self.year, self.month), ..*self }
486    }
487
488    #[cfg(test)]
489    pub(crate) fn at(
490        &self,
491        hour: i8,
492        minute: i8,
493        second: i8,
494        subsec_nanosecond: i32,
495    ) -> IDateTime {
496        let time = ITime { hour, minute, second, subsec_nanosecond };
497        IDateTime { date: *self, time }
498    }
499}
500
501/// Represents a clock time.
502///
503/// This uses units of hours, minutes, seconds and fractional seconds (to
504/// nanosecond precision).
505#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
506pub(crate) struct ITime {
507    pub(crate) hour: i8,
508    pub(crate) minute: i8,
509    pub(crate) second: i8,
510    pub(crate) subsec_nanosecond: i32,
511}
512
513impl ITime {
514    pub(crate) const ZERO: ITime =
515        ITime { hour: 0, minute: 0, second: 0, subsec_nanosecond: 0 };
516    pub(crate) const MIN: ITime =
517        ITime { hour: 0, minute: 0, second: 0, subsec_nanosecond: 0 };
518    pub(crate) const MAX: ITime = ITime {
519        hour: 23,
520        minute: 59,
521        second: 59,
522        subsec_nanosecond: 999_999_999,
523    };
524
525    #[cfg_attr(feature = "perf-inline", inline(always))]
526    pub(crate) const fn to_second(&self) -> ITimeSecond {
527        let mut second: i32 = 0;
528        second += (self.hour as i32) * 3600;
529        second += (self.minute as i32) * 60;
530        second += self.second as i32;
531        ITimeSecond { second }
532    }
533
534    #[cfg_attr(feature = "perf-inline", inline(always))]
535    pub(crate) const fn to_nanosecond(&self) -> ITimeNanosecond {
536        let mut nanosecond: i64 = 0;
537        nanosecond += (self.hour as i64) * 3_600_000_000_000;
538        nanosecond += (self.minute as i64) * 60_000_000_000;
539        nanosecond += (self.second as i64) * 1_000_000_000;
540        nanosecond += self.subsec_nanosecond as i64;
541        ITimeNanosecond { nanosecond }
542    }
543}
544
545/// Represents a single point in the day, to second precision.
546#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
547pub(crate) struct ITimeSecond {
548    pub(crate) second: i32,
549}
550
551impl ITimeSecond {
552    #[cfg_attr(feature = "perf-inline", inline(always))]
553    pub(crate) const fn to_time(&self) -> ITime {
554        let mut second = self.second;
555        let mut time = ITime::ZERO;
556        if second != 0 {
557            time.hour = (second / 3600) as i8;
558            second %= 3600;
559            if second != 0 {
560                time.minute = (second / 60) as i8;
561                time.second = (second % 60) as i8;
562            }
563        }
564        time
565    }
566}
567
568/// Represents a single point in the day, to nanosecond precision.
569#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
570pub(crate) struct ITimeNanosecond {
571    pub(crate) nanosecond: i64,
572}
573
574impl ITimeNanosecond {
575    #[cfg_attr(feature = "perf-inline", inline(always))]
576    pub(crate) const fn to_time(&self) -> ITime {
577        let mut nanosecond = self.nanosecond;
578        let mut time = ITime::ZERO;
579        if nanosecond != 0 {
580            time.hour = (nanosecond / 3_600_000_000_000) as i8;
581            nanosecond %= 3_600_000_000_000;
582            if nanosecond != 0 {
583                time.minute = (nanosecond / 60_000_000_000) as i8;
584                nanosecond %= 60_000_000_000;
585                if nanosecond != 0 {
586                    time.second = (nanosecond / 1_000_000_000) as i8;
587                    time.subsec_nanosecond =
588                        (nanosecond % 1_000_000_000) as i32;
589                }
590            }
591        }
592        time
593    }
594}
595
596/// Represents a weekday.
597#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
598pub(crate) struct IWeekday {
599    /// Range is `1..=6` with `1=Monday`.
600    offset: i8,
601}
602
603impl IWeekday {
604    /// Creates a weekday assuming the week starts on Monday and Monday is at
605    /// offset `0`.
606    #[inline]
607    pub(crate) const fn from_monday_zero_offset(offset: i8) -> IWeekday {
608        assert!(0 <= offset && offset <= 6);
609        IWeekday::from_monday_one_offset(offset + 1)
610    }
611
612    /// Creates a weekday assuming the week starts on Monday and Monday is at
613    /// offset `1`.
614    #[inline]
615    pub(crate) const fn from_monday_one_offset(offset: i8) -> IWeekday {
616        assert!(1 <= offset && offset <= 7);
617        IWeekday { offset }
618    }
619
620    /// Creates a weekday assuming the week starts on Sunday and Sunday is at
621    /// offset `0`.
622    #[inline]
623    pub(crate) const fn from_sunday_zero_offset(offset: i8) -> IWeekday {
624        assert!(0 <= offset && offset <= 6);
625        IWeekday::from_monday_zero_offset((offset - 1).rem_euclid(7))
626    }
627
628    /// Creates a weekday assuming the week starts on Sunday and Sunday is at
629    /// offset `1`.
630    #[cfg(test)] // currently dead code
631    #[inline]
632    pub(crate) const fn from_sunday_one_offset(offset: i8) -> IWeekday {
633        assert!(1 <= offset && offset <= 7);
634        IWeekday::from_sunday_zero_offset(offset - 1)
635    }
636
637    /// Returns this weekday as an offset in the range `0..=6` where
638    /// `0=Monday`.
639    #[inline]
640    pub(crate) const fn to_monday_zero_offset(self) -> i8 {
641        self.to_monday_one_offset() - 1
642    }
643
644    /// Returns this weekday as an offset in the range `1..=7` where
645    /// `1=Monday`.
646    #[inline]
647    pub(crate) const fn to_monday_one_offset(self) -> i8 {
648        self.offset
649    }
650
651    /// Returns this weekday as an offset in the range `0..=6` where
652    /// `0=Sunday`.
653    #[cfg(test)] // currently dead code
654    #[inline]
655    pub(crate) const fn to_sunday_zero_offset(self) -> i8 {
656        (self.to_monday_zero_offset() + 1) % 7
657    }
658
659    /// Returns this weekday as an offset in the range `1..=7` where
660    /// `1=Sunday`.
661    #[cfg(test)] // currently dead code
662    #[inline]
663    pub(crate) const fn to_sunday_one_offset(self) -> i8 {
664        self.to_sunday_zero_offset() + 1
665    }
666
667    #[inline]
668    pub(crate) const fn since(self, other: IWeekday) -> i8 {
669        (self.to_monday_zero_offset() - other.to_monday_zero_offset())
670            .rem_euclid(7)
671    }
672}
673
674#[derive(Clone, Copy, Debug, Eq, PartialEq)]
675pub(crate) enum IAmbiguousOffset {
676    Unambiguous { offset: IOffset },
677    Gap { before: IOffset, after: IOffset },
678    Fold { before: IOffset, after: IOffset },
679}
680
681#[derive(Clone, Debug, Eq, PartialEq)]
682pub(crate) enum RangeError {
683    DateInvalidDayOfYear { year: i16 },
684    DateInvalidDayOfYearNoLeap,
685    DateInvalidDays { year: i16, month: i8 },
686    DateTimeSeconds,
687    DayOfYear,
688    EpochDayDays,
689    EpochDayI32,
690    NthWeekdayOfMonth,
691    Tomorrow,
692    YearNext,
693    YearPrevious,
694    Yesterday,
695}
696
697impl core::fmt::Display for RangeError {
698    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
699        use self::RangeError::*;
700
701        match *self {
702            DateInvalidDayOfYear { year } => write!(
703                f,
704                "number of days for `{year:04}` is invalid, \
705                 must be in range `1..={max_day}`",
706                max_day = days_in_year(year),
707            ),
708            DateInvalidDayOfYearNoLeap => f.write_str(
709                "number of days is invalid, must be in range `1..=365`",
710            ),
711            DateInvalidDays { year, month } => write!(
712                f,
713                "parameter 'day' for `{year:04}-{month:02}` is invalid, \
714                 must be in range `1..={max_day}`",
715                max_day = days_in_month(year, month),
716            ),
717            DateTimeSeconds => {
718                f.write_str("adding seconds to datetime overflowed")
719            }
720            DayOfYear => f.write_str("day of year is invalid"),
721            EpochDayDays => write!(
722                f,
723                "adding to epoch day resulted in a value outside \
724                 the allowed range of `{min}..={max}`",
725                min = IEpochDay::MIN.epoch_day,
726                max = IEpochDay::MAX.epoch_day,
727            ),
728            EpochDayI32 => f.write_str(
729                "adding to epoch day overflowed 32-bit signed integer",
730            ),
731            NthWeekdayOfMonth => f.write_str(
732                "invalid nth weekday of month, \
733                 must be non-zero and in range `-5..=5`",
734            ),
735            Tomorrow => f.write_str(
736                "returning tomorrow for `9999-12-31` is not \
737                 possible because it is greater than Jiff's supported
738                 maximum date",
739            ),
740            YearNext => f.write_str(
741                "creating a date for a year following `9999` is \
742                 not possible because it is greater than Jiff's supported \
743                 maximum date",
744            ),
745            YearPrevious => f.write_str(
746                "creating a date for a year preceding `-9999` is \
747                 not possible because it is less than Jiff's supported \
748                 minimum date",
749            ),
750            Yesterday => f.write_str(
751                "returning yesterday for `-9999-01-01` is not \
752                 possible because it is less than Jiff's supported
753                 minimum date",
754            ),
755        }
756    }
757}
758
759/// Returns true if and only if the given year is a leap year.
760///
761/// A leap year is a year with 366 days. Typical years have 365 days.
762#[inline]
763pub(crate) const fn is_leap_year(year: i16) -> bool {
764    // From: https://github.com/BurntSushi/jiff/pull/23
765    let d = if year % 25 != 0 { 4 } else { 16 };
766    (year % d) == 0
767}
768
769/// Return the number of days in the given year.
770#[inline]
771pub(crate) const fn days_in_year(year: i16) -> i16 {
772    if is_leap_year(year) {
773        366
774    } else {
775        365
776    }
777}
778
779/// Return the number of days in the given month.
780#[inline]
781pub(crate) const fn days_in_month(year: i16, month: i8) -> i8 {
782    // From: https://github.com/BurntSushi/jiff/pull/23
783    if month == 2 {
784        if is_leap_year(year) {
785            29
786        } else {
787            28
788        }
789    } else {
790        30 | (month ^ month >> 3)
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797
798    #[test]
799    fn roundtrip_epochday_date() {
800        for year in -9999..=9999 {
801            for month in 1..=12 {
802                for day in 1..=days_in_month(year, month) {
803                    let date = IDate { year, month, day };
804                    let epoch_day = date.to_epoch_day();
805                    let date_roundtrip = epoch_day.to_date();
806                    assert_eq!(date, date_roundtrip);
807                }
808            }
809        }
810    }
811
812    #[test]
813    fn roundtrip_second_time() {
814        for second in 0..=86_399 {
815            let second = ITimeSecond { second };
816            let time = second.to_time();
817            let second_roundtrip = time.to_second();
818            assert_eq!(second, second_roundtrip);
819        }
820    }
821
822    #[test]
823    fn roundtrip_nanosecond_time() {
824        for second in 0..=86_399 {
825            for nanosecond in
826                [0, 250_000_000, 500_000_000, 750_000_000, 900_000_000]
827            {
828                let nanosecond = ITimeNanosecond {
829                    nanosecond: (second * 1_000_000_000 + nanosecond),
830                };
831                let time = nanosecond.to_time();
832                let nanosecond_roundtrip = time.to_nanosecond();
833                assert_eq!(nanosecond, nanosecond_roundtrip);
834            }
835        }
836    }
837
838    #[test]
839    fn nth_weekday() {
840        let d1 = IDate { year: 2017, month: 3, day: 1 };
841        let wday = IWeekday::from_sunday_zero_offset(5);
842        let d2 = d1.nth_weekday_of_month(2, wday).unwrap();
843        assert_eq!(d2, IDate { year: 2017, month: 3, day: 10 });
844
845        let d1 = IDate { year: 2024, month: 3, day: 1 };
846        let wday = IWeekday::from_sunday_zero_offset(4);
847        let d2 = d1.nth_weekday_of_month(-1, wday).unwrap();
848        assert_eq!(d2, IDate { year: 2024, month: 3, day: 28 });
849
850        let d1 = IDate { year: 2024, month: 3, day: 25 };
851        let wday = IWeekday::from_sunday_zero_offset(1);
852        assert!(d1.nth_weekday_of_month(5, wday).is_err());
853        assert!(d1.nth_weekday_of_month(-5, wday).is_err());
854
855        let d1 = IDate { year: 1998, month: 1, day: 1 };
856        let wday = IWeekday::from_sunday_zero_offset(6);
857        let d2 = d1.nth_weekday_of_month(5, wday).unwrap();
858        assert_eq!(d2, IDate { year: 1998, month: 1, day: 31 });
859    }
860
861    #[test]
862    fn weekday() {
863        let wday = IWeekday::from_sunday_zero_offset(0);
864        assert_eq!(wday.to_monday_one_offset(), 7);
865
866        let wday = IWeekday::from_monday_one_offset(7);
867        assert_eq!(wday.to_sunday_zero_offset(), 0);
868
869        let wday = IWeekday::from_sunday_one_offset(1);
870        assert_eq!(wday.to_monday_zero_offset(), 6);
871
872        let wday = IWeekday::from_monday_zero_offset(6);
873        assert_eq!(wday.to_sunday_one_offset(), 1);
874    }
875
876    #[test]
877    fn weekday_since() {
878        let wday1 = IWeekday::from_sunday_zero_offset(0);
879        let wday2 = IWeekday::from_sunday_zero_offset(6);
880        assert_eq!(wday2.since(wday1), 6);
881        assert_eq!(wday1.since(wday2), 1);
882    }
883
884    #[test]
885    fn leap_year() {
886        assert!(!is_leap_year(1900));
887        assert!(is_leap_year(2000));
888        assert!(!is_leap_year(2001));
889        assert!(!is_leap_year(2002));
890        assert!(!is_leap_year(2003));
891        assert!(is_leap_year(2004));
892    }
893
894    #[test]
895    fn number_of_days_in_month() {
896        assert_eq!(days_in_month(2024, 1), 31);
897        assert_eq!(days_in_month(2024, 2), 29);
898        assert_eq!(days_in_month(2024, 3), 31);
899        assert_eq!(days_in_month(2024, 4), 30);
900        assert_eq!(days_in_month(2024, 5), 31);
901        assert_eq!(days_in_month(2024, 6), 30);
902        assert_eq!(days_in_month(2024, 7), 31);
903        assert_eq!(days_in_month(2024, 8), 31);
904        assert_eq!(days_in_month(2024, 9), 30);
905        assert_eq!(days_in_month(2024, 10), 31);
906        assert_eq!(days_in_month(2024, 11), 30);
907        assert_eq!(days_in_month(2024, 12), 31);
908
909        assert_eq!(days_in_month(2025, 1), 31);
910        assert_eq!(days_in_month(2025, 2), 28);
911        assert_eq!(days_in_month(2025, 3), 31);
912        assert_eq!(days_in_month(2025, 4), 30);
913        assert_eq!(days_in_month(2025, 5), 31);
914        assert_eq!(days_in_month(2025, 6), 30);
915        assert_eq!(days_in_month(2025, 7), 31);
916        assert_eq!(days_in_month(2025, 8), 31);
917        assert_eq!(days_in_month(2025, 9), 30);
918        assert_eq!(days_in_month(2025, 10), 31);
919        assert_eq!(days_in_month(2025, 11), 30);
920        assert_eq!(days_in_month(2025, 12), 31);
921
922        assert_eq!(days_in_month(1900, 2), 28);
923        assert_eq!(days_in_month(2000, 2), 29);
924    }
925
926    #[test]
927    fn yesterday() {
928        let d1 = IDate { year: 2025, month: 4, day: 7 };
929        let d2 = d1.yesterday().unwrap();
930        assert_eq!(d2, IDate { year: 2025, month: 4, day: 6 });
931
932        let d1 = IDate { year: 2025, month: 4, day: 1 };
933        let d2 = d1.yesterday().unwrap();
934        assert_eq!(d2, IDate { year: 2025, month: 3, day: 31 });
935
936        let d1 = IDate { year: 2025, month: 1, day: 1 };
937        let d2 = d1.yesterday().unwrap();
938        assert_eq!(d2, IDate { year: 2024, month: 12, day: 31 });
939
940        let d1 = IDate { year: -9999, month: 1, day: 1 };
941        assert_eq!(d1.yesterday().ok(), None);
942    }
943
944    #[test]
945    fn tomorrow() {
946        let d1 = IDate { year: 2025, month: 4, day: 7 };
947        let d2 = d1.tomorrow().unwrap();
948        assert_eq!(d2, IDate { year: 2025, month: 4, day: 8 });
949
950        let d1 = IDate { year: 2025, month: 3, day: 31 };
951        let d2 = d1.tomorrow().unwrap();
952        assert_eq!(d2, IDate { year: 2025, month: 4, day: 1 });
953
954        let d1 = IDate { year: 2025, month: 12, day: 31 };
955        let d2 = d1.tomorrow().unwrap();
956        assert_eq!(d2, IDate { year: 2026, month: 1, day: 1 });
957
958        let d1 = IDate { year: 9999, month: 12, day: 31 };
959        assert_eq!(d1.tomorrow().ok(), None);
960    }
961
962    #[test]
963    fn from_day_of_year() {
964        assert_eq!(
965            IDate::from_day_of_year(9999, 365),
966            Ok(IDate { year: 9999, month: 12, day: 31 }),
967        );
968        assert_eq!(
969            IDate::from_day_of_year(9998, 366),
970            Err(RangeError::DateInvalidDayOfYear { year: 9998 }),
971        );
972        assert_eq!(
973            IDate::from_day_of_year(9999, 366),
974            Err(RangeError::DayOfYear),
975        );
976    }
977
978    #[test]
979    fn timestamp_to_datetime() {
980        let ts = ITimestamp { second: 0, nanosecond: 1 };
981        let dt = ts.to_datetime(IOffset { second: 1 });
982        assert_eq!(
983            dt,
984            IDateTime {
985                date: IDate { year: 1970, month: 1, day: 1 },
986                time: ITime {
987                    hour: 0,
988                    minute: 0,
989                    second: 1,
990                    subsec_nanosecond: 1
991                },
992            }
993        );
994
995        let ts = ITimestamp { second: 0, nanosecond: 1 };
996        let dt = ts.to_datetime(IOffset { second: -1 });
997        assert_eq!(
998            dt,
999            IDateTime {
1000                date: IDate { year: 1969, month: 12, day: 31 },
1001                time: ITime {
1002                    hour: 23,
1003                    minute: 59,
1004                    second: 59,
1005                    subsec_nanosecond: 1,
1006                },
1007            }
1008        );
1009
1010        let ts = ITimestamp { second: 0, nanosecond: -1 };
1011        let dt = ts.to_datetime(IOffset { second: 1 });
1012        assert_eq!(
1013            dt,
1014            IDateTime {
1015                date: IDate { year: 1970, month: 1, day: 1 },
1016                time: ITime {
1017                    hour: 0,
1018                    minute: 0,
1019                    second: 0,
1020                    subsec_nanosecond: 999_999_999
1021                },
1022            }
1023        );
1024
1025        let ts = ITimestamp { second: 0, nanosecond: -1 };
1026        let dt = ts.to_datetime(IOffset { second: -1 });
1027        assert_eq!(
1028            dt,
1029            IDateTime {
1030                date: IDate { year: 1969, month: 12, day: 31 },
1031                time: ITime {
1032                    hour: 23,
1033                    minute: 59,
1034                    second: 58,
1035                    subsec_nanosecond: 999_999_999,
1036                },
1037            }
1038        );
1039    }
1040}