From b5acb0ed950f5762f3d1ea04788c6ba7c9bb1704 Mon Sep 17 00:00:00 2001 From: David Kellum Date: Tue, 30 Jan 2018 12:12:16 -0800 Subject: [PATCH 1/5] Add SubSecondRound extension trait with impl for Timelike --- src/lib.rs | 3 + src/round.rs | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/round.rs diff --git a/src/lib.rs b/src/lib.rs index 5ced85a..9ca1578 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -409,6 +409,7 @@ pub use date::{Date, MIN_DATE, MAX_DATE}; pub use datetime::{DateTime, SecondsFormat}; #[cfg(feature = "rustc-serialize")] pub use datetime::rustc_serialize::TsSeconds; pub use format::{ParseError, ParseResult}; +pub use round::SubSecondRound; /// A convenience module appropriate for glob imports (`use chrono::prelude::*;`). pub mod prelude { @@ -418,6 +419,7 @@ pub mod prelude { #[doc(no_inline)] pub use {NaiveDate, NaiveTime, NaiveDateTime}; #[doc(no_inline)] pub use Date; #[doc(no_inline)] pub use {DateTime, SecondsFormat}; + #[doc(no_inline)] pub use {SubSecondRound}; } // useful throughout the codebase @@ -464,6 +466,7 @@ pub mod naive { mod date; mod datetime; pub mod format; +mod round; /// Serialization/Deserialization in alternate formats /// diff --git a/src/round.rs b/src/round.rs new file mode 100644 index 0000000..22b0132 --- /dev/null +++ b/src/round.rs @@ -0,0 +1,162 @@ +// This is a part of Chrono. +// See README.md and LICENSE.txt for details. + +use Timelike; +use std::ops::{Add, Sub}; +use oldtime::Duration; + +/// Extension trait for subsecond rounding or truncation to a maximum number +/// of digits. Rounding can be used to decrease the error variance when +/// serializing/persisting to lower precision. Truncation is the default +/// behavior in Chrono display formatting. Either can be used to guarantee +/// equality (e.g. for testing) when round-tripping through a lower precision +/// format. +pub trait SubSecondRound { + /// Return a copy rounded to the specified number of subsecond digits. With + /// 9 or more digits, self is returned unmodified. Halfway values are + /// rounded up (away from zero). + fn round(self, digits: u16) -> Self; + + /// Return a copy truncated to the specified number of subsecond + /// digits. With 9 or more digits, self is returned unmodified. + fn trunc(self, digits: u16) -> Self; +} + +impl SubSecondRound for T +where T: Timelike + Add + Sub +{ + fn round(self, digits: u16) -> T { + let span = span_for_digits(digits); + let rem = self.nanosecond() % span; + if rem > 0 { + let rev = span - rem; + if rev <= rem { + self + Duration::nanoseconds(rev.into()) // up + } else { + self - Duration::nanoseconds(rem.into()) // down + } + } else { + self // unchanged + } + } + + fn trunc(self, digits: u16) -> T { + let span = span_for_digits(digits); + let rem = self.nanosecond() % span; + if rem > 0 { + self - Duration::nanoseconds(rem.into()) // truncate + } else { + self // unchanged + } + } +} + +// Return the maximum span in nanoseconds for the target number of digits. +fn span_for_digits(digits: u16) -> u32 { + // fast lookup form of: 10^(9-min(9,digits)) + match digits { + 0 => 1_000_000_000, + 1 => 100_000_000, + 2 => 10_000_000, + 3 => 1_000_000, + 4 => 100_000, + 5 => 10_000, + 6 => 1_000, + 7 => 100, + 8 => 10, + _ => 1 + } +} + +#[cfg(test)] +mod tests { + use Timelike; + use offset::{FixedOffset, TimeZone, Utc}; + use super::SubSecondRound; + + #[test] + fn test_round() { + let pst = FixedOffset::east(8 * 60 * 60); + let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 13, 084_660_684); + + assert_eq!(dt.round(10), dt); + assert_eq!(dt.round(9), dt); + assert_eq!(dt.round(8).nanosecond(), 084_660_680); + assert_eq!(dt.round(7).nanosecond(), 084_660_700); + assert_eq!(dt.round(6).nanosecond(), 084_661_000); + assert_eq!(dt.round(5).nanosecond(), 084_660_000); + assert_eq!(dt.round(4).nanosecond(), 084_700_000); + assert_eq!(dt.round(3).nanosecond(), 085_000_000); + assert_eq!(dt.round(2).nanosecond(), 080_000_000); + assert_eq!(dt.round(1).nanosecond(), 100_000_000); + + assert_eq!(dt.round(0).nanosecond(), 0); + assert_eq!(dt.round(0).second(), 13); + + let dt = Utc.ymd(2018, 1, 11).and_hms_nano(10, 5, 27, 750_500_000); + assert_eq!(dt.round(9), dt); + assert_eq!(dt.round(4), dt); + assert_eq!(dt.round(3).nanosecond(), 751_000_000); + assert_eq!(dt.round(2).nanosecond(), 750_000_000); + assert_eq!(dt.round(1).nanosecond(), 800_000_000); + + assert_eq!(dt.round(0).nanosecond(), 0); + assert_eq!(dt.round(0).second(), 28); + } + + #[test] + fn test_round_leap_nanos() { + let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_750_500_000); + assert_eq!(dt.round(9), dt); + assert_eq!(dt.round(4), dt); + assert_eq!(dt.round(2).nanosecond(), 1_750_000_000); + assert_eq!(dt.round(1).nanosecond(), 1_800_000_000); + assert_eq!(dt.round(1).second(), 59); + + assert_eq!(dt.round(0).nanosecond(), 0); + assert_eq!(dt.round(0).second(), 0); + } + + #[test] + fn test_trunc() { + let pst = FixedOffset::east(8 * 60 * 60); + let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 13, 084_660_684); + + assert_eq!(dt.trunc(10), dt); + assert_eq!(dt.trunc(9), dt); + assert_eq!(dt.trunc(8).nanosecond(), 084_660_680); + assert_eq!(dt.trunc(7).nanosecond(), 084_660_600); + assert_eq!(dt.trunc(6).nanosecond(), 084_660_000); + assert_eq!(dt.trunc(5).nanosecond(), 084_660_000); + assert_eq!(dt.trunc(4).nanosecond(), 084_600_000); + assert_eq!(dt.trunc(3).nanosecond(), 084_000_000); + assert_eq!(dt.trunc(2).nanosecond(), 080_000_000); + assert_eq!(dt.trunc(1).nanosecond(), 0); + + assert_eq!(dt.trunc(0).nanosecond(), 0); + assert_eq!(dt.trunc(0).second(), 13); + + let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 27, 750_500_000); + assert_eq!(dt.trunc(9), dt); + assert_eq!(dt.trunc(4), dt); + assert_eq!(dt.trunc(3).nanosecond(), 750_000_000); + assert_eq!(dt.trunc(2).nanosecond(), 750_000_000); + assert_eq!(dt.trunc(1).nanosecond(), 700_000_000); + + assert_eq!(dt.trunc(0).nanosecond(), 0); + assert_eq!(dt.trunc(0).second(), 27); + } + + #[test] + fn test_trunc_leap_nanos() { + let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_750_500_000); + assert_eq!(dt.trunc(9), dt); + assert_eq!(dt.trunc(4), dt); + assert_eq!(dt.trunc(2).nanosecond(), 1_750_000_000); + assert_eq!(dt.trunc(1).nanosecond(), 1_700_000_000); + assert_eq!(dt.trunc(1).second(), 59); + + assert_eq!(dt.trunc(0).nanosecond(), 1_000_000_000); + assert_eq!(dt.trunc(0).second(), 59); + } +} From 74ec7aa5b2065718e89ea69a3d7c2eb175bc720d Mon Sep 17 00:00:00 2001 From: David Kellum Date: Sun, 4 Mar 2018 18:27:36 -0800 Subject: [PATCH 2/5] Improve up/down variable naming --- src/round.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/round.rs b/src/round.rs index 22b0132..fc9ff1d 100644 --- a/src/round.rs +++ b/src/round.rs @@ -27,13 +27,13 @@ where T: Timelike + Add + Sub { fn round(self, digits: u16) -> T { let span = span_for_digits(digits); - let rem = self.nanosecond() % span; - if rem > 0 { - let rev = span - rem; - if rev <= rem { - self + Duration::nanoseconds(rev.into()) // up + let delta_down = self.nanosecond() % span; + if delta_down > 0 { + let delta_up = span - delta_down; + if delta_up <= delta_down { + self + Duration::nanoseconds(delta_up.into()) } else { - self - Duration::nanoseconds(rem.into()) // down + self - Duration::nanoseconds(delta_down.into()) } } else { self // unchanged @@ -42,9 +42,9 @@ where T: Timelike + Add + Sub fn trunc(self, digits: u16) -> T { let span = span_for_digits(digits); - let rem = self.nanosecond() % span; - if rem > 0 { - self - Duration::nanoseconds(rem.into()) // truncate + let delta_down = self.nanosecond() % span; + if delta_down > 0 { + self - Duration::nanoseconds(delta_down.into()) } else { self // unchanged } From cb3a73aa86c3aa707ade45596413ab76ace3dec4 Mon Sep 17 00:00:00 2001 From: David Kellum Date: Mon, 5 Mar 2018 10:37:32 -0800 Subject: [PATCH 3/5] Rename to (round/trunc)_subsecs; add doc tests --- src/round.rs | 128 +++++++++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 56 deletions(-) diff --git a/src/round.rs b/src/round.rs index fc9ff1d..fcdd01e 100644 --- a/src/round.rs +++ b/src/round.rs @@ -15,17 +15,33 @@ pub trait SubSecondRound { /// Return a copy rounded to the specified number of subsecond digits. With /// 9 or more digits, self is returned unmodified. Halfway values are /// rounded up (away from zero). - fn round(self, digits: u16) -> Self; + /// + /// # Example + /// ``` rust + /// # use chrono::{DateTime, SubSecondRound, Timelike, TimeZone, Utc}; + /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154); + /// assert_eq!(dt.round_subsecs(2).nanosecond(), 150_000_000); + /// assert_eq!(dt.round_subsecs(1).nanosecond(), 200_000_000); + /// ``` + fn round_subsecs(self, digits: u16) -> Self; /// Return a copy truncated to the specified number of subsecond /// digits. With 9 or more digits, self is returned unmodified. - fn trunc(self, digits: u16) -> Self; + /// + /// # Example + /// ``` rust + /// # use chrono::{DateTime, SubSecondRound, Timelike, TimeZone, Utc}; + /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154); + /// assert_eq!(dt.trunc_subsecs(2).nanosecond(), 150_000_000); + /// assert_eq!(dt.trunc_subsecs(1).nanosecond(), 100_000_000); + /// ``` + fn trunc_subsecs(self, digits: u16) -> Self; } impl SubSecondRound for T where T: Timelike + Add + Sub { - fn round(self, digits: u16) -> T { + fn round_subsecs(self, digits: u16) -> T { let span = span_for_digits(digits); let delta_down = self.nanosecond() % span; if delta_down > 0 { @@ -40,7 +56,7 @@ where T: Timelike + Add + Sub } } - fn trunc(self, digits: u16) -> T { + fn trunc_subsecs(self, digits: u16) -> T { let span = span_for_digits(digits); let delta_down = self.nanosecond() % span; if delta_down > 0 { @@ -79,42 +95,42 @@ mod tests { let pst = FixedOffset::east(8 * 60 * 60); let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 13, 084_660_684); - assert_eq!(dt.round(10), dt); - assert_eq!(dt.round(9), dt); - assert_eq!(dt.round(8).nanosecond(), 084_660_680); - assert_eq!(dt.round(7).nanosecond(), 084_660_700); - assert_eq!(dt.round(6).nanosecond(), 084_661_000); - assert_eq!(dt.round(5).nanosecond(), 084_660_000); - assert_eq!(dt.round(4).nanosecond(), 084_700_000); - assert_eq!(dt.round(3).nanosecond(), 085_000_000); - assert_eq!(dt.round(2).nanosecond(), 080_000_000); - assert_eq!(dt.round(1).nanosecond(), 100_000_000); + assert_eq!(dt.round_subsecs(10), dt); + assert_eq!(dt.round_subsecs(9), dt); + assert_eq!(dt.round_subsecs(8).nanosecond(), 084_660_680); + assert_eq!(dt.round_subsecs(7).nanosecond(), 084_660_700); + assert_eq!(dt.round_subsecs(6).nanosecond(), 084_661_000); + assert_eq!(dt.round_subsecs(5).nanosecond(), 084_660_000); + assert_eq!(dt.round_subsecs(4).nanosecond(), 084_700_000); + assert_eq!(dt.round_subsecs(3).nanosecond(), 085_000_000); + assert_eq!(dt.round_subsecs(2).nanosecond(), 080_000_000); + assert_eq!(dt.round_subsecs(1).nanosecond(), 100_000_000); - assert_eq!(dt.round(0).nanosecond(), 0); - assert_eq!(dt.round(0).second(), 13); + assert_eq!(dt.round_subsecs(0).nanosecond(), 0); + assert_eq!(dt.round_subsecs(0).second(), 13); let dt = Utc.ymd(2018, 1, 11).and_hms_nano(10, 5, 27, 750_500_000); - assert_eq!(dt.round(9), dt); - assert_eq!(dt.round(4), dt); - assert_eq!(dt.round(3).nanosecond(), 751_000_000); - assert_eq!(dt.round(2).nanosecond(), 750_000_000); - assert_eq!(dt.round(1).nanosecond(), 800_000_000); + assert_eq!(dt.round_subsecs(9), dt); + assert_eq!(dt.round_subsecs(4), dt); + assert_eq!(dt.round_subsecs(3).nanosecond(), 751_000_000); + assert_eq!(dt.round_subsecs(2).nanosecond(), 750_000_000); + assert_eq!(dt.round_subsecs(1).nanosecond(), 800_000_000); - assert_eq!(dt.round(0).nanosecond(), 0); - assert_eq!(dt.round(0).second(), 28); + assert_eq!(dt.round_subsecs(0).nanosecond(), 0); + assert_eq!(dt.round_subsecs(0).second(), 28); } #[test] fn test_round_leap_nanos() { let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_750_500_000); - assert_eq!(dt.round(9), dt); - assert_eq!(dt.round(4), dt); - assert_eq!(dt.round(2).nanosecond(), 1_750_000_000); - assert_eq!(dt.round(1).nanosecond(), 1_800_000_000); - assert_eq!(dt.round(1).second(), 59); + assert_eq!(dt.round_subsecs(9), dt); + assert_eq!(dt.round_subsecs(4), dt); + assert_eq!(dt.round_subsecs(2).nanosecond(), 1_750_000_000); + assert_eq!(dt.round_subsecs(1).nanosecond(), 1_800_000_000); + assert_eq!(dt.round_subsecs(1).second(), 59); - assert_eq!(dt.round(0).nanosecond(), 0); - assert_eq!(dt.round(0).second(), 0); + assert_eq!(dt.round_subsecs(0).nanosecond(), 0); + assert_eq!(dt.round_subsecs(0).second(), 0); } #[test] @@ -122,41 +138,41 @@ mod tests { let pst = FixedOffset::east(8 * 60 * 60); let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 13, 084_660_684); - assert_eq!(dt.trunc(10), dt); - assert_eq!(dt.trunc(9), dt); - assert_eq!(dt.trunc(8).nanosecond(), 084_660_680); - assert_eq!(dt.trunc(7).nanosecond(), 084_660_600); - assert_eq!(dt.trunc(6).nanosecond(), 084_660_000); - assert_eq!(dt.trunc(5).nanosecond(), 084_660_000); - assert_eq!(dt.trunc(4).nanosecond(), 084_600_000); - assert_eq!(dt.trunc(3).nanosecond(), 084_000_000); - assert_eq!(dt.trunc(2).nanosecond(), 080_000_000); - assert_eq!(dt.trunc(1).nanosecond(), 0); + assert_eq!(dt.trunc_subsecs(10), dt); + assert_eq!(dt.trunc_subsecs(9), dt); + assert_eq!(dt.trunc_subsecs(8).nanosecond(), 084_660_680); + assert_eq!(dt.trunc_subsecs(7).nanosecond(), 084_660_600); + assert_eq!(dt.trunc_subsecs(6).nanosecond(), 084_660_000); + assert_eq!(dt.trunc_subsecs(5).nanosecond(), 084_660_000); + assert_eq!(dt.trunc_subsecs(4).nanosecond(), 084_600_000); + assert_eq!(dt.trunc_subsecs(3).nanosecond(), 084_000_000); + assert_eq!(dt.trunc_subsecs(2).nanosecond(), 080_000_000); + assert_eq!(dt.trunc_subsecs(1).nanosecond(), 0); - assert_eq!(dt.trunc(0).nanosecond(), 0); - assert_eq!(dt.trunc(0).second(), 13); + assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0); + assert_eq!(dt.trunc_subsecs(0).second(), 13); let dt = pst.ymd(2018, 1, 11).and_hms_nano(10, 5, 27, 750_500_000); - assert_eq!(dt.trunc(9), dt); - assert_eq!(dt.trunc(4), dt); - assert_eq!(dt.trunc(3).nanosecond(), 750_000_000); - assert_eq!(dt.trunc(2).nanosecond(), 750_000_000); - assert_eq!(dt.trunc(1).nanosecond(), 700_000_000); + assert_eq!(dt.trunc_subsecs(9), dt); + assert_eq!(dt.trunc_subsecs(4), dt); + assert_eq!(dt.trunc_subsecs(3).nanosecond(), 750_000_000); + assert_eq!(dt.trunc_subsecs(2).nanosecond(), 750_000_000); + assert_eq!(dt.trunc_subsecs(1).nanosecond(), 700_000_000); - assert_eq!(dt.trunc(0).nanosecond(), 0); - assert_eq!(dt.trunc(0).second(), 27); + assert_eq!(dt.trunc_subsecs(0).nanosecond(), 0); + assert_eq!(dt.trunc_subsecs(0).second(), 27); } #[test] fn test_trunc_leap_nanos() { let dt = Utc.ymd(2016, 12, 31).and_hms_nano(23, 59, 59, 1_750_500_000); - assert_eq!(dt.trunc(9), dt); - assert_eq!(dt.trunc(4), dt); - assert_eq!(dt.trunc(2).nanosecond(), 1_750_000_000); - assert_eq!(dt.trunc(1).nanosecond(), 1_700_000_000); - assert_eq!(dt.trunc(1).second(), 59); + assert_eq!(dt.trunc_subsecs(9), dt); + assert_eq!(dt.trunc_subsecs(4), dt); + assert_eq!(dt.trunc_subsecs(2).nanosecond(), 1_750_000_000); + assert_eq!(dt.trunc_subsecs(1).nanosecond(), 1_700_000_000); + assert_eq!(dt.trunc_subsecs(1).second(), 59); - assert_eq!(dt.trunc(0).nanosecond(), 1_000_000_000); - assert_eq!(dt.trunc(0).second(), 59); + assert_eq!(dt.trunc_subsecs(0).nanosecond(), 1_000_000_000); + assert_eq!(dt.trunc_subsecs(0).second(), 59); } } From 08b7e0bc6b495d8ce351f5361220cf73dfbe8c5b Mon Sep 17 00:00:00 2001 From: David Kellum Date: Mon, 5 Mar 2018 10:40:50 -0800 Subject: [PATCH 4/5] Fix prelude SubSecondRound --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9ca1578..57b330e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -419,7 +419,7 @@ pub mod prelude { #[doc(no_inline)] pub use {NaiveDate, NaiveTime, NaiveDateTime}; #[doc(no_inline)] pub use Date; #[doc(no_inline)] pub use {DateTime, SecondsFormat}; - #[doc(no_inline)] pub use {SubSecondRound}; + #[doc(no_inline)] pub use SubSecondRound; } // useful throughout the codebase From 449d7277b9f719c20ec48d655bdf8aa75f6aeb66 Mon Sep 17 00:00:00 2001 From: David Kellum Date: Mon, 5 Mar 2018 12:57:26 -0800 Subject: [PATCH 5/5] Rename SubSecondRound to SubsecRound Per review request. --- src/lib.rs | 4 ++-- src/round.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 57b330e..63c511c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -409,7 +409,7 @@ pub use date::{Date, MIN_DATE, MAX_DATE}; pub use datetime::{DateTime, SecondsFormat}; #[cfg(feature = "rustc-serialize")] pub use datetime::rustc_serialize::TsSeconds; pub use format::{ParseError, ParseResult}; -pub use round::SubSecondRound; +pub use round::SubsecRound; /// A convenience module appropriate for glob imports (`use chrono::prelude::*;`). pub mod prelude { @@ -419,7 +419,7 @@ pub mod prelude { #[doc(no_inline)] pub use {NaiveDate, NaiveTime, NaiveDateTime}; #[doc(no_inline)] pub use Date; #[doc(no_inline)] pub use {DateTime, SecondsFormat}; - #[doc(no_inline)] pub use SubSecondRound; + #[doc(no_inline)] pub use SubsecRound; } // useful throughout the codebase diff --git a/src/round.rs b/src/round.rs index fcdd01e..bf62762 100644 --- a/src/round.rs +++ b/src/round.rs @@ -11,14 +11,14 @@ use oldtime::Duration; /// behavior in Chrono display formatting. Either can be used to guarantee /// equality (e.g. for testing) when round-tripping through a lower precision /// format. -pub trait SubSecondRound { +pub trait SubsecRound { /// Return a copy rounded to the specified number of subsecond digits. With /// 9 or more digits, self is returned unmodified. Halfway values are /// rounded up (away from zero). /// /// # Example /// ``` rust - /// # use chrono::{DateTime, SubSecondRound, Timelike, TimeZone, Utc}; + /// # use chrono::{DateTime, SubsecRound, Timelike, TimeZone, Utc}; /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154); /// assert_eq!(dt.round_subsecs(2).nanosecond(), 150_000_000); /// assert_eq!(dt.round_subsecs(1).nanosecond(), 200_000_000); @@ -30,7 +30,7 @@ pub trait SubSecondRound { /// /// # Example /// ``` rust - /// # use chrono::{DateTime, SubSecondRound, Timelike, TimeZone, Utc}; + /// # use chrono::{DateTime, SubsecRound, Timelike, TimeZone, Utc}; /// let dt = Utc.ymd(2018, 1, 11).and_hms_milli(12, 0, 0, 154); /// assert_eq!(dt.trunc_subsecs(2).nanosecond(), 150_000_000); /// assert_eq!(dt.trunc_subsecs(1).nanosecond(), 100_000_000); @@ -38,7 +38,7 @@ pub trait SubSecondRound { fn trunc_subsecs(self, digits: u16) -> Self; } -impl SubSecondRound for T +impl SubsecRound for T where T: Timelike + Add + Sub { fn round_subsecs(self, digits: u16) -> T { @@ -88,7 +88,7 @@ fn span_for_digits(digits: u16) -> u32 { mod tests { use Timelike; use offset::{FixedOffset, TimeZone, Utc}; - use super::SubSecondRound; + use super::SubsecRound; #[test] fn test_round() {