fundu_gnu/
datetime.rs

1// Copyright (c) 2023 Joining7943 <joining@posteo.de>
2//
3// This software is released under the MIT License.
4// https://opensource.org/licenses/MIT
5
6use std::ops::Neg;
7use std::time::{Duration as StdDuration, SystemTime};
8
9#[cfg(feature = "chrono")]
10use chrono::{Datelike, Timelike};
11use fundu_core::time::Duration;
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14#[cfg(feature = "time")]
15use time::{OffsetDateTime, PrimitiveDateTime};
16
17use crate::util::{self, floor_div};
18
19const DAYS_IN_PREVIOUS_MONTH: [u16; 12] = [306, 337, 0, 31, 61, 92, 122, 153, 184, 214, 245, 275];
20const ORDINAL_TO_MONTH: [u8; 366] = [
21    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4,
22    4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5,
23    5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6,
24    6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7,
25    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8,
26    8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
27    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10,
28    10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
29    11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
30    11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
31    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
32    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
33    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
34];
35
36const NANOS_PER_SEC_I64: i64 = 1_000_000_000;
37const NANOS_PER_SEC_U64: u64 = NANOS_PER_SEC_I64 as u64;
38const NANOS_PER_SEC_U128: u128 = NANOS_PER_SEC_I64 as u128;
39const NANOS_PER_DAY_I64: i64 = 86_400_000_000_000;
40const NANOS_PER_DAY_I128: i128 = NANOS_PER_DAY_I64 as i128;
41
42const SECS_PER_MINUTE_I64: i64 = 60;
43const SECS_PER_MINUTE_U64: u64 = SECS_PER_MINUTE_I64 as u64;
44const SECS_PER_HOUR_I64: i64 = 3600;
45const SECS_PER_HOUR_U64: u64 = SECS_PER_HOUR_I64 as u64;
46
47const JD_BASE: i64 = 1_721_119;
48
49/// Store a proleptic gregorian date as [`JulianDay`]
50///
51/// The Julian Day Number or Julian Day is the number of days since noon UTC on November 24 of the
52/// year -4713 in the Gregorian Calendar. The year 0 equals 1BC. The JD number is a possibly
53/// negative integer representing the number of whole days since the reference instant to noon of
54/// that day. This `JulianDay` implementation does not have a fraction and stores the day as if no
55/// time was specified which is equivalent of a time being always __noon__.
56///
57/// # Examples
58///
59/// ```rust
60/// use fundu_gnu::JulianDay;
61///
62/// assert_eq!(JulianDay(0).to_gregorian(), Some((-4713, 11, 24)));
63/// assert_eq!(JulianDay(-365).to_gregorian(), Some((-4714, 11, 24)));
64/// assert_eq!(JulianDay(1_721_060).to_gregorian(), Some((0, 1, 1)));
65/// assert_eq!(JulianDay(2_440_588).to_gregorian(), Some((1970, 1, 1)));
66/// ```
67#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69pub struct JulianDay(pub i64);
70
71impl JulianDay {
72    /// Calculate the `JulianDay` from a proleptic gregorian date
73    ///
74    /// For a non-panicking version see [`JulianDay::try_from_gregorian`].
75    ///
76    /// # Panics
77    ///
78    /// Panics if the input arguments are invalid. Valid ranges are:
79    ///
80    /// * `1 <= month <= 12`
81    /// * `1 <= day <= 31`
82    ///
83    /// or an overflow occurred. Use the safer alternative [`JulianDay::try_from_gregorian`] if very
84    /// high values for years are expected.
85    ///
86    /// # Examples
87    ///
88    /// ```rust
89    /// use fundu_gnu::JulianDay;
90    ///
91    /// assert_eq!(JulianDay::from_gregorian(-4713, 11, 24), JulianDay(0));
92    /// assert_eq!(JulianDay::from_gregorian(-4714, 11, 24), JulianDay(-365));
93    /// assert_eq!(JulianDay::from_gregorian(0, 1, 1), JulianDay(1_721_060));
94    /// assert_eq!(JulianDay::from_gregorian(1970, 1, 1), JulianDay(2440588));
95    /// ```
96    pub const fn from_gregorian(year: i64, month: u8, day: u8) -> Self {
97        match Self::try_from_gregorian(year, month, day) {
98            Some(jd) => jd,
99            None => panic!("Overflow calculating julian day from gregorian date"),
100        }
101    }
102
103    /// Calculate the `JulianDay` from a proleptic gregorian date
104    ///
105    /// Returns None if an overflow occurred most likely to a year greater than approximately
106    /// `25_200_470_046_046_596` or smaller than approximately `-25_200_470_046_046_596`
107    ///
108    /// This method is based on the work of Peter Baum and his publication of
109    /// [Date Algorithms](https://www.researchgate.net/publication/316558298_Date_Algorithms).
110    ///
111    /// # Panics
112    ///
113    /// Panics if the input arguments are invalid. Valid ranges are:
114    ///
115    /// * `1 <= month <= 12`
116    /// * `1 <= day <= 31`
117    ///
118    /// Note it is not an error to specify `day = 31` for example for the month 4 (April). In such a
119    /// case the month is assumed to be the next month (here May) and `day = 1`.
120    ///
121    /// # Examples
122    ///
123    /// ```rust
124    /// use fundu_gnu::JulianDay;
125    ///
126    /// assert_eq!(
127    ///     JulianDay::try_from_gregorian(-4713, 11, 24),
128    ///     Some(JulianDay(0))
129    /// );
130    /// assert_eq!(
131    ///     JulianDay::try_from_gregorian(-4714, 11, 24),
132    ///     Some(JulianDay(-365))
133    /// );
134    /// assert_eq!(
135    ///     JulianDay::try_from_gregorian(0, 1, 1),
136    ///     Some(JulianDay(1_721_060))
137    /// );
138    /// assert_eq!(
139    ///     JulianDay::try_from_gregorian(1970, 1, 1),
140    ///     Some(JulianDay(2440588))
141    /// );
142    /// ```
143    pub const fn try_from_gregorian(year: i64, month: u8, day: u8) -> Option<Self> {
144        validate!(month, 1, 12);
145        validate!(day, 1, 31);
146
147        let year = if month < 3 {
148            match year.checked_sub(1) {
149                Some(y) => y,
150                None => return None,
151            }
152        } else {
153            year
154        };
155
156        if let Some(y) = year.checked_mul(365) {
157            if let Some(y) = y.checked_add(
158                day as i64
159                    + DAYS_IN_PREVIOUS_MONTH[(month - 1) as usize] as i64
160                    + floor_div(year, 4)
161                    - floor_div(year, 100)
162                    + floor_div(year, 400)
163                    + JD_BASE,
164            ) {
165                return Some(Self(y));
166            }
167        }
168        None
169    }
170
171    /// Return the julian day
172    ///
173    /// # Examples
174    ///
175    /// ```rust
176    /// use fundu_gnu::JulianDay;
177    ///
178    /// assert_eq!(JulianDay(-1).as_days(), -1);
179    /// ```
180    pub const fn as_days(self) -> i64 {
181        self.0
182    }
183
184    /// Calculate the proleptic gregorian date from this `JulianDay`
185    ///
186    /// The method returns None if an overflow occurred.
187    ///
188    /// This method is based on the work of Peter Baum and his publication of
189    /// [Date Algorithms](https://www.researchgate.net/publication/316558298_Date_Algorithms).
190    ///
191    /// # Examples
192    ///
193    /// ```rust
194    /// use fundu_gnu::JulianDay;
195    ///
196    /// assert_eq!(JulianDay(-365).to_gregorian(), Some((-4714, 11, 24)));
197    /// assert_eq!(JulianDay(0).to_gregorian(), Some((-4713, 11, 24)));
198    /// assert_eq!(JulianDay(1721060).to_gregorian(), Some((0, 1, 1)));
199    /// assert_eq!(JulianDay(2440588).to_gregorian(), Some((1970, 1, 1)));
200    /// ```
201    #[allow(clippy::missing_panics_doc)]
202    pub fn to_gregorian(self) -> Option<(i64, u8, u8)> {
203        let zero = self.0.checked_sub(JD_BASE)?;
204        let c = zero.checked_mul(100)? - 25;
205        // Calculate the number of full centuries in the Gregorian Calendar
206        let full_centuries = floor_div(c, 3_652_425);
207        // Calculate the days within the whole centuries in the Julian Calendar by adding back days
208        // removed in the Gregorian Calendar
209        let days_centuries = full_centuries - floor_div(full_centuries, 4);
210
211        // Calculate the year in a calendar whose years start on March 1
212        let year = floor_div((100 * days_centuries).checked_add(c)?, 36525);
213
214        // Calculate the value of the day count in the current year
215        // unwrap is safe since 1 <= ordinal <= 366
216        let ordinal =
217            u16::try_from(days_centuries + zero - 365 * year - floor_div(year, 4)).unwrap();
218
219        // Calculate the value of the month in the current year using a lookup table
220        // unwrap is safe since 1 <= month <= 12
221        let month = ORDINAL_TO_MONTH[usize::from(ordinal - 1)];
222
223        // Calculate the day of the month using a lookup table
224        // unwrap is safe since 1 <= day <= 31
225        let day = u8::try_from(ordinal - DAYS_IN_PREVIOUS_MONTH[usize::from(month - 1)]).unwrap();
226
227        // Convert the month and year to a calendar starting January 1 and return the result
228        if month < 3 {
229            Some((year + 1, month, day))
230        } else {
231            Some((year, month, day))
232        }
233    }
234
235    /// Checked days addition. Computes `self.0 + days`, returning None if an overflow occurred.
236    ///
237    /// # Examples
238    ///
239    /// ```rust
240    /// use fundu_gnu::JulianDay;
241    ///
242    /// assert_eq!(
243    ///     JulianDay(0).checked_add_days(10_000),
244    ///     Some(JulianDay(10_000))
245    /// );
246    /// assert_eq!(JulianDay(0).checked_add_days(-1), Some(JulianDay(-1)));
247    /// ```
248    pub const fn checked_add_days(self, days: i64) -> Option<Self> {
249        match self.0.checked_add(days) {
250            Some(x) => Some(Self(x)),
251            None => None,
252        }
253    }
254
255    /// Checked days subtraction. Computes `self.0 - days`, returning None if an overflow occurred.
256    ///
257    /// # Examples
258    ///
259    /// ```rust
260    /// use fundu_gnu::JulianDay;
261    ///
262    /// assert_eq!(
263    ///     JulianDay(1_700_000).checked_sub_days(1),
264    ///     Some(JulianDay(1_699_999))
265    /// );
266    /// assert_eq!(JulianDay(0).checked_sub_days(366), Some(JulianDay(-366)));
267    /// ```
268    pub const fn checked_sub_days(self, days: i64) -> Option<Self> {
269        match self.0.checked_sub(days) {
270            Some(x) => Some(Self(x)),
271            None => None,
272        }
273    }
274
275    /// Checked addition. Computes `self + rhs`, returning None if an overflow occurred.
276    ///
277    /// # Examples
278    ///
279    /// ```rust
280    /// use fundu_gnu::JulianDay;
281    ///
282    /// assert_eq!(
283    ///     JulianDay(0).checked_add(JulianDay(10_000)),
284    ///     Some(JulianDay(10_000))
285    /// );
286    /// assert_eq!(JulianDay(0).checked_add(JulianDay(-1)), Some(JulianDay(-1)));
287    /// ```
288    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
289        match self.0.checked_add(rhs.0) {
290            Some(x) => Some(Self(x)),
291            None => None,
292        }
293    }
294
295    /// Checked subtraction. Computes `self - rhs`, returning None if an overflow occurred.
296    ///
297    /// # Examples
298    ///
299    /// ```rust
300    /// use fundu_gnu::JulianDay;
301    ///
302    /// assert_eq!(
303    ///     JulianDay(1_700_000).checked_sub(JulianDay(1)),
304    ///     Some(JulianDay(1_699_999))
305    /// );
306    /// assert_eq!(
307    ///     JulianDay(0).checked_sub(JulianDay(366)),
308    ///     Some(JulianDay(-366))
309    /// );
310    /// ```
311    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
312        match self.0.checked_sub(rhs.0) {
313            Some(x) => Some(Self(x)),
314            None => None,
315        }
316    }
317}
318
319/// Proleptic gregorian date and time with fast calculations of differences in date and time
320///
321/// This struct is primarily designed to provide fast calculations of possibly very large
322/// differences in date and time in the proleptic gregorian calendar. It is a superset of date and
323/// time structs like [`time::PrimitiveDateTime`], [`time::OffsetDateTime`] which is limited to
324/// years ranging from `-999_999` to `999_999` if the `large-dates` feature is activated and
325/// [`chrono::DateTime`] which is limited to years ranging from `-262_144` to `262_143`. Since gnu
326/// supports much larger dates, and to provide maximum possible compatibility with `gnu`, this
327/// struct provides support for dates ranging from approximately `-583_344_214_028` years to
328/// `583_344_214_028` years which is far more than the age of the universe.
329///
330/// This struct implements `From<OffsetDateTime>` and `From<PrimitiveDateTime>` if the `time`
331/// feature is activated. If the `chrono` feature is activated `From<DateTime>` is available.
332///
333/// # Examples
334///
335/// ```rust
336/// use fundu_gnu::DateTime;
337///
338/// let date_time = DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
339/// assert_eq!(date_time, DateTime::UNIX_EPOCH);
340/// ```
341///
342/// If the `chrono` feature is activated a chrono date and time can be converted lossless to a
343/// `DateTime`
344#[cfg_attr(feature = "chrono", doc = "```rust")]
345#[cfg_attr(not(feature = "chrono"), doc = "```rust,ignore")]
346/// use chrono::{FixedOffset, TimeZone};
347/// use fundu_gnu::DateTime;
348///
349/// let chrono_date = FixedOffset::east_opt(0)
350///     .unwrap()
351///     .with_ymd_and_hms(2000, 12, 31, 23, 59, 59)
352///     .unwrap();
353/// assert_eq!(
354///     DateTime::from(chrono_date),
355///     DateTime::from_gregorian_date_time(2000, 12, 31, 23, 59, 59, 0)
356/// );
357///
358/// // Now with an offset
359/// let chrono_date = FixedOffset::east_opt(2 * 3600)
360///     .unwrap()
361///     .with_ymd_and_hms(2000, 12, 31, 23, 59, 59)
362///     .unwrap();
363/// assert_eq!(
364///     DateTime::from(chrono_date),
365///     DateTime::from_gregorian_date_time(2000, 12, 31, 21, 59, 59, 0)
366/// );
367/// ```
368/// 
369/// And if the `time` feature is activated:
370#[cfg_attr(feature = "time", doc = "```rust")]
371#[cfg_attr(not(feature = "time"), doc = "```rust,ignore")]
372/// use time::macros::{datetime, offset};
373/// use fundu_gnu::DateTime;
374///
375/// assert_eq!(
376///     DateTime::from(datetime!(2000-12-31 23:59:59.200_000_000)),
377///     DateTime::from_gregorian_date_time(2000, 12, 31, 23, 59, 59, 200_000_000)
378/// );
379///
380/// // And with an offset
381/// assert_eq!(
382///     DateTime::from(datetime!(2000-12-31 23:59:59.200_000_000 UTC).to_offset(offset!(-2))),
383///     DateTime::from_gregorian_date_time(2000, 12, 31, 21, 59, 59, 200_000_000)
384/// );
385/// ```
386/// 
387/// [`time::PrimitiveDateTime`]: https://docs.rs/time/latest/time/struct.PrimitiveDateTime.html
388/// [`time::OffsetDateTime`]: https://docs.rs/time/latest/time/struct.OffsetDateTime.html
389#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
390#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
391pub struct DateTime {
392    days: JulianDay,
393    time: u64,
394}
395
396impl DateTime {
397    /// The `DateTime` of the unix epoch in UTC +0
398    pub const UNIX_EPOCH: Self = Self::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
399
400    /// Create a `DateTime` from a given proleptic gregorian date and time
401    ///
402    /// # Panics
403    ///
404    /// This method panics if the input arguments are invalid. Valid ranges are:
405    ///
406    /// * `1 <= month <= 12`
407    /// * `1 <= day <= 31`
408    /// * `0 <= hour <= 23`
409    /// * `0 <= minute <= 59`
410    /// * `0 <= second <= 59`
411    /// * `0 <= nanos <= 999_999_999`
412    ///
413    /// Note it is not an error to specify `day = 31` for example for the month 4 (April). In such a
414    /// case the month is assumed to be the next month (here May) and `day = 1`.
415    ///
416    /// In theory, the year may not exceed approximately `25_200_470_046_046_596` or
417    /// `-25_200_470_046_046_596` years within this function or else a panic occurs. To be sure that
418    /// the year doesn't cause overflows and returning `None` from other functions of `DateTime`, a
419    /// safer range is `-583_344_214_028` to `583_344_214_028` years.
420    ///
421    /// # Examples
422    ///
423    /// ```rust
424    /// use fundu_gnu::DateTime;
425    ///
426    /// assert_eq!(
427    ///     DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
428    ///     DateTime::UNIX_EPOCH
429    /// );
430    ///
431    /// // 1972 is a leap year
432    /// let date_time = DateTime::from_gregorian_date_time(1972, 2, 29, 1, 30, 59, 700_000_000);
433    /// assert_eq!(
434    ///     date_time.to_gregorian_date_time(),
435    ///     Some((1972, 2, 29, 1, 30, 59, 700_000_000))
436    /// );
437    ///
438    /// // Given 1960-4-31, the day carries over into the next month
439    /// assert_eq!(
440    ///     DateTime::from_gregorian_date_time(1960, 4, 31, 1, 30, 59, 700_000_000),
441    ///     DateTime::from_gregorian_date_time(1960, 5, 1, 1, 30, 59, 700_000_000),
442    /// );
443    /// ```
444    pub const fn from_gregorian_date_time(
445        year: i64,
446        month: u8,
447        day: u8,
448        hour: u8,
449        minute: u8,
450        second: u8,
451        nanos: u32,
452    ) -> Self {
453        validate!(hour <= 23);
454        validate!(minute <= 59);
455        validate!(second <= 59);
456        validate!(nanos <= 999_999_999);
457
458        let days = JulianDay::from_gregorian(year, month, day);
459        let time = {
460            (hour as u64 * SECS_PER_HOUR_U64 + minute as u64 * SECS_PER_MINUTE_U64 + second as u64)
461                * NANOS_PER_SEC_U64
462                + nanos as u64
463        };
464        Self { days, time }
465    }
466
467    /// Return the proleptic gregorian date
468    ///
469    /// # Examples
470    ///
471    /// ```rust
472    /// use fundu_gnu::DateTime;
473    ///
474    /// assert_eq!(DateTime::UNIX_EPOCH.to_gregorian_date(), Some((1970, 1, 1)));
475    /// assert_eq!(
476    ///     DateTime::from_gregorian_date_time(1959, 1, 12, 10, 3, 59, 0).to_gregorian_date(),
477    ///     Some((1959, 1, 12))
478    /// );
479    /// ```
480    pub fn to_gregorian_date(&self) -> Option<(i64, u8, u8)> {
481        self.days.to_gregorian()
482    }
483
484    /// Return the proleptic gregorian date and time
485    ///
486    /// # Examples
487    ///
488    /// ```rust
489    /// use fundu_gnu::DateTime;
490    ///
491    /// assert_eq!(
492    ///     DateTime::UNIX_EPOCH.to_gregorian_date_time(),
493    ///     Some((1970, 1, 1, 0, 0, 0, 0))
494    /// );
495    /// assert_eq!(
496    ///     DateTime::from_gregorian_date_time(1959, 1, 12, 10, 3, 59, 500_000_000)
497    ///         .to_gregorian_date_time(),
498    ///     Some((1959, 1, 12, 10, 3, 59, 500_000_000))
499    /// );
500    /// ```
501    pub fn to_gregorian_date_time(&self) -> Option<(i64, u8, u8, u8, u8, u8, u32)> {
502        let (year, month, day) = self.days.to_gregorian()?;
503        let (h, m, s, n) = self.as_hmsn();
504        Some((year, month, day, h, m, s, n))
505    }
506
507    /// Return the time as tuple with (hour, minute, second, nanos)
508    ///
509    /// This method cannot fail
510    ///
511    /// # Examples
512    ///
513    /// ```rust
514    /// use fundu_gnu::DateTime;
515    ///
516    /// assert_eq!(DateTime::UNIX_EPOCH.as_hmsn(), (0, 0, 0, 0));
517    /// assert_eq!(
518    ///     DateTime::from_gregorian_date_time(1959, 1, 12, 10, 3, 59, 500_000_000).as_hmsn(),
519    ///     (10, 3, 59, 500_000_000)
520    /// );
521    /// ```
522    pub const fn as_hmsn(&self) -> (u8, u8, u8, u32) {
523        let mut time = self.time;
524
525        #[allow(clippy::cast_sign_loss)]
526        let nanos = (time % NANOS_PER_SEC_U64) as u32;
527        time /= NANOS_PER_SEC_U64;
528        let hour = time / SECS_PER_HOUR_U64;
529        time %= SECS_PER_HOUR_U64;
530        let min = time / SECS_PER_MINUTE_U64;
531        time %= SECS_PER_MINUTE_U64;
532
533        #[allow(clippy::cast_possible_truncation)]
534        #[allow(clippy::cast_sign_loss)]
535        (hour as u8, min as u8, time as u8, nanos)
536    }
537
538    /// Return the proleptic gregorian date as [`JulianDay`]
539    ///
540    /// This `JulianDay` implementation does not have a fraction and returns the day as if no time
541    /// was specified which is equivalent of a time being always __noon__
542    ///
543    /// # Examples
544    ///
545    /// ```rust
546    /// use fundu_gnu::{DateTime, JulianDay};
547    ///
548    /// assert_eq!(DateTime::UNIX_EPOCH.as_julian_day(), JulianDay(2_440_588));
549    /// assert_eq!(
550    ///     DateTime::from_gregorian_date_time(-4713, 11, 24, 10, 3, 59, 500_000_000).as_julian_day(),
551    ///     JulianDay(0)
552    /// );
553    /// assert_eq!(
554    ///     DateTime::from_gregorian_date_time(2023, 3, 9, 23, 59, 59, 999_999_999).as_julian_day(),
555    ///     JulianDay(2460013)
556    /// );
557    /// ```
558    pub const fn as_julian_day(&self) -> JulianDay {
559        self.days
560    }
561
562    fn now_utc_with_system_time(now: SystemTime) -> Self {
563        let date_unix_epoch = Self::UNIX_EPOCH;
564        match now.duration_since(SystemTime::UNIX_EPOCH) {
565            Ok(duration) => date_unix_epoch
566                .checked_add_duration(&duration.into())
567                .expect("Overflow when adding current system time difference to unix epoch"),
568            Err(error) => date_unix_epoch
569                .checked_add_duration(&Duration::from_std(true, error.duration()))
570                .expect("Overflow when subtracting current system time difference from unix epoch"),
571        }
572    }
573
574    /// Return the current `DateTime` with an offset of UTC +-0
575    ///
576    /// # Platform-specific behavior
577    ///
578    /// This method is subject to the same
579    /// [restrictions](https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior)
580    /// as [`SystemTime`] does.
581    ///
582    /// # Examples
583    ///
584    /// ```rust
585    /// use fundu_gnu::DateTime;
586    ///
587    /// let now = DateTime::now_utc();
588    /// ```
589    pub fn now_utc() -> Self {
590        Self::now_utc_with_system_time(util::now())
591    }
592
593    /// Add a [`Duration`] to the `DateTime` returning some new `DateTime` or `None` on overflow
594    ///
595    /// # Examples
596    ///
597    /// ```rust
598    /// use fundu_gnu::{DateTime, Duration};
599    ///
600    /// let date_time = DateTime::from_gregorian_date_time(1970, 1, 1, 23, 59, 59, 0);
601    /// assert_eq!(
602    ///     date_time.checked_add_duration(&Duration::positive(3600, 0)),
603    ///     Some(DateTime::from_gregorian_date_time(1970, 1, 2, 0, 59, 59, 0))
604    /// );
605    /// ```
606    #[allow(clippy::missing_panics_doc)]
607    pub fn checked_add_duration(self, duration: &Duration) -> Option<Self> {
608        let mut duration = *duration;
609        let days = duration.extract_days();
610
611        self.days.checked_add_days(days).and_then(|jd| {
612            // The unwrap is safe here because we extracted the days
613            let nanos = i64::try_from(duration.as_nanos()).unwrap();
614
615            // The unwrap is safe here because 0 <= time <= 85_399_999_999_999
616            let time = i64::try_from(self.time).unwrap() + nanos;
617            // -85_399_999_999_999 <= time <= 2 * 85_399_999_999_999
618            if time < 0 {
619                jd.checked_add_days(-1).map(|jd| Self {
620                    days: jd,
621                    time: u64::try_from(time + NANOS_PER_DAY_I64).unwrap(),
622                })
623            } else if time < NANOS_PER_DAY_I64 {
624                Some(Self {
625                    days: jd,
626                    time: u64::try_from(time).unwrap(),
627                })
628            } else {
629                jd.checked_add_days(1).map(|jd| Self {
630                    days: jd,
631                    time: u64::try_from(time - NANOS_PER_DAY_I64).unwrap(),
632                })
633            }
634        })
635    }
636
637    /// Subtract a [`Duration`] from the `DateTime`
638    ///
639    /// This method returns some new `DateTime` or `None` on overflow
640    ///
641    /// # Examples
642    ///
643    /// ```rust
644    /// use fundu_gnu::{DateTime, Duration};
645    ///
646    /// let date_time = DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
647    /// assert_eq!(
648    ///     date_time.checked_sub_duration(&Duration::positive(3600, 0)),
649    ///     Some(DateTime::from_gregorian_date_time(
650    ///         1969, 12, 31, 23, 0, 0, 0
651    ///     ))
652    /// );
653    /// ```
654    pub fn checked_sub_duration(self, duration: &Duration) -> Option<Self> {
655        self.checked_add_duration(&duration.neg())
656    }
657
658    /// Add years, months and days to the `DateTime` in the proleptic gregorian calendar
659    ///
660    /// This method returns some new `DateTime` or `None` on overflow
661    ///
662    /// # Examples
663    ///
664    /// ```rust
665    /// use fundu_gnu::{DateTime, Duration};
666    ///
667    /// let date_time = DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
668    /// assert_eq!(
669    ///     date_time.checked_add_gregorian(2000, 0, 0),
670    ///     Some(DateTime::from_gregorian_date_time(3970, 1, 1, 0, 0, 0, 0))
671    /// );
672    ///
673    /// let date_time = DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
674    /// assert_eq!(
675    ///     date_time.checked_add_gregorian(0, 121, 0),
676    ///     Some(DateTime::from_gregorian_date_time(1980, 2, 1, 0, 0, 0, 0))
677    /// );
678    ///
679    /// let date_time = DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
680    /// assert_eq!(
681    ///     date_time.checked_add_gregorian(0, 0, 365),
682    ///     Some(DateTime::from_gregorian_date_time(1971, 1, 1, 0, 0, 0, 0))
683    /// );
684    ///
685    /// // 1972 is a leap year
686    /// let date_time = DateTime::from_gregorian_date_time(1972, 1, 1, 0, 0, 0, 0);
687    /// assert_eq!(
688    ///     date_time.checked_add_gregorian(0, 0, 366),
689    ///     Some(DateTime::from_gregorian_date_time(1973, 1, 1, 0, 0, 0, 0))
690    /// );
691    /// ```
692    #[allow(clippy::missing_panics_doc)]
693    pub fn checked_add_gregorian(self, years: i64, months: i64, days: i64) -> Option<Self> {
694        let (year, month, day) = self.days.to_gregorian()?;
695        let (month, years) = years.checked_add(months / 12).and_then(|y| {
696            // now.month() is 1-based, we need it 0-based for now
697            let month = i8::try_from(months % 12).unwrap() + i8::try_from(month).unwrap() - 1;
698            if month < 0 {
699                y.checked_sub(1)
700                    .map(|y| (u8::try_from(month + 12).unwrap(), y))
701            } else if month < 12 {
702                Some((u8::try_from(month).unwrap(), y))
703            } else {
704                y.checked_add(1)
705                    .map(|y| (u8::try_from(month - 12).unwrap(), y))
706            }
707        })?;
708
709        year.checked_add(years).and_then(|year| {
710            JulianDay::try_from_gregorian(year, month + 1, day).and_then(|jd| {
711                jd.checked_add_days(days).map(|jd| Self {
712                    days: jd,
713                    time: self.time,
714                })
715            })
716        })
717    }
718
719    /// Calculate the [`Duration`] between this `DateTime` and another `DateTime`
720    ///
721    /// If the other `DateTime` is greater than this `DateTime` the [`Duration`] is negative. This
722    /// method returns `None` when an overflow occurs.
723    ///
724    /// # Examples
725    ///
726    /// ```rust
727    /// use fundu_gnu::{DateTime, Duration};
728    ///
729    /// let dt = DateTime::from_gregorian_date_time(1971, 1, 1, 23, 0, 0, 0);
730    /// assert_eq!(
731    ///     dt.duration_since(DateTime::UNIX_EPOCH),
732    ///     Some(Duration::positive(365 * 86400 + 23 * 3600, 0))
733    /// );
734    ///
735    /// let dt = DateTime::from_gregorian_date_time(1973, 1, 1, 0, 0, 10, 123_456_789);
736    /// let other = DateTime::from_gregorian_date_time(1972, 1, 1, 0, 0, 0, 0);
737    /// assert_eq!(
738    ///     dt.duration_since(other),
739    ///     Some(Duration::positive(366 * 86400 + 10, 123_456_789))
740    /// );
741    /// assert_eq!(
742    ///     other.duration_since(dt),
743    ///     Some(Duration::negative(366 * 86400 + 10, 123_456_789))
744    /// );
745    /// ```
746    #[allow(clippy::missing_panics_doc)]
747    pub fn duration_since(self, rhs: Self) -> Option<Duration> {
748        let jd = self.days.checked_sub(rhs.days)?;
749        let time = i64::try_from(self.time)
750            .unwrap()
751            .checked_sub(i64::try_from(rhs.time).unwrap())?;
752
753        let total = i128::from(jd.as_days())
754            .checked_mul(NANOS_PER_DAY_I128)
755            .and_then(|t| t.checked_add(i128::from(time)))?;
756        let is_negative = total.is_negative();
757        let total = total.unsigned_abs();
758
759        let secs = u64::try_from(total / NANOS_PER_SEC_U128).ok()?;
760        let nanos = u32::try_from(total % NANOS_PER_SEC_U128).unwrap();
761        Some(Duration::from_std(
762            is_negative,
763            StdDuration::new(secs, nanos),
764        ))
765    }
766}
767
768#[cfg(feature = "time")]
769impl From<OffsetDateTime> for DateTime {
770    fn from(value: OffsetDateTime) -> Self {
771        let (year, month, day) = value.to_calendar_date();
772        let (h, m, s, n) = value.to_hms_nano();
773        Self::from_gregorian_date_time(i64::from(year), u8::from(month), day, h, m, s, n)
774    }
775}
776
777#[cfg(feature = "time")]
778impl From<PrimitiveDateTime> for DateTime {
779    fn from(value: PrimitiveDateTime) -> Self {
780        value.assume_utc().into()
781    }
782}
783
784#[cfg(feature = "chrono")]
785impl<T: chrono::TimeZone> From<chrono::DateTime<T>> for DateTime {
786    fn from(value: chrono::DateTime<T>) -> Self {
787        value.naive_utc().into()
788    }
789}
790
791#[cfg(feature = "chrono")]
792impl From<chrono::NaiveDateTime> for DateTime {
793    fn from(value: chrono::NaiveDateTime) -> Self {
794        let (year, month, day, h, m, s, n) = (
795            i64::from(value.year()),
796            u8::try_from(value.month()).unwrap(),
797            u8::try_from(value.day()).unwrap(),
798            u8::try_from(value.hour()).unwrap(),
799            u8::try_from(value.minute()).unwrap(),
800            u8::try_from(value.second()).unwrap(),
801            value.nanosecond(),
802        );
803        Self::from_gregorian_date_time(year, month, day, h, m, s, n)
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    #[cfg(feature = "chrono")]
810    use chrono::{Duration as ChronoDuration, FixedOffset, TimeZone};
811    use rstest::rstest;
812    use rstest_reuse::{apply, template};
813    #[cfg(feature = "serde")]
814    use serde_test::{assert_tokens, Token};
815    #[cfg(feature = "time")]
816    use time::{macros::datetime, Date as TimeDate, Time as TimeTime, UtcOffset};
817
818    use super::*;
819
820    #[cfg(feature = "serde")]
821    #[test]
822    fn test_serde_julian_day() {
823        let julian_day = JulianDay(1);
824
825        assert_tokens(
826            &julian_day,
827            &[Token::NewtypeStruct { name: "JulianDay" }, Token::I64(1)],
828        );
829    }
830
831    #[cfg(feature = "serde")]
832    #[test]
833    fn test_serde_date_time() {
834        let date_time = DateTime::from_gregorian_date_time(0, 1, 2, 3, 4, 5, 6);
835
836        assert_tokens(
837            &date_time,
838            &[
839                Token::Struct {
840                    name: "DateTime",
841                    len: 2,
842                },
843                Token::Str("days"),
844                Token::NewtypeStruct { name: "JulianDay" },
845                Token::I64(1_721_061),
846                Token::Str("time"),
847                Token::U64((3 * 3600 + 4 * 60 + 5) * NANOS_PER_SEC_U64 + 6),
848                Token::StructEnd,
849            ],
850        );
851    }
852    #[rstest]
853    #[case::jd_minus_one((-4713, 11, 23), -1)]
854    #[case::jd_day_zero((-4713, 11, 24), 0)]
855    #[case::jd_year_one((-4712, 1, 1), 38)]
856    #[case::first_day_of_one_bc((0, 1, 1), 1_721_060)]
857    #[case::year_zero_leap((0, 2, 29), 1_721_119)]
858    #[case::third_month_day_one((0, 3, 1), 1_721_120)]
859    #[case::last_day_in_year_zero((0, 12, 31), 1_721_425)]
860    #[case::year_one_first_day((1, 1, 1), 1_721_426)]
861    #[case::unix_timestamp((1970, 1, 1), 2_440_588)]
862    #[case::end_of_year((1979, 12, 31), 2_444_239)]
863    #[case::end_of_year_plus_one((1980, 1, 1), 2_444_240)]
864    #[case::month_is_12((0, 12, 1), 1_721_395)]
865    #[case::day_is_31_when_month_has_31((1, 12, 31), 1_721_790)]
866    #[case::day_is_31_when_month_has_30((1, 11, 31), 1_721_760)]
867    #[case::day_is_31_when_month_has_28((1971, 2, 31), 2_441_014)]
868    #[case::day_is_31_when_month_has_29((1972, 2, 31), 2_441_379)]
869    #[case::around_min_possible_year(
870        ((i64::MIN) / 366, 1, 1),
871        -9_204_282_680_793_170_880
872    )]
873    #[case::around_max_possible_year(
874        ((i64::MAX - 31 - 337 - 1_721_119) / 366, 12, 31),
875        9_204_282_680_794_895_265
876    )]
877    fn test_julian_days_from_gregorian(#[case] ymd: (i64, u8, u8), #[case] expected: i64) {
878        assert_eq!(
879            JulianDay::from_gregorian(ymd.0, ymd.1, ymd.2),
880            JulianDay(expected)
881        );
882    }
883
884    #[template]
885    #[rstest]
886    #[case::max_year((i64::MAX, 1, 1))]
887    #[case::min_year((i64::MIN, 1, 1))]
888    #[case::min_year_march((i64::MIN, 3, 1))]
889    #[case::around_min_possible_year(
890        ((i64::MIN) / 365, 1, 1),
891    )]
892    #[case::around_max_possible_year(
893        ((i64::MAX - 31 - 337 - 1_721_119) / 365, 12, 31),
894    )]
895    fn test_julian_days_from_gregorian_error_template(#[case] ymd: (i64, u8, u8)) {}
896
897    #[should_panic(expected = "Overflow calculating julian day from gregorian date")]
898    #[apply(test_julian_days_from_gregorian_error_template)]
899    fn test_julian_days_from_gregorian_then_panic(ymd: (i64, u8, u8)) {
900        JulianDay::from_gregorian(ymd.0, ymd.1, ymd.2);
901    }
902
903    #[apply(test_julian_days_from_gregorian_error_template)]
904    fn test_julian_days_try_from_gregorian_then_none(ymd: (i64, u8, u8)) {
905        assert_eq!(JulianDay::try_from_gregorian(ymd.0, ymd.1, ymd.2), None);
906    }
907
908    #[rstest]
909    #[should_panic(expected = "Invalid month: Valid range is 1 <= month <= 12")]
910    #[case::illegal_month_too_low((0, 0, 1))]
911    #[should_panic(expected = "Invalid month: Valid range is 1 <= month <= 12")]
912    #[case::illegal_month_too_high((0, 13, 1))]
913    #[should_panic(expected = "Invalid day: Valid range is 1 <= day <= 31")]
914    #[case::illegal_day_too_low((0, 1, 0))]
915    #[should_panic(expected = "Invalid day: Valid range is 1 <= day <= 31")]
916    #[case::illegal_day_too_high((0, 1, 32))]
917    fn test_julian_days_try_from_gregorian_with_illegal_argument_then_panic(
918        #[case] ymd: (i64, u8, u8),
919    ) {
920        JulianDay::try_from_gregorian(ymd.0, ymd.1, ymd.2);
921    }
922
923    #[test]
924    fn test_julian_days_as_days() {
925        assert_eq!(JulianDay(1).as_days(), 1);
926    }
927
928    #[test]
929    #[cfg_attr(miri, ignore)] // test takes too long with miri
930    fn test_julian_days_from_and_to_gregorian_brute_force_2000() {
931        for y in -2000..2000 {
932            for m in 1..=12 {
933                for d in 1..=28 {
934                    let jd = JulianDay::from_gregorian(y, m, d);
935                    assert_eq!(jd.to_gregorian().unwrap(), (y, m, d));
936                }
937            }
938        }
939    }
940
941    #[rstest]
942    #[case::barely_below_max_possible(i64::MAX / 101, (250_027_078_488_026, 1, 22))]
943    fn test_julian_days_to_gregorian(#[case] jd: i64, #[case] expected: (i64, u8, u8)) {
944        assert_eq!(JulianDay(jd).to_gregorian(), Some(expected));
945    }
946
947    #[rstest]
948    #[case::min(i64::MIN)]
949    #[case::max(i64::MAX)]
950    #[case::barely_above_max_possible(i64::MAX / 100)]
951    fn test_julian_days_to_gregorian_then_none(#[case] jd: i64) {
952        assert_eq!(JulianDay(jd).to_gregorian(), None);
953    }
954
955    #[template]
956    #[rstest]
957    #[case::zero(0, 0, 0)]
958    #[case::one(0, 1, 1)]
959    #[case::minus_one(0, -1, -1)]
960    #[case::one_zero(1, 0, 1)]
961    #[case::one_one(1, 1, 2)]
962    #[case::minus_one_one(-1, -1, -2)]
963    #[case::min(i64::MIN, 0, i64::MIN)]
964    #[case::max(0, i64::MAX, i64::MAX)]
965    #[case::min_plus_max(i64::MIN, i64::MAX, -1)]
966    fn test_julian_days_arithmetic_template(
967        #[case] lhs: i64,
968        #[case] rhs: i64,
969        #[case] expected: i64,
970    ) {
971    }
972
973    #[apply(test_julian_days_arithmetic_template)]
974    fn test_julian_days_checked_add(lhs: i64, rhs: i64, expected: i64) {
975        assert_eq!(
976            JulianDay(lhs).checked_add(JulianDay(rhs)),
977            Some(JulianDay(expected))
978        );
979        assert_eq!(
980            JulianDay(rhs).checked_add(JulianDay(lhs)),
981            Some(JulianDay(expected))
982        );
983    }
984
985    #[rstest]
986    #[case::one(1, i64::MAX)]
987    #[case::minus_one(-1, i64::MIN)]
988    fn test_julian_days_checked_add_then_none(#[case] jd: i64, #[case] add: i64) {
989        assert_eq!(JulianDay(jd).checked_add(JulianDay(add)), None);
990    }
991
992    #[apply(test_julian_days_arithmetic_template)]
993    fn test_julian_days_checked_sub(lhs: i64, rhs: i64, expected: i64) {
994        assert_eq!(
995            JulianDay(lhs).checked_sub(JulianDay(-rhs)),
996            Some(JulianDay(expected))
997        );
998    }
999
1000    #[test]
1001    fn test_julian_days_checked_sub_then_none() {
1002        assert_eq!(JulianDay(i64::MIN).checked_sub(JulianDay(1)), None);
1003    }
1004
1005    #[apply(test_julian_days_arithmetic_template)]
1006    fn test_julian_days_checked_add_days(lhs: i64, rhs: i64, expected: i64) {
1007        assert_eq!(
1008            JulianDay(lhs).checked_add_days(rhs),
1009            Some(JulianDay(expected))
1010        );
1011        assert_eq!(
1012            JulianDay(rhs).checked_add_days(lhs),
1013            Some(JulianDay(expected))
1014        );
1015    }
1016
1017    #[test]
1018    fn test_julian_checked_add_days_then_none() {
1019        assert_eq!(JulianDay(i64::MAX).checked_add_days(1), None);
1020    }
1021
1022    #[apply(test_julian_days_arithmetic_template)]
1023    fn test_julian_days_checked_sub_days(lhs: i64, rhs: i64, expected: i64) {
1024        assert_eq!(
1025            JulianDay(lhs).checked_sub_days(-rhs),
1026            Some(JulianDay(expected))
1027        );
1028    }
1029
1030    #[test]
1031    fn test_julian_days_checked_sub_days_then_none() {
1032        assert_eq!(JulianDay(i64::MIN).checked_sub_days(1), None);
1033    }
1034
1035    #[rstest]
1036    #[case::some(
1037        (0, 1, 1, 23, 59, 59, 0),
1038        JulianDay(1_721_060),
1039        86399 * 1_000_000_000
1040    )]
1041    fn test_date_time_from_gregorian_date_time(
1042        #[case] date_time: (i64, u8, u8, u8, u8, u8, u32),
1043        #[case] expected_days: JulianDay,
1044        #[case] expected_time: u64,
1045    ) {
1046        let actual = DateTime::from_gregorian_date_time(
1047            date_time.0,
1048            date_time.1,
1049            date_time.2,
1050            date_time.3,
1051            date_time.4,
1052            date_time.5,
1053            date_time.6,
1054        );
1055        let expected = DateTime {
1056            days: expected_days,
1057            time: expected_time,
1058        };
1059        assert_eq!(actual, expected);
1060    }
1061
1062    #[test]
1063    fn test_date_time_to_gregorian_date() {
1064        let date_time = DateTime::UNIX_EPOCH;
1065        assert_eq!(date_time.to_gregorian_date(), Some((1970, 1, 1)));
1066    }
1067
1068    #[test]
1069    fn test_date_time_to_gregorian_date_time() {
1070        let date_time = DateTime::UNIX_EPOCH;
1071        assert_eq!(
1072            date_time.to_gregorian_date_time(),
1073            Some((1970, 1, 1, 0, 0, 0, 0))
1074        );
1075    }
1076
1077    #[test]
1078    fn test_date_time_as_julian_day() {
1079        let date_time = DateTime::UNIX_EPOCH;
1080        assert_eq!(date_time.as_julian_day(), JulianDay(2_440_588));
1081    }
1082
1083    #[rstest]
1084    #[case::min((0, 0, 0, 0))]
1085    #[case::one_nano((0, 0, 0, 1))]
1086    #[case::one_sec((0, 0, 1, 0))]
1087    #[case::one_min((0, 1, 0, 0))]
1088    #[case::one_hour((1, 0, 0, 0))]
1089    #[case::all_one((1, 1, 1, 1))]
1090    #[case::max((23, 59, 59, 999_999_999))]
1091    fn test_date_time_as_hmsn(#[case] hmsn: (u8, u8, u8, u32)) {
1092        assert_eq!(
1093            DateTime::from_gregorian_date_time(1, 1, 1, hmsn.0, hmsn.1, hmsn.2, hmsn.3).as_hmsn(),
1094            hmsn
1095        );
1096    }
1097
1098    #[template]
1099    #[rstest]
1100    #[case::zero(
1101        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1102        Duration::ZERO,
1103        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0)
1104    )]
1105    #[case::max(
1106        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1107        Duration::MAX,
1108        DateTime::from_gregorian_date_time(584_554_051_223, 11, 9, 7, 0, 15, 999_999_999)
1109    )]
1110    #[case::min(
1111        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1112        Duration::MIN,
1113        DateTime::from_gregorian_date_time(-584_554_047_284, 2, 23, 16, 59, 44, 1)
1114    )]
1115    #[case::leap_year_plus_something(
1116        DateTime::from_gregorian_date_time(1972, 1, 1, 0, 0, 0, 0),
1117        Duration::positive(100 * 60 * 60, 0),
1118        DateTime::from_gregorian_date_time(1972, 1, 5, 4, 0, 0, 0)
1119    )]
1120    #[case::leap_year_plus_days_until_end_of_feb(
1121        DateTime::from_gregorian_date_time(1972, 1, 1, 0, 0, 0, 0),
1122        Duration::positive(86400 * (29 + 30), 0),
1123        DateTime::from_gregorian_date_time(1972, 2, 29, 0, 0, 0, 0)
1124    )]
1125    #[case::with_high_hms(
1126        DateTime::from_gregorian_date_time(1972, 1, 1, 23, 59, 59, 999_999_999),
1127        Duration::positive(86399, 999_999_999),
1128        DateTime::from_gregorian_date_time(1972, 1, 2, 23, 59, 59, 999_999_998)
1129    )]
1130    #[case::nano_second(
1131        DateTime::from_gregorian_date_time(1972, 1, 1, 23, 59, 59, 999_999_999),
1132        Duration::positive(0, 1),
1133        DateTime::from_gregorian_date_time(1972, 1, 2, 0, 0, 0, 0)
1134    )]
1135    #[case::nano_second_year_overflow(
1136        DateTime::from_gregorian_date_time(1969, 12, 31, 23, 59, 59, 999_999_999),
1137        Duration::positive(0, 1),
1138        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0)
1139    )]
1140    #[case::day_year_overflow(
1141        DateTime::from_gregorian_date_time(1969, 12, 31, 23, 59, 59, 999_999_999),
1142        Duration::positive(86400, 0),
1143        DateTime::from_gregorian_date_time(1970, 1, 1, 23, 59, 59, 999_999_999)
1144    )]
1145    #[case::day_and_nano_year_overflow(
1146        DateTime::from_gregorian_date_time(1969, 12, 30, 23, 59, 59, 999_999_999),
1147        Duration::positive(86400, 1),
1148        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0)
1149    )]
1150    #[case::negative_nano_second(
1151        DateTime::from_gregorian_date_time(1972, 1, 1, 0, 0, 0, 0),
1152        Duration::negative(0, 1),
1153        DateTime::from_gregorian_date_time(1971, 12, 31, 23, 59, 59, 999_999_999)
1154    )]
1155    #[case::negative_nano_second_year_overflow(
1156        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1157        Duration::negative(0, 1),
1158        DateTime::from_gregorian_date_time(1969, 12, 31, 23, 59, 59, 999_999_999)
1159    )]
1160    #[case::negative_day(
1161        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1162        Duration::negative(86400, 0),
1163        DateTime::from_gregorian_date_time(1969, 12, 31, 0, 0, 0, 0)
1164    )]
1165    #[case::negative_day_and_nano(
1166        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1167        Duration::negative(86400, 1),
1168        DateTime::from_gregorian_date_time(1969, 12, 30, 23, 59, 59, 999_999_999)
1169    )]
1170    fn test_date_time_checked_add_sub_duration_template(
1171        #[case] datetime: DateTime,
1172        #[case] duration: Duration,
1173        #[case] expected: DateTime,
1174    ) {
1175    }
1176
1177    #[apply(test_date_time_checked_add_sub_duration_template)]
1178    fn test_date_time_checked_add_duration(
1179        datetime: DateTime,
1180        duration: Duration,
1181        expected: DateTime,
1182    ) {
1183        let new = datetime.checked_add_duration(&duration).unwrap();
1184        assert_eq!(
1185            new,
1186            expected,
1187            "as gregorian: {:?} {:?}",
1188            new.to_gregorian_date(),
1189            new.as_hmsn()
1190        );
1191    }
1192
1193    #[apply(test_date_time_checked_add_sub_duration_template)]
1194    fn test_date_time_checked_sub_duration(
1195        expected: DateTime,
1196        duration: Duration,
1197        datetime: DateTime,
1198    ) {
1199        let new = datetime.checked_sub_duration(&duration).unwrap();
1200        assert_eq!(
1201            new,
1202            expected,
1203            "as gregorian: {:?} {:?}",
1204            new.to_gregorian_date(),
1205            new.as_hmsn()
1206        );
1207    }
1208
1209    #[rstest]
1210    #[case::zero(
1211        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1212        (0, 0, 0),
1213        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1214    )]
1215    #[case::one_year(
1216        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1217        (1, 0, 0),
1218        DateTime::from_gregorian_date_time(1971, 1, 1, 0, 0, 0, 0),
1219    )]
1220    #[case::one_month(
1221        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1222        (0, 1, 0),
1223        DateTime::from_gregorian_date_time(1970, 2, 1, 0, 0, 0, 0),
1224    )]
1225    #[case::one_day(
1226        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1227        (0, 0, 1),
1228        DateTime::from_gregorian_date_time(1970, 1, 2, 0, 0, 0, 0),
1229    )]
1230    #[case::all_one(
1231        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1232        (1, 1, 1),
1233        DateTime::from_gregorian_date_time(1971, 2, 2, 0, 0, 0, 0),
1234    )]
1235    #[case::minus_one_year(
1236        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1237        (-1, 0, 0),
1238        DateTime::from_gregorian_date_time(1969, 1, 1, 0, 0, 0, 0),
1239    )]
1240    #[case::minus_one_month(
1241        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1242        (0, -1, 0),
1243        DateTime::from_gregorian_date_time(1969, 12, 1, 0, 0, 0, 0),
1244    )]
1245    #[case::minus_one_day(
1246        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1247        (0, 0, -1),
1248        DateTime::from_gregorian_date_time(1969, 12, 31, 0, 0, 0, 0),
1249    )]
1250    #[case::all_minus_one(
1251        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1252        (-1, -1, -1),
1253        DateTime::from_gregorian_date_time(1968, 11, 30, 0, 0, 0, 0),
1254    )]
1255    #[case::month_overflow(
1256        DateTime::from_gregorian_date_time(1970, 12, 1, 0, 0, 0, 0),
1257        (0, 11, 0),
1258        DateTime::from_gregorian_date_time(1971, 11, 1, 0, 0, 0, 0),
1259    )]
1260    #[case::month(
1261        DateTime::from_gregorian_date_time(1972, 2, 1, 0, 0, 0, 0),
1262        (0, 1, 0),
1263        DateTime::from_gregorian_date_time(1972, 3, 1, 0, 0, 0, 0),
1264    )]
1265    fn test_date_time_checked_add_gregorian(
1266        #[case] datetime: DateTime,
1267        #[case] ymd: (i64, i64, i64),
1268        #[case] expected: DateTime,
1269    ) {
1270        assert_eq!(
1271            datetime.checked_add_gregorian(ymd.0, ymd.1, ymd.2),
1272            Some(expected)
1273        );
1274    }
1275
1276    #[rstest]
1277    #[case::max_years(
1278        DateTime::from_gregorian_date_time(-4713, 11, 24, 0, 0, 0, 0),
1279        (i64::MAX, 0, 0),
1280    )]
1281    #[case::min_years(
1282        DateTime::from_gregorian_date_time(-4713, 11, 24, 0, 0, 0, 0),
1283        (i64::MIN, 0, 0),
1284    )]
1285    fn test_date_time_checked_add_gregorian_then_none(
1286        #[case] datetime: DateTime,
1287        #[case] ymd: (i64, i64, i64),
1288    ) {
1289        assert_eq!(datetime.checked_add_gregorian(ymd.0, ymd.1, ymd.2), None);
1290    }
1291
1292    #[rstest]
1293    #[case::one_nano(
1294        DateTime::from_gregorian_date_time(1970, 1, 2, 0, 0, 0, 2),
1295        DateTime::from_gregorian_date_time(1970, 1, 2, 0, 0, 0, 1),
1296        Duration::positive(0, 1)
1297    )]
1298    #[case::one_nano_when_year_overflow(
1299        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1300        DateTime::from_gregorian_date_time(1969, 12, 31, 23, 59, 59, 999_999_999),
1301        Duration::positive(0, 1)
1302    )]
1303    #[case::one_month_year_overflow(
1304        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1305        DateTime::from_gregorian_date_time(1969, 12, 1, 0, 0, 0, 0),
1306        Duration::positive(86400 * 31, 0)
1307    )]
1308    #[case::one_nano_negative(
1309        DateTime::from_gregorian_date_time(1970, 1, 2, 23, 59, 59, 0),
1310        DateTime::from_gregorian_date_time(1970, 1, 2, 23, 59, 59, 1),
1311        Duration::negative(0, 1)
1312    )]
1313    #[case::one_nano_negative(
1314        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1315        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 1),
1316        Duration::negative(0, 1)
1317    )]
1318    #[case::one_day_negative(
1319        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0),
1320        DateTime::from_gregorian_date_time(1970, 1, 2, 0, 0, 0, 0),
1321        Duration::negative(86400, 0)
1322    )]
1323    fn test_date_time_checked_sub(
1324        #[case] datetime: DateTime,
1325        #[case] rhs: DateTime,
1326        #[case] expected: Duration,
1327    ) {
1328        assert_eq!(datetime.duration_since(rhs), Some(expected));
1329    }
1330
1331    // SystemTime is not accurate on windows targets and results sometimes differ by 1 nano second
1332    #[cfg(not(target_os = "windows"))]
1333    #[rstest]
1334    #[case::unix_epoch(SystemTime::UNIX_EPOCH, DateTime::UNIX_EPOCH)]
1335    #[case::second_before_unix_epoch(
1336        SystemTime::UNIX_EPOCH - StdDuration::new(1, 0),
1337        DateTime::from_gregorian_date_time(1969, 12, 31, 23, 59, 59, 0)
1338    )]
1339    #[case::nano_before_unix_epoch(
1340        SystemTime::UNIX_EPOCH - StdDuration::new(0, 1),
1341        DateTime::from_gregorian_date_time(1969, 12, 31, 23, 59, 59, 999_999_999)
1342    )]
1343    #[case::second_and_nano_before_unix_epoch(
1344        SystemTime::UNIX_EPOCH - StdDuration::new(1, 1),
1345        DateTime::from_gregorian_date_time(1969, 12, 31, 23, 59, 58, 999_999_999)
1346    )]
1347    #[case::second_after_unix_epoch(
1348        SystemTime::UNIX_EPOCH + StdDuration::new(1, 0),
1349        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 1, 0)
1350    )]
1351    #[case::nano_after_unix_epoch(
1352        SystemTime::UNIX_EPOCH + StdDuration::new(0, 1),
1353        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 1)
1354    )]
1355    #[case::second_and_nano_after_unix_epoch(
1356        SystemTime::UNIX_EPOCH + StdDuration::new(1, 1),
1357        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 1, 1)
1358    )]
1359    fn test_date_time_now_utc(#[case] now: SystemTime, #[case] expected: DateTime) {
1360        assert_eq!(DateTime::now_utc_with_system_time(now), expected);
1361    }
1362
1363    #[test]
1364    fn test_date_time_now_utc_calls_now() {
1365        assert_eq!(DateTime::now_utc(), DateTime::UNIX_EPOCH);
1366    }
1367
1368    #[test]
1369    fn test_ordinal_to_month_lookup_table() {
1370        let mut ordinal = 0usize;
1371        for m in 0..=11usize {
1372            let days_of_month: usize =
1373                // m == 0 is March
1374                if m == 0 || m == 2 || m == 4 || m == 5 || m == 7 || m == 9 || m == 10 {
1375                    31
1376                } else if m == 1 || m == 3 || m == 6 || m == 8 {
1377                    30
1378                } else {
1379                    29
1380                };
1381            for _ in 1..=days_of_month {
1382                assert_eq!(
1383                    ORDINAL_TO_MONTH[ordinal],
1384                    u8::try_from((m + 2) % 12 + 1).unwrap()
1385                );
1386                ordinal += 1;
1387            }
1388        }
1389    }
1390
1391    #[cfg(any(feature = "time", feature = "chrono"))]
1392    #[template]
1393    #[rstest]
1394    #[case::year_zero(
1395        (0i32, 1, 1, 0, 0, 0, 0, 0i32),
1396        DateTime::from_gregorian_date_time(0, 1, 1, 0, 0, 0, 0)
1397    )]
1398    #[case::positive_offset(
1399        (0i32, 1, 1, 0, 0, 0, 0, 2i32 * 3600i32),
1400        DateTime::from_gregorian_date_time(0, 1, 1, 2, 0, 0, 0)
1401    )]
1402    #[case::max_positive_offset(
1403        (0i32, 1, 1, 0, 0, 0, 0, 86399i32),
1404        DateTime::from_gregorian_date_time(0, 1, 1, 23, 59, 59, 0)
1405    )]
1406    #[case::negative_offset(
1407        (0i32, 1, 1, 0, 0, 0, 0, -2i32 * 3600i32),
1408        DateTime::from_gregorian_date_time(-1, 12, 31, 22, 0, 0, 0)
1409    )]
1410    #[case::max_negative_offset(
1411        (0i32, 1, 1, 0, 0, 0, 0, -86399i32),
1412        DateTime::from_gregorian_date_time(-1, 12, 31, 0, 0, 1, 0)
1413    )]
1414    #[case::unix_epoch(
1415        (1970i32, 1, 1, 0, 0, 0, 0, 0i32),
1416        DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0)
1417    )]
1418    #[case::some_positive_year(
1419        (1453i32, 6, 23, 14, 57, 29, 123456789, 0i32),
1420        DateTime::from_gregorian_date_time(1453, 6, 23, 14, 57, 29, 123_456_789),
1421    )]
1422    #[case::some_negative_year(
1423        (-1453i32, 6, 23, 14, 57, 29, 123456789, 0i32),
1424        DateTime::from_gregorian_date_time(-1453, 6, 23, 14, 57, 29, 123_456_789),
1425    )]
1426    fn test_into_date_time_template(
1427        #[case] ymdhmsno: (i32, u8, u8, u8, u8, u8, u32, i32),
1428        #[case] date_time: DateTime,
1429    ) {
1430    }
1431
1432    #[cfg(feature = "time")]
1433    #[apply(test_into_date_time_template)]
1434    fn test_time_offset_date_time_into_date_time(
1435        ymdhmsno: (i32, u8, u8, u8, u8, u8, u32, i32),
1436        date_time: DateTime,
1437    ) {
1438        let offset_date = PrimitiveDateTime::new(
1439            TimeDate::from_calendar_date(ymdhmsno.0, ymdhmsno.1.try_into().unwrap(), ymdhmsno.2)
1440                .unwrap(),
1441            TimeTime::from_hms_nano(ymdhmsno.3, ymdhmsno.4, ymdhmsno.5, ymdhmsno.6).unwrap(),
1442        )
1443        .assume_utc()
1444        .to_offset(UtcOffset::from_whole_seconds(ymdhmsno.7).unwrap());
1445        assert_eq!(DateTime::from(offset_date), date_time);
1446    }
1447
1448    #[cfg(feature = "time")]
1449    #[rstest]
1450    #[case::max(999_999i32)]
1451    #[case::min(-999_999i32)]
1452    fn test_time_offset_date_time_min_max_into_date_time(#[case] year: i32) {
1453        let offset_date = PrimitiveDateTime::new(
1454            TimeDate::from_calendar_date(year, 12.try_into().unwrap(), 31).unwrap(),
1455            TimeTime::from_hms_nano(23, 59, 59, 999_999_999).unwrap(),
1456        )
1457        .assume_utc();
1458        assert_eq!(
1459            DateTime::from(offset_date),
1460            DateTime::from_gregorian_date_time(year.into(), 12, 31, 23, 59, 59, 999_999_999)
1461        );
1462    }
1463
1464    #[cfg(feature = "time")]
1465    #[test]
1466    fn test_time_primitive_date_time_into_date_time() {
1467        assert_eq!(
1468            DateTime::from(datetime!(0-1-1 00:00:00)),
1469            DateTime::from_gregorian_date_time(0, 1, 1, 0, 0, 0, 0)
1470        );
1471    }
1472
1473    #[cfg(feature = "chrono")]
1474    #[apply(test_into_date_time_template)]
1475    fn test_chrono_date_time_into_date_time(
1476        #[case] ymdhmsno: (i32, u8, u8, u8, u8, u8, u32, i32),
1477        #[case] date_time: DateTime,
1478    ) {
1479        let mut chrono_date = FixedOffset::west_opt(ymdhmsno.7)
1480            .unwrap()
1481            .with_ymd_and_hms(
1482                ymdhmsno.0,
1483                ymdhmsno.1.into(),
1484                ymdhmsno.2.into(),
1485                ymdhmsno.3.into(),
1486                ymdhmsno.4.into(),
1487                ymdhmsno.5.into(),
1488            )
1489            .unwrap();
1490        chrono_date += ChronoDuration::nanoseconds(ymdhmsno.6.into());
1491        assert_eq!(DateTime::from(chrono_date), date_time);
1492    }
1493
1494    #[cfg(feature = "chrono")]
1495    #[rstest]
1496    #[case::max((i32::MAX >> 13i32) - 1i32)]
1497    #[case::min((i32::MIN >> 14i32) + 1i32)]
1498    fn test_chrono_date_time_min_max_into_date_time(#[case] year: i32) {
1499        let mut chrono_date = FixedOffset::west_opt(0)
1500            .unwrap()
1501            .with_ymd_and_hms(year, 12, 31, 23, 59, 59)
1502            .unwrap();
1503        chrono_date += ChronoDuration::nanoseconds(999_999_999);
1504        assert_eq!(
1505            DateTime::from(chrono_date),
1506            DateTime::from_gregorian_date_time(year.into(), 12, 31, 23, 59, 59, 999_999_999)
1507        );
1508    }
1509}