chrono/
round.rs

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