Skip to main content

ai_chrono/
round.rs

1// This is a part of Chrono.
2// See README.md and LICENSE.txt for details.
3
4//! Functionality for rounding or truncating a `DateTime` by a `TimeDelta`.
5
6use crate::{DateTime, NaiveDateTime, TimeDelta, TimeZone, Timelike};
7use core::cmp::Ordering;
8use core::fmt;
9use core::ops::{Add, Sub};
10
11/// Extension trait for subsecond rounding or truncation to a maximum number
12/// of digits. Rounding can be used to decrease the error variance when
13/// serializing/persisting to lower precision. Truncation is the default
14/// behavior in Chrono display formatting.  Either can be used to guarantee
15/// equality (e.g. for testing) when round-tripping through a lower precision
16/// format.
17pub trait SubsecRound {
18    /// Return a copy rounded to the specified number of subsecond digits. With
19    /// 9 or more digits, self is returned unmodified. Halfway values are
20    /// rounded up (away from zero).
21    ///
22    /// # Example
23    /// ``` rust
24    /// # use ai_chrono::{SubsecRound, Timelike, NaiveDate};
25    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11)
26    ///     .unwrap()
27    ///     .and_hms_milli_opt(12, 0, 0, 154)
28    ///     .unwrap()
29    ///     .and_utc();
30    /// assert_eq!(dt.round_subsecs(2).nanosecond(), 150_000_000);
31    /// assert_eq!(dt.round_subsecs(1).nanosecond(), 200_000_000);
32    /// ```
33    fn round_subsecs(self, digits: u16) -> Self;
34
35    /// Return a copy truncated to the specified number of subsecond
36    /// digits. With 9 or more digits, self is returned unmodified.
37    ///
38    /// # Example
39    /// ``` rust
40    /// # use ai_chrono::{SubsecRound, Timelike, NaiveDate};
41    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11)
42    ///     .unwrap()
43    ///     .and_hms_milli_opt(12, 0, 0, 154)
44    ///     .unwrap()
45    ///     .and_utc();
46    /// assert_eq!(dt.trunc_subsecs(2).nanosecond(), 150_000_000);
47    /// assert_eq!(dt.trunc_subsecs(1).nanosecond(), 100_000_000);
48    /// ```
49    fn trunc_subsecs(self, digits: u16) -> Self;
50}
51
52impl<T> SubsecRound for T
53where
54    T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
55{
56    fn round_subsecs(self, digits: u16) -> T {
57        let span = span_for_digits(digits);
58        let delta_down = self.nanosecond() % span;
59        if delta_down > 0 {
60            let delta_up = span - delta_down;
61            if delta_up <= delta_down {
62                self + TimeDelta::nanoseconds(delta_up.into())
63            } else {
64                self - TimeDelta::nanoseconds(delta_down.into())
65            }
66        } else {
67            self // unchanged
68        }
69    }
70
71    fn trunc_subsecs(self, digits: u16) -> T {
72        let span = span_for_digits(digits);
73        let delta_down = self.nanosecond() % span;
74        if delta_down > 0 {
75            self - TimeDelta::nanoseconds(delta_down.into())
76        } else {
77            self // unchanged
78        }
79    }
80}
81
82// Return the maximum span in nanoseconds for the target number of digits.
83const fn span_for_digits(digits: u16) -> u32 {
84    // fast lookup form of: 10^(9-min(9,digits))
85    match digits {
86        0 => 1_000_000_000,
87        1 => 100_000_000,
88        2 => 10_000_000,
89        3 => 1_000_000,
90        4 => 100_000,
91        5 => 10_000,
92        6 => 1_000,
93        7 => 100,
94        8 => 10,
95        _ => 1,
96    }
97}
98
99/// Extension trait for rounding or truncating a DateTime by a TimeDelta.
100///
101/// # Limitations
102/// Both rounding and truncating are done via [`TimeDelta::num_nanoseconds`] and
103/// [`DateTime::timestamp_nanos_opt`]. This means that they will fail if either the
104/// `TimeDelta` or the `DateTime` are too big to represented as nanoseconds. They
105/// will also fail if the `TimeDelta` is bigger than the timestamp, negative or zero.
106pub trait DurationRound: Sized {
107    /// Error that can occur in rounding or truncating
108    type Err: core::error::Error;
109
110    /// Return a copy rounded by TimeDelta.
111    ///
112    /// # Example
113    /// ``` rust
114    /// # use ai_chrono::{DurationRound, TimeDelta, NaiveDate};
115    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11)
116    ///     .unwrap()
117    ///     .and_hms_milli_opt(12, 0, 0, 154)
118    ///     .unwrap()
119    ///     .and_utc();
120    /// assert_eq!(
121    ///     dt.duration_round(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
122    ///     "2018-01-11 12:00:00.150 UTC"
123    /// );
124    /// assert_eq!(
125    ///     dt.duration_round(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
126    ///     "2018-01-12 00:00:00 UTC"
127    /// );
128    /// ```
129    fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err>;
130
131    /// Return a copy truncated by TimeDelta.
132    ///
133    /// # Example
134    /// ``` rust
135    /// # use ai_chrono::{DurationRound, TimeDelta, NaiveDate};
136    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11)
137    ///     .unwrap()
138    ///     .and_hms_milli_opt(12, 0, 0, 154)
139    ///     .unwrap()
140    ///     .and_utc();
141    /// assert_eq!(
142    ///     dt.duration_trunc(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
143    ///     "2018-01-11 12:00:00.150 UTC"
144    /// );
145    /// assert_eq!(
146    ///     dt.duration_trunc(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
147    ///     "2018-01-11 00:00:00 UTC"
148    /// );
149    /// ```
150    fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err>;
151
152    /// Return a copy rounded **up** by TimeDelta.
153    ///
154    /// # Example
155    /// ``` rust
156    /// # use ai_chrono::{DurationRound, TimeDelta, NaiveDate};
157    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11)
158    ///     .unwrap()
159    ///     .and_hms_milli_opt(12, 0, 0, 154)
160    ///     .unwrap()
161    ///     .and_utc();
162    /// assert_eq!(
163    ///     dt.duration_round_up(TimeDelta::milliseconds(10)).unwrap().to_string(),
164    ///     "2018-01-11 12:00:00.160 UTC"
165    /// );
166    /// assert_eq!(
167    ///     dt.duration_round_up(TimeDelta::hours(1)).unwrap().to_string(),
168    ///     "2018-01-11 13:00:00 UTC"
169    /// );
170    ///
171    /// assert_eq!(
172    ///     dt.duration_round_up(TimeDelta::days(1)).unwrap().to_string(),
173    ///     "2018-01-12 00:00:00 UTC"
174    /// );
175    /// ```
176    fn duration_round_up(self, duration: TimeDelta) -> Result<Self, Self::Err>;
177}
178
179impl<Tz: TimeZone> DurationRound for DateTime<Tz> {
180    type Err = RoundingError;
181
182    fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err> {
183        duration_round(self.naive_local(), self, duration)
184    }
185
186    fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err> {
187        duration_trunc(self.naive_local(), self, duration)
188    }
189
190    fn duration_round_up(self, duration: TimeDelta) -> Result<Self, Self::Err> {
191        duration_round_up(self.naive_local(), self, duration)
192    }
193}
194
195impl DurationRound for NaiveDateTime {
196    type Err = RoundingError;
197
198    fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err> {
199        duration_round(self, self, duration)
200    }
201
202    fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err> {
203        duration_trunc(self, self, duration)
204    }
205
206    fn duration_round_up(self, duration: TimeDelta) -> Result<Self, Self::Err> {
207        duration_round_up(self, self, duration)
208    }
209}
210
211fn duration_round<T>(
212    naive: NaiveDateTime,
213    original: T,
214    duration: TimeDelta,
215) -> Result<T, RoundingError>
216where
217    T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
218{
219    if let Some(span) = duration.num_nanoseconds() {
220        if span <= 0 {
221            return Err(RoundingError::DurationExceedsLimit);
222        }
223        let stamp =
224            naive.and_utc().timestamp_nanos_opt().ok_or(RoundingError::TimestampExceedsLimit)?;
225        let delta_down = stamp % span;
226        if delta_down == 0 {
227            Ok(original)
228        } else {
229            let (delta_up, delta_down) = if delta_down < 0 {
230                (delta_down.abs(), span - delta_down.abs())
231            } else {
232                (span - delta_down, delta_down)
233            };
234            if delta_up <= delta_down {
235                Ok(original + TimeDelta::nanoseconds(delta_up))
236            } else {
237                Ok(original - TimeDelta::nanoseconds(delta_down))
238            }
239        }
240    } else {
241        Err(RoundingError::DurationExceedsLimit)
242    }
243}
244
245fn duration_trunc<T>(
246    naive: NaiveDateTime,
247    original: T,
248    duration: TimeDelta,
249) -> Result<T, RoundingError>
250where
251    T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
252{
253    if let Some(span) = duration.num_nanoseconds() {
254        if span <= 0 {
255            return Err(RoundingError::DurationExceedsLimit);
256        }
257        let stamp =
258            naive.and_utc().timestamp_nanos_opt().ok_or(RoundingError::TimestampExceedsLimit)?;
259        let delta_down = stamp % span;
260        match delta_down.cmp(&0) {
261            Ordering::Equal => Ok(original),
262            Ordering::Greater => Ok(original - TimeDelta::nanoseconds(delta_down)),
263            Ordering::Less => Ok(original - TimeDelta::nanoseconds(span - delta_down.abs())),
264        }
265    } else {
266        Err(RoundingError::DurationExceedsLimit)
267    }
268}
269
270fn duration_round_up<T>(
271    naive: NaiveDateTime,
272    original: T,
273    duration: TimeDelta,
274) -> Result<T, RoundingError>
275where
276    T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
277{
278    if let Some(span) = duration.num_nanoseconds() {
279        if span <= 0 {
280            return Err(RoundingError::DurationExceedsLimit);
281        }
282        let stamp =
283            naive.and_utc().timestamp_nanos_opt().ok_or(RoundingError::TimestampExceedsLimit)?;
284        let delta_down = stamp % span;
285        match delta_down.cmp(&0) {
286            Ordering::Equal => Ok(original),
287            Ordering::Greater => Ok(original + TimeDelta::nanoseconds(span - delta_down)),
288            Ordering::Less => Ok(original + TimeDelta::nanoseconds(delta_down.abs())),
289        }
290    } else {
291        Err(RoundingError::DurationExceedsLimit)
292    }
293}
294
295/// An error from rounding by `TimeDelta`
296///
297/// See: [`DurationRound`]
298#[derive(Debug, Clone, PartialEq, Eq, Copy)]
299#[cfg_attr(feature = "defmt", derive(defmt::Format))]
300pub enum RoundingError {
301    /// Error when the TimeDelta exceeds the TimeDelta from or until the Unix epoch.
302    ///
303    /// Note: this error is not produced anymore.
304    DurationExceedsTimestamp,
305
306    /// Error when `TimeDelta.num_nanoseconds` exceeds the limit.
307    ///
308    /// ``` rust
309    /// # use ai_chrono::{DurationRound, TimeDelta, RoundingError, NaiveDate};
310    /// let dt = NaiveDate::from_ymd_opt(2260, 12, 31)
311    ///     .unwrap()
312    ///     .and_hms_nano_opt(23, 59, 59, 1_75_500_000)
313    ///     .unwrap()
314    ///     .and_utc();
315    ///
316    /// assert_eq!(
317    ///     dt.duration_round(TimeDelta::try_days(300 * 365).unwrap()),
318    ///     Err(RoundingError::DurationExceedsLimit)
319    /// );
320    /// ```
321    DurationExceedsLimit,
322
323    /// Error when `DateTime.timestamp_nanos` exceeds the limit.
324    ///
325    /// ``` rust
326    /// # use ai_chrono::{DurationRound, TimeDelta, RoundingError, TimeZone, Utc};
327    /// let dt = Utc.with_ymd_and_hms(2300, 12, 12, 0, 0, 0).unwrap();
328    ///
329    /// assert_eq!(
330    ///     dt.duration_round(TimeDelta::try_days(1).unwrap()),
331    ///     Err(RoundingError::TimestampExceedsLimit)
332    /// );
333    /// ```
334    TimestampExceedsLimit,
335}
336
337impl fmt::Display for RoundingError {
338    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
339        match *self {
340            RoundingError::DurationExceedsTimestamp => {
341                write!(f, "duration in nanoseconds exceeds timestamp")
342            }
343            RoundingError::DurationExceedsLimit => {
344                write!(f, "duration exceeds num_nanoseconds limit")
345            }
346            RoundingError::TimestampExceedsLimit => {
347                write!(f, "timestamp exceeds num_nanoseconds limit")
348            }
349        }
350    }
351}
352
353impl core::error::Error for RoundingError {
354    #[allow(deprecated)]
355    fn description(&self) -> &str {
356        "error from rounding or truncating with DurationRound"
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::{DurationRound, RoundingError, SubsecRound, TimeDelta};
363    use crate::Timelike;
364    use crate::offset::{FixedOffset, TimeZone, Utc};
365    use crate::{DateTime, NaiveDate};
366
367    #[test]
368    fn test_round_subsecs() {
369        let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap();
370        let dt = pst
371            .from_local_datetime(
372                &NaiveDate::from_ymd_opt(2018, 1, 11)
373                    .unwrap()
374                    .and_hms_nano_opt(10, 5, 13, 84_660_684)
375                    .unwrap(),
376            )
377            .unwrap();
378
379        assert_eq!(dt.round_subsecs(10), dt);
380        assert_eq!(dt.round_subsecs(9), dt);
381        assert_eq!(dt.round_subsecs(8).nanosecond(), 84_660_680);
382        assert_eq!(dt.round_subsecs(7).nanosecond(), 84_660_700);
383        assert_eq!(dt.round_subsecs(6).nanosecond(), 84_661_000);
384        assert_eq!(dt.round_subsecs(5).nanosecond(), 84_660_000);
385        assert_eq!(dt.round_subsecs(4).nanosecond(), 84_700_000);
386        assert_eq!(dt.round_subsecs(3).nanosecond(), 85_000_000);
387        assert_eq!(dt.round_subsecs(2).nanosecond(), 80_000_000);
388        assert_eq!(dt.round_subsecs(1).nanosecond(), 100_000_000);
389
390        assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
391        assert_eq!(dt.round_subsecs(0).second(), 13);
392
393        let dt = Utc
394            .from_local_datetime(
395                &NaiveDate::from_ymd_opt(2018, 1, 11)
396                    .unwrap()
397                    .and_hms_nano_opt(10, 5, 27, 750_500_000)
398                    .unwrap(),
399            )
400            .unwrap();
401        assert_eq!(dt.round_subsecs(9), dt);
402        assert_eq!(dt.round_subsecs(4), dt);
403        assert_eq!(dt.round_subsecs(3).nanosecond(), 751_000_000);
404        assert_eq!(dt.round_subsecs(2).nanosecond(), 750_000_000);
405        assert_eq!(dt.round_subsecs(1).nanosecond(), 800_000_000);
406
407        assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
408        assert_eq!(dt.round_subsecs(0).second(), 28);
409    }
410
411    #[test]
412    fn test_round_leap_nanos() {
413        let dt = Utc
414            .from_local_datetime(
415                &NaiveDate::from_ymd_opt(2016, 12, 31)
416                    .unwrap()
417                    .and_hms_nano_opt(23, 59, 59, 1_750_500_000)
418                    .unwrap(),
419            )
420            .unwrap();
421        assert_eq!(dt.round_subsecs(9), dt);
422        assert_eq!(dt.round_subsecs(4), dt);
423        assert_eq!(dt.round_subsecs(2).nanosecond(), 1_750_000_000);
424        assert_eq!(dt.round_subsecs(1).nanosecond(), 1_800_000_000);
425        assert_eq!(dt.round_subsecs(1).second(), 59);
426
427        assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
428        assert_eq!(dt.round_subsecs(0).second(), 0);
429    }
430
431    #[test]
432    fn test_trunc_subsecs() {
433        let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap();
434        let dt = pst
435            .from_local_datetime(
436                &NaiveDate::from_ymd_opt(2018, 1, 11)
437                    .unwrap()
438                    .and_hms_nano_opt(10, 5, 13, 84_660_684)
439                    .unwrap(),
440            )
441            .unwrap();
442
443        assert_eq!(dt.trunc_subsecs(10), dt);
444        assert_eq!(dt.trunc_subsecs(9), dt);
445        assert_eq!(dt.trunc_subsecs(8).nanosecond(), 84_660_680);
446        assert_eq!(dt.trunc_subsecs(7).nanosecond(), 84_660_600);
447        assert_eq!(dt.trunc_subsecs(6).nanosecond(), 84_660_000);
448        assert_eq!(dt.trunc_subsecs(5).nanosecond(), 84_660_000);
449        assert_eq!(dt.trunc_subsecs(4).nanosecond(), 84_600_000);
450        assert_eq!(dt.trunc_subsecs(3).nanosecond(), 84_000_000);
451        assert_eq!(dt.trunc_subsecs(2).nanosecond(), 80_000_000);
452        assert_eq!(dt.trunc_subsecs(1).nanosecond(), 0);
453
454        assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0);
455        assert_eq!(dt.trunc_subsecs(0).second(), 13);
456
457        let dt = pst
458            .from_local_datetime(
459                &NaiveDate::from_ymd_opt(2018, 1, 11)
460                    .unwrap()
461                    .and_hms_nano_opt(10, 5, 27, 750_500_000)
462                    .unwrap(),
463            )
464            .unwrap();
465        assert_eq!(dt.trunc_subsecs(9), dt);
466        assert_eq!(dt.trunc_subsecs(4), dt);
467        assert_eq!(dt.trunc_subsecs(3).nanosecond(), 750_000_000);
468        assert_eq!(dt.trunc_subsecs(2).nanosecond(), 750_000_000);
469        assert_eq!(dt.trunc_subsecs(1).nanosecond(), 700_000_000);
470
471        assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0);
472        assert_eq!(dt.trunc_subsecs(0).second(), 27);
473    }
474
475    #[test]
476    fn test_trunc_leap_nanos() {
477        let dt = Utc
478            .from_local_datetime(
479                &NaiveDate::from_ymd_opt(2016, 12, 31)
480                    .unwrap()
481                    .and_hms_nano_opt(23, 59, 59, 1_750_500_000)
482                    .unwrap(),
483            )
484            .unwrap();
485        assert_eq!(dt.trunc_subsecs(9), dt);
486        assert_eq!(dt.trunc_subsecs(4), dt);
487        assert_eq!(dt.trunc_subsecs(2).nanosecond(), 1_750_000_000);
488        assert_eq!(dt.trunc_subsecs(1).nanosecond(), 1_700_000_000);
489        assert_eq!(dt.trunc_subsecs(1).second(), 59);
490
491        assert_eq!(dt.trunc_subsecs(0).nanosecond(), 1_000_000_000);
492        assert_eq!(dt.trunc_subsecs(0).second(), 59);
493    }
494
495    #[test]
496    fn test_duration_round() {
497        let dt = Utc
498            .from_local_datetime(
499                &NaiveDate::from_ymd_opt(2016, 12, 31)
500                    .unwrap()
501                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
502                    .unwrap(),
503            )
504            .unwrap();
505
506        assert_eq!(
507            dt.duration_round(TimeDelta::new(-1, 0).unwrap()),
508            Err(RoundingError::DurationExceedsLimit)
509        );
510        assert_eq!(dt.duration_round(TimeDelta::zero()), Err(RoundingError::DurationExceedsLimit));
511
512        assert_eq!(
513            dt.duration_round(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
514            "2016-12-31 23:59:59.180 UTC"
515        );
516
517        // round up
518        let dt = Utc
519            .from_local_datetime(
520                &NaiveDate::from_ymd_opt(2012, 12, 12)
521                    .unwrap()
522                    .and_hms_milli_opt(18, 22, 30, 0)
523                    .unwrap(),
524            )
525            .unwrap();
526        assert_eq!(
527            dt.duration_round(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
528            "2012-12-12 18:25:00 UTC"
529        );
530        // round down
531        let dt = Utc
532            .from_local_datetime(
533                &NaiveDate::from_ymd_opt(2012, 12, 12)
534                    .unwrap()
535                    .and_hms_milli_opt(18, 22, 29, 999)
536                    .unwrap(),
537            )
538            .unwrap();
539        assert_eq!(
540            dt.duration_round(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
541            "2012-12-12 18:20:00 UTC"
542        );
543
544        assert_eq!(
545            dt.duration_round(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
546            "2012-12-12 18:20:00 UTC"
547        );
548        assert_eq!(
549            dt.duration_round(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
550            "2012-12-12 18:30:00 UTC"
551        );
552        assert_eq!(
553            dt.duration_round(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
554            "2012-12-12 18:00:00 UTC"
555        );
556        assert_eq!(
557            dt.duration_round(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
558            "2012-12-13 00:00:00 UTC"
559        );
560
561        // timezone east
562        let dt =
563            FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
564        assert_eq!(
565            dt.duration_round(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
566            "2020-10-28 00:00:00 +01:00"
567        );
568        assert_eq!(
569            dt.duration_round(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
570            "2020-10-29 00:00:00 +01:00"
571        );
572
573        // timezone west
574        let dt =
575            FixedOffset::west_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
576        assert_eq!(
577            dt.duration_round(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
578            "2020-10-28 00:00:00 -01:00"
579        );
580        assert_eq!(
581            dt.duration_round(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
582            "2020-10-29 00:00:00 -01:00"
583        );
584    }
585
586    #[test]
587    fn test_duration_round_naive() {
588        let dt = Utc
589            .from_local_datetime(
590                &NaiveDate::from_ymd_opt(2016, 12, 31)
591                    .unwrap()
592                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
593                    .unwrap(),
594            )
595            .unwrap()
596            .naive_utc();
597
598        assert_eq!(
599            dt.duration_round(TimeDelta::new(-1, 0).unwrap()),
600            Err(RoundingError::DurationExceedsLimit)
601        );
602        assert_eq!(dt.duration_round(TimeDelta::zero()), Err(RoundingError::DurationExceedsLimit));
603
604        assert_eq!(
605            dt.duration_round(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
606            "2016-12-31 23:59:59.180"
607        );
608
609        // round up
610        let dt = Utc
611            .from_local_datetime(
612                &NaiveDate::from_ymd_opt(2012, 12, 12)
613                    .unwrap()
614                    .and_hms_milli_opt(18, 22, 30, 0)
615                    .unwrap(),
616            )
617            .unwrap()
618            .naive_utc();
619        assert_eq!(
620            dt.duration_round(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
621            "2012-12-12 18:25:00"
622        );
623        // round down
624        let dt = Utc
625            .from_local_datetime(
626                &NaiveDate::from_ymd_opt(2012, 12, 12)
627                    .unwrap()
628                    .and_hms_milli_opt(18, 22, 29, 999)
629                    .unwrap(),
630            )
631            .unwrap()
632            .naive_utc();
633        assert_eq!(
634            dt.duration_round(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
635            "2012-12-12 18:20:00"
636        );
637
638        assert_eq!(
639            dt.duration_round(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
640            "2012-12-12 18:20:00"
641        );
642        assert_eq!(
643            dt.duration_round(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
644            "2012-12-12 18:30:00"
645        );
646        assert_eq!(
647            dt.duration_round(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
648            "2012-12-12 18:00:00"
649        );
650        assert_eq!(
651            dt.duration_round(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
652            "2012-12-13 00:00:00"
653        );
654    }
655
656    #[test]
657    fn test_duration_round_pre_epoch() {
658        let dt = Utc.with_ymd_and_hms(1969, 12, 12, 12, 12, 12).unwrap();
659        assert_eq!(
660            dt.duration_round(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
661            "1969-12-12 12:10:00 UTC"
662        );
663    }
664
665    #[test]
666    fn test_duration_trunc() {
667        let dt = Utc
668            .from_local_datetime(
669                &NaiveDate::from_ymd_opt(2016, 12, 31)
670                    .unwrap()
671                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
672                    .unwrap(),
673            )
674            .unwrap();
675
676        assert_eq!(
677            dt.duration_trunc(TimeDelta::new(-1, 0).unwrap()),
678            Err(RoundingError::DurationExceedsLimit)
679        );
680        assert_eq!(dt.duration_trunc(TimeDelta::zero()), Err(RoundingError::DurationExceedsLimit));
681
682        assert_eq!(
683            dt.duration_trunc(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
684            "2016-12-31 23:59:59.170 UTC"
685        );
686
687        // would round up
688        let dt = Utc
689            .from_local_datetime(
690                &NaiveDate::from_ymd_opt(2012, 12, 12)
691                    .unwrap()
692                    .and_hms_milli_opt(18, 22, 30, 0)
693                    .unwrap(),
694            )
695            .unwrap();
696        assert_eq!(
697            dt.duration_trunc(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
698            "2012-12-12 18:20:00 UTC"
699        );
700        // would round down
701        let dt = Utc
702            .from_local_datetime(
703                &NaiveDate::from_ymd_opt(2012, 12, 12)
704                    .unwrap()
705                    .and_hms_milli_opt(18, 22, 29, 999)
706                    .unwrap(),
707            )
708            .unwrap();
709        assert_eq!(
710            dt.duration_trunc(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
711            "2012-12-12 18:20:00 UTC"
712        );
713        assert_eq!(
714            dt.duration_trunc(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
715            "2012-12-12 18:20:00 UTC"
716        );
717        assert_eq!(
718            dt.duration_trunc(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
719            "2012-12-12 18:00:00 UTC"
720        );
721        assert_eq!(
722            dt.duration_trunc(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
723            "2012-12-12 18:00:00 UTC"
724        );
725        assert_eq!(
726            dt.duration_trunc(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
727            "2012-12-12 00:00:00 UTC"
728        );
729
730        // timezone east
731        let dt =
732            FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
733        assert_eq!(
734            dt.duration_trunc(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
735            "2020-10-27 00:00:00 +01:00"
736        );
737        assert_eq!(
738            dt.duration_trunc(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
739            "2020-10-22 00:00:00 +01:00"
740        );
741
742        // timezone west
743        let dt =
744            FixedOffset::west_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
745        assert_eq!(
746            dt.duration_trunc(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
747            "2020-10-27 00:00:00 -01:00"
748        );
749        assert_eq!(
750            dt.duration_trunc(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
751            "2020-10-22 00:00:00 -01:00"
752        );
753    }
754
755    #[test]
756    fn test_duration_trunc_naive() {
757        let dt = Utc
758            .from_local_datetime(
759                &NaiveDate::from_ymd_opt(2016, 12, 31)
760                    .unwrap()
761                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
762                    .unwrap(),
763            )
764            .unwrap()
765            .naive_utc();
766
767        assert_eq!(
768            dt.duration_trunc(TimeDelta::new(-1, 0).unwrap()),
769            Err(RoundingError::DurationExceedsLimit)
770        );
771        assert_eq!(dt.duration_trunc(TimeDelta::zero()), Err(RoundingError::DurationExceedsLimit));
772
773        assert_eq!(
774            dt.duration_trunc(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
775            "2016-12-31 23:59:59.170"
776        );
777
778        // would round up
779        let dt = Utc
780            .from_local_datetime(
781                &NaiveDate::from_ymd_opt(2012, 12, 12)
782                    .unwrap()
783                    .and_hms_milli_opt(18, 22, 30, 0)
784                    .unwrap(),
785            )
786            .unwrap()
787            .naive_utc();
788        assert_eq!(
789            dt.duration_trunc(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
790            "2012-12-12 18:20:00"
791        );
792        // would round down
793        let dt = Utc
794            .from_local_datetime(
795                &NaiveDate::from_ymd_opt(2012, 12, 12)
796                    .unwrap()
797                    .and_hms_milli_opt(18, 22, 29, 999)
798                    .unwrap(),
799            )
800            .unwrap()
801            .naive_utc();
802        assert_eq!(
803            dt.duration_trunc(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
804            "2012-12-12 18:20:00"
805        );
806        assert_eq!(
807            dt.duration_trunc(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
808            "2012-12-12 18:20:00"
809        );
810        assert_eq!(
811            dt.duration_trunc(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
812            "2012-12-12 18:00:00"
813        );
814        assert_eq!(
815            dt.duration_trunc(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
816            "2012-12-12 18:00:00"
817        );
818        assert_eq!(
819            dt.duration_trunc(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
820            "2012-12-12 00:00:00"
821        );
822    }
823
824    #[test]
825    fn test_duration_trunc_pre_epoch() {
826        let dt = Utc.with_ymd_and_hms(1969, 12, 12, 12, 12, 12).unwrap();
827        assert_eq!(
828            dt.duration_trunc(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
829            "1969-12-12 12:10:00 UTC"
830        );
831    }
832
833    #[test]
834    fn issue1010() {
835        let dt = DateTime::from_timestamp(-4_227_854_320, 678_774_288).unwrap();
836        let span = TimeDelta::microseconds(-7_019_067_213_869_040);
837        assert_eq!(dt.duration_trunc(span), Err(RoundingError::DurationExceedsLimit));
838
839        let dt = DateTime::from_timestamp(320_041_586, 920_103_021).unwrap();
840        let span = TimeDelta::nanoseconds(-8_923_838_508_697_114_584);
841        assert_eq!(dt.duration_round(span), Err(RoundingError::DurationExceedsLimit));
842
843        let dt = DateTime::from_timestamp(-2_621_440, 0).unwrap();
844        let span = TimeDelta::nanoseconds(-9_223_372_036_854_771_421);
845        assert_eq!(dt.duration_round(span), Err(RoundingError::DurationExceedsLimit));
846    }
847
848    #[test]
849    fn test_duration_trunc_close_to_epoch() {
850        let span = TimeDelta::try_minutes(15).unwrap();
851
852        let dt = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(0, 0, 15).unwrap();
853        assert_eq!(dt.duration_trunc(span).unwrap().to_string(), "1970-01-01 00:00:00");
854
855        let dt = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap().and_hms_opt(23, 59, 45).unwrap();
856        assert_eq!(dt.duration_trunc(span).unwrap().to_string(), "1969-12-31 23:45:00");
857    }
858
859    #[test]
860    fn test_duration_round_close_to_epoch() {
861        let span = TimeDelta::try_minutes(15).unwrap();
862
863        let dt = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(0, 0, 15).unwrap();
864        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00");
865
866        let dt = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap().and_hms_opt(23, 59, 45).unwrap();
867        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00");
868    }
869
870    #[test]
871    fn test_duration_round_close_to_min_max() {
872        let span = TimeDelta::nanoseconds(i64::MAX);
873
874        let dt = DateTime::from_timestamp_nanos(i64::MIN / 2 - 1);
875        assert_eq!(
876            dt.duration_round(span).unwrap().to_string(),
877            "1677-09-21 00:12:43.145224193 UTC"
878        );
879
880        let dt = DateTime::from_timestamp_nanos(i64::MIN / 2 + 1);
881        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00 UTC");
882
883        let dt = DateTime::from_timestamp_nanos(i64::MAX / 2 + 1);
884        assert_eq!(
885            dt.duration_round(span).unwrap().to_string(),
886            "2262-04-11 23:47:16.854775807 UTC"
887        );
888
889        let dt = DateTime::from_timestamp_nanos(i64::MAX / 2 - 1);
890        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00 UTC");
891    }
892
893    #[test]
894    fn test_duration_round_up() {
895        let dt = NaiveDate::from_ymd_opt(2016, 12, 31)
896            .unwrap()
897            .and_hms_nano_opt(23, 59, 59, 175_500_000)
898            .unwrap()
899            .and_utc();
900
901        assert_eq!(
902            dt.duration_round_up(TimeDelta::new(-1, 0).unwrap()),
903            Err(RoundingError::DurationExceedsLimit)
904        );
905
906        assert_eq!(
907            dt.duration_round_up(TimeDelta::zero()),
908            Err(RoundingError::DurationExceedsLimit)
909        );
910
911        assert_eq!(dt.duration_round_up(TimeDelta::MAX), Err(RoundingError::DurationExceedsLimit));
912
913        assert_eq!(
914            dt.duration_round_up(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
915            "2016-12-31 23:59:59.180 UTC"
916        );
917
918        // round up
919        let dt = NaiveDate::from_ymd_opt(2012, 12, 12)
920            .unwrap()
921            .and_hms_milli_opt(18, 22, 30, 0)
922            .unwrap()
923            .and_utc();
924
925        assert_eq!(
926            dt.duration_round_up(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
927            "2012-12-12 18:25:00 UTC"
928        );
929
930        assert_eq!(
931            dt.duration_round_up(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
932            "2012-12-12 18:30:00 UTC"
933        );
934        assert_eq!(
935            dt.duration_round_up(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
936            "2012-12-12 18:30:00 UTC"
937        );
938        assert_eq!(
939            dt.duration_round_up(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
940            "2012-12-12 19:00:00 UTC"
941        );
942        assert_eq!(
943            dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
944            "2012-12-13 00:00:00 UTC"
945        );
946
947        // timezone east
948        let dt =
949            FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
950        assert_eq!(
951            dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
952            "2020-10-28 00:00:00 +01:00"
953        );
954        assert_eq!(
955            dt.duration_round_up(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
956            "2020-10-29 00:00:00 +01:00"
957        );
958
959        // timezone west
960        let dt =
961            FixedOffset::west_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
962        assert_eq!(
963            dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
964            "2020-10-28 00:00:00 -01:00"
965        );
966        assert_eq!(
967            dt.duration_round_up(TimeDelta::try_weeks(1).unwrap()).unwrap().to_string(),
968            "2020-10-29 00:00:00 -01:00"
969        );
970    }
971
972    #[test]
973    fn test_duration_round_up_naive() {
974        let dt = NaiveDate::from_ymd_opt(2016, 12, 31)
975            .unwrap()
976            .and_hms_nano_opt(23, 59, 59, 175_500_000)
977            .unwrap();
978
979        assert_eq!(
980            dt.duration_round_up(TimeDelta::new(-1, 0).unwrap()),
981            Err(RoundingError::DurationExceedsLimit)
982        );
983        assert_eq!(
984            dt.duration_round_up(TimeDelta::zero()),
985            Err(RoundingError::DurationExceedsLimit)
986        );
987
988        assert_eq!(dt.duration_round_up(TimeDelta::MAX), Err(RoundingError::DurationExceedsLimit));
989
990        assert_eq!(
991            dt.duration_round_up(TimeDelta::try_milliseconds(10).unwrap()).unwrap().to_string(),
992            "2016-12-31 23:59:59.180"
993        );
994
995        let dt = Utc
996            .from_local_datetime(
997                &NaiveDate::from_ymd_opt(2012, 12, 12)
998                    .unwrap()
999                    .and_hms_milli_opt(18, 22, 30, 0)
1000                    .unwrap(),
1001            )
1002            .unwrap()
1003            .naive_utc();
1004        assert_eq!(
1005            dt.duration_round_up(TimeDelta::try_minutes(5).unwrap()).unwrap().to_string(),
1006            "2012-12-12 18:25:00"
1007        );
1008        assert_eq!(
1009            dt.duration_round_up(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
1010            "2012-12-12 18:30:00"
1011        );
1012        assert_eq!(
1013            dt.duration_round_up(TimeDelta::try_minutes(30).unwrap()).unwrap().to_string(),
1014            "2012-12-12 18:30:00"
1015        );
1016        assert_eq!(
1017            dt.duration_round_up(TimeDelta::try_hours(1).unwrap()).unwrap().to_string(),
1018            "2012-12-12 19:00:00"
1019        );
1020        assert_eq!(
1021            dt.duration_round_up(TimeDelta::try_days(1).unwrap()).unwrap().to_string(),
1022            "2012-12-13 00:00:00"
1023        );
1024    }
1025
1026    #[test]
1027    fn test_duration_round_up_pre_epoch() {
1028        let dt = Utc.with_ymd_and_hms(1969, 12, 12, 12, 12, 12).unwrap();
1029        assert_eq!(
1030            dt.duration_round_up(TimeDelta::try_minutes(10).unwrap()).unwrap().to_string(),
1031            "1969-12-12 12:20:00 UTC"
1032        );
1033
1034        let time_delta = TimeDelta::minutes(30);
1035        assert_eq!(
1036            DateTime::UNIX_EPOCH.duration_round_up(time_delta).unwrap().to_string(),
1037            "1970-01-01 00:00:00 UTC"
1038        )
1039    }
1040
1041    #[test]
1042    fn test_duration_round_up_close_to_min_max() {
1043        let mut dt = NaiveDate::from_ymd_opt(2012, 12, 12)
1044            .unwrap()
1045            .and_hms_milli_opt(18, 22, 30, 0)
1046            .unwrap()
1047            .and_utc();
1048
1049        let span = TimeDelta::nanoseconds(i64::MAX);
1050
1051        assert_eq!(
1052            dt.duration_round_up(span).unwrap().to_string(),
1053            DateTime::from_timestamp_nanos(i64::MAX).to_string()
1054        );
1055
1056        dt = DateTime::UNIX_EPOCH + TimeDelta::nanoseconds(1);
1057        assert_eq!(dt.duration_round_up(span).unwrap(), DateTime::from_timestamp_nanos(i64::MAX));
1058
1059        let dt = DateTime::from_timestamp_nanos(1);
1060        assert_eq!(
1061            dt.duration_round_up(span).unwrap().to_string(),
1062            "2262-04-11 23:47:16.854775807 UTC"
1063        );
1064
1065        let dt = DateTime::from_timestamp_nanos(-1);
1066        assert_eq!(dt.duration_round_up(span).unwrap(), DateTime::UNIX_EPOCH);
1067
1068        // Rounds to 1677-09-21 00:12:43.145224193 UTC if at i64::MIN.
1069        // because i64::MIN is 1677-09-21 00:12:43.145224192 UTC.
1070        //
1071        //                                                v
1072        // We add 2 to get to 1677-09-21 00:12:43.145224194 UTC
1073        // this issue is because abs(i64::MIN) == i64::MAX + 1
1074        let dt = DateTime::from_timestamp_nanos(i64::MIN + 2);
1075        assert_eq!(dt.duration_round_up(span).unwrap(), DateTime::UNIX_EPOCH);
1076    }
1077}