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