1use 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#[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 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 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 pub const fn as_days(self) -> i64 {
181 self.0
182 }
183
184 #[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 let full_centuries = floor_div(c, 3_652_425);
207 let days_centuries = full_centuries - floor_div(full_centuries, 4);
210
211 let year = floor_div((100 * days_centuries).checked_add(c)?, 36525);
213
214 let ordinal =
217 u16::try_from(days_centuries + zero - 365 * year - floor_div(year, 4)).unwrap();
218
219 let month = ORDINAL_TO_MONTH[usize::from(ordinal - 1)];
222
223 let day = u8::try_from(ordinal - DAYS_IN_PREVIOUS_MONTH[usize::from(month - 1)]).unwrap();
226
227 if month < 3 {
229 Some((year + 1, month, day))
230 } else {
231 Some((year, month, day))
232 }
233 }
234
235 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 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 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 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#[cfg_attr(feature = "chrono", doc = "```rust")]
345#[cfg_attr(not(feature = "chrono"), doc = "```rust,ignore")]
346#[cfg_attr(feature = "time", doc = "```rust")]
371#[cfg_attr(not(feature = "time"), doc = "```rust,ignore")]
372#[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 pub const UNIX_EPOCH: Self = Self::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
399
400 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 pub fn to_gregorian_date(&self) -> Option<(i64, u8, u8)> {
481 self.days.to_gregorian()
482 }
483
484 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 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 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 pub fn now_utc() -> Self {
590 Self::now_utc_with_system_time(util::now())
591 }
592
593 #[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 let nanos = i64::try_from(duration.as_nanos()).unwrap();
614
615 let time = i64::try_from(self.time).unwrap() + nanos;
617 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 pub fn checked_sub_duration(self, duration: &Duration) -> Option<Self> {
655 self.checked_add_duration(&duration.neg())
656 }
657
658 #[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 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 #[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)] 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 #[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 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}