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