From 71696026f60f38e5666aea057d0a86b10fc99f10 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Sat, 19 Nov 2022 23:16:19 +1100 Subject: [PATCH] add Day --- src/day.rs | 243 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/naive/time/mod.rs | 2 +- 3 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/day.rs diff --git a/src/day.rs b/src/day.rs new file mode 100644 index 0000000000..0debe12f20 --- /dev/null +++ b/src/day.rs @@ -0,0 +1,243 @@ +use core::cmp::Ordering; +use core::convert::TryFrom; +use core::fmt; +use core::fmt::Debug; +use core::fmt::Display; +use core::fmt::Write; +use core::ops::Add; +use core::ops::Sub; + +use crate::oldtime; +use crate::DateTime; +use crate::Days; +use crate::NaiveDate; +use crate::NaiveTime; +use crate::TimeZone; +use oldtime::Duration; + +/// Represents the full range of local timestamps in the day +/// +/// +#[derive(Clone, Copy)] +pub struct Day +where + Tz: TimeZone + Copy + Display, +{ + date: NaiveDate, + tz: Tz, +} + +impl Day +where + Tz: TimeZone + Copy + Display, +{ + /// + pub fn date(&self) -> NaiveDate { + self.date + } + + /// + pub fn zone(&self) -> Tz { + self.tz + } + + /// + pub fn new(date: NaiveDate, tz: Tz) -> Day { + Day { date, tz } + } + + /// Returns the earliest datetime on the given day + /// + /// panics: This will panic in the following cases: + /// * There are no valid local times in the first six hours of the day + /// other: This will return the incorrect value when: + /// * There are valid local times not aligned to a 15-minute reslution + pub fn start(&self) -> DateTime { + // All possible offsets: https://en.wikipedia.org/wiki/List_of_UTC_offsets + // means a gap of 15 minutes should be reasonable + + // while looping here is less than ideal, in the vast majority of cases + // the inital start time guess will be valid. We have to loop here because + // we don't know ex-ante what the earliest valid local time on the day will be + // and so we have to attempt a number of reasonable local times until we find one. + // + // Reasonable in this case means that this function will work for all timezones + // mentioned in the above wikipedia list, but will panic in custom implementations + // that allow offsets not aligned to a 15-minute resolution. + + let base = NaiveTime::MIN; + for multiple in 0..=24 { + let start_time = base + oldtime::Duration::minutes(multiple * 15); + match self.tz.from_local_datetime(&self.date.and_time(start_time)) { + crate::LocalResult::None => continue, + crate::LocalResult::Single(dt) => return dt, + // in the ambiguous case we pick the one which has an + // earlier UTC timestamp + // (this could be done without calling `naive_utc`, but + // this potentially better expresses the intent) + crate::LocalResult::Ambiguous(dt1, dt2) => { + if dt1.naive_utc() < dt2.naive_utc() { + return dt1; + } else { + return dt2; + } + } + } + } + + panic!("Unable to calculate start time for date {} and time zone {}", self.date, self.tz) + } + + /// Returns the exclusive end date of the day, equivalent to the start of the next day + /// + /// Returns None when it would otherwise overflow + pub fn exclusive_end(&self) -> Option> { + self.checked_add_days(Days::new(1))?.start().into() + } + + /// + pub fn checked_add_days(self, days: Days) -> Option { + if days.0 == 0 { + return Some(self); + } + + i64::try_from(days.0).ok().and_then(|d| self.diff_days(d)) + } + + /// + pub fn checked_sub_days(self, days: Days) -> Option { + if days.0 == 0 { + return Some(self); + } + + i64::try_from(days.0).ok().and_then(|d| self.diff_days(-d)) + } + + fn diff_days(self, days: i64) -> Option { + let date = self.date.checked_add_signed(Duration::days(days))?; + Some(Day { date, ..self }) + } +} + +impl Debug for Day +where + Tz: TimeZone + Copy + Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Debug::fmt(&self.date, f)?; + f.write_char(' ')?; + self.tz.fmt(f) + } +} + +impl Display for Day +where + Tz: TimeZone + Copy + Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Debug::fmt(self, f) + } +} + +impl PartialOrd for Day +where + Tz: TimeZone + Copy + Display, +{ + fn partial_cmp(&self, other: &Self) -> Option { + self.date.partial_cmp(&other.date) + } +} + +impl Ord for Day +where + Tz: TimeZone + Copy + Display, +{ + fn cmp(&self, other: &Self) -> Ordering { + self.date.cmp(&other.date) + } +} + +impl PartialEq for Day +where + Tz: TimeZone + Copy + Display, +{ + fn eq(&self, other: &Self) -> bool { + self.date.eq(&other.date) + } +} + +impl Eq for Day where Tz: TimeZone + Copy + Display {} + +impl Add for Day +where + Tz: TimeZone + Copy + Display, +{ + type Output = Day; + + fn add(self, days: Days) -> Self::Output { + self.checked_add_days(days).unwrap() + } +} + +impl Sub for Day +where + Tz: TimeZone + Copy + Display, +{ + type Output = Day; + + fn sub(self, days: Days) -> Self::Output { + self.checked_sub_days(days).unwrap() + } +} + +impl From> for Day +where + Tz: TimeZone + Copy + Display, +{ + fn from(dt: DateTime) -> Self { + Day { date: dt.date_naive(), tz: dt.timezone() } + } +} + +#[cfg(test)] +mod tests { + use super::Day; + use crate::Utc; + + #[test] + fn test_start_time() { + assert_eq!( + Day::from(Utc::now()).start(), + Utc::now() + .date_naive() + .and_hms_opt(0, 0, 0) + .unwrap() + .and_local_timezone(Utc) + .single() + .unwrap(), + ); + } +} + +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +mod serde { + use crate::{Day, TimeZone}; + use core::fmt::Display; + use serde::ser; + + // Currently no `Deserialize` option as there is no generic way to create a timezone + // from a string representation of it. This could be added to the `TimeZone` trait in future + + impl ser::Serialize for Day + where + Tz: TimeZone + Copy + Display, + { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.collect_str(&self) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 861ee10593..9ed03eb585 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -457,6 +457,9 @@ mod date; #[allow(deprecated)] pub use date::{Date, MAX_DATE, MIN_DATE}; +mod day; +pub use day::Day; + mod datetime; #[cfg(feature = "rustc-serialize")] #[cfg_attr(docsrs, doc(cfg(feature = "rustc-serialize")))] diff --git a/src/naive/time/mod.rs b/src/naive/time/mod.rs index 260423a9d1..8c5d0145f8 100644 --- a/src/naive/time/mod.rs +++ b/src/naive/time/mod.rs @@ -752,7 +752,7 @@ impl NaiveTime { (hour, min, sec) } - pub(super) const MIN: Self = Self { secs: 0, frac: 0 }; + pub(crate) const MIN: Self = Self { secs: 0, frac: 0 }; pub(super) const MAX: Self = Self { secs: 23 * 3600 + 59 * 60 + 59, frac: 999_999_999 }; }