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