1 // This is a part of Chrono.
2 // See README.md and LICENSE.txt for details.
3
4 use core::cmp::Ordering;
5 use core::fmt;
6 use core::marker::Sized;
7 use core::ops::{Add, Sub};
8 use datetime::DateTime;
9 use oldtime::Duration;
10 #[cfg(any(feature = "std", test))]
11 use std;
12 use TimeZone;
13 use Timelike;
14
15 /// Extension trait for subsecond rounding or truncation to a maximum number
16 /// of digits. Rounding can be used to decrease the error variance when
17 /// serializing/persisting to lower precision. Truncation is the default
18 /// behavior in Chrono display formatting. Either can be used to guarantee
19 /// equality (e.g. for testing) when round-tripping through a lower precision
20 /// format.
21 pub trait SubsecRound {
22 /// Return a copy rounded to the specified number of subsecond digits. With
23 /// 9 or more digits, self is returned unmodified. Halfway values are
24 /// rounded up (away from zero).
25 ///
26 /// # Example
27 /// ``` rust
28 /// # use chrono::{DateTime, SubsecRound, Timelike, TimeZone, Utc};
29 /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154);
30 /// assert_eq!(dt.round_subsecs(2).nanosecond(), 150_000_000);
31 /// assert_eq!(dt.round_subsecs(1).nanosecond(), 200_000_000);
32 /// ```
round_subsecs(self, digits: u16) -> Self33 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::{DateTime, SubsecRound, Timelike, TimeZone, Utc};
41 /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154);
42 /// assert_eq!(dt.trunc_subsecs(2).nanosecond(), 150_000_000);
43 /// assert_eq!(dt.trunc_subsecs(1).nanosecond(), 100_000_000);
44 /// ```
trunc_subsecs(self, digits: u16) -> Self45 fn trunc_subsecs(self, digits: u16) -> Self;
46 }
47
48 impl<T> SubsecRound for T
49 where
50 T: Timelike + Add<Duration, Output = T> + Sub<Duration, Output = T>,
51 {
round_subsecs(self, digits: u16) -> T52 fn round_subsecs(self, digits: u16) -> T {
53 let span = span_for_digits(digits);
54 let delta_down = self.nanosecond() % span;
55 if delta_down > 0 {
56 let delta_up = span - delta_down;
57 if delta_up <= delta_down {
58 self + Duration::nanoseconds(delta_up.into())
59 } else {
60 self - Duration::nanoseconds(delta_down.into())
61 }
62 } else {
63 self // unchanged
64 }
65 }
66
trunc_subsecs(self, digits: u16) -> T67 fn trunc_subsecs(self, digits: u16) -> T {
68 let span = span_for_digits(digits);
69 let delta_down = self.nanosecond() % span;
70 if delta_down > 0 {
71 self - Duration::nanoseconds(delta_down.into())
72 } else {
73 self // unchanged
74 }
75 }
76 }
77
78 // Return the maximum span in nanoseconds for the target number of digits.
span_for_digits(digits: u16) -> u3279 fn span_for_digits(digits: u16) -> u32 {
80 // fast lookup form of: 10^(9-min(9,digits))
81 match digits {
82 0 => 1_000_000_000,
83 1 => 100_000_000,
84 2 => 10_000_000,
85 3 => 1_000_000,
86 4 => 100_000,
87 5 => 10_000,
88 6 => 1_000,
89 7 => 100,
90 8 => 10,
91 _ => 1,
92 }
93 }
94
95 /// Extension trait for rounding or truncating a DateTime by a Duration.
96 ///
97 /// # Limitations
98 /// Both rounding and truncating are done via [`Duration::num_nanoseconds`] and
99 /// [`DateTime::timestamp_nanos`]. This means that they will fail if either the
100 /// `Duration` or the `DateTime` are too big to represented as nanoseconds. They
101 /// will also fail if the `Duration` is bigger than the timestamp.
102 pub trait DurationRound: Sized {
103 /// Error that can occur in rounding or truncating
104 #[cfg(any(feature = "std", test))]
105 type Err: std::error::Error;
106
107 /// Error that can occur in rounding or truncating
108 #[cfg(not(any(feature = "std", test)))]
109 type Err: fmt::Debug + fmt::Display;
110
111 /// Return a copy rounded by Duration.
112 ///
113 /// # Example
114 /// ``` rust
115 /// # use chrono::{DateTime, DurationRound, Duration, TimeZone, Utc};
116 /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154);
117 /// assert_eq!(
118 /// dt.duration_round(Duration::milliseconds(10)).unwrap().to_string(),
119 /// "2018-01-11 12:00:00.150 UTC"
120 /// );
121 /// assert_eq!(
122 /// dt.duration_round(Duration::days(1)).unwrap().to_string(),
123 /// "2018-01-12 00:00:00 UTC"
124 /// );
125 /// ```
duration_round(self, duration: Duration) -> Result<Self, Self::Err>126 fn duration_round(self, duration: Duration) -> Result<Self, Self::Err>;
127
128 /// Return a copy truncated by Duration.
129 ///
130 /// # Example
131 /// ``` rust
132 /// # use chrono::{DateTime, DurationRound, Duration, TimeZone, Utc};
133 /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154);
134 /// assert_eq!(
135 /// dt.duration_trunc(Duration::milliseconds(10)).unwrap().to_string(),
136 /// "2018-01-11 12:00:00.150 UTC"
137 /// );
138 /// assert_eq!(
139 /// dt.duration_trunc(Duration::days(1)).unwrap().to_string(),
140 /// "2018-01-11 00:00:00 UTC"
141 /// );
142 /// ```
duration_trunc(self, duration: Duration) -> Result<Self, Self::Err>143 fn duration_trunc(self, duration: Duration) -> Result<Self, Self::Err>;
144 }
145
146 /// The maximum number of seconds a DateTime can be to be represented as nanoseconds
147 const MAX_SECONDS_TIMESTAMP_FOR_NANOS: i64 = 9_223_372_036;
148
149 impl<Tz: TimeZone> DurationRound for DateTime<Tz> {
150 type Err = RoundingError;
151
duration_round(self, duration: Duration) -> Result<Self, Self::Err>152 fn duration_round(self, duration: Duration) -> Result<Self, Self::Err> {
153 if let Some(span) = duration.num_nanoseconds() {
154 if self.timestamp().abs() > MAX_SECONDS_TIMESTAMP_FOR_NANOS {
155 return Err(RoundingError::TimestampExceedsLimit);
156 }
157 let stamp = self.timestamp_nanos();
158 if span > stamp.abs() {
159 return Err(RoundingError::DurationExceedsTimestamp);
160 }
161 let delta_down = stamp % span;
162 if delta_down == 0 {
163 Ok(self)
164 } else {
165 let (delta_up, delta_down) = if delta_down < 0 {
166 (delta_down.abs(), span - delta_down.abs())
167 } else {
168 (span - delta_down, delta_down)
169 };
170 if delta_up <= delta_down {
171 Ok(self + Duration::nanoseconds(delta_up))
172 } else {
173 Ok(self - Duration::nanoseconds(delta_down))
174 }
175 }
176 } else {
177 Err(RoundingError::DurationExceedsLimit)
178 }
179 }
180
duration_trunc(self, duration: Duration) -> Result<Self, Self::Err>181 fn duration_trunc(self, duration: Duration) -> Result<Self, Self::Err> {
182 if let Some(span) = duration.num_nanoseconds() {
183 if self.timestamp().abs() > MAX_SECONDS_TIMESTAMP_FOR_NANOS {
184 return Err(RoundingError::TimestampExceedsLimit);
185 }
186 let stamp = self.timestamp_nanos();
187 if span > stamp.abs() {
188 return Err(RoundingError::DurationExceedsTimestamp);
189 }
190 let delta_down = stamp % span;
191 match delta_down.cmp(&0) {
192 Ordering::Equal => Ok(self),
193 Ordering::Greater => Ok(self - Duration::nanoseconds(delta_down)),
194 Ordering::Less => Ok(self - Duration::nanoseconds(span - delta_down.abs())),
195 }
196 } else {
197 Err(RoundingError::DurationExceedsLimit)
198 }
199 }
200 }
201
202 /// An error from rounding by `Duration`
203 ///
204 /// See: [`DurationRound`]
205 #[derive(Debug, Clone, PartialEq, Eq, Copy)]
206 pub enum RoundingError {
207 /// Error when the Duration exceeds the Duration from or until the Unix epoch.
208 ///
209 /// ``` rust
210 /// # use chrono::{DateTime, DurationRound, Duration, RoundingError, TimeZone, Utc};
211 /// let dt = Utc.ymd(1970, 12, 12).and_hms(0, 0, 0);
212 ///
213 /// assert_eq!(
214 /// dt.duration_round(Duration::days(365)),
215 /// Err(RoundingError::DurationExceedsTimestamp),
216 /// );
217 /// ```
218 DurationExceedsTimestamp,
219
220 /// Error when `Duration.num_nanoseconds` exceeds the limit.
221 ///
222 /// ``` rust
223 /// # use chrono::{DateTime, DurationRound, Duration, RoundingError, TimeZone, Utc};
224 /// let dt = Utc.ymd(2260, 12, 31).and_hms_nano(23, 59, 59, 1_75_500_000);
225 ///
226 /// assert_eq!(
227 /// dt.duration_round(Duration::days(300 * 365)),
228 /// Err(RoundingError::DurationExceedsLimit)
229 /// );
230 /// ```
231 DurationExceedsLimit,
232
233 /// Error when `DateTime.timestamp_nanos` exceeds the limit.
234 ///
235 /// ``` rust
236 /// # use chrono::{DateTime, DurationRound, Duration, RoundingError, TimeZone, Utc};
237 /// let dt = Utc.ymd(2300, 12, 12).and_hms(0, 0, 0);
238 ///
239 /// assert_eq!(dt.duration_round(Duration::days(1)), Err(RoundingError::TimestampExceedsLimit),);
240 /// ```
241 TimestampExceedsLimit,
242 }
243
244 impl fmt::Display for RoundingError {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result245 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
246 match *self {
247 RoundingError::DurationExceedsTimestamp => {
248 write!(f, "duration in nanoseconds exceeds timestamp")
249 }
250 RoundingError::DurationExceedsLimit => {
251 write!(f, "duration exceeds num_nanoseconds limit")
252 }
253 RoundingError::TimestampExceedsLimit => {
254 write!(f, "timestamp exceeds num_nanoseconds limit")
255 }
256 }
257 }
258 }
259
260 #[cfg(any(feature = "std", test))]
261 impl std::error::Error for RoundingError {
262 #[allow(deprecated)]
description(&self) -> &str263 fn description(&self) -> &str {
264 "error from rounding or truncating with DurationRound"
265 }
266 }
267
268 #[cfg(test)]
269 mod tests {
270 use super::{Duration, DurationRound, SubsecRound};
271 use offset::{FixedOffset, TimeZone, Utc};
272 use Timelike;
273
274 #[test]
test_round_subsecs()275 fn test_round_subsecs() {
276 let pst = FixedOffset::east(8 * 60 * 60);
277 let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 13, 084_660_684);
278
279 assert_eq!(dt.round_subsecs(10), dt);
280 assert_eq!(dt.round_subsecs(9), dt);
281 assert_eq!(dt.round_subsecs(8).nanosecond(), 084_660_680);
282 assert_eq!(dt.round_subsecs(7).nanosecond(), 084_660_700);
283 assert_eq!(dt.round_subsecs(6).nanosecond(), 084_661_000);
284 assert_eq!(dt.round_subsecs(5).nanosecond(), 084_660_000);
285 assert_eq!(dt.round_subsecs(4).nanosecond(), 084_700_000);
286 assert_eq!(dt.round_subsecs(3).nanosecond(), 085_000_000);
287 assert_eq!(dt.round_subsecs(2).nanosecond(), 080_000_000);
288 assert_eq!(dt.round_subsecs(1).nanosecond(), 100_000_000);
289
290 assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
291 assert_eq!(dt.round_subsecs(0).second(), 13);
292
293 let dt = Utc.ymd(2018, 1, 11).and_hms_nano(10, 5, 27, 750_500_000);
294 assert_eq!(dt.round_subsecs(9), dt);
295 assert_eq!(dt.round_subsecs(4), dt);
296 assert_eq!(dt.round_subsecs(3).nanosecond(), 751_000_000);
297 assert_eq!(dt.round_subsecs(2).nanosecond(), 750_000_000);
298 assert_eq!(dt.round_subsecs(1).nanosecond(), 800_000_000);
299
300 assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
301 assert_eq!(dt.round_subsecs(0).second(), 28);
302 }
303
304 #[test]
test_round_leap_nanos()305 fn test_round_leap_nanos() {
306 let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_750_500_000);
307 assert_eq!(dt.round_subsecs(9), dt);
308 assert_eq!(dt.round_subsecs(4), dt);
309 assert_eq!(dt.round_subsecs(2).nanosecond(), 1_750_000_000);
310 assert_eq!(dt.round_subsecs(1).nanosecond(), 1_800_000_000);
311 assert_eq!(dt.round_subsecs(1).second(), 59);
312
313 assert_eq!(dt.round_subsecs(0).nanosecond(), 0);
314 assert_eq!(dt.round_subsecs(0).second(), 0);
315 }
316
317 #[test]
test_trunc_subsecs()318 fn test_trunc_subsecs() {
319 let pst = FixedOffset::east(8 * 60 * 60);
320 let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 13, 084_660_684);
321
322 assert_eq!(dt.trunc_subsecs(10), dt);
323 assert_eq!(dt.trunc_subsecs(9), dt);
324 assert_eq!(dt.trunc_subsecs(8).nanosecond(), 084_660_680);
325 assert_eq!(dt.trunc_subsecs(7).nanosecond(), 084_660_600);
326 assert_eq!(dt.trunc_subsecs(6).nanosecond(), 084_660_000);
327 assert_eq!(dt.trunc_subsecs(5).nanosecond(), 084_660_000);
328 assert_eq!(dt.trunc_subsecs(4).nanosecond(), 084_600_000);
329 assert_eq!(dt.trunc_subsecs(3).nanosecond(), 084_000_000);
330 assert_eq!(dt.trunc_subsecs(2).nanosecond(), 080_000_000);
331 assert_eq!(dt.trunc_subsecs(1).nanosecond(), 0);
332
333 assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0);
334 assert_eq!(dt.trunc_subsecs(0).second(), 13);
335
336 let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 27, 750_500_000);
337 assert_eq!(dt.trunc_subsecs(9), dt);
338 assert_eq!(dt.trunc_subsecs(4), dt);
339 assert_eq!(dt.trunc_subsecs(3).nanosecond(), 750_000_000);
340 assert_eq!(dt.trunc_subsecs(2).nanosecond(), 750_000_000);
341 assert_eq!(dt.trunc_subsecs(1).nanosecond(), 700_000_000);
342
343 assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0);
344 assert_eq!(dt.trunc_subsecs(0).second(), 27);
345 }
346
347 #[test]
test_trunc_leap_nanos()348 fn test_trunc_leap_nanos() {
349 let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_750_500_000);
350 assert_eq!(dt.trunc_subsecs(9), dt);
351 assert_eq!(dt.trunc_subsecs(4), dt);
352 assert_eq!(dt.trunc_subsecs(2).nanosecond(), 1_750_000_000);
353 assert_eq!(dt.trunc_subsecs(1).nanosecond(), 1_700_000_000);
354 assert_eq!(dt.trunc_subsecs(1).second(), 59);
355
356 assert_eq!(dt.trunc_subsecs(0).nanosecond(), 1_000_000_000);
357 assert_eq!(dt.trunc_subsecs(0).second(), 59);
358 }
359
360 #[test]
test_duration_round()361 fn test_duration_round() {
362 let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 175_500_000);
363
364 assert_eq!(
365 dt.duration_round(Duration::milliseconds(10)).unwrap().to_string(),
366 "2016-12-31 23:59:59.180 UTC"
367 );
368
369 // round up
370 let dt = Utc.ymd(2012, 12, 12).and_hms_milli(18, 22, 30, 0);
371 assert_eq!(
372 dt.duration_round(Duration::minutes(5)).unwrap().to_string(),
373 "2012-12-12 18:25:00 UTC"
374 );
375 // round down
376 let dt = Utc.ymd(2012, 12, 12).and_hms_milli(18, 22, 29, 999);
377 assert_eq!(
378 dt.duration_round(Duration::minutes(5)).unwrap().to_string(),
379 "2012-12-12 18:20:00 UTC"
380 );
381
382 assert_eq!(
383 dt.duration_round(Duration::minutes(10)).unwrap().to_string(),
384 "2012-12-12 18:20:00 UTC"
385 );
386 assert_eq!(
387 dt.duration_round(Duration::minutes(30)).unwrap().to_string(),
388 "2012-12-12 18:30:00 UTC"
389 );
390 assert_eq!(
391 dt.duration_round(Duration::hours(1)).unwrap().to_string(),
392 "2012-12-12 18:00:00 UTC"
393 );
394 assert_eq!(
395 dt.duration_round(Duration::days(1)).unwrap().to_string(),
396 "2012-12-13 00:00:00 UTC"
397 );
398 }
399
400 #[test]
test_duration_round_pre_epoch()401 fn test_duration_round_pre_epoch() {
402 let dt = Utc.ymd(1969, 12, 12).and_hms(12, 12, 12);
403 assert_eq!(
404 dt.duration_round(Duration::minutes(10)).unwrap().to_string(),
405 "1969-12-12 12:10:00 UTC"
406 );
407 }
408
409 #[test]
test_duration_trunc()410 fn test_duration_trunc() {
411 let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_75_500_000);
412
413 assert_eq!(
414 dt.duration_trunc(Duration::milliseconds(10)).unwrap().to_string(),
415 "2016-12-31 23:59:59.170 UTC"
416 );
417
418 // would round up
419 let dt = Utc.ymd(2012, 12, 12).and_hms_milli(18, 22, 30, 0);
420 assert_eq!(
421 dt.duration_trunc(Duration::minutes(5)).unwrap().to_string(),
422 "2012-12-12 18:20:00 UTC"
423 );
424 // would round down
425 let dt = Utc.ymd(2012, 12, 12).and_hms_milli(18, 22, 29, 999);
426 assert_eq!(
427 dt.duration_trunc(Duration::minutes(5)).unwrap().to_string(),
428 "2012-12-12 18:20:00 UTC"
429 );
430 assert_eq!(
431 dt.duration_trunc(Duration::minutes(10)).unwrap().to_string(),
432 "2012-12-12 18:20:00 UTC"
433 );
434 assert_eq!(
435 dt.duration_trunc(Duration::minutes(30)).unwrap().to_string(),
436 "2012-12-12 18:00:00 UTC"
437 );
438 assert_eq!(
439 dt.duration_trunc(Duration::hours(1)).unwrap().to_string(),
440 "2012-12-12 18:00:00 UTC"
441 );
442 assert_eq!(
443 dt.duration_trunc(Duration::days(1)).unwrap().to_string(),
444 "2012-12-12 00:00:00 UTC"
445 );
446 }
447
448 #[test]
test_duration_trunc_pre_epoch()449 fn test_duration_trunc_pre_epoch() {
450 let dt = Utc.ymd(1969, 12, 12).and_hms(12, 12, 12);
451 assert_eq!(
452 dt.duration_trunc(Duration::minutes(10)).unwrap().to_string(),
453 "1969-12-12 12:10:00 UTC"
454 );
455 }
456 }
457