radix_common/time/
utc_date_time.rs

1use crate::internal_prelude::*;
2use crate::time::constants::*;
3use crate::time::Instant;
4use sbor::rust::fmt;
5use sbor::rust::fmt::Display;
6use sbor::rust::num::ParseIntError;
7use sbor::rust::str::FromStr;
8use sbor::rust::vec::Vec;
9
10const UNIX_EPOCH_YEAR: u32 = 1970;
11
12const SECONDS_IN_A_NON_LEAP_YEAR: i64 = 365 * 24 * 60 * 60;
13const SECONDS_IN_A_LEAP_YEAR: i64 = 366 * 24 * 60 * 60;
14
15const DAYS_PER_4Y: i64 = 365 * 4 + 1;
16const DAYS_PER_100Y: i64 = 365 * 100 + 24;
17const DAYS_PER_400Y: i64 = 365 * 400 + 97;
18
19const LEAP_YEAR_DAYS_IN_MONTHS: [u8; 12] = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
20
21/// A shift (in seconds) from the Unix epoch (1970-01-01 00:00:00)
22/// to a base date that is a multiple of a 400-year leap cycle.
23///
24/// Used in `Instant` -> `UtcDateTime` conversion.
25///
26/// The date we're using is `2000-03-01 00:00:00`, for two reasons:
27/// 1. It's a multiple of 400, to make it easier to work with leap years
28/// 2. We're also shifting the month to 1st March, so that
29///    the extra day on leap years is added to the last month (Feb),
30///    not in the middle of a year (makes some calculations easier)
31const SHIFT_FROM_UNIX_TIME_TO_MARCH_Y2K: i64 = 946684800 + 86400 * (31 + 29);
32
33/// A minimum Unix timestamp value that is supported by `UtcDateTime`.
34///
35/// This value corresponds to a date of `1-1-1 00:00:00`. Year `0` isn't allowed.
36const MIN_SUPPORTED_TIMESTAMP: i64 = -62135596800;
37
38/// A maximum Unix timestamp value that is supported by `UtcDateTime`.
39///
40/// This value corresponds to a date of `4294967295-12-31 23:59:59`,
41/// where year `4294967295` equals `u32::MAX`.
42const MAX_SUPPORTED_TIMESTAMP: i64 = 135536014634284799;
43
44#[derive(Sbor, PartialEq, Eq, Copy, Clone, Debug)]
45pub enum DateTimeError {
46    InvalidYear,
47    InvalidMonth,
48    InvalidDayOfMonth,
49    InvalidHour,
50    InvalidMinute,
51    InvalidSecond,
52    InstantIsOutOfRange,
53}
54
55#[cfg(not(feature = "alloc"))]
56impl std::error::Error for DateTimeError {}
57
58impl fmt::Display for DateTimeError {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        match self {
61            DateTimeError::InvalidYear =>
62                write!(f, "Invalid year. Expected a value strictly greater than 0"),
63            DateTimeError::InvalidMonth =>
64                write!(f, "Invalid month. Expected a value between 1 (inclusive) and 12 (inclusive)"),
65            DateTimeError::InvalidDayOfMonth =>
66                write!(f, "Invalid day of month. Expected a value between 1 (inclusive) and, depending on a month, 28, 29 (Feb on a leap year), 30 or 31 (inclusive)"),
67            DateTimeError::InvalidHour =>
68                write!(f, "Invalid hour. Expected a value between 0 (inclusive) and 23 (inclusive)"),
69            DateTimeError::InvalidMinute =>
70                write!(f, "Invalid minute. Expected a value between 0 (inclusive) and 59 (inclusive)"),
71            DateTimeError::InvalidSecond =>
72                write!(f, "Invalid second. Expected a value between 0 (inclusive) and 59 (inclusive)"),
73            DateTimeError::InstantIsOutOfRange =>
74                write!(f, "Instant out of supported range [{}, {}]", MIN_SUPPORTED_TIMESTAMP, MAX_SUPPORTED_TIMESTAMP),
75        }
76    }
77}
78
79/// A `UtcDateTime` represents a Unix timestamp on the UTC Calendar.
80///
81/// It can represent any date between `1-1-1 00:00:00` and `[u32::MAX]-12-31 23:59:59` (inclusive).
82///
83/// In terms of indexing:
84/// * Months and days of month are 1-based (i.e. `Dec 15th 2022` corresponds to `2022-12-15`).
85/// * Hour, minute and second are 0-based, based on the 24-hour clock.
86///   Midnight is represented as `00:00:00` and `23:59:59` is the last second before midnight.
87///   Following Unix timestamp conventions, leap seconds are not supported.
88///
89/// `UtcDateTime` supports methods for easy conversion to and from the [`Instant`](super::Instant) type, which
90/// can be queried from the Radix Engine.
91#[derive(
92    PartialEq,
93    Eq,
94    Copy,
95    Clone,
96    Debug,
97    Categorize,
98    Encode,
99    Decode,
100    BasicDescribe,
101    PartialOrd,
102    Ord,
103    Hash,
104)]
105pub struct UtcDateTime {
106    year: u32,
107    month: u8,
108    day_of_month: u8,
109    hour: u8,
110    minute: u8,
111    second: u8,
112}
113
114impl Describe<ScryptoCustomTypeKind> for UtcDateTime {
115    const TYPE_ID: RustTypeId =
116        RustTypeId::WellKnown(well_known_scrypto_custom_types::UTC_DATE_TIME_TYPE);
117
118    fn type_data() -> TypeData<ScryptoCustomTypeKind, RustTypeId> {
119        well_known_scrypto_custom_types::utc_date_time_type_data()
120    }
121}
122
123impl UtcDateTime {
124    /// Creates a `UtcDateTime` from its individual components.
125    ///
126    /// Months and days of month are 1-based, with the following limits:
127    /// * Valid year range is: `1` to `u32::MAX`inclusive.
128    /// * Valid month values are: `1` to `12` inclusive.
129    /// * Valid day of month values are: `1` to `{28, 29, 30, 31}` inclusive.
130    ///   The upper limit depends on the month and year, as per the Gregorian calendar.
131    ///
132    /// An attempt to create an invalid date (e.g. `2022-04-31` or `2023-02-29`)
133    /// will result in a corresponding `Err(DateTimeError)`.
134    ///
135    /// Hour, minute and second constitute a 0-based 24-hour clock.
136    /// Midnight is represented as `00:00:00` and `23:59:59` is the maximum possible time (a second before midnight).
137    /// Following Unix time conventions, leap seconds are not represented in this system.
138    pub fn new(
139        year: u32,
140        month: u8,
141        day_of_month: u8,
142        hour: u8,
143        minute: u8,
144        second: u8,
145    ) -> Result<Self, DateTimeError> {
146        if year == 0 {
147            return Err(DateTimeError::InvalidYear);
148        }
149
150        if !(1..=12).contains(&month) {
151            return Err(DateTimeError::InvalidMonth);
152        }
153
154        if day_of_month < 1 ||
155            // Check leap year Feb + all other months
156            day_of_month > LEAP_YEAR_DAYS_IN_MONTHS[(month - 1) as usize] ||
157            // Check Feb on non-leap years
158            (!Self::is_leap_year(year) && month == 2 && day_of_month > 28)
159        {
160            return Err(DateTimeError::InvalidDayOfMonth);
161        }
162
163        if hour > 23 {
164            return Err(DateTimeError::InvalidHour);
165        }
166
167        if minute > 59 {
168            return Err(DateTimeError::InvalidMinute);
169        }
170
171        if second > 59 {
172            return Err(DateTimeError::InvalidSecond);
173        }
174
175        Ok(Self {
176            year,
177            month,
178            day_of_month,
179            hour,
180            minute,
181            second,
182        })
183    }
184
185    /// Creates a `UtcDateTime` from an [`Instant`](super::Instant).
186    ///
187    /// The minimum supported `seconds_since_unix_epoch` value is `-62135596800` (corresponding to `1-1-1 00:00:00`)
188    /// and the maximum value is `135536014634284799` (corresponding to `[u32::Max]-12-31 23:59:59`).
189    pub fn from_instant(instant: &Instant) -> Result<Self, DateTimeError> {
190        if instant.seconds_since_unix_epoch < MIN_SUPPORTED_TIMESTAMP
191            || instant.seconds_since_unix_epoch > MAX_SUPPORTED_TIMESTAMP
192        {
193            return Err(DateTimeError::InstantIsOutOfRange);
194        }
195
196        // First, convert the base to 1 Mar 2000 for easier leap year calculation
197        let secs_since_march_y2k =
198            instant.seconds_since_unix_epoch - SHIFT_FROM_UNIX_TIME_TO_MARCH_Y2K;
199
200        let mut days_since_march_y2k = secs_since_march_y2k / SECONDS_IN_A_DAY;
201        let mut remaining_secs = secs_since_march_y2k % SECONDS_IN_A_DAY;
202        if remaining_secs < 0 {
203            remaining_secs += SECONDS_IN_A_DAY;
204            days_since_march_y2k -= 1;
205        }
206
207        let mut num_400_year_cycles = days_since_march_y2k / DAYS_PER_400Y;
208        let mut remaining_days = days_since_march_y2k % DAYS_PER_400Y;
209        if remaining_days < 0 {
210            remaining_days += DAYS_PER_400Y;
211            num_400_year_cycles -= 1;
212        }
213
214        let mut num_100_year_cycles = remaining_days / DAYS_PER_100Y;
215        if num_100_year_cycles == 4 {
216            // Subtract one due 400 years cycle (400 years cycle fits 4 100y cycles)
217            num_100_year_cycles -= 1;
218        }
219        remaining_days -= num_100_year_cycles * DAYS_PER_100Y;
220
221        let mut num_4_year_cycles = remaining_days / DAYS_PER_4Y;
222        if num_4_year_cycles == 25 {
223            // Subtract one due 100 years cycle (100 years cycle fits 25 4y cycles)
224            num_4_year_cycles -= 1;
225        }
226        remaining_days -= num_4_year_cycles * DAYS_PER_4Y;
227
228        let mut remaining_years = remaining_days / 365;
229        if remaining_years == 4 {
230            // Subtract one due to four years cycle
231            remaining_years -= 1;
232        }
233        remaining_days -= remaining_years * 365;
234
235        let mut year =
236            remaining_years +
237                4 * num_4_year_cycles +
238                100 * num_100_year_cycles +
239                400 * num_400_year_cycles
240                + 2000 /* Add the base year (after shifting) */;
241
242        let mut days_in_months_starting_on_march = LEAP_YEAR_DAYS_IN_MONTHS;
243        days_in_months_starting_on_march.rotate_left(2);
244
245        let mut month = 0;
246        while days_in_months_starting_on_march[month] as i64 <= remaining_days {
247            remaining_days -= days_in_months_starting_on_march[month] as i64;
248            month += 1;
249        }
250
251        // Shift the month back to Jan
252        // Handle any overflows, in case we need to add another year after shifting
253        month += 2;
254        if month >= 12 {
255            month -= 12;
256            year += 1;
257        }
258
259        // Shift 0-based month to 1-based
260        month += 1;
261
262        // Shift 0-based day of month to 1-based
263        let day_of_month = remaining_days + 1;
264
265        let hour = remaining_secs / SECONDS_IN_AN_HOUR;
266        let minute = remaining_secs / SECONDS_IN_A_MINUTE % SECONDS_IN_A_MINUTE;
267        let second = remaining_secs % SECONDS_IN_A_MINUTE;
268
269        Ok(Self {
270            year: u32::try_from(year).expect("year overflow"),
271            month: u8::try_from(month).expect("month overflow"),
272            day_of_month: u8::try_from(day_of_month).expect("day_of_month overflow"),
273            hour: u8::try_from(hour).expect("hour overflow"),
274            minute: u8::try_from(minute).expect("minute overflow"),
275            second: u8::try_from(second).expect("second overflow"),
276        })
277    }
278
279    /// Creates an [`Instant`](super::Instant) from this `UtcDateTime`
280    pub fn to_instant(&self) -> Instant {
281        let is_leap_year = Self::is_leap_year(self.year);
282
283        // Separating pre-1970 (negative) and 1970 onward (non-negative)
284        // timestamps for better readability
285        if self.year >= UNIX_EPOCH_YEAR {
286            // Count ended leap and non-leap years between Unix epoch and dt
287            let num_leap_years_between_self_and_epoch =
288                (Self::num_leap_years_up_to_exclusive(self.year)
289                    - Self::num_leap_years_up_to_exclusive(UNIX_EPOCH_YEAR + 1))
290                    as i64;
291
292            let num_non_leap_years_between_self_and_epoch =
293                (self.year - UNIX_EPOCH_YEAR) as i64 - num_leap_years_between_self_and_epoch;
294
295            // Given the number of ended leap and non-leap years, count the elapsed seconds
296            let seconds_up_to_the_beginning_of_the_year = (num_non_leap_years_between_self_and_epoch
297                * SECONDS_IN_A_NON_LEAP_YEAR)
298                + (num_leap_years_between_self_and_epoch * SECONDS_IN_A_LEAP_YEAR);
299
300            // Count the seconds for ended months
301            let mut seconds_in_ended_months = 0;
302            for n in 0..self.month - 1 {
303                seconds_in_ended_months +=
304                    LEAP_YEAR_DAYS_IN_MONTHS[n as usize] as i64 * SECONDS_IN_A_DAY;
305                // Subtract one day for any non-leap Feb
306                if !is_leap_year && n == 1 {
307                    seconds_in_ended_months -= SECONDS_IN_A_DAY;
308                }
309            }
310
311            // Sum it all together and add remaining days, hours, minutes and seconds
312            let total_seconds_since_unix_epoch = seconds_up_to_the_beginning_of_the_year
313                + seconds_in_ended_months
314                + (self.day_of_month - 1) as i64 * SECONDS_IN_A_DAY
315                + self.hour as i64 * SECONDS_IN_AN_HOUR
316                + self.minute as i64 * SECONDS_IN_A_MINUTE
317                + self.second as i64;
318
319            Instant::new(total_seconds_since_unix_epoch)
320        } else {
321            // Similarly, count the number of leap and non-leap years...
322            let num_leap_years_between_epoch_and_self =
323                (Self::num_leap_years_up_to_exclusive(UNIX_EPOCH_YEAR)
324                    - Self::num_leap_years_up_to_exclusive(self.year + 1)) as i64;
325
326            let num_non_leap_days_between_epoch_and_self =
327                (UNIX_EPOCH_YEAR - self.year - 1) as i64 - num_leap_years_between_epoch_and_self;
328
329            // ...and use it to count the number of seconds up (down?) to the end of year,
330            // remember, we're counting backwards!
331            let seconds_up_to_the_end_of_the_year = (num_non_leap_days_between_epoch_and_self
332                * SECONDS_IN_A_NON_LEAP_YEAR)
333                + (num_leap_years_between_epoch_and_self * SECONDS_IN_A_LEAP_YEAR);
334
335            // We're counting backwards so add seconds for any non-started months
336            let mut seconds_in_non_started_months = 0;
337            let mut curr_month = 11;
338            while curr_month > self.month - 1 {
339                seconds_in_non_started_months +=
340                    LEAP_YEAR_DAYS_IN_MONTHS[curr_month as usize] as i64 * SECONDS_IN_A_DAY;
341                // Subtract one day for any non-leap Feb
342                if !is_leap_year && curr_month == 1 {
343                    seconds_in_non_started_months -= SECONDS_IN_A_DAY;
344                }
345                curr_month -= 1;
346            }
347
348            let mut days_in_month = LEAP_YEAR_DAYS_IN_MONTHS[self.month as usize - 1] as i64;
349            if !is_leap_year && curr_month == 1 {
350                days_in_month -= 1;
351            }
352
353            // Add the remaining days of the current month
354            let remaining_days_in_month = days_in_month - self.day_of_month as i64;
355
356            let total_seconds_since_unix_epoch = seconds_up_to_the_end_of_the_year
357                + seconds_in_non_started_months
358                + remaining_days_in_month * SECONDS_IN_A_DAY
359                + (23 - self.hour) as i64 * SECONDS_IN_AN_HOUR
360                + (59 - self.minute) as i64 * SECONDS_IN_A_MINUTE
361                + (59 - self.second) as i64;
362
363            Instant::new(
364                // Pre-1970 timestamps are negative
365                -total_seconds_since_unix_epoch - 1,
366            )
367        }
368    }
369
370    fn num_leap_years_up_to_exclusive(year: u32) -> u32 {
371        let prev = year - 1;
372        (prev / 4) - (prev / 100) + (prev / 400)
373    }
374
375    fn is_leap_year(year: u32) -> bool {
376        year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
377    }
378
379    pub fn year(&self) -> u32 {
380        self.year
381    }
382
383    pub fn month(&self) -> u8 {
384        self.month
385    }
386
387    pub fn day_of_month(&self) -> u8 {
388        self.day_of_month
389    }
390
391    pub fn hour(&self) -> u8 {
392        self.hour
393    }
394
395    pub fn minute(&self) -> u8 {
396        self.minute
397    }
398
399    pub fn second(&self) -> u8 {
400        self.second
401    }
402
403    pub fn add_days(&self, days_to_add: i64) -> Option<UtcDateTime> {
404        self.to_instant()
405            .add_days(days_to_add)
406            .and_then(|i| Self::from_instant(&i).ok())
407    }
408
409    pub fn add_hours(&self, hours_to_add: i64) -> Option<UtcDateTime> {
410        self.to_instant()
411            .add_hours(hours_to_add)
412            .and_then(|i| Self::from_instant(&i).ok())
413    }
414
415    pub fn add_minutes(&self, minutes_to_add: i64) -> Option<UtcDateTime> {
416        self.to_instant()
417            .add_minutes(minutes_to_add)
418            .and_then(|i| Self::from_instant(&i).ok())
419    }
420
421    pub fn add_seconds(&self, seconds_to_add: i64) -> Option<UtcDateTime> {
422        self.to_instant()
423            .add_seconds(seconds_to_add)
424            .and_then(|i| Self::from_instant(&i).ok())
425    }
426}
427
428impl TryFrom<Instant> for UtcDateTime {
429    type Error = DateTimeError;
430    fn try_from(instant: Instant) -> Result<Self, Self::Error> {
431        UtcDateTime::from_instant(&instant)
432    }
433}
434
435impl From<UtcDateTime> for Instant {
436    fn from(dt: UtcDateTime) -> Self {
437        dt.to_instant()
438    }
439}
440
441impl Display for UtcDateTime {
442    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443        write!(
444            f,
445            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
446            self.year, self.month, self.day_of_month, self.hour, self.minute, self.second,
447        )
448    }
449}
450
451#[derive(Debug, Clone)]
452pub enum ParseUtcDateTimeError {
453    InvalidFormat,
454    DateTimeError(DateTimeError),
455}
456
457#[cfg(not(feature = "alloc"))]
458impl std::error::Error for ParseUtcDateTimeError {}
459
460impl fmt::Display for ParseUtcDateTimeError {
461    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
462        match self {
463            ParseUtcDateTimeError::InvalidFormat => write!(f, "Invalid date time format. Must be in ISO-8601 format, up to second precision, such as '2011-12-03T10:15:30Z'."),
464            ParseUtcDateTimeError::DateTimeError(e) => fmt::Display::fmt(e, f)
465        }
466    }
467}
468
469impl From<ParseIntError> for ParseUtcDateTimeError {
470    fn from(_value: ParseIntError) -> Self {
471        Self::InvalidFormat
472    }
473}
474
475impl From<DateTimeError> for ParseUtcDateTimeError {
476    fn from(value: DateTimeError) -> Self {
477        Self::DateTimeError(value)
478    }
479}
480
481impl FromStr for UtcDateTime {
482    type Err = ParseUtcDateTimeError;
483
484    fn from_str(s: &str) -> Result<Self, Self::Err> {
485        let chars: Vec<char> = s.chars().collect();
486        if chars.len() == 20
487            && chars[4] == '-'
488            && chars[7] == '-'
489            && chars[10] == 'T'
490            && chars[13] == ':'
491            && chars[16] == ':'
492            && chars[19] == 'Z'
493        {
494            Ok(UtcDateTime::new(
495                s[0..4].parse::<u32>()?,
496                s[5..7].parse::<u8>()?,
497                s[8..10].parse::<u8>()?,
498                s[11..13].parse::<u8>()?,
499                s[14..16].parse::<u8>()?,
500                s[17..19].parse::<u8>()?,
501            )?)
502        } else {
503            Err(ParseUtcDateTimeError::InvalidFormat)
504        }
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511    use crate::time::utc_date_time::MAX_SUPPORTED_TIMESTAMP;
512    use radix_common::time::utc_date_time::MIN_SUPPORTED_TIMESTAMP;
513
514    #[test]
515    pub fn test_to_string() {
516        let expected_str = "2023-01-27T12:17:25Z";
517        let instant = Instant {
518            seconds_since_unix_epoch: 1674821845,
519        };
520        let date_time = UtcDateTime::from_instant(&instant).unwrap();
521        assert_eq!(date_time.to_string(), expected_str);
522        assert_eq!(format!("{}", date_time), expected_str);
523        assert_eq!(
524            UtcDateTime::from_str(expected_str).unwrap().to_instant(),
525            instant
526        );
527    }
528
529    #[test]
530    pub fn test_instant_date_time_conversions() {
531        let test_data = vec![
532            (MIN_SUPPORTED_TIMESTAMP, [1, 1, 1, 0, 0, 0]),
533            (-62104060800, [2, 1, 1, 0, 0, 0]),
534            (-62035804801, [4, 2, 29, 23, 59, 59]),
535            (-30578688000, [1001, 1, 1, 0, 0, 0]),
536            (-5233420801, [1804, 2, 28, 23, 59, 59]),
537            (-2147483648, [1901, 12, 13, 20, 45, 52]),
538            (-58147200, [1968, 2, 28, 00, 00, 00]),
539            (-58147199, [1968, 2, 28, 00, 00, 1]),
540            (-58060801, [1968, 2, 28, 23, 59, 59]),
541            (-58060800, [1968, 2, 29, 00, 00, 00]),
542            (-1, [1969, 12, 31, 23, 59, 59]),
543            (0, [1970, 1, 1, 0, 0, 0]),
544            (1, [1970, 1, 1, 0, 0, 1]),
545            (365 * 24 * 60 * 60, [1971, 1, 1, 0, 0, 0]),
546            (366 * 24 * 60 * 60, [1971, 1, 2, 0, 0, 0]),
547            (395 * 24 * 60 * 60, [1971, 1, 31, 0, 0, 0]),
548            (396 * 24 * 60 * 60, [1971, 2, 1, 0, 0, 0]),
549            (68180521, [1972, 2, 29, 3, 2, 1]),
550            (194476271, [1976, 2, 29, 21, 11, 11]),
551            (446947199, [1984, 2, 29, 23, 59, 59]),
552            (447012859, [1984, 3, 1, 18, 14, 19]),
553            (951865200, [2000, 2, 29, 23, 0, 0]),
554            (951868800, [2000, 3, 1, 0, 0, 0]),
555            (1109548800, [2005, 2, 28, 0, 0, 0]),
556            (1670420819, [2022, 12, 7, 13, 46, 59]),
557            (1835395199, [2028, 2, 28, 23, 59, 59]),
558            (1835395200, [2028, 2, 29, 00, 00, 00]),
559            (1835481599, [2028, 2, 29, 23, 59, 59]),
560            (1835481600, [2028, 3, 1, 00, 00, 00]),
561            (51442991999, [3600, 2, 29, 23, 59, 59]),
562            (64065686400, [4000, 2, 29, 0, 0, 0]),
563            (569034205384, [20001, 12, 23, 1, 3, 4]),
564            (MAX_SUPPORTED_TIMESTAMP, [u32::MAX, 12, 31, 23, 59, 59]),
565        ];
566
567        for (timestamp, dt_components) in test_data {
568            let expected_dt = UtcDateTime::from(dt_components);
569            let expected_instant = Instant::new(timestamp);
570
571            assert_eq!(expected_dt.to_instant(), expected_instant);
572            assert_eq!(
573                UtcDateTime::from_instant(&expected_instant).unwrap(),
574                expected_dt
575            );
576        }
577
578        // Some error assertions (no unexpected panics)
579        assert!(UtcDateTime::from_instant(&Instant::new(MAX_SUPPORTED_TIMESTAMP + 1)).is_err());
580        assert!(UtcDateTime::from_instant(&Instant::new(MIN_SUPPORTED_TIMESTAMP - 1)).is_err());
581        assert!(UtcDateTime::from_instant(&Instant::new(i64::MIN)).is_err());
582        assert!(UtcDateTime::from_instant(&Instant::new(i64::MAX)).is_err());
583    }
584
585    #[test]
586    pub fn test_date_time_add_xyz_methods() {
587        assert_dates(
588            [2022, 1, 1, 12, 12, 12],
589            |dt| dt.add_days(2),
590            [2022, 1, 3, 12, 12, 12],
591        );
592
593        assert_dates(
594            [1968, 2, 29, 00, 00, 00],
595            |dt| dt.add_days(2).and_then(|dt| dt.add_hours(2)),
596            [1968, 3, 2, 2, 00, 00],
597        );
598
599        assert_dates(
600            [2028, 2, 29, 23, 59, 59],
601            |dt| dt.add_hours(49).and_then(|dt| dt.add_seconds(1)),
602            [2028, 3, 3, 1, 00, 00],
603        );
604
605        assert_dates(
606            [2022, 1, 1, 12, 12, 12],
607            |dt| dt.add_days(2),
608            [2022, 1, 3, 12, 12, 12],
609        );
610
611        assert_dates(
612            [1, 1, 1, 0, 0, 0],
613            |dt| dt.add_minutes(1000 * 365 * 23 * 60),
614            [958, 9, 12, 16, 0, 0],
615        );
616
617        assert_dates(
618            [1970, 1, 1, 0, 0, 0],
619            |dt| dt.add_days(365),
620            [1971, 1, 1, 0, 0, 0],
621        );
622
623        assert_dates(
624            [1971, 1, 1, 0, 0, 0],
625            |dt| dt.add_days(-365),
626            [1970, 1, 1, 0, 0, 0],
627        );
628
629        assert_dates(
630            [1968, 3, 1, 00, 00, 00],
631            |dt| dt.add_seconds(-1),
632            [1968, 2, 29, 23, 59, 59],
633        );
634
635        assert_fails([u32::MAX, 12, 31, 00, 00, 00], |dt| dt.add_days(1));
636        assert_fails([u32::MAX, 12, 31, 23, 59, 59], |dt| dt.add_hours(1));
637        assert_fails([u32::MAX, 12, 31, 23, 59, 59], |dt| dt.add_minutes(1));
638        assert_fails([u32::MAX, 12, 31, 23, 59, 59], |dt| dt.add_seconds(1));
639
640        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_days(i64::MAX));
641        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_hours(i64::MAX));
642        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_minutes(i64::MAX));
643        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_seconds(i64::MAX));
644
645        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_days(i64::MIN));
646        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_hours(i64::MIN));
647        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_minutes(i64::MIN));
648        assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_seconds(i64::MIN));
649    }
650
651    fn assert_dates<F: FnOnce(UtcDateTime) -> Option<UtcDateTime>>(
652        start: [u32; 6],
653        op: F,
654        expected_end: [u32; 6],
655    ) {
656        let start_dt = UtcDateTime::from(start);
657        let expected_end_dt = UtcDateTime::from(expected_end);
658        assert_eq!(op(start_dt).unwrap(), expected_end_dt);
659    }
660
661    fn assert_fails<F: FnOnce(UtcDateTime) -> Option<UtcDateTime>>(start: [u32; 6], op: F) {
662        let start_dt = UtcDateTime::from(start);
663        assert!(op(start_dt).is_none());
664    }
665
666    impl From<[u32; 6]> for UtcDateTime {
667        fn from(dt: [u32; 6]) -> UtcDateTime {
668            UtcDateTime::new(
669                dt[0],
670                dt[1] as u8,
671                dt[2] as u8,
672                dt[3] as u8,
673                dt[4] as u8,
674                dt[5] as u8,
675            )
676            .unwrap()
677        }
678    }
679}