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