1// This is a part of Chrono.
2// See README.md and LICENSE.txt for details.
3
4use crate::datetime::DateTime;
5use crate::oldtime::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(any(feature = "std", test))]
104 type Err: std::error::Error;
105
106 /// Error that can occur in rounding or truncating
107 #[cfg(not(any(feature = "std", test)))]
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: i64) = 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: i64 = naive.timestamp_nanos();
230 if span > stamp.abs() {
231 return Err(RoundingError::DurationExceedsTimestamp);
232 }
233 let delta_down: i64 = stamp % span;
234 match delta_down.cmp(&0) {
235 Ordering::Equal => Ok(original),
236 Ordering::Greater => Ok(original - Duration::nanoseconds(nanos:delta_down)),
237 Ordering::Less => Ok(original - Duration::nanoseconds(nanos: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(any(feature = "std", test))]
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}
785