From 285ff63f2557b3ef6e53525880428492d778a53a Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Sat, 1 Oct 2022 23:16:27 +1000 Subject: [PATCH 1/3] exploration on timezone trait --- src/timezone_alternative_a.rs | 198 ++++++++++++++++++++++++++++++++++ src/timezone_alternative_b.rs | 180 +++++++++++++++++++++++++++++++ 2 files changed, 378 insertions(+) create mode 100644 src/timezone_alternative_a.rs create mode 100644 src/timezone_alternative_b.rs diff --git a/src/timezone_alternative_a.rs b/src/timezone_alternative_a.rs new file mode 100644 index 0000000000..d17641cb92 --- /dev/null +++ b/src/timezone_alternative_a.rs @@ -0,0 +1,198 @@ +#![allow(dead_code, unreachable_pub)] + +use crate::offset::FixedOffset; +use crate::NaiveDateTime; +use crate::ParseResult; +use crate::TimeDelta; +use crate::Utc; + +pub struct DateTime { + datetime: NaiveDateTime, + zone: Tz, +} + +impl DateTime +where + Tz: TimeZoneA + Clone, +{ + // keep this out of the `TimeZone` trait to avoid object safety problems + fn parse_from_str_tz( + input: &str, + format: &str, + timezone: &Tz, + ) -> ParseResult, InvalidLocalTimeInfoTz>> { + todo!() + } +} + +impl NaiveDateTime { + fn and_local_timezone_2( + self, + timezone: &Tz, + ) -> Result, InvalidLocalTimeInfoTz> + where + Tz: TimeZoneA + Clone, + { + match timezone.offset_at_local(self) { + Ok(offset) => Ok(DateTime { datetime: self - offset, zone: timezone.clone() }), + Err(e) => Err(InvalidLocalTimeInfoTz { + local: e.local, + transition: e.transition, + tz: timezone.clone(), + }), + } + } + + fn and_timezone_2(self, timezone: &Tz) -> DateTime + where + Tz: TimeZoneA + Clone, + { + DateTime { datetime: self, zone: timezone.clone() } + } +} + +impl Clone for DateTime +where + Tz: TimeZoneA + Clone, +{ + fn clone(&self) -> Self { + DateTime { datetime: self.datetime, zone: self.zone.clone() } + } +} + +impl Copy for DateTime where Tz: TimeZoneA + Copy {} + +#[derive(Clone, PartialEq, Eq)] +pub struct Transition { + at: NaiveDateTime, + from: FixedOffset, + to: FixedOffset, +} + +impl Transition { + fn current_offset(&self) -> FixedOffset { + self.to + } + fn local_start(&self) -> NaiveDateTime { + self.at + self.from + } + fn local_end(&self) -> NaiveDateTime { + self.at + self.to + } +} + +pub struct InvalidLocalTimeInfo { + local: NaiveDateTime, + transition: Transition, +} + +pub struct InvalidLocalTimeInfoTz { + local: NaiveDateTime, + transition: Transition, + tz: Tz, +} + +// here the TimeZoneA should be something small or static, like an empty enum variant or an empty struct. +// potentially all the methods here should be fallible +// +// the implementor of TimeZoneA will usually store its current offset internally (if dynamic) or make it available as a +// const if static. +// +// where the TimeZoneA implemention handles daylight savings or other timezone that needs more data than just an offset +// it might store a `String` or enum variant which enables the `%Z` formatting, extracted via the `.name()` method. +// +// we move the `datetime_from_str` to the `DateTime` impl +// we have to avoid `from_local_datetime` and `from_utc_datetime` here +// and point users towards `and_local_timezone()` and `.and_timezone()` instead. +// because there is no way to force the `TimeZoneA` to implement `Clone` but still keep object safety. +// for all practical purposes all `TimeZoneA` implementors should probably implement at least `Clone` and likely `Copy` as well. +pub trait TimeZoneA { + // this could have a default implementation if there was a `from_fixed_offset` method + // in the trait, but that would be problematic for object safety, so instead + // the implemention is left to the user. + #[cfg(feature = "clock")] + fn offset_now(&self) -> FixedOffset { + self.offset_at(Utc::now().naive_utc()) + } + + fn offset(&self) -> FixedOffset; + + fn offset_at_local(&self, local: NaiveDateTime) -> Result { + match self.closest_transitions_from_local(local) { + None => Ok(self.offset()), + Some((previous, next)) if previous == next => { + Err(InvalidLocalTimeInfo { local, transition: previous }) + } + Some((previous, _)) => Ok(previous.to), + } + } + + fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset { + if let Some((from, _)) = self.closest_transitions(utc) { + from.current_offset() + } else { + self.offset() + } + } + + // potentially the `_transitions` functions should take a `local: bool` parameter + // as it would be incorrect to implement one but leave the other with the default impl + + // this is not hugely useful as it will just be the + // previous and next transitions, but it might be nice + // to expose this in public API what is currently just in `tzinfo`. + fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { + None + } + + // if the local timestamp is valid, then these transitions will each be different + // if the local timestamp is either ambiguous or invalid, then both fields of the + // tuple will be the same + fn closest_transitions_from_local( + &self, + local: NaiveDateTime, + ) -> Option<(Transition, Transition)> { + None + } + + // to be used in %Z formatting + fn name(&self) -> Option<&str> { + None + } +} + +impl TimeZoneA for Box { + fn offset_now(&self) -> FixedOffset { + self.as_ref().offset_now() + } + + fn offset(&self) -> FixedOffset { + self.as_ref().offset() + } + + fn offset_at_local(&self, local: NaiveDateTime) -> Result + where + Self: Sized, + { + self.as_ref().offset_at_local(local) + } + + fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset { + self.as_ref().offset_at(utc) + } + + fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { + self.as_ref().closest_transitions(utc) + } + + fn closest_transitions_from_local( + &self, + local: NaiveDateTime, + ) -> Option<(Transition, Transition)> { + self.as_ref().closest_transitions_from_local(local) + } + + fn name(&self) -> Option<&str> { + self.as_ref().name() + } +} diff --git a/src/timezone_alternative_b.rs b/src/timezone_alternative_b.rs new file mode 100644 index 0000000000..f068546622 --- /dev/null +++ b/src/timezone_alternative_b.rs @@ -0,0 +1,180 @@ +#![allow(dead_code, unreachable_pub)] + +use crate::offset::FixedOffset; +use crate::Days; +use crate::Months; +use crate::NaiveDateTime; +use crate::ParseResult; +use crate::TimeDelta; +use crate::Utc; + +// this is nice because it avoids the type paramter. +// alternatively the `TimeZoneManager` could have an assocaited `TimeZone` ZST or equivalent that this is parametized by +#[derive(Clone, Copy)] +pub struct DateTime { + datetime: NaiveDateTime, + offset: FixedOffset, + // could potentially include some information on the timezone name here to allow `%Z` style formatting +} + +impl DateTime { + // keep this out of the `TimeZone` trait to avoid object safety problems + fn parse_from_str_tz(input: &str, format: &str, timezone: &Tz) -> ParseResult + where + Tz: TimeZoneManager, + { + todo!() + } + + fn naive_utc(&self) -> NaiveDateTime { + self.datetime + } +} + +pub struct LocalTransition { + transition_start: (NaiveDateTime, FixedOffset), + transition_end: (NaiveDateTime, FixedOffset), +} + +pub struct Transition { + at: NaiveDateTime, + from: FixedOffset, + to: FixedOffset, +} + +impl Transition { + fn current_offset(&self) -> FixedOffset { + self.to + } +} + +pub struct LocalResult { + local_timestamp: NaiveDateTime, + transition: LocalTransition, +} + +// the timezone is used to manage the DateTime, but the datetime never contains the timezone itself +// this is useful as the TimeZone might contain a bunch of information from a parsed tzinfo file +// and so it is useful that the user can control the caching of this +// +// this is also simpler as there is no longer a type parameter needed in the DateTime +// +// object safety is argurably less useful here as the `TimeZone` lives outside the `DateTime`, so +// some of the choices made to enable could be unwound if it is not deemed necessary +// +// This could offer a nice migration path by having `DateTime` and +// calling this trait `TimeZoneManager` or something better +pub trait TimeZoneManager { + fn add_months(&self, dt: DateTime, months: Months) -> DateTime { + let new_datetime = dt.naive_utc() + months; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn sub_months(&self, dt: DateTime, months: Months) -> DateTime { + let new_datetime = dt.naive_utc() - months; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn add_days(&self, dt: DateTime, days: Days) -> DateTime { + let new_datetime = dt.naive_utc() + days; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn sub_days(&self, dt: DateTime, days: Days) -> DateTime { + let new_datetime = dt.naive_utc() - days; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn add(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + let new_datetime = dt.naive_utc() + duration; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn sub(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + let new_datetime = dt.naive_utc() - duration; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + + #[cfg(feature = "clock")] + fn now(&self) -> DateTime { + let now = Utc::now().naive_utc(); + DateTime { datetime: now, offset: self.offset_at(now) } + } + + fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset; + + fn offset_at_local(&self, local: NaiveDateTime) -> LocalResult; + + // we can likely avoid `from_local_datetime` and `from_utc_datetime` here + // and point users towards `and_local_timezone()` and `.and_timezone()` instead. + + // potentially the `_transitions` functions should take a `local: bool` parameter + // as it would be incorrect to implement one but leave the other with the default impl + + // this is not hugely useful as it will just be the + // previous and next transitions, but it might be nice + // to expose this in public API what is currently just in `tzinfo`. + fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { + None + } + + // if the local timestamp is valid, then these transitions will each be different + // if the local timestamp is either ambiguous or invalid, then both fields of the + // tuple will be the same + fn closest_transitions_from_local( + &self, + local: NaiveDateTime, + ) -> Option<(Transition, Transition)> { + None + } + + // to be used in %Z formatting + fn name(&self) -> Option<&str> { + None + } + + fn parse_from_str(&self, input: &str, format: &str) -> ParseResult; +} + +impl TimeZoneManager for Box { + fn add_months(&self, dt: DateTime, months: Months) -> DateTime { + self.as_ref().add_months(dt, months) + } + fn sub_months(&self, dt: DateTime, months: Months) -> DateTime { + self.as_ref().sub_months(dt, months) + } + fn add_days(&self, dt: DateTime, days: Days) -> DateTime { + self.as_ref().add_days(dt, days) + } + fn sub_days(&self, dt: DateTime, days: Days) -> DateTime { + self.as_ref().sub_days(dt, days) + } + fn add(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + self.as_ref().add(dt, duration) + } + fn sub(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + self.as_ref().sub(dt, duration) + } + + fn now(&self) -> DateTime { + self.as_ref().now() + } + + fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset { + self.as_ref().offset_at(utc) + } + + fn offset_at_local(&self, local: NaiveDateTime) -> LocalResult { + self.as_ref().offset_at_local(local) + } + + fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { + self.as_ref().closest_transitions(utc) + } + + fn closest_transitions_from_local( + &self, + local: NaiveDateTime, + ) -> Option<(Transition, Transition)> { + self.as_ref().closest_transitions_from_local(local) + } + + fn parse_from_str(&self, input: &str, format: &str) -> ParseResult { + self.as_ref().parse_from_str(input, format) + } +} From 60cbdcb19da2bc11a5c03b4db3dbea895e476970 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Wed, 5 Oct 2022 22:45:06 +1100 Subject: [PATCH 2/3] add option c --- src/lib.rs | 4 + src/timezone_alternative_c.rs | 198 ++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 src/timezone_alternative_c.rs diff --git a/src/lib.rs b/src/lib.rs index 4db2128d47..a3afa272ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -432,6 +432,10 @@ pub mod prelude { pub use crate::{Offset, TimeZone}; } +mod timezone_alternative_a; +mod timezone_alternative_b; +mod timezone_alternative_c; + mod date; #[allow(deprecated)] pub use date::{Date, MAX_DATE, MIN_DATE}; diff --git a/src/timezone_alternative_c.rs b/src/timezone_alternative_c.rs new file mode 100644 index 0000000000..3518aef517 --- /dev/null +++ b/src/timezone_alternative_c.rs @@ -0,0 +1,198 @@ +#![allow(dead_code, unreachable_pub)] + +use crate::offset::FixedOffset; +use crate::Days; +use crate::Months; +use crate::NaiveDateTime; +use crate::ParseResult; +use crate::TimeDelta; +use crate::Utc; + +// when using fixed offsets, can just call this `DateTime`, in other cases, have to provide the type parameter. +#[derive(Clone, Copy)] +pub struct DateTime +where + O: Offset, +{ + datetime: NaiveDateTime, + offset: O, +} + +pub trait Offset { + fn fix(&self) -> FixedOffset; + fn name(&self) -> Option<&str>; +} + +impl Offset for FixedOffset { + fn fix(&self) -> FixedOffset { + *self + } + fn name(&self) -> Option<&str> { + None + } +} + +// no add/sub methods, these require the timezone instance +impl DateTime +where + O: Offset, +{ + fn naive_utc(&self) -> NaiveDateTime { + self.datetime + } + fn fixed(&self) -> DateTime { + DateTime { offset: self.offset.fix(), datetime: self.datetime } + } +} + +// can have add/sub methods and impls here as this doesn't +// require the timezone instance +impl DateTime { + fn add_months(&self, months: Months) -> DateTime { + let new_datetime = self.naive_utc() + months; + DateTime { datetime: new_datetime, ..*self } + } + fn sub_months(&self, months: Months) -> DateTime { + let new_datetime = self.naive_utc() - months; + DateTime { datetime: new_datetime, ..*self } + } + fn add_days(&self, days: Days) -> DateTime { + let new_datetime = self.naive_utc() + days; + DateTime { datetime: new_datetime, ..*self } + } + fn sub_days(&self, days: Days) -> DateTime { + let new_datetime = self.naive_utc() - days; + DateTime { datetime: new_datetime, ..*self } + } + fn add(&self, duration: TimeDelta) -> DateTime { + let new_datetime = self.naive_utc() + duration; + DateTime { datetime: new_datetime, ..*self } + } + fn sub(&self, duration: TimeDelta) -> DateTime { + let new_datetime = self.naive_utc() - duration; + DateTime { datetime: new_datetime, ..*self } + } +} + +pub struct LocalTransition +where + O: Offset, +{ + transition_start: (NaiveDateTime, O), + transition_end: (NaiveDateTime, O), +} + +pub struct Transition +where + O: Offset, +{ + at: NaiveDateTime, + from: O, + to: O, +} + +pub struct ClosestTransitions +where + O: Offset, +{ + before: Transition, + after: Transition, +} + +impl Transition { + fn current_offset(&self) -> FixedOffset { + self.to + } +} + +pub struct InvalidLocalTimeInfo +where + O: Offset, +{ + local_timestamp: NaiveDateTime, + transition: LocalTransition, +} + +// the timezone is used to manage the DateTime, but the datetime never contains the timezone itself +// this is useful as the TimeZone might contain a bunch of information from a parsed tzinfo file +// and so it is useful that the user can control the caching of this +// +// this is also simpler as there is no longer a type parameter needed in the DateTime +// +// object safety is argurably less useful here as the `TimeZone` lives outside the `DateTime`, so +// some of the choices made to enable could be unwound if it is not deemed necessary +// +// This could offer a nice migration path by having `DateTime` and +// calling this trait `TimeZoneManager` or something better +pub trait TimeZoneManager { + type Offset: Offset; + + fn add_months(&self, dt: DateTime, months: Months) -> DateTime { + let new_datetime = dt.naive_utc() + months; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn sub_months(&self, dt: DateTime, months: Months) -> DateTime { + let new_datetime = dt.naive_utc() - months; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn add_days(&self, dt: DateTime, days: Days) -> DateTime { + let new_datetime = dt.naive_utc() + days; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn sub_days(&self, dt: DateTime, days: Days) -> DateTime { + let new_datetime = dt.naive_utc() - days; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn add(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + let new_datetime = dt.naive_utc() + duration; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + fn sub(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + let new_datetime = dt.naive_utc() - duration; + DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } + } + + #[cfg(feature = "clock")] + fn now(&self) -> DateTime { + let now = Utc::now().naive_utc(); + DateTime { datetime: now, offset: self.offset_at(now) } + } + + fn offset_at(&self, utc: NaiveDateTime) -> Self::Offset; + + fn offset_at_local(&self, local: NaiveDateTime) -> InvalidLocalTimeInfo; + + // we can likely avoid `from_local_datetime` and `from_utc_datetime` here + // and point users towards `and_local_timezone()` and `.and_timezone()` instead. + + // potentially the `_transitions` functions should take a `local: bool` parameter + // as it would be incorrect to implement one but leave the other with the default impl + + // this is not hugely useful as it will just be the + // previous and next transitions, but it might be nice + // to expose this in public API what is currently just in `tzinfo`. + fn closest_transitions(&self, _utc: NaiveDateTime) -> Option> { + None + } + + // if the local timestamp is valid, then these transitions will each be different + // if the local timestamp is either ambiguous or invalid, then both fields of the + // tuple will be the same + fn closest_transitions_from_local( + &self, + _local: NaiveDateTime, + ) -> Option> { + None + } + + // to be used in %Z formatting + fn name(&self) -> Option<&str> { + None + } + + fn parse_from_str( + &self, + input: &str, + format: &str, + ) -> ParseResult>; +} From 5a8f10ebc7dd763139da666e02b2480a1b6228d6 Mon Sep 17 00:00:00 2001 From: Eric Sheppard Date: Fri, 7 Oct 2022 22:58:05 +1100 Subject: [PATCH 3/3] add option d, remove b and c --- src/lib.rs | 3 +- src/timezone_alternative_a.rs | 210 +++++++++++++--- src/timezone_alternative_b.rs | 180 ------------- src/timezone_alternative_c.rs | 198 --------------- src/timezone_alternative_d.rs | 458 ++++++++++++++++++++++++++++++++++ 5 files changed, 636 insertions(+), 413 deletions(-) delete mode 100644 src/timezone_alternative_b.rs delete mode 100644 src/timezone_alternative_c.rs create mode 100644 src/timezone_alternative_d.rs diff --git a/src/lib.rs b/src/lib.rs index a3afa272ca..e64c3c0c36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -433,8 +433,7 @@ pub mod prelude { } mod timezone_alternative_a; -mod timezone_alternative_b; -mod timezone_alternative_c; +mod timezone_alternative_d; mod date; #[allow(deprecated)] diff --git a/src/timezone_alternative_a.rs b/src/timezone_alternative_a.rs index d17641cb92..15c3bef567 100644 --- a/src/timezone_alternative_a.rs +++ b/src/timezone_alternative_a.rs @@ -1,28 +1,68 @@ #![allow(dead_code, unreachable_pub)] use crate::offset::FixedOffset; +use crate::Days; +use crate::Months; use crate::NaiveDateTime; use crate::ParseResult; use crate::TimeDelta; use crate::Utc; +use core::fmt::{Debug, Display}; +use std::error::Error; -pub struct DateTime { +pub struct DateTime { datetime: NaiveDateTime, zone: Tz, } impl DateTime where - Tz: TimeZoneA + Clone, + Tz: TimeZone, +{ + fn naive_utc(&self) -> NaiveDateTime { + self.datetime + } + fn fixed(&self) -> DateTime { + DateTime { zone: self.zone.offset(), datetime: self.datetime } + } +} + +impl DateTime +where + Tz: TimeZone + Clone, { // keep this out of the `TimeZone` trait to avoid object safety problems fn parse_from_str_tz( - input: &str, - format: &str, - timezone: &Tz, + _input: &str, + _format: &str, + _timezone: &Tz, ) -> ParseResult, InvalidLocalTimeInfoTz>> { todo!() } + fn add_months(&self, months: Months) -> DateTime { + let new_datetime = self.naive_utc() + months; + new_datetime.and_timezone_2(&self.zone) + } + fn sub_months(&self, months: Months) -> DateTime { + let new_datetime = self.naive_utc() - months; + new_datetime.and_timezone_2(&self.zone) + } + fn add_days(&self, days: Days) -> DateTime { + let new_datetime = self.naive_utc() + days; + new_datetime.and_timezone_2(&self.zone) + } + fn sub_days(&self, days: Days) -> DateTime { + let new_datetime = self.naive_utc() - days; + new_datetime.and_timezone_2(&self.zone) + } + fn add(&self, duration: TimeDelta) -> DateTime { + let new_datetime = self.naive_utc() + duration; + new_datetime.and_timezone_2(&self.zone) + } + fn sub(&self, duration: TimeDelta) -> DateTime { + let new_datetime = self.naive_utc() - duration; + new_datetime.and_timezone_2(&self.zone) + } } impl NaiveDateTime { @@ -31,10 +71,14 @@ impl NaiveDateTime { timezone: &Tz, ) -> Result, InvalidLocalTimeInfoTz> where - Tz: TimeZoneA + Clone, + Tz: TimeZone + Clone, { match timezone.offset_at_local(self) { - Ok(offset) => Ok(DateTime { datetime: self - offset, zone: timezone.clone() }), + Ok(offset) => { + let mut zone = timezone.clone(); + zone.update_offset_at_local(self).map_err(|e| e.and_tz(timezone.clone()))?; + Ok(DateTime { datetime: self - offset, zone }) + } Err(e) => Err(InvalidLocalTimeInfoTz { local: e.local, transition: e.transition, @@ -45,31 +89,45 @@ impl NaiveDateTime { fn and_timezone_2(self, timezone: &Tz) -> DateTime where - Tz: TimeZoneA + Clone, + Tz: TimeZone + Clone, { - DateTime { datetime: self, zone: timezone.clone() } + let mut zone = timezone.clone(); + zone.update_offset_at(self); + DateTime { datetime: self, zone } } } impl Clone for DateTime where - Tz: TimeZoneA + Clone, + Tz: TimeZone + Clone, { fn clone(&self) -> Self { DateTime { datetime: self.datetime, zone: self.zone.clone() } } } -impl Copy for DateTime where Tz: TimeZoneA + Copy {} +impl Copy for DateTime where Tz: TimeZone + Copy {} -#[derive(Clone, PartialEq, Eq)] -pub struct Transition { +// a given transition time, similar format to tzinfo, +// including the Utc timestamp of the transition, +// the offset prior to the transition, and the offset +// after the transition +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UtcTransition { at: NaiveDateTime, from: FixedOffset, to: FixedOffset, } -impl Transition { +// a transition but where the NaiveDateTime's represent +// a local time rather than a Utc time. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LocalTransition { + transition_start: (NaiveDateTime, FixedOffset), + transition_end: (NaiveDateTime, FixedOffset), +} + +impl UtcTransition { fn current_offset(&self) -> FixedOffset { self.to } @@ -79,19 +137,87 @@ impl Transition { fn local_end(&self) -> NaiveDateTime { self.at + self.to } + fn local(&self) -> LocalTransition { + LocalTransition { + transition_start: (self.local_start(), self.from), + transition_end: (self.local_end(), self.to), + } + } } +// this structure is returned when asking for the transitions +// immediately prior to and after a given Utc or Local time. +// when asking with a given local time, the before and after +// will occasionally be equal +pub struct ClosestTransitions { + before: UtcTransition, + after: UtcTransition, +} + +// a replacement for the Err part of a LocalResult. +// this allows us to use a regular std::result::Result +// and pass this in the Err branch +// +// this should also contain enough of the original data +// such that it is possible to implement helper methods \ +// to, for example, get a "good enough" conversion from a +// local time where the local timestamp is invalid or ambiguous +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InvalidLocalTimeInfo { local: NaiveDateTime, - transition: Transition, + transition: UtcTransition, } +impl Display for InvalidLocalTimeInfo { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + todo!() + } +} + +impl Error for InvalidLocalTimeInfo {} + +impl InvalidLocalTimeInfo { + fn and_tz(&self, tz: Tz) -> InvalidLocalTimeInfoTz { + InvalidLocalTimeInfoTz { local: self.local, transition: self.transition.clone(), tz } + } +} + +// as above, but with the TimeZone parameter. This exists because some API's +// can't return this version due to object safety, but it is still nice to +// have where possible. pub struct InvalidLocalTimeInfoTz { local: NaiveDateTime, - transition: Transition, + transition: UtcTransition, tz: Tz, } +impl Clone for InvalidLocalTimeInfoTz +where + Tz: Clone, +{ + fn clone(&self) -> Self { + InvalidLocalTimeInfoTz { + local: self.local, + transition: self.transition.clone(), + tz: self.tz.clone(), + } + } +} + +impl Debug for InvalidLocalTimeInfoTz { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + todo!() + } +} + +impl Display for InvalidLocalTimeInfoTz { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + todo!() + } +} + +impl Error for InvalidLocalTimeInfoTz {} + // here the TimeZoneA should be something small or static, like an empty enum variant or an empty struct. // potentially all the methods here should be fallible // @@ -106,7 +232,7 @@ pub struct InvalidLocalTimeInfoTz { // and point users towards `and_local_timezone()` and `.and_timezone()` instead. // because there is no way to force the `TimeZoneA` to implement `Clone` but still keep object safety. // for all practical purposes all `TimeZoneA` implementors should probably implement at least `Clone` and likely `Copy` as well. -pub trait TimeZoneA { +pub trait TimeZone { // this could have a default implementation if there was a `from_fixed_offset` method // in the trait, but that would be problematic for object safety, so instead // the implemention is left to the user. @@ -120,38 +246,45 @@ pub trait TimeZoneA { fn offset_at_local(&self, local: NaiveDateTime) -> Result { match self.closest_transitions_from_local(local) { None => Ok(self.offset()), - Some((previous, next)) if previous == next => { - Err(InvalidLocalTimeInfo { local, transition: previous }) + Some(ClosestTransitions { before, after }) if before == after => { + Err(InvalidLocalTimeInfo { local, transition: before }) } - Some((previous, _)) => Ok(previous.to), + Some(ClosestTransitions { before, .. }) => Ok(before.to), } } fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset { - if let Some((from, _)) = self.closest_transitions(utc) { - from.current_offset() + if let Some(ClosestTransitions { before, .. }) = self.closest_transitions(utc) { + before.current_offset() } else { self.offset() } } + // this is needed as we can't construct a new `TimeZoneA` and still be object safe + fn update_offset_at_local( + &mut self, + _local: NaiveDateTime, + ) -> Result<(), InvalidLocalTimeInfo> { + Ok(()) + } + + fn update_offset_at(&mut self, _utc: NaiveDateTime) {} + // potentially the `_transitions` functions should take a `local: bool` parameter // as it would be incorrect to implement one but leave the other with the default impl // this is not hugely useful as it will just be the // previous and next transitions, but it might be nice // to expose this in public API what is currently just in `tzinfo`. - fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { + fn closest_transitions(&self, _utc: NaiveDateTime) -> Option { None } // if the local timestamp is valid, then these transitions will each be different // if the local timestamp is either ambiguous or invalid, then both fields of the // tuple will be the same - fn closest_transitions_from_local( - &self, - local: NaiveDateTime, - ) -> Option<(Transition, Transition)> { + fn closest_transitions_from_local(&self, _local: NaiveDateTime) -> Option { None } @@ -161,7 +294,7 @@ pub trait TimeZoneA { } } -impl TimeZoneA for Box { +impl TimeZone for Box { fn offset_now(&self) -> FixedOffset { self.as_ref().offset_now() } @@ -181,14 +314,19 @@ impl TimeZoneA for Box { self.as_ref().offset_at(utc) } - fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { + fn update_offset_at_local(&mut self, local: NaiveDateTime) -> Result<(), InvalidLocalTimeInfo> { + self.as_mut().update_offset_at_local(local) + } + + fn update_offset_at(&mut self, utc: NaiveDateTime) { + self.as_mut().update_offset_at(utc) + } + + fn closest_transitions(&self, utc: NaiveDateTime) -> Option { self.as_ref().closest_transitions(utc) } - fn closest_transitions_from_local( - &self, - local: NaiveDateTime, - ) -> Option<(Transition, Transition)> { + fn closest_transitions_from_local(&self, local: NaiveDateTime) -> Option { self.as_ref().closest_transitions_from_local(local) } @@ -196,3 +334,9 @@ impl TimeZoneA for Box { self.as_ref().name() } } + +impl TimeZone for FixedOffset { + fn offset(&self) -> FixedOffset { + crate::offset::Offset::fix(self) + } +} diff --git a/src/timezone_alternative_b.rs b/src/timezone_alternative_b.rs deleted file mode 100644 index f068546622..0000000000 --- a/src/timezone_alternative_b.rs +++ /dev/null @@ -1,180 +0,0 @@ -#![allow(dead_code, unreachable_pub)] - -use crate::offset::FixedOffset; -use crate::Days; -use crate::Months; -use crate::NaiveDateTime; -use crate::ParseResult; -use crate::TimeDelta; -use crate::Utc; - -// this is nice because it avoids the type paramter. -// alternatively the `TimeZoneManager` could have an assocaited `TimeZone` ZST or equivalent that this is parametized by -#[derive(Clone, Copy)] -pub struct DateTime { - datetime: NaiveDateTime, - offset: FixedOffset, - // could potentially include some information on the timezone name here to allow `%Z` style formatting -} - -impl DateTime { - // keep this out of the `TimeZone` trait to avoid object safety problems - fn parse_from_str_tz(input: &str, format: &str, timezone: &Tz) -> ParseResult - where - Tz: TimeZoneManager, - { - todo!() - } - - fn naive_utc(&self) -> NaiveDateTime { - self.datetime - } -} - -pub struct LocalTransition { - transition_start: (NaiveDateTime, FixedOffset), - transition_end: (NaiveDateTime, FixedOffset), -} - -pub struct Transition { - at: NaiveDateTime, - from: FixedOffset, - to: FixedOffset, -} - -impl Transition { - fn current_offset(&self) -> FixedOffset { - self.to - } -} - -pub struct LocalResult { - local_timestamp: NaiveDateTime, - transition: LocalTransition, -} - -// the timezone is used to manage the DateTime, but the datetime never contains the timezone itself -// this is useful as the TimeZone might contain a bunch of information from a parsed tzinfo file -// and so it is useful that the user can control the caching of this -// -// this is also simpler as there is no longer a type parameter needed in the DateTime -// -// object safety is argurably less useful here as the `TimeZone` lives outside the `DateTime`, so -// some of the choices made to enable could be unwound if it is not deemed necessary -// -// This could offer a nice migration path by having `DateTime` and -// calling this trait `TimeZoneManager` or something better -pub trait TimeZoneManager { - fn add_months(&self, dt: DateTime, months: Months) -> DateTime { - let new_datetime = dt.naive_utc() + months; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn sub_months(&self, dt: DateTime, months: Months) -> DateTime { - let new_datetime = dt.naive_utc() - months; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn add_days(&self, dt: DateTime, days: Days) -> DateTime { - let new_datetime = dt.naive_utc() + days; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn sub_days(&self, dt: DateTime, days: Days) -> DateTime { - let new_datetime = dt.naive_utc() - days; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn add(&self, dt: DateTime, duration: TimeDelta) -> DateTime { - let new_datetime = dt.naive_utc() + duration; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn sub(&self, dt: DateTime, duration: TimeDelta) -> DateTime { - let new_datetime = dt.naive_utc() - duration; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - - #[cfg(feature = "clock")] - fn now(&self) -> DateTime { - let now = Utc::now().naive_utc(); - DateTime { datetime: now, offset: self.offset_at(now) } - } - - fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset; - - fn offset_at_local(&self, local: NaiveDateTime) -> LocalResult; - - // we can likely avoid `from_local_datetime` and `from_utc_datetime` here - // and point users towards `and_local_timezone()` and `.and_timezone()` instead. - - // potentially the `_transitions` functions should take a `local: bool` parameter - // as it would be incorrect to implement one but leave the other with the default impl - - // this is not hugely useful as it will just be the - // previous and next transitions, but it might be nice - // to expose this in public API what is currently just in `tzinfo`. - fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { - None - } - - // if the local timestamp is valid, then these transitions will each be different - // if the local timestamp is either ambiguous or invalid, then both fields of the - // tuple will be the same - fn closest_transitions_from_local( - &self, - local: NaiveDateTime, - ) -> Option<(Transition, Transition)> { - None - } - - // to be used in %Z formatting - fn name(&self) -> Option<&str> { - None - } - - fn parse_from_str(&self, input: &str, format: &str) -> ParseResult; -} - -impl TimeZoneManager for Box { - fn add_months(&self, dt: DateTime, months: Months) -> DateTime { - self.as_ref().add_months(dt, months) - } - fn sub_months(&self, dt: DateTime, months: Months) -> DateTime { - self.as_ref().sub_months(dt, months) - } - fn add_days(&self, dt: DateTime, days: Days) -> DateTime { - self.as_ref().add_days(dt, days) - } - fn sub_days(&self, dt: DateTime, days: Days) -> DateTime { - self.as_ref().sub_days(dt, days) - } - fn add(&self, dt: DateTime, duration: TimeDelta) -> DateTime { - self.as_ref().add(dt, duration) - } - fn sub(&self, dt: DateTime, duration: TimeDelta) -> DateTime { - self.as_ref().sub(dt, duration) - } - - fn now(&self) -> DateTime { - self.as_ref().now() - } - - fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset { - self.as_ref().offset_at(utc) - } - - fn offset_at_local(&self, local: NaiveDateTime) -> LocalResult { - self.as_ref().offset_at_local(local) - } - - fn closest_transitions(&self, utc: NaiveDateTime) -> Option<(Transition, Transition)> { - self.as_ref().closest_transitions(utc) - } - - fn closest_transitions_from_local( - &self, - local: NaiveDateTime, - ) -> Option<(Transition, Transition)> { - self.as_ref().closest_transitions_from_local(local) - } - - fn parse_from_str(&self, input: &str, format: &str) -> ParseResult { - self.as_ref().parse_from_str(input, format) - } -} diff --git a/src/timezone_alternative_c.rs b/src/timezone_alternative_c.rs deleted file mode 100644 index 3518aef517..0000000000 --- a/src/timezone_alternative_c.rs +++ /dev/null @@ -1,198 +0,0 @@ -#![allow(dead_code, unreachable_pub)] - -use crate::offset::FixedOffset; -use crate::Days; -use crate::Months; -use crate::NaiveDateTime; -use crate::ParseResult; -use crate::TimeDelta; -use crate::Utc; - -// when using fixed offsets, can just call this `DateTime`, in other cases, have to provide the type parameter. -#[derive(Clone, Copy)] -pub struct DateTime -where - O: Offset, -{ - datetime: NaiveDateTime, - offset: O, -} - -pub trait Offset { - fn fix(&self) -> FixedOffset; - fn name(&self) -> Option<&str>; -} - -impl Offset for FixedOffset { - fn fix(&self) -> FixedOffset { - *self - } - fn name(&self) -> Option<&str> { - None - } -} - -// no add/sub methods, these require the timezone instance -impl DateTime -where - O: Offset, -{ - fn naive_utc(&self) -> NaiveDateTime { - self.datetime - } - fn fixed(&self) -> DateTime { - DateTime { offset: self.offset.fix(), datetime: self.datetime } - } -} - -// can have add/sub methods and impls here as this doesn't -// require the timezone instance -impl DateTime { - fn add_months(&self, months: Months) -> DateTime { - let new_datetime = self.naive_utc() + months; - DateTime { datetime: new_datetime, ..*self } - } - fn sub_months(&self, months: Months) -> DateTime { - let new_datetime = self.naive_utc() - months; - DateTime { datetime: new_datetime, ..*self } - } - fn add_days(&self, days: Days) -> DateTime { - let new_datetime = self.naive_utc() + days; - DateTime { datetime: new_datetime, ..*self } - } - fn sub_days(&self, days: Days) -> DateTime { - let new_datetime = self.naive_utc() - days; - DateTime { datetime: new_datetime, ..*self } - } - fn add(&self, duration: TimeDelta) -> DateTime { - let new_datetime = self.naive_utc() + duration; - DateTime { datetime: new_datetime, ..*self } - } - fn sub(&self, duration: TimeDelta) -> DateTime { - let new_datetime = self.naive_utc() - duration; - DateTime { datetime: new_datetime, ..*self } - } -} - -pub struct LocalTransition -where - O: Offset, -{ - transition_start: (NaiveDateTime, O), - transition_end: (NaiveDateTime, O), -} - -pub struct Transition -where - O: Offset, -{ - at: NaiveDateTime, - from: O, - to: O, -} - -pub struct ClosestTransitions -where - O: Offset, -{ - before: Transition, - after: Transition, -} - -impl Transition { - fn current_offset(&self) -> FixedOffset { - self.to - } -} - -pub struct InvalidLocalTimeInfo -where - O: Offset, -{ - local_timestamp: NaiveDateTime, - transition: LocalTransition, -} - -// the timezone is used to manage the DateTime, but the datetime never contains the timezone itself -// this is useful as the TimeZone might contain a bunch of information from a parsed tzinfo file -// and so it is useful that the user can control the caching of this -// -// this is also simpler as there is no longer a type parameter needed in the DateTime -// -// object safety is argurably less useful here as the `TimeZone` lives outside the `DateTime`, so -// some of the choices made to enable could be unwound if it is not deemed necessary -// -// This could offer a nice migration path by having `DateTime` and -// calling this trait `TimeZoneManager` or something better -pub trait TimeZoneManager { - type Offset: Offset; - - fn add_months(&self, dt: DateTime, months: Months) -> DateTime { - let new_datetime = dt.naive_utc() + months; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn sub_months(&self, dt: DateTime, months: Months) -> DateTime { - let new_datetime = dt.naive_utc() - months; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn add_days(&self, dt: DateTime, days: Days) -> DateTime { - let new_datetime = dt.naive_utc() + days; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn sub_days(&self, dt: DateTime, days: Days) -> DateTime { - let new_datetime = dt.naive_utc() - days; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn add(&self, dt: DateTime, duration: TimeDelta) -> DateTime { - let new_datetime = dt.naive_utc() + duration; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - fn sub(&self, dt: DateTime, duration: TimeDelta) -> DateTime { - let new_datetime = dt.naive_utc() - duration; - DateTime { datetime: new_datetime, offset: self.offset_at(new_datetime) } - } - - #[cfg(feature = "clock")] - fn now(&self) -> DateTime { - let now = Utc::now().naive_utc(); - DateTime { datetime: now, offset: self.offset_at(now) } - } - - fn offset_at(&self, utc: NaiveDateTime) -> Self::Offset; - - fn offset_at_local(&self, local: NaiveDateTime) -> InvalidLocalTimeInfo; - - // we can likely avoid `from_local_datetime` and `from_utc_datetime` here - // and point users towards `and_local_timezone()` and `.and_timezone()` instead. - - // potentially the `_transitions` functions should take a `local: bool` parameter - // as it would be incorrect to implement one but leave the other with the default impl - - // this is not hugely useful as it will just be the - // previous and next transitions, but it might be nice - // to expose this in public API what is currently just in `tzinfo`. - fn closest_transitions(&self, _utc: NaiveDateTime) -> Option> { - None - } - - // if the local timestamp is valid, then these transitions will each be different - // if the local timestamp is either ambiguous or invalid, then both fields of the - // tuple will be the same - fn closest_transitions_from_local( - &self, - _local: NaiveDateTime, - ) -> Option> { - None - } - - // to be used in %Z formatting - fn name(&self) -> Option<&str> { - None - } - - fn parse_from_str( - &self, - input: &str, - format: &str, - ) -> ParseResult>; -} diff --git a/src/timezone_alternative_d.rs b/src/timezone_alternative_d.rs new file mode 100644 index 0000000000..781182f4ce --- /dev/null +++ b/src/timezone_alternative_d.rs @@ -0,0 +1,458 @@ +#![allow(dead_code, unreachable_pub)] + +use crate::offset::FixedOffset; +use crate::Days; +use crate::Months; +use crate::NaiveDateTime; +use crate::ParseResult; +use crate::TimeDelta; +use crate::Utc; +use core::fmt::{Debug, Display}; +use std::error::Error; + +pub struct DateTime { + datetime: NaiveDateTime, + zone: Tz, +} + +impl DateTime +where + Tz: TimeZone, +{ + fn naive_utc(&self) -> NaiveDateTime { + self.datetime + } + fn fixed(&self) -> DateTime { + DateTime { zone: self.zone.offset(), datetime: self.datetime } + } +} + +impl DateTime +where + Tz: TimeZone + Clone, +{ + // keep this out of the `TimeZone` trait to avoid object safety problems + fn parse_from_str_tz( + _input: &str, + _format: &str, + _timezone: &Tz, + ) -> ParseResult, InvalidLocalTimeInfoTz>> { + todo!() + } +} + +/// Marker trait which signifies that the +/// timezone implementor can statically access +/// the timezone info (if required) +/// +/// potentially this should be sealed? +/// +/// For types like `FixedOffset`, `Utc` and `chrono_tz::Tz` this is trivial +/// +/// for `Local` it actually caches the parsed Tzinfo and/or TZ environment variable +/// in a `thread_local!` +pub trait EnableDirectOpsImpls: TimeZone {} + +// DateTime conditionally impl's add and sub (the operators would be implemented here as well) +// when the Tz declares that it has `EnableDirectOpsImpls`. +// this includes `FixedOffset`, `Utc`, `Local` and `chrono_tz::Tz`. +// +// however, if a user desires to maintain their parsed tzinfo file externally for whatever reason +// then they can use a `Tz` which doesn't implement `EnableDirectOpsImpls` and then use the +// `TimeZoneManager` trait to do add and sub operations. +impl DateTime +where + Tz: EnableDirectOpsImpls + Clone, +{ + fn add_months(&self, months: Months) -> DateTime { + let new_datetime = self.naive_utc() + months; + new_datetime.and_timezone_3(&self.zone) + } + fn sub_months(&self, months: Months) -> DateTime { + let new_datetime = self.naive_utc() - months; + new_datetime.and_timezone_3(&self.zone) + } + fn add_days(&self, days: Days) -> DateTime { + let new_datetime = self.naive_utc() + days; + new_datetime.and_timezone_3(&self.zone) + } + fn sub_days(&self, days: Days) -> DateTime { + let new_datetime = self.naive_utc() - days; + new_datetime.and_timezone_3(&self.zone) + } + fn add(&self, duration: TimeDelta) -> DateTime { + let new_datetime = self.naive_utc() + duration; + new_datetime.and_timezone_3(&self.zone) + } + fn sub(&self, duration: TimeDelta) -> DateTime { + let new_datetime = self.naive_utc() - duration; + new_datetime.and_timezone_3(&self.zone) + } +} + +impl NaiveDateTime { + fn and_local_timezone_3( + self, + timezone: &Tz, + ) -> Result, InvalidLocalTimeInfoTz> + where + Tz: TimeZone + Clone, + { + match timezone.offset_at_local(self) { + Ok(offset) => { + let mut zone = timezone.clone(); + zone.update_offset_at_local(self).map_err(|e| e.and_tz(timezone.clone()))?; + Ok(DateTime { datetime: self - offset, zone }) + } + Err(e) => Err(InvalidLocalTimeInfoTz { + local: e.local, + transition: e.transition, + tz: timezone.clone(), + }), + } + } + + fn and_timezone_3(self, timezone: &Tz) -> DateTime + where + Tz: TimeZone + Clone, + { + let mut zone = timezone.clone(); + zone.update_offset_at(self); + DateTime { datetime: self, zone } + } +} + +impl Clone for DateTime +where + Tz: TimeZone + Clone, +{ + fn clone(&self) -> Self { + DateTime { datetime: self.datetime, zone: self.zone.clone() } + } +} + +impl Copy for DateTime where Tz: TimeZone + Copy {} + +// a given transition time, similar format to tzinfo, +// including the Utc timestamp of the transition, +// the offset prior to the transition, and the offset +// after the transition +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UtcTransition { + at: NaiveDateTime, + from: FixedOffset, + to: FixedOffset, +} + +// a transition but where the NaiveDateTime's represent +// a local time rather than a Utc time. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LocalTransition { + transition_start: (NaiveDateTime, FixedOffset), + transition_end: (NaiveDateTime, FixedOffset), +} + +impl UtcTransition { + fn current_offset(&self) -> FixedOffset { + self.to + } + fn local_start(&self) -> NaiveDateTime { + self.at + self.from + } + fn local_end(&self) -> NaiveDateTime { + self.at + self.to + } + fn local(&self) -> LocalTransition { + LocalTransition { + transition_start: (self.local_start(), self.from), + transition_end: (self.local_end(), self.to), + } + } +} + +// this structure is returned when asking for the transitions +// immediately prior to and after a given Utc or Local time. +// when asking with a given local time, the before and after +// will occasionally be equal +pub struct ClosestTransitions { + before: UtcTransition, + after: UtcTransition, +} + +// a replacement for the Err part of a LocalResult. +// this allows us to use a regular std::result::Result +// and pass this in the Err branch +// +// this should also contain enough of the original data +// such that it is possible to implement helper methods \ +// to, for example, get a "good enough" conversion from a +// local time where the local timestamp is invalid or ambiguous +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InvalidLocalTimeInfo { + local: NaiveDateTime, + transition: UtcTransition, +} + +impl Display for InvalidLocalTimeInfo { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + todo!() + } +} + +impl Error for InvalidLocalTimeInfo {} + +impl InvalidLocalTimeInfo { + fn and_tz(&self, tz: Tz) -> InvalidLocalTimeInfoTz { + InvalidLocalTimeInfoTz { local: self.local, transition: self.transition.clone(), tz } + } +} + +// as above, but with the TimeZone parameter. This exists because some API's +// can't return this version due to object safety, but it is still nice to +// have where possible. +pub struct InvalidLocalTimeInfoTz { + local: NaiveDateTime, + transition: UtcTransition, + tz: Tz, +} + +impl Clone for InvalidLocalTimeInfoTz +where + Tz: Clone, +{ + fn clone(&self) -> Self { + InvalidLocalTimeInfoTz { + local: self.local, + transition: self.transition.clone(), + tz: self.tz.clone(), + } + } +} + +impl Debug for InvalidLocalTimeInfoTz { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + todo!() + } +} + +impl Display for InvalidLocalTimeInfoTz { + fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + todo!() + } +} + +impl Error for InvalidLocalTimeInfoTz {} + +// here the TimeZoneA should be something small or static, like an empty enum variant or an empty struct. +// potentially all the methods here should be fallible +// +// the implementor of TimeZoneA will usually store its current offset internally (if dynamic) or make it available as a +// const if static. +// +// where the TimeZoneA implemention handles daylight savings or other timezone that needs more data than just an offset +// it might store a `String` or enum variant which enables the `%Z` formatting, extracted via the `.name()` method. +// +// we move the `datetime_from_str` to the `DateTime` impl +// we have to avoid `from_local_datetime` and `from_utc_datetime` here +// and point users towards `and_local_timezone()` and `.and_timezone()` instead. +// because there is no way to force the `TimeZoneA` to implement `Clone` but still keep object safety. +// for all practical purposes all `TimeZoneA` implementors should probably implement at least `Clone` and likely `Copy` as well. +pub trait TimeZone { + // this could have a default implementation if there was a `from_fixed_offset` method + // in the trait, but that would be problematic for object safety, so instead + // the implemention is left to the user. + #[cfg(feature = "clock")] + fn offset_now(&self) -> FixedOffset { + self.offset_at(Utc::now().naive_utc()) + } + + fn offset(&self) -> FixedOffset; + + fn offset_at_local(&self, local: NaiveDateTime) -> Result { + match self.closest_transitions_from_local(local) { + None => Ok(self.offset()), + Some(ClosestTransitions { before, after }) if before == after => { + Err(InvalidLocalTimeInfo { local, transition: before }) + } + Some(ClosestTransitions { before, .. }) => Ok(before.to), + } + } + + fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset { + if let Some(ClosestTransitions { before, .. }) = self.closest_transitions(utc) { + before.current_offset() + } else { + self.offset() + } + } + + // this is needed as we can't construct a new `TimeZoneA` and still be object safe + fn update_offset_at_local( + &mut self, + _local: NaiveDateTime, + ) -> Result<(), InvalidLocalTimeInfo> { + Ok(()) + } + + fn update_offset_at(&mut self, _utc: NaiveDateTime) {} + + // potentially the `_transitions` functions should take a `local: bool` parameter + // as it would be incorrect to implement one but leave the other with the default impl + + // this is not hugely useful as it will just be the + // previous and next transitions, but it might be nice + // to expose this in public API what is currently just in `tzinfo`. + fn closest_transitions(&self, _utc: NaiveDateTime) -> Option { + None + } + + // if the local timestamp is valid, then these transitions will each be different + // if the local timestamp is either ambiguous or invalid, then both fields of the + // tuple will be the same + fn closest_transitions_from_local(&self, _local: NaiveDateTime) -> Option { + None + } + + // to be used in %Z formatting + fn name(&self) -> Option<&str> { + None + } +} + +impl TimeZone for Box { + fn offset_now(&self) -> FixedOffset { + self.as_ref().offset_now() + } + + fn offset(&self) -> FixedOffset { + self.as_ref().offset() + } + + fn offset_at_local(&self, local: NaiveDateTime) -> Result + where + Self: Sized, + { + self.as_ref().offset_at_local(local) + } + + fn offset_at(&self, utc: NaiveDateTime) -> FixedOffset { + self.as_ref().offset_at(utc) + } + + fn update_offset_at_local(&mut self, local: NaiveDateTime) -> Result<(), InvalidLocalTimeInfo> { + self.as_mut().update_offset_at_local(local) + } + + fn update_offset_at(&mut self, utc: NaiveDateTime) { + self.as_mut().update_offset_at(utc) + } + + fn closest_transitions(&self, utc: NaiveDateTime) -> Option { + self.as_ref().closest_transitions(utc) + } + + fn closest_transitions_from_local(&self, local: NaiveDateTime) -> Option { + self.as_ref().closest_transitions_from_local(local) + } + + fn name(&self) -> Option<&str> { + self.as_ref().name() + } +} + +impl TimeZone for FixedOffset { + fn offset(&self) -> FixedOffset { + crate::offset::Offset::fix(self) + } +} + +mod manager { + use super::*; + use crate::Days; + use crate::Months; + + // the timezone is used to manage the DateTime, but the datetime never contains the timezone itself + // this is useful as the TimeZone might contain a bunch of information from a parsed tzinfo file + // and so it is useful that the user can control the caching of this + // + // this is also simpler as there is no longer a type parameter needed in the DateTime + // + // object safety is argurably less useful here as the `TimeZone` lives outside the `DateTime`, so + // some of the choices made to enable could be unwound if it is not deemed necessary + // + // This could offer a nice migration path by having `DateTime` and + // calling this trait `TimeZoneManager` or something better + // + // This can be quite useful as it can be cached in a `Arc` or `Arc` - the tzinfo + // data is only updated on the scale of weeks, so an application can either cache it for the + // life of the process, or occasionally update it within a `Arc`. `tokio::sync::Watch` + // or similar could also be useful here. + pub trait TimeZoneManager { + type Zone: TimeZone + Clone; + + fn add_months(&self, dt: DateTime, months: Months) -> DateTime { + let new_datetime = dt.naive_utc() + months; + DateTime { datetime: new_datetime, zone: self.offset_at(new_datetime) } + } + fn sub_months(&self, dt: DateTime, months: Months) -> DateTime { + let new_datetime = dt.naive_utc() - months; + DateTime { datetime: new_datetime, zone: self.offset_at(new_datetime) } + } + fn add_days(&self, dt: DateTime, days: Days) -> DateTime { + let new_datetime = dt.naive_utc() + days; + DateTime { datetime: new_datetime, zone: self.offset_at(new_datetime) } + } + fn sub_days(&self, dt: DateTime, days: Days) -> DateTime { + let new_datetime = dt.naive_utc() - days; + DateTime { datetime: new_datetime, zone: self.offset_at(new_datetime) } + } + fn add(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + let new_datetime = dt.naive_utc() + duration; + DateTime { datetime: new_datetime, zone: self.offset_at(new_datetime) } + } + fn sub(&self, dt: DateTime, duration: TimeDelta) -> DateTime { + let new_datetime = dt.naive_utc() - duration; + DateTime { datetime: new_datetime, zone: self.offset_at(new_datetime) } + } + + #[cfg(feature = "clock")] + fn now(&self) -> DateTime { + let now = Utc::now().naive_utc(); + DateTime { datetime: now, zone: self.offset_at(now) } + } + + fn offset_at(&self, utc: NaiveDateTime) -> Self::Zone; + + fn offset_at_local(&self, local: NaiveDateTime) -> InvalidLocalTimeInfo; + + // we can likely avoid `from_local_datetime` and `from_utc_datetime` here + // and point users towards `and_local_timezone()` and `.and_timezone()` instead. + + // potentially the `_transitions` functions should take a `local: bool` parameter + // as it would be incorrect to implement one but leave the other with the default impl + + // this is not hugely useful as it will just be the + // previous and next transitions, but it might be nice + // to expose this in public API what is currently just in `tzinfo`. + fn closest_transitions(&self, _utc: NaiveDateTime) -> Option { + None + } + + // if the local timestamp is valid, then these transitions will each be different + // if the local timestamp is either ambiguous or invalid, then both fields of the + // tuple will be the same + fn closest_transitions_from_local( + &self, + _local: NaiveDateTime, + ) -> Option { + None + } + + // to be used in %Z formatting + fn name(&self) -> Option<&str> { + None + } + + fn parse_from_str(&self, input: &str, format: &str) -> ParseResult; + } +}