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