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