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::marker::Sized;
10use core::ops::{Add, Sub};
11
12/// Extension trait for subsecond rounding or truncation to a maximum number
13/// of digits. Rounding can be used to decrease the error variance when
14/// serializing/persisting to lower precision. Truncation is the default
15/// behavior in Chrono display formatting.  Either can be used to guarantee
16/// equality (e.g. for testing) when round-tripping through a lower precision
17/// format.
18pub trait SubsecRound {
19    /// Return a copy rounded to the specified number of subsecond digits. With
20    /// 9 or more digits, self is returned unmodified. Halfway values are
21    /// rounded up (away from zero).
22    ///
23    /// # Example
24    /// ``` rust
25    /// # use chrono::{SubsecRound, Timelike, Utc, NaiveDate};
26    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11).unwrap().and_hms_milli_opt(12, 0, 0, 154).unwrap().and_local_timezone(Utc).unwrap();
27    /// assert_eq!(dt.round_subsecs(2).nanosecond(), 150_000_000);
28    /// assert_eq!(dt.round_subsecs(1).nanosecond(), 200_000_000);
29    /// ```
30    fn round_subsecs(self, digits: u16) -> Self;
31
32    /// Return a copy truncated to the specified number of subsecond
33    /// digits. With 9 or more digits, self is returned unmodified.
34    ///
35    /// # Example
36    /// ``` rust
37    /// # use chrono::{SubsecRound, Timelike, Utc, NaiveDate};
38    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11).unwrap().and_hms_milli_opt(12, 0, 0, 154).unwrap().and_local_timezone(Utc).unwrap();
39    /// assert_eq!(dt.trunc_subsecs(2).nanosecond(), 150_000_000);
40    /// assert_eq!(dt.trunc_subsecs(1).nanosecond(), 100_000_000);
41    /// ```
42    fn trunc_subsecs(self, digits: u16) -> Self;
43}
44
45impl<T> SubsecRound for T
46where
47    T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
48{
49    fn round_subsecs(self, digits: u16) -> T {
50        let span = span_for_digits(digits);
51        let delta_down = self.nanosecond() % span;
52        if delta_down > 0 {
53            let delta_up = span - delta_down;
54            if delta_up <= delta_down {
55                self + TimeDelta::nanoseconds(delta_up.into())
56            } else {
57                self - TimeDelta::nanoseconds(delta_down.into())
58            }
59        } else {
60            self // unchanged
61        }
62    }
63
64    fn trunc_subsecs(self, digits: u16) -> T {
65        let span = span_for_digits(digits);
66        let delta_down = self.nanosecond() % span;
67        if delta_down > 0 {
68            self - TimeDelta::nanoseconds(delta_down.into())
69        } else {
70            self // unchanged
71        }
72    }
73}
74
75// Return the maximum span in nanoseconds for the target number of digits.
76const fn span_for_digits(digits: u16) -> u32 {
77    // fast lookup form of: 10^(9-min(9,digits))
78    match digits {
79        0 => 1_000_000_000,
80        1 => 100_000_000,
81        2 => 10_000_000,
82        3 => 1_000_000,
83        4 => 100_000,
84        5 => 10_000,
85        6 => 1_000,
86        7 => 100,
87        8 => 10,
88        _ => 1,
89    }
90}
91
92/// Extension trait for rounding or truncating a DateTime by a TimeDelta.
93///
94/// # Limitations
95/// Both rounding and truncating are done via [`TimeDelta::num_nanoseconds`] and
96/// [`DateTime::timestamp_nanos_opt`]. This means that they will fail if either the
97/// `TimeDelta` or the `DateTime` are too big to represented as nanoseconds. They
98/// will also fail if the `TimeDelta` is bigger than the timestamp.
99pub trait DurationRound: Sized {
100    /// Error that can occur in rounding or truncating
101    #[cfg(feature = "std")]
102    type Err: std::error::Error;
103
104    /// Error that can occur in rounding or truncating
105    #[cfg(not(feature = "std"))]
106    type Err: fmt::Debug + fmt::Display;
107
108    /// Return a copy rounded by TimeDelta.
109    ///
110    /// # Example
111    /// ``` rust
112    /// # use chrono::{DurationRound, TimeDelta, Utc, NaiveDate};
113    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11).unwrap().and_hms_milli_opt(12, 0, 0, 154).unwrap().and_local_timezone(Utc).unwrap();
114    /// assert_eq!(
115    ///     dt.duration_round(TimeDelta::milliseconds(10)).unwrap().to_string(),
116    ///     "2018-01-11 12:00:00.150 UTC"
117    /// );
118    /// assert_eq!(
119    ///     dt.duration_round(TimeDelta::days(1)).unwrap().to_string(),
120    ///     "2018-01-12 00:00:00 UTC"
121    /// );
122    /// ```
123    fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err>;
124
125    /// Return a copy truncated by TimeDelta.
126    ///
127    /// # Example
128    /// ``` rust
129    /// # use chrono::{DurationRound, TimeDelta, Utc, NaiveDate};
130    /// let dt = NaiveDate::from_ymd_opt(2018, 1, 11).unwrap().and_hms_milli_opt(12, 0, 0, 154).unwrap().and_local_timezone(Utc).unwrap();
131    /// assert_eq!(
132    ///     dt.duration_trunc(TimeDelta::milliseconds(10)).unwrap().to_string(),
133    ///     "2018-01-11 12:00:00.150 UTC"
134    /// );
135    /// assert_eq!(
136    ///     dt.duration_trunc(TimeDelta::days(1)).unwrap().to_string(),
137    ///     "2018-01-11 00:00:00 UTC"
138    /// );
139    /// ```
140    fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err>;
141}
142
143impl<Tz: TimeZone> DurationRound for DateTime<Tz> {
144    type Err = RoundingError;
145
146    fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err> {
147        duration_round(self.naive_local(), self, duration)
148    }
149
150    fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err> {
151        duration_trunc(self.naive_local(), self, duration)
152    }
153}
154
155impl DurationRound for NaiveDateTime {
156    type Err = RoundingError;
157
158    fn duration_round(self, duration: TimeDelta) -> Result<Self, Self::Err> {
159        duration_round(self, self, duration)
160    }
161
162    fn duration_trunc(self, duration: TimeDelta) -> Result<Self, Self::Err> {
163        duration_trunc(self, self, duration)
164    }
165}
166
167fn duration_round<T>(
168    naive: NaiveDateTime,
169    original: T,
170    duration: TimeDelta,
171) -> Result<T, RoundingError>
172where
173    T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
174{
175    if let Some(span) = duration.num_nanoseconds() {
176        if span < 0 {
177            return Err(RoundingError::DurationExceedsLimit);
178        }
179        let stamp = naive.timestamp_nanos_opt().ok_or(RoundingError::TimestampExceedsLimit)?;
180        if span == 0 {
181            return Ok(original);
182        }
183        let delta_down = stamp % span;
184        if delta_down == 0 {
185            Ok(original)
186        } else {
187            let (delta_up, delta_down) = if delta_down < 0 {
188                (delta_down.abs(), span - delta_down.abs())
189            } else {
190                (span - delta_down, delta_down)
191            };
192            if delta_up <= delta_down {
193                Ok(original + TimeDelta::nanoseconds(delta_up))
194            } else {
195                Ok(original - TimeDelta::nanoseconds(delta_down))
196            }
197        }
198    } else {
199        Err(RoundingError::DurationExceedsLimit)
200    }
201}
202
203fn duration_trunc<T>(
204    naive: NaiveDateTime,
205    original: T,
206    duration: TimeDelta,
207) -> Result<T, RoundingError>
208where
209    T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
210{
211    if let Some(span) = duration.num_nanoseconds() {
212        if span < 0 {
213            return Err(RoundingError::DurationExceedsLimit);
214        }
215        let stamp = naive.timestamp_nanos_opt().ok_or(RoundingError::TimestampExceedsLimit)?;
216        let delta_down = stamp % span;
217        match delta_down.cmp(&0) {
218            Ordering::Equal => Ok(original),
219            Ordering::Greater => Ok(original - TimeDelta::nanoseconds(delta_down)),
220            Ordering::Less => Ok(original - TimeDelta::nanoseconds(span - delta_down.abs())),
221        }
222    } else {
223        Err(RoundingError::DurationExceedsLimit)
224    }
225}
226
227/// An error from rounding by `TimeDelta`
228///
229/// See: [`DurationRound`]
230#[derive(Debug, Clone, PartialEq, Eq, Copy)]
231pub enum RoundingError {
232    /// Error when the TimeDelta exceeds the TimeDelta from or until the Unix epoch.
233    ///
234    /// Note: this error is not produced anymore.
235    DurationExceedsTimestamp,
236
237    /// Error when `TimeDelta.num_nanoseconds` exceeds the limit.
238    ///
239    /// ``` rust
240    /// # use chrono::{DurationRound, TimeDelta, RoundingError, Utc, NaiveDate};
241    /// let dt = NaiveDate::from_ymd_opt(2260, 12, 31).unwrap().and_hms_nano_opt(23, 59, 59, 1_75_500_000).unwrap().and_local_timezone(Utc).unwrap();
242    ///
243    /// assert_eq!(
244    ///     dt.duration_round(TimeDelta::days(300 * 365)),
245    ///     Err(RoundingError::DurationExceedsLimit)
246    /// );
247    /// ```
248    DurationExceedsLimit,
249
250    /// Error when `DateTime.timestamp_nanos` exceeds the limit.
251    ///
252    /// ``` rust
253    /// # use chrono::{DurationRound, TimeDelta, RoundingError, TimeZone, Utc};
254    /// let dt = Utc.with_ymd_and_hms(2300, 12, 12, 0, 0, 0).unwrap();
255    ///
256    /// assert_eq!(dt.duration_round(TimeDelta::days(1)), Err(RoundingError::TimestampExceedsLimit),);
257    /// ```
258    TimestampExceedsLimit,
259}
260
261impl fmt::Display for RoundingError {
262    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263        match *self {
264            RoundingError::DurationExceedsTimestamp => {
265                write!(f, "duration in nanoseconds exceeds timestamp")
266            }
267            RoundingError::DurationExceedsLimit => {
268                write!(f, "duration exceeds num_nanoseconds limit")
269            }
270            RoundingError::TimestampExceedsLimit => {
271                write!(f, "timestamp exceeds num_nanoseconds limit")
272            }
273        }
274    }
275}
276
277#[cfg(feature = "std")]
278impl std::error::Error for RoundingError {
279    #[allow(deprecated)]
280    fn description(&self) -> &str {
281        "error from rounding or truncating with DurationRound"
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::{DurationRound, RoundingError, SubsecRound, TimeDelta};
288    use crate::offset::{FixedOffset, TimeZone, Utc};
289    use crate::Timelike;
290    use crate::{NaiveDate, NaiveDateTime};
291
292    #[test]
293    fn test_round_subsecs() {
294        let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap();
295        let dt = pst
296            .from_local_datetime(
297                &NaiveDate::from_ymd_opt(2018, 1, 11)
298                    .unwrap()
299                    .and_hms_nano_opt(10, 5, 13, 84_660_684)
300                    .unwrap(),
301            )
302            .unwrap();
303
304        assert_eq!(dt.round_subsecs(10), dt);
305        assert_eq!(dt.round_subsecs(9), dt);
306        assert_eq!(dt.round_subsecs(8).nanosecond(), 84_660_680);
307        assert_eq!(dt.round_subsecs(7).nanosecond(), 84_660_700);
308        assert_eq!(dt.round_subsecs(6).nanosecond(), 84_661_000);
309        assert_eq!(dt.round_subsecs(5).nanosecond(), 84_660_000);
310        assert_eq!(dt.round_subsecs(4).nanosecond(), 84_700_000);
311        assert_eq!(dt.round_subsecs(3).nanosecond(), 85_000_000);
312        assert_eq!(dt.round_subsecs(2).nanosecond(), 80_000_000);
313        assert_eq!(dt.round_subsecs(1).nanosecond(), 100_000_000);
314
315        assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
316        assert_eq!(dt.round_subsecs(0).second(), 13);
317
318        let dt = Utc
319            .from_local_datetime(
320                &NaiveDate::from_ymd_opt(2018, 1, 11)
321                    .unwrap()
322                    .and_hms_nano_opt(10, 5, 27, 750_500_000)
323                    .unwrap(),
324            )
325            .unwrap();
326        assert_eq!(dt.round_subsecs(9), dt);
327        assert_eq!(dt.round_subsecs(4), dt);
328        assert_eq!(dt.round_subsecs(3).nanosecond(), 751_000_000);
329        assert_eq!(dt.round_subsecs(2).nanosecond(), 750_000_000);
330        assert_eq!(dt.round_subsecs(1).nanosecond(), 800_000_000);
331
332        assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
333        assert_eq!(dt.round_subsecs(0).second(), 28);
334    }
335
336    #[test]
337    fn test_round_leap_nanos() {
338        let dt = Utc
339            .from_local_datetime(
340                &NaiveDate::from_ymd_opt(2016, 12, 31)
341                    .unwrap()
342                    .and_hms_nano_opt(23, 59, 59, 1_750_500_000)
343                    .unwrap(),
344            )
345            .unwrap();
346        assert_eq!(dt.round_subsecs(9), dt);
347        assert_eq!(dt.round_subsecs(4), dt);
348        assert_eq!(dt.round_subsecs(2).nanosecond(), 1_750_000_000);
349        assert_eq!(dt.round_subsecs(1).nanosecond(), 1_800_000_000);
350        assert_eq!(dt.round_subsecs(1).second(), 59);
351
352        assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
353        assert_eq!(dt.round_subsecs(0).second(), 0);
354    }
355
356    #[test]
357    fn test_trunc_subsecs() {
358        let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap();
359        let dt = pst
360            .from_local_datetime(
361                &NaiveDate::from_ymd_opt(2018, 1, 11)
362                    .unwrap()
363                    .and_hms_nano_opt(10, 5, 13, 84_660_684)
364                    .unwrap(),
365            )
366            .unwrap();
367
368        assert_eq!(dt.trunc_subsecs(10), dt);
369        assert_eq!(dt.trunc_subsecs(9), dt);
370        assert_eq!(dt.trunc_subsecs(8).nanosecond(), 84_660_680);
371        assert_eq!(dt.trunc_subsecs(7).nanosecond(), 84_660_600);
372        assert_eq!(dt.trunc_subsecs(6).nanosecond(), 84_660_000);
373        assert_eq!(dt.trunc_subsecs(5).nanosecond(), 84_660_000);
374        assert_eq!(dt.trunc_subsecs(4).nanosecond(), 84_600_000);
375        assert_eq!(dt.trunc_subsecs(3).nanosecond(), 84_000_000);
376        assert_eq!(dt.trunc_subsecs(2).nanosecond(), 80_000_000);
377        assert_eq!(dt.trunc_subsecs(1).nanosecond(), 0);
378
379        assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0);
380        assert_eq!(dt.trunc_subsecs(0).second(), 13);
381
382        let dt = pst
383            .from_local_datetime(
384                &NaiveDate::from_ymd_opt(2018, 1, 11)
385                    .unwrap()
386                    .and_hms_nano_opt(10, 5, 27, 750_500_000)
387                    .unwrap(),
388            )
389            .unwrap();
390        assert_eq!(dt.trunc_subsecs(9), dt);
391        assert_eq!(dt.trunc_subsecs(4), dt);
392        assert_eq!(dt.trunc_subsecs(3).nanosecond(), 750_000_000);
393        assert_eq!(dt.trunc_subsecs(2).nanosecond(), 750_000_000);
394        assert_eq!(dt.trunc_subsecs(1).nanosecond(), 700_000_000);
395
396        assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0);
397        assert_eq!(dt.trunc_subsecs(0).second(), 27);
398    }
399
400    #[test]
401    fn test_trunc_leap_nanos() {
402        let dt = Utc
403            .from_local_datetime(
404                &NaiveDate::from_ymd_opt(2016, 12, 31)
405                    .unwrap()
406                    .and_hms_nano_opt(23, 59, 59, 1_750_500_000)
407                    .unwrap(),
408            )
409            .unwrap();
410        assert_eq!(dt.trunc_subsecs(9), dt);
411        assert_eq!(dt.trunc_subsecs(4), dt);
412        assert_eq!(dt.trunc_subsecs(2).nanosecond(), 1_750_000_000);
413        assert_eq!(dt.trunc_subsecs(1).nanosecond(), 1_700_000_000);
414        assert_eq!(dt.trunc_subsecs(1).second(), 59);
415
416        assert_eq!(dt.trunc_subsecs(0).nanosecond(), 1_000_000_000);
417        assert_eq!(dt.trunc_subsecs(0).second(), 59);
418    }
419
420    #[test]
421    fn test_duration_round() {
422        let dt = Utc
423            .from_local_datetime(
424                &NaiveDate::from_ymd_opt(2016, 12, 31)
425                    .unwrap()
426                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
427                    .unwrap(),
428            )
429            .unwrap();
430
431        assert_eq!(
432            dt.duration_round(TimeDelta::zero()).unwrap().to_string(),
433            "2016-12-31 23:59:59.175500 UTC"
434        );
435
436        assert_eq!(
437            dt.duration_round(TimeDelta::milliseconds(10)).unwrap().to_string(),
438            "2016-12-31 23:59:59.180 UTC"
439        );
440
441        // round up
442        let dt = Utc
443            .from_local_datetime(
444                &NaiveDate::from_ymd_opt(2012, 12, 12)
445                    .unwrap()
446                    .and_hms_milli_opt(18, 22, 30, 0)
447                    .unwrap(),
448            )
449            .unwrap();
450        assert_eq!(
451            dt.duration_round(TimeDelta::minutes(5)).unwrap().to_string(),
452            "2012-12-12 18:25:00 UTC"
453        );
454        // round down
455        let dt = Utc
456            .from_local_datetime(
457                &NaiveDate::from_ymd_opt(2012, 12, 12)
458                    .unwrap()
459                    .and_hms_milli_opt(18, 22, 29, 999)
460                    .unwrap(),
461            )
462            .unwrap();
463        assert_eq!(
464            dt.duration_round(TimeDelta::minutes(5)).unwrap().to_string(),
465            "2012-12-12 18:20:00 UTC"
466        );
467
468        assert_eq!(
469            dt.duration_round(TimeDelta::minutes(10)).unwrap().to_string(),
470            "2012-12-12 18:20:00 UTC"
471        );
472        assert_eq!(
473            dt.duration_round(TimeDelta::minutes(30)).unwrap().to_string(),
474            "2012-12-12 18:30:00 UTC"
475        );
476        assert_eq!(
477            dt.duration_round(TimeDelta::hours(1)).unwrap().to_string(),
478            "2012-12-12 18:00:00 UTC"
479        );
480        assert_eq!(
481            dt.duration_round(TimeDelta::days(1)).unwrap().to_string(),
482            "2012-12-13 00:00:00 UTC"
483        );
484
485        // timezone east
486        let dt =
487            FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
488        assert_eq!(
489            dt.duration_round(TimeDelta::days(1)).unwrap().to_string(),
490            "2020-10-28 00:00:00 +01:00"
491        );
492        assert_eq!(
493            dt.duration_round(TimeDelta::weeks(1)).unwrap().to_string(),
494            "2020-10-29 00:00:00 +01:00"
495        );
496
497        // timezone west
498        let dt =
499            FixedOffset::west_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
500        assert_eq!(
501            dt.duration_round(TimeDelta::days(1)).unwrap().to_string(),
502            "2020-10-28 00:00:00 -01:00"
503        );
504        assert_eq!(
505            dt.duration_round(TimeDelta::weeks(1)).unwrap().to_string(),
506            "2020-10-29 00:00:00 -01:00"
507        );
508    }
509
510    #[test]
511    fn test_duration_round_naive() {
512        let dt = Utc
513            .from_local_datetime(
514                &NaiveDate::from_ymd_opt(2016, 12, 31)
515                    .unwrap()
516                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
517                    .unwrap(),
518            )
519            .unwrap()
520            .naive_utc();
521
522        assert_eq!(
523            dt.duration_round(TimeDelta::zero()).unwrap().to_string(),
524            "2016-12-31 23:59:59.175500"
525        );
526
527        assert_eq!(
528            dt.duration_round(TimeDelta::milliseconds(10)).unwrap().to_string(),
529            "2016-12-31 23:59:59.180"
530        );
531
532        // round up
533        let dt = Utc
534            .from_local_datetime(
535                &NaiveDate::from_ymd_opt(2012, 12, 12)
536                    .unwrap()
537                    .and_hms_milli_opt(18, 22, 30, 0)
538                    .unwrap(),
539            )
540            .unwrap()
541            .naive_utc();
542        assert_eq!(
543            dt.duration_round(TimeDelta::minutes(5)).unwrap().to_string(),
544            "2012-12-12 18:25:00"
545        );
546        // round down
547        let dt = Utc
548            .from_local_datetime(
549                &NaiveDate::from_ymd_opt(2012, 12, 12)
550                    .unwrap()
551                    .and_hms_milli_opt(18, 22, 29, 999)
552                    .unwrap(),
553            )
554            .unwrap()
555            .naive_utc();
556        assert_eq!(
557            dt.duration_round(TimeDelta::minutes(5)).unwrap().to_string(),
558            "2012-12-12 18:20:00"
559        );
560
561        assert_eq!(
562            dt.duration_round(TimeDelta::minutes(10)).unwrap().to_string(),
563            "2012-12-12 18:20:00"
564        );
565        assert_eq!(
566            dt.duration_round(TimeDelta::minutes(30)).unwrap().to_string(),
567            "2012-12-12 18:30:00"
568        );
569        assert_eq!(
570            dt.duration_round(TimeDelta::hours(1)).unwrap().to_string(),
571            "2012-12-12 18:00:00"
572        );
573        assert_eq!(
574            dt.duration_round(TimeDelta::days(1)).unwrap().to_string(),
575            "2012-12-13 00:00:00"
576        );
577    }
578
579    #[test]
580    fn test_duration_round_pre_epoch() {
581        let dt = Utc.with_ymd_and_hms(1969, 12, 12, 12, 12, 12).unwrap();
582        assert_eq!(
583            dt.duration_round(TimeDelta::minutes(10)).unwrap().to_string(),
584            "1969-12-12 12:10:00 UTC"
585        );
586    }
587
588    #[test]
589    fn test_duration_trunc() {
590        let dt = Utc
591            .from_local_datetime(
592                &NaiveDate::from_ymd_opt(2016, 12, 31)
593                    .unwrap()
594                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
595                    .unwrap(),
596            )
597            .unwrap();
598
599        assert_eq!(
600            dt.duration_trunc(TimeDelta::milliseconds(10)).unwrap().to_string(),
601            "2016-12-31 23:59:59.170 UTC"
602        );
603
604        // would round up
605        let dt = Utc
606            .from_local_datetime(
607                &NaiveDate::from_ymd_opt(2012, 12, 12)
608                    .unwrap()
609                    .and_hms_milli_opt(18, 22, 30, 0)
610                    .unwrap(),
611            )
612            .unwrap();
613        assert_eq!(
614            dt.duration_trunc(TimeDelta::minutes(5)).unwrap().to_string(),
615            "2012-12-12 18:20:00 UTC"
616        );
617        // would round down
618        let dt = Utc
619            .from_local_datetime(
620                &NaiveDate::from_ymd_opt(2012, 12, 12)
621                    .unwrap()
622                    .and_hms_milli_opt(18, 22, 29, 999)
623                    .unwrap(),
624            )
625            .unwrap();
626        assert_eq!(
627            dt.duration_trunc(TimeDelta::minutes(5)).unwrap().to_string(),
628            "2012-12-12 18:20:00 UTC"
629        );
630        assert_eq!(
631            dt.duration_trunc(TimeDelta::minutes(10)).unwrap().to_string(),
632            "2012-12-12 18:20:00 UTC"
633        );
634        assert_eq!(
635            dt.duration_trunc(TimeDelta::minutes(30)).unwrap().to_string(),
636            "2012-12-12 18:00:00 UTC"
637        );
638        assert_eq!(
639            dt.duration_trunc(TimeDelta::hours(1)).unwrap().to_string(),
640            "2012-12-12 18:00:00 UTC"
641        );
642        assert_eq!(
643            dt.duration_trunc(TimeDelta::days(1)).unwrap().to_string(),
644            "2012-12-12 00:00:00 UTC"
645        );
646
647        // timezone east
648        let dt =
649            FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
650        assert_eq!(
651            dt.duration_trunc(TimeDelta::days(1)).unwrap().to_string(),
652            "2020-10-27 00:00:00 +01:00"
653        );
654        assert_eq!(
655            dt.duration_trunc(TimeDelta::weeks(1)).unwrap().to_string(),
656            "2020-10-22 00:00:00 +01:00"
657        );
658
659        // timezone west
660        let dt =
661            FixedOffset::west_opt(3600).unwrap().with_ymd_and_hms(2020, 10, 27, 15, 0, 0).unwrap();
662        assert_eq!(
663            dt.duration_trunc(TimeDelta::days(1)).unwrap().to_string(),
664            "2020-10-27 00:00:00 -01:00"
665        );
666        assert_eq!(
667            dt.duration_trunc(TimeDelta::weeks(1)).unwrap().to_string(),
668            "2020-10-22 00:00:00 -01:00"
669        );
670    }
671
672    #[test]
673    fn test_duration_trunc_naive() {
674        let dt = Utc
675            .from_local_datetime(
676                &NaiveDate::from_ymd_opt(2016, 12, 31)
677                    .unwrap()
678                    .and_hms_nano_opt(23, 59, 59, 175_500_000)
679                    .unwrap(),
680            )
681            .unwrap()
682            .naive_utc();
683
684        assert_eq!(
685            dt.duration_trunc(TimeDelta::milliseconds(10)).unwrap().to_string(),
686            "2016-12-31 23:59:59.170"
687        );
688
689        // would round up
690        let dt = Utc
691            .from_local_datetime(
692                &NaiveDate::from_ymd_opt(2012, 12, 12)
693                    .unwrap()
694                    .and_hms_milli_opt(18, 22, 30, 0)
695                    .unwrap(),
696            )
697            .unwrap()
698            .naive_utc();
699        assert_eq!(
700            dt.duration_trunc(TimeDelta::minutes(5)).unwrap().to_string(),
701            "2012-12-12 18:20:00"
702        );
703        // would round down
704        let dt = Utc
705            .from_local_datetime(
706                &NaiveDate::from_ymd_opt(2012, 12, 12)
707                    .unwrap()
708                    .and_hms_milli_opt(18, 22, 29, 999)
709                    .unwrap(),
710            )
711            .unwrap()
712            .naive_utc();
713        assert_eq!(
714            dt.duration_trunc(TimeDelta::minutes(5)).unwrap().to_string(),
715            "2012-12-12 18:20:00"
716        );
717        assert_eq!(
718            dt.duration_trunc(TimeDelta::minutes(10)).unwrap().to_string(),
719            "2012-12-12 18:20:00"
720        );
721        assert_eq!(
722            dt.duration_trunc(TimeDelta::minutes(30)).unwrap().to_string(),
723            "2012-12-12 18:00:00"
724        );
725        assert_eq!(
726            dt.duration_trunc(TimeDelta::hours(1)).unwrap().to_string(),
727            "2012-12-12 18:00:00"
728        );
729        assert_eq!(
730            dt.duration_trunc(TimeDelta::days(1)).unwrap().to_string(),
731            "2012-12-12 00:00:00"
732        );
733    }
734
735    #[test]
736    fn test_duration_trunc_pre_epoch() {
737        let dt = Utc.with_ymd_and_hms(1969, 12, 12, 12, 12, 12).unwrap();
738        assert_eq!(
739            dt.duration_trunc(TimeDelta::minutes(10)).unwrap().to_string(),
740            "1969-12-12 12:10:00 UTC"
741        );
742    }
743
744    #[test]
745    fn issue1010() {
746        let dt = NaiveDateTime::from_timestamp_opt(-4_227_854_320, 678_774_288).unwrap();
747        let span = TimeDelta::microseconds(-7_019_067_213_869_040);
748        assert_eq!(dt.duration_trunc(span), Err(RoundingError::DurationExceedsLimit));
749
750        let dt = NaiveDateTime::from_timestamp_opt(320_041_586, 920_103_021).unwrap();
751        let span = TimeDelta::nanoseconds(-8_923_838_508_697_114_584);
752        assert_eq!(dt.duration_round(span), Err(RoundingError::DurationExceedsLimit));
753
754        let dt = NaiveDateTime::from_timestamp_opt(-2_621_440, 0).unwrap();
755        let span = TimeDelta::nanoseconds(-9_223_372_036_854_771_421);
756        assert_eq!(dt.duration_round(span), Err(RoundingError::DurationExceedsLimit));
757    }
758
759    #[test]
760    fn test_duration_trunc_close_to_epoch() {
761        let span = TimeDelta::minutes(15);
762
763        let dt = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(0, 0, 15).unwrap();
764        assert_eq!(dt.duration_trunc(span).unwrap().to_string(), "1970-01-01 00:00:00");
765
766        let dt = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap().and_hms_opt(23, 59, 45).unwrap();
767        assert_eq!(dt.duration_trunc(span).unwrap().to_string(), "1969-12-31 23:45:00");
768    }
769
770    #[test]
771    fn test_duration_round_close_to_epoch() {
772        let span = TimeDelta::minutes(15);
773
774        let dt = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(0, 0, 15).unwrap();
775        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00");
776
777        let dt = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap().and_hms_opt(23, 59, 45).unwrap();
778        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00");
779    }
780
781    #[test]
782    fn test_duration_round_close_to_min_max() {
783        let span = TimeDelta::nanoseconds(i64::MAX);
784
785        let dt = NaiveDateTime::from_timestamp_nanos(i64::MIN / 2 - 1).unwrap();
786        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1677-09-21 00:12:43.145224193");
787
788        let dt = NaiveDateTime::from_timestamp_nanos(i64::MIN / 2 + 1).unwrap();
789        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00");
790
791        let dt = NaiveDateTime::from_timestamp_nanos(i64::MAX / 2 + 1).unwrap();
792        assert_eq!(dt.duration_round(span).unwrap().to_string(), "2262-04-11 23:47:16.854775807");
793
794        let dt = NaiveDateTime::from_timestamp_nanos(i64::MAX / 2 - 1).unwrap();
795        assert_eq!(dt.duration_round(span).unwrap().to_string(), "1970-01-01 00:00:00");
796    }
797}