diff --git a/Cargo.toml b/Cargo.toml index c85a7b981..8345356eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,9 +67,9 @@ combine = { workspace = true, optional = true } web-time = { workspace = true, optional = true } [features] -default = ["now"] +default = ["full"] log = ["dep:log"] -experimental = ["tzdb"] +full = ["tzdb", "now"] now = ["std", "dep:web-time"] tzdb = ["dep:tzif", "std", "dep:jiff-tzdb", "dep:combine"] std = [] diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..ba1931d35 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,112 @@ +# Library Architecture + +TODO: FFI docs + +This doc provides an overview of the layout of `temporal_rs`. + +We will go over the Temporal Date/Time builtins, general primitives, and +utiltity crates. + +## `temporal_rs` design considerations + +`temporal_rs` is first and foremost designed to be a fully spec +compliant implementation of ECMAScript's Temporal date/time builtins. + +As such, the main design consideration of `temporal_rs` is that it needs +to be able to service language interpreters / engines. + +Thus, `temporal_rs` aims to provide an API along with tools to implement +Temporal while minimizing issue with integrating Temporal into engines. + +## Date/Time builtins + +The primary date & time builtins/components are located in the +`builtins` directory. + +These builtins are then reexported from `lib.rs` to be available from +`temporal_rs`'s root module. + +### Core vs. Native + +`temporal_rs`'s builtins are split in two distinct directories `core` +and `native`. The core implementation contains the core implementation +of the Temporal builtins; meanwhile, the `native` implementation is a +Rust wrapper around the `core` implementation that simplifies some +"lower" level date/time API that may not be necessary for a general use +case. + +### Core implementation + +The core implementation is always publicly available, but may not be +available to import from the `temporal_rs`'s root. + +The core implementation can be made available from the root by providing +the `--no-default-features` flag. + +The core implementation exposes the Provider API that allows the user to +supply a "provider", or any type that implements the `TimeZoneProvider` +trait, for time zone data that the library can use to complete it's +calculations. This is useful from an engine / implementor perspective +because it allows the engine to source time zone data in their preferred +manner without locking them into a library specific implementation that +may or may not have side effects. + +A `TimeZoneProvider` API on a core builtin will look like the below. + +```rust +impl ZonedDateTime { + pub fn day_with_provider(&self, provider: &impl TimeZoneProvider) -> TemporalResult { + // Code goes here. + } +} +``` + +### Native implementation + +The native implementation is only available via the "full" default +feature flag. + +For the same reason that the Provider API is useful for language +implementors, it is a deterent from a general use case perspective. Most +people using a datetime library, outside of the self-proclaimed time +zone nerds, probably won't care from where their time zone data is being +sourced. + +The native Rust wrapper of the core implementation provides a default +provider implementation to remove the need of the user to think or deal +with `TimeZoneProvider`. + +```rust +impl ZonedDateTime { + pub fn day(&self) -> TemporalResult { + // Code goes here. + } +} +``` + +This greatly simplifies the API for general use cases. + +## Primitives + + + +`temporal_rs` has a primitive number implementation `FiniteF64` along +with a few date and time primitives: `IsoDate`, `IsoTime`, +`IsoDateTime`, and `EpochNanoseconds`. + +`FiniteF64` allows an interface to translate between ECMAScript's number +type vs. `temporal_rs`'s strictly typed API. + +Meanwhile the Date and Time primitives allow certain invariants to be +enforced on their records. + +## Utiltiies + +`temporal_rs` provides one implementation of the `TimeZoneProvider` +trait: `FsTzdbProvider`. + +`FsTzdbProvider` reads from the file systems' tzdb and when not +available on the system, `FsTzdbProvider` relies on a prepackaged +`tzdb`. + + diff --git a/src/components/calendar.rs b/src/builtins/core/calendar.rs similarity index 99% rename from src/components/calendar.rs rename to src/builtins/core/calendar.rs index e1332ef8c..e8cc3d074 100644 --- a/src/components/calendar.rs +++ b/src/builtins/core/calendar.rs @@ -11,7 +11,7 @@ use core::str::FromStr; use icu_calendar::types::{Era as IcuEra, MonthCode as IcuMonthCode, MonthInfo, YearInfo}; use crate::{ - components::{ + builtins::core::{ duration::{DateDuration, TimeDuration}, Duration, PlainDate, PlainDateTime, PlainMonthDay, PlainYearMonth, }, diff --git a/src/components/calendar/era.rs b/src/builtins/core/calendar/era.rs similarity index 100% rename from src/components/calendar/era.rs rename to src/builtins/core/calendar/era.rs diff --git a/src/components/calendar/types.rs b/src/builtins/core/calendar/types.rs similarity index 99% rename from src/components/calendar/types.rs rename to src/builtins/core/calendar/types.rs index 7420bb634..1221a291e 100644 --- a/src/components/calendar/types.rs +++ b/src/builtins/core/calendar/types.rs @@ -9,7 +9,7 @@ use crate::iso::{constrain_iso_day, is_valid_iso_day}; use crate::options::ArithmeticOverflow; use crate::{TemporalError, TemporalResult}; -use crate::components::{calendar::Calendar, PartialDate}; +use crate::builtins::core::{calendar::Calendar, PartialDate}; /// `ResolvedCalendarFields` represents the resolved field values necessary for /// creating a Date from potentially partial values. @@ -322,7 +322,7 @@ mod tests { use tinystr::tinystr; use crate::{ - components::{calendar::Calendar, PartialDate}, + builtins::core::{calendar::Calendar, PartialDate}, options::ArithmeticOverflow, }; diff --git a/src/components/date.rs b/src/builtins/core/date.rs similarity index 99% rename from src/components/date.rs rename to src/builtins/core/date.rs index 3ba2af21a..caba7f1a7 100644 --- a/src/components/date.rs +++ b/src/builtins/core/date.rs @@ -1,7 +1,9 @@ //! This module implements `Date` and any directly related algorithms. use crate::{ - components::{calendar::Calendar, duration::DateDuration, Duration, PlainDateTime}, + builtins::core::{ + calendar::Calendar, duration::DateDuration, Duration, PlainDateTime, PlainTime, + }, iso::{IsoDate, IsoDateTime, IsoTime}, options::{ ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar, @@ -9,6 +11,7 @@ use crate::{ }, parsers::{parse_date_time, IxdtfStringBuilder}, primitive::FiniteF64, + provider::NeverProvider, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, }; use alloc::{format, string::String}; @@ -17,8 +20,7 @@ use core::str::FromStr; use super::{ calendar::{ascii_four_to_integer, month_to_month_code}, duration::{normalized::NormalizedDurationRecord, TimeDuration}, - timezone::NeverProvider, - PlainMonthDay, PlainTime, PlainYearMonth, + PlainMonthDay, PlainYearMonth, }; use tinystr::TinyAsciiStr; diff --git a/src/components/datetime.rs b/src/builtins/core/datetime.rs similarity index 99% rename from src/components/datetime.rs rename to src/builtins/core/datetime.rs index 1e50e155b..d9f063758 100644 --- a/src/components/datetime.rs +++ b/src/builtins/core/datetime.rs @@ -1,23 +1,22 @@ //! This module implements `DateTime` any directly related algorithms. +use super::{ + duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration}, + Duration, PartialDate, PartialTime, PlainDate, PlainTime, +}; use crate::{ - components::{calendar::Calendar, Instant}, + builtins::core::{calendar::Calendar, Instant}, iso::{IsoDate, IsoDateTime, IsoTime}, options::{ ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar, ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions, }, parsers::{parse_date_time, IxdtfStringBuilder}, + provider::NeverProvider, temporal_assert, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, }; use alloc::string::String; use core::{cmp::Ordering, str::FromStr}; - -use super::{ - duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration}, - timezone::NeverProvider, - Duration, PartialDate, PartialTime, PlainDate, PlainTime, -}; use tinystr::TinyAsciiStr; /// A partial PlainDateTime record @@ -693,7 +692,7 @@ mod tests { use tinystr::{tinystr, TinyAsciiStr}; use crate::{ - components::{ + builtins::core::{ calendar::Calendar, duration::DateDuration, Duration, PartialDate, PartialDateTime, PartialTime, PlainDateTime, }, diff --git a/src/components/duration.rs b/src/builtins/core/duration.rs similarity index 97% rename from src/components/duration.rs rename to src/builtins/core/duration.rs index c9d7b3f07..6daac65d1 100644 --- a/src/components/duration.rs +++ b/src/builtins/core/duration.rs @@ -1,14 +1,15 @@ //! This module implements `Duration` along with it's methods and components. use crate::{ - components::{timezone::TimeZoneProvider, PlainDateTime, PlainTime}, + builtins::core::{options::RelativeTo, PlainDateTime, PlainTime, ZonedDateTime}, iso::{IsoDateTime, IsoTime}, options::{ - ArithmeticOverflow, RelativeTo, ResolvedRoundingOptions, RoundingIncrement, - RoundingOptions, TemporalUnit, ToStringRoundingOptions, + ArithmeticOverflow, ResolvedRoundingOptions, RoundingIncrement, RoundingOptions, + TemporalUnit, ToStringRoundingOptions, }, parsers::{FormattableDuration, Precision}, primitive::FiniteF64, + provider::TimeZoneProvider, temporal_assert, Sign, TemporalError, TemporalResult, }; use alloc::format; @@ -25,9 +26,6 @@ use num_traits::AsPrimitive; use self::normalized::NormalizedTimeDuration; -#[cfg(feature = "experimental")] -use crate::components::timezone::TZ_PROVIDER; - mod date; pub(crate) mod normalized; mod time; @@ -40,8 +38,6 @@ pub use date::DateDuration; #[doc(inline)] pub use time::TimeDuration; -use super::ZonedDateTime; - /// A `PartialDuration` is a Duration that may have fields not set. #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] pub struct PartialDuration { @@ -91,7 +87,7 @@ impl core::fmt::Display for Duration { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str( &self - .to_temporal_string(ToStringRoundingOptions::default()) + .as_temporal_string(ToStringRoundingOptions::default()) .expect("Duration must return a valid string with default options."), ) } @@ -639,7 +635,7 @@ impl Duration { } } - pub fn to_temporal_string(&self, options: ToStringRoundingOptions) -> TemporalResult { + pub fn as_temporal_string(&self, options: ToStringRoundingOptions) -> TemporalResult { if options.smallest_unit == Some(TemporalUnit::Hour) || options.smallest_unit == Some(TemporalUnit::Minute) { @@ -732,20 +728,6 @@ pub fn duration_to_formattable( }) } -#[cfg(feature = "experimental")] -impl Duration { - pub fn round( - &self, - options: RoundingOptions, - relative_to: Option, - ) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.round_with_provider(options, relative_to, &*provider) - } -} - // TODO: Update, optimize, and fix the below. is_valid_duration should probably be generic over a T. const TWO_POWER_FIFTY_THREE: i128 = 9_007_199_254_740_992; diff --git a/src/components/duration/date.rs b/src/builtins/core/duration/date.rs similarity index 100% rename from src/components/duration/date.rs rename to src/builtins/core/duration/date.rs diff --git a/src/components/duration/normalized.rs b/src/builtins/core/duration/normalized.rs similarity index 99% rename from src/components/duration/normalized.rs rename to src/builtins/core/duration/normalized.rs index 814b02819..3055bc23a 100644 --- a/src/components/duration/normalized.rs +++ b/src/builtins/core/duration/normalized.rs @@ -5,16 +5,14 @@ use core::{num::NonZeroU128, ops::Add}; use num_traits::{AsPrimitive, Euclid, FromPrimitive}; use crate::{ - components::{ - timezone::{TimeZone, TimeZoneProvider}, - PlainDate, PlainDateTime, - }, + builtins::core::{timezone::TimeZone, PlainDate, PlainDateTime}, iso::{IsoDate, IsoDateTime}, options::{ ArithmeticOverflow, Disambiguation, ResolvedRoundingOptions, TemporalRoundingMode, TemporalUnit, }, primitive::FiniteF64, + provider::TimeZoneProvider, rounding::{IncrementRounder, Round}, TemporalError, TemporalResult, TemporalUnwrap, NS_PER_DAY, }; diff --git a/src/builtins/core/duration/tests.rs b/src/builtins/core/duration/tests.rs new file mode 100644 index 000000000..a3c5e6922 --- /dev/null +++ b/src/builtins/core/duration/tests.rs @@ -0,0 +1,188 @@ +use crate::{ + options::ToStringRoundingOptions, parsers::Precision, partial::PartialDuration, + primitive::FiniteF64, +}; + +use super::Duration; + +#[test] +fn partial_duration_empty() { + let err = Duration::from_partial_duration(PartialDuration::default()); + assert!(err.is_err()) +} + +#[test] +fn partial_duration_values() { + let mut partial = PartialDuration::default(); + let _ = partial.years.insert(FiniteF64(20.0)); + let result = Duration::from_partial_duration(partial).unwrap(); + assert_eq!(result.years(), 20.0); +} + +#[test] +fn default_duration_string() { + let duration = Duration::default(); + + let options = ToStringRoundingOptions { + precision: Precision::Auto, + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.as_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0S"); + + let options = ToStringRoundingOptions { + precision: Precision::Digit(0), + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.as_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0S"); + + let options = ToStringRoundingOptions { + precision: Precision::Digit(1), + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.as_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0.0S"); + + let options = ToStringRoundingOptions { + precision: Precision::Digit(3), + smallest_unit: None, + rounding_mode: None, + }; + let result = duration.as_temporal_string(options).unwrap(); + assert_eq!(&result, "PT0.000S"); +} + +#[test] +fn duration_to_string_auto_precision() { + let duration = Duration::new( + 1.into(), + 2.into(), + 3.into(), + 4.into(), + 5.into(), + 6.into(), + 7.into(), + FiniteF64::default(), + FiniteF64::default(), + FiniteF64::default(), + ) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "P1Y2M3W4DT5H6M7S"); + + let duration = Duration::new( + 1.into(), + 2.into(), + 3.into(), + 4.into(), + 5.into(), + 6.into(), + 7.into(), + 987.into(), + 650.into(), + FiniteF64::default(), + ) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "P1Y2M3W4DT5H6M7.98765S"); +} + +#[test] +fn empty_date_duration() { + let duration = Duration::from_partial_duration(PartialDuration { + hours: Some(1.into()), + ..Default::default() + }) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "PT1H"); +} + +#[test] +fn negative_fields_to_string() { + let duration = Duration::from_partial_duration(PartialDuration { + years: Some(FiniteF64::from(-1)), + months: Some(FiniteF64::from(-1)), + weeks: Some(FiniteF64::from(-1)), + days: Some(FiniteF64::from(-1)), + hours: Some(FiniteF64::from(-1)), + minutes: Some(FiniteF64::from(-1)), + seconds: Some(FiniteF64::from(-1)), + milliseconds: Some(FiniteF64::from(-1)), + microseconds: Some(FiniteF64::from(-1)), + nanoseconds: Some(FiniteF64::from(-1)), + }) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-P1Y1M1W1DT1H1M1.001001001S"); + + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::from(-250)), + ..Default::default() + }) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-PT0.25S"); + + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::from(-3500)), + ..Default::default() + }) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-PT3.5S"); + + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::from(-3500)), + ..Default::default() + }) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + assert_eq!(&result, "-PT3.5S"); + + let duration = Duration::from_partial_duration(PartialDuration { + weeks: Some(FiniteF64::from(-1)), + days: Some(FiniteF64::from(-1)), + ..Default::default() + }) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + + assert_eq!(&result, "-P1W1D"); +} + +#[test] +fn preserve_precision_loss() { + const MAX_SAFE_INT: f64 = 9_007_199_254_740_991.0; + let duration = Duration::from_partial_duration(PartialDuration { + milliseconds: Some(FiniteF64::try_from(MAX_SAFE_INT).unwrap()), + microseconds: Some(FiniteF64::try_from(MAX_SAFE_INT).unwrap()), + ..Default::default() + }) + .unwrap(); + let result = duration + .as_temporal_string(ToStringRoundingOptions::default()) + .unwrap(); + + assert_eq!(&result, "PT9016206453995.731991S"); +} diff --git a/src/components/duration/time.rs b/src/builtins/core/duration/time.rs similarity index 100% rename from src/components/duration/time.rs rename to src/builtins/core/duration/time.rs diff --git a/src/components/instant.rs b/src/builtins/core/instant.rs similarity index 92% rename from src/components/instant.rs rename to src/builtins/core/instant.rs index ab3a56211..51b648bf9 100644 --- a/src/components/instant.rs +++ b/src/builtins/core/instant.rs @@ -4,7 +4,7 @@ use alloc::string::String; use core::{num::NonZeroU128, str::FromStr}; use crate::{ - components::{ + builtins::core::{ duration::TimeDuration, zoneddatetime::nanoseconds_to_formattable_offset_minutes, Duration, }, iso::{IsoDate, IsoDateTime, IsoTime}, @@ -14,16 +14,16 @@ use crate::{ }, parsers::{parse_instant, IxdtfStringBuilder}, primitive::FiniteF64, + provider::TimeZoneProvider, rounding::{IncrementRounder, Round}, - TemporalError, TemporalResult, TemporalUnwrap, TimeZone, NS_MAX_INSTANT, + time::EpochNanoseconds, + TemporalError, TemporalResult, TemporalUnwrap, TimeZone, }; use ixdtf::parsers::records::UtcOffsetRecordOrZ; -use num_traits::FromPrimitive; use super::{ duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration}, - timezone::TimeZoneProvider, DateDuration, }; @@ -31,42 +31,6 @@ const NANOSECONDS_PER_SECOND: i128 = 1_000_000_000; const NANOSECONDS_PER_MINUTE: i128 = 60 * NANOSECONDS_PER_SECOND; const NANOSECONDS_PER_HOUR: i128 = 60 * NANOSECONDS_PER_MINUTE; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct EpochNanoseconds(pub(crate) i128); - -impl TryFrom for EpochNanoseconds { - type Error = TemporalError; - fn try_from(value: i128) -> Result { - if !is_valid_epoch_nanos(&value) { - return Err(TemporalError::range() - .with_message("Instant nanoseconds are not within a valid epoch range.")); - } - Ok(Self(value)) - } -} - -impl TryFrom for EpochNanoseconds { - type Error = TemporalError; - fn try_from(value: u128) -> Result { - if (NS_MAX_INSTANT as u128) < value { - return Err(TemporalError::range() - .with_message("Instant nanoseconds are not within a valid epoch range.")); - } - Ok(Self(value as i128)) - } -} - -impl TryFrom for EpochNanoseconds { - type Error = TemporalError; - fn try_from(value: f64) -> Result { - let Some(value) = i128::from_f64(value) else { - return Err(TemporalError::range() - .with_message("Instant nanoseconds are not within a valid epoch range.")); - }; - Self::try_from(value) - } -} - /// The native Rust implementation of `Temporal.Instant` #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -272,7 +236,7 @@ impl Instant { // ==== Instant Provider API ==== impl Instant { - pub fn to_ixdtf_string_with_provider( + pub fn as_ixdtf_string_with_provider( &self, timezone: Option<&TimeZone>, options: ToStringRoundingOptions, @@ -306,13 +270,6 @@ impl Instant { // ==== Utility Functions ==== -/// Utility for determining if the nanos are within a valid range. -#[inline] -#[must_use] -pub(crate) fn is_valid_epoch_nanos(nanos: &i128) -> bool { - (crate::NS_MIN_INSTANT..=crate::NS_MAX_INSTANT).contains(nanos) -} - impl FromStr for Instant { type Err = TemporalError; fn from_str(s: &str) -> Result { @@ -358,7 +315,7 @@ impl FromStr for Instant { mod tests { use crate::{ - components::{duration::TimeDuration, Instant}, + builtins::core::{duration::TimeDuration, Instant}, options::{DifferenceSettings, TemporalRoundingMode, TemporalUnit}, primitive::FiniteF64, NS_MAX_INSTANT, NS_MIN_INSTANT, @@ -564,9 +521,9 @@ mod tests { #[cfg(feature = "tzdb")] #[test] fn instant_add_across_epoch() { + use crate::builtins::core::Duration; use crate::{ options::ToStringRoundingOptions, partial::PartialDuration, tzdb::FsTzdbProvider, - Duration, }; use core::str::FromStr; @@ -624,13 +581,13 @@ mod tests { // Assert the to_string is valid. let provider = &FsTzdbProvider::default(); let inst_string = instant - .to_ixdtf_string_with_provider(None, ToStringRoundingOptions::default(), provider) + .as_ixdtf_string_with_provider(None, ToStringRoundingOptions::default(), provider) .unwrap(); let one_string = one - .to_ixdtf_string_with_provider(None, ToStringRoundingOptions::default(), provider) + .as_ixdtf_string_with_provider(None, ToStringRoundingOptions::default(), provider) .unwrap(); let two_string = two - .to_ixdtf_string_with_provider(None, ToStringRoundingOptions::default(), provider) + .as_ixdtf_string_with_provider(None, ToStringRoundingOptions::default(), provider) .unwrap(); assert_eq!(&inst_string, "1969-12-25T12:23:45.678901234Z"); diff --git a/src/components/mod.rs b/src/builtins/core/mod.rs similarity index 88% rename from src/components/mod.rs rename to src/builtins/core/mod.rs index 92d7f7dfd..1ae13dd03 100644 --- a/src/components/mod.rs +++ b/src/builtins/core/mod.rs @@ -14,6 +14,7 @@ mod date; mod datetime; mod instant; mod month_day; +pub(crate) mod options; mod time; mod year_month; pub(crate) mod zoneddatetime; @@ -30,9 +31,9 @@ pub use date::{PartialDate, PlainDate}; #[doc(inline)] pub use datetime::{PartialDateTime, PlainDateTime}; #[doc(inline)] -pub use duration::{DateDuration, Duration, TimeDuration}; +pub use duration::{DateDuration, Duration, PartialDuration, TimeDuration}; #[doc(inline)] -pub use instant::{EpochNanoseconds, Instant}; +pub use instant::Instant; #[doc(inline)] pub use month_day::PlainMonthDay; #[doc(inline)] diff --git a/src/components/month_day.rs b/src/builtins/core/month_day.rs similarity index 97% rename from src/components/month_day.rs rename to src/builtins/core/month_day.rs index a42ec972c..3c18ab3c2 100644 --- a/src/components/month_day.rs +++ b/src/builtins/core/month_day.rs @@ -6,11 +6,10 @@ use core::str::FromStr; use tinystr::TinyAsciiStr; use crate::{ - components::calendar::Calendar, iso::IsoDate, options::{ArithmeticOverflow, DisplayCalendar}, parsers::{FormattableCalendar, FormattableDate, FormattableMonthDay}, - TemporalError, TemporalResult, TemporalUnwrap, + Calendar, TemporalError, TemporalResult, TemporalUnwrap, }; /// The native Rust implementation of `Temporal.PlainMonthDay` diff --git a/src/components/now.rs b/src/builtins/core/now.rs similarity index 79% rename from src/components/now.rs rename to src/builtins/core/now.rs index 5f91f7b2c..71c92a02c 100644 --- a/src/components/now.rs +++ b/src/builtins/core/now.rs @@ -1,21 +1,17 @@ //! The Temporal Now component +use crate::provider::TimeZoneProvider; +use crate::{iso::IsoDateTime, time::EpochNanoseconds, TemporalUnwrap}; use crate::{sys, TemporalResult}; use alloc::string::String; use num_traits::FromPrimitive; -use crate::{iso::IsoDateTime, TemporalUnwrap}; - use super::{ - calendar::Calendar, - timezone::{TimeZone, TimeZoneProvider}, - EpochNanoseconds, Instant, PlainDate, PlainDateTime, PlainTime, ZonedDateTime, + calendar::Calendar, timezone::TimeZone, Instant, PlainDate, PlainDateTime, PlainTime, + ZonedDateTime, }; -#[cfg(feature = "experimental")] -use crate::{components::timezone::TZ_PROVIDER, TemporalError}; - /// The Temporal Now object. pub struct Now; @@ -87,30 +83,6 @@ impl Now { } } -#[cfg(feature = "experimental")] -impl Now { - pub fn plain_datetime_iso(timezone: Option) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - Now::plain_datetime_iso_with_provider(timezone, &*provider) - } - - pub fn plain_date_iso(timezone: Option) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - Now::plain_date_iso_with_provider(timezone, &*provider) - } - - pub fn plain_time_iso(timezone: Option) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - Now::plain_time_iso_with_provider(timezone, &*provider) - } -} - fn system_datetime( tz: Option, provider: &impl TimeZoneProvider, @@ -138,7 +110,8 @@ mod tests { use std::thread; use std::time::Duration as StdDuration; - use crate::{options::DifferenceSettings, tzdb::FsTzdbProvider, Now}; + use crate::builtins::core::Now; + use crate::{options::DifferenceSettings, tzdb::FsTzdbProvider}; #[test] fn now_datetime_test() { diff --git a/src/builtins/core/options.rs b/src/builtins/core/options.rs new file mode 100644 index 000000000..0929d1e6e --- /dev/null +++ b/src/builtins/core/options.rs @@ -0,0 +1,135 @@ +//! A module to handle RelativeTo + +use alloc::string::String; + +use crate::builtins::core::zoneddatetime::interpret_isodatetime_offset; +use crate::builtins::core::{calendar::Calendar, timezone::TimeZone, PlainDate, ZonedDateTime}; +use crate::iso::{IsoDate, IsoTime}; +use crate::options::{ArithmeticOverflow, Disambiguation, OffsetDisambiguation}; +use crate::parsers::parse_date_time; +use crate::provider::TimeZoneProvider; +use crate::{TemporalError, TemporalResult, TemporalUnwrap}; + +use ixdtf::parsers::records::{TimeZoneRecord, UtcOffsetRecordOrZ}; + +// ==== RelativeTo Object ==== + +#[derive(Debug, Clone)] +pub enum RelativeTo { + PlainDate(PlainDate), + ZonedDateTime(ZonedDateTime), +} + +impl From for RelativeTo { + fn from(value: PlainDate) -> Self { + Self::PlainDate(value) + } +} + +impl From for RelativeTo { + fn from(value: ZonedDateTime) -> Self { + Self::ZonedDateTime(value) + } +} + +impl RelativeTo { + /// Attempts to parse a `ZonedDateTime` string falling back to a `PlainDate` + /// if possible. + /// + /// If the fallback fails or either the `ZonedDateTime` or `PlainDate` + /// is invalid, then an error is returned. + pub fn try_from_str_with_provider( + source: &str, + provider: &impl TimeZoneProvider, + ) -> TemporalResult { + let result = parse_date_time(source)?; + + let Some(annotation) = result.tz else { + let date_record = result.date.temporal_unwrap()?; + + let calendar = result + .calendar + .map(Calendar::from_utf8) + .transpose()? + .unwrap_or_default(); + + return Ok(PlainDate::try_new( + date_record.year, + date_record.month, + date_record.day, + calendar, + )? + .into()); + }; + + let timezone = match annotation.tz { + TimeZoneRecord::Name(s) => { + TimeZone::IanaIdentifier(String::from_utf8_lossy(s).into_owned()) + } + TimeZoneRecord::Offset(offset_record) => { + // NOTE: ixdtf parser restricts minute/second to 0..=60 + let minutes = i16::from(offset_record.hour) * 60 + offset_record.minute as i16; + TimeZone::OffsetMinutes(minutes * i16::from(offset_record.sign as i8)) + } + // TimeZoneRecord is non_exhaustive, but all current branches are matching. + _ => return Err(TemporalError::assert()), + }; + + let (offset_nanos, is_exact) = result + .offset + .map(|record| { + let UtcOffsetRecordOrZ::Offset(offset) = record else { + return (None, true); + }; + let hours_in_ns = i64::from(offset.hour) * 3_600_000_000_000_i64; + let minutes_in_ns = i64::from(offset.minute) * 60_000_000_000_i64; + let seconds_in_ns = i64::from(offset.minute) * 1_000_000_000_i64; + ( + Some( + (hours_in_ns + + minutes_in_ns + + seconds_in_ns + + i64::from(offset.nanosecond)) + * i64::from(offset.sign as i8), + ), + false, + ) + }) + .unwrap_or((None, false)); + + let calendar = result + .calendar + .map(Calendar::from_utf8) + .transpose()? + .unwrap_or_default(); + + let time = result + .time + .map(|time| { + IsoTime::from_components(time.hour, time.minute, time.second, time.nanosecond) + }) + .transpose()?; + + let date = result.date.temporal_unwrap()?; + let iso = IsoDate::new_with_overflow( + date.year, + date.month, + date.day, + ArithmeticOverflow::Constrain, + )?; + + let epoch_ns = interpret_isodatetime_offset( + iso, + time, + is_exact, + offset_nanos, + &timezone, + Disambiguation::Compatible, + OffsetDisambiguation::Reject, + true, + provider, + )?; + + Ok(ZonedDateTime::try_new(epoch_ns.0, calendar, timezone)?.into()) + } +} diff --git a/src/components/time.rs b/src/builtins/core/time.rs similarity index 99% rename from src/components/time.rs rename to src/builtins/core/time.rs index 2626228d8..f475d449d 100644 --- a/src/components/time.rs +++ b/src/builtins/core/time.rs @@ -1,7 +1,7 @@ //! This module implements `Time` and any directly related algorithms. use crate::{ - components::{duration::TimeDuration, Duration}, + builtins::core::{duration::TimeDuration, Duration}, iso::IsoTime, options::{ ArithmeticOverflow, DifferenceOperation, DifferenceSettings, ResolvedRoundingOptions, @@ -434,7 +434,7 @@ impl PlainTime { Ok(Self::new_unchecked(result)) } - pub fn to_ixdtf_string(&self, options: ToStringRoundingOptions) -> TemporalResult { + pub fn as_ixdtf_string(&self, options: ToStringRoundingOptions) -> TemporalResult { let resolved = options.resolve()?; let (_, result) = self .iso @@ -470,7 +470,7 @@ impl FromStr for PlainTime { #[cfg(test)] mod tests { use crate::{ - components::Duration, + builtins::core::Duration, iso::IsoTime, options::{ArithmeticOverflow, DifferenceSettings, RoundingIncrement, TemporalUnit}, }; diff --git a/src/components/timezone.rs b/src/builtins/core/timezone.rs similarity index 87% rename from src/components/timezone.rs rename to src/builtins/core/timezone.rs index 1af3e3b31..7ef1d424a 100644 --- a/src/components/timezone.rs +++ b/src/builtins/core/timezone.rs @@ -7,76 +7,20 @@ use core::{iter::Peekable, str::Chars}; use num_traits::ToPrimitive; -use crate::components::duration::DateDuration; +use crate::builtins::core::duration::DateDuration; use crate::parsers::{FormattableOffset, FormattableTime, Precision}; +use crate::provider::{TimeZoneOffset, TimeZoneProvider}; use crate::{ - components::{duration::normalized::NormalizedTimeDuration, EpochNanoseconds, Instant}, + builtins::core::{duration::normalized::NormalizedTimeDuration, Instant}, iso::{IsoDate, IsoDateTime, IsoTime}, options::Disambiguation, + time::EpochNanoseconds, TemporalError, TemporalResult, ZonedDateTime, }; use crate::{Calendar, Sign}; -#[cfg(feature = "experimental")] -use crate::tzdb::FsTzdbProvider; -#[cfg(feature = "experimental")] -use std::sync::{LazyLock, Mutex}; - -#[cfg(feature = "experimental")] -pub static TZ_PROVIDER: LazyLock> = - LazyLock::new(|| Mutex::new(FsTzdbProvider::default())); - const NS_IN_HOUR: i128 = 60 * 60 * 1000 * 1000 * 1000; -/// `TimeZoneOffset` represents the number of seconds to be added to UT in order to determine local time. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TimeZoneOffset { - /// The transition time epoch at which the offset needs to be applied. - pub transition_epoch: Option, - /// The time zone offset in seconds. - pub offset: i64, -} - -// NOTE: It may be a good idea to eventually move this into it's -// own individual crate rather than having it tied directly into `temporal_rs` -/// The `TimeZoneProvider` trait provides methods required for a provider -/// to implement in order to source time zone data from that provider. -pub trait TimeZoneProvider { - fn check_identifier(&self, identifier: &str) -> bool; - - fn get_named_tz_epoch_nanoseconds( - &self, - identifier: &str, - local_datetime: IsoDateTime, - ) -> TemporalResult>; - - fn get_named_tz_offset_nanoseconds( - &self, - identifier: &str, - utc_epoch: i128, - ) -> TemporalResult; -} - -pub struct NeverProvider; - -impl TimeZoneProvider for NeverProvider { - fn check_identifier(&self, _: &str) -> bool { - unimplemented!() - } - - fn get_named_tz_epoch_nanoseconds( - &self, - _: &str, - _: IsoDateTime, - ) -> TemporalResult> { - unimplemented!() - } - - fn get_named_tz_offset_nanoseconds(&self, _: &str, _: i128) -> TemporalResult { - unimplemented!() - } -} - // TODO: Potentially migrate to Cow<'a, str> // TODO: There may be an argument to have Offset minutes be a (Cow<'a, str>,, i16) to // prevent allocations / writing, TBD @@ -87,32 +31,70 @@ pub enum TimeZone { } impl TimeZone { - #[cfg(feature = "experimental")] + #[cfg(feature = "full")] pub fn try_from_str(source: &str) -> TemporalResult { + use crate::builtins::timezone::TZ_PROVIDER; + let provider = TZ_PROVIDER .lock() .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - Self::try_from_str_with_provider(source, &*provider) + try_timezone_from_str_with_provider(source, &*provider) } /// Parses a `TimeZone` from a provided `&str`. + #[cfg(not(feature = "full"))] pub fn try_from_str_with_provider( source: &str, provider: &impl TimeZoneProvider, ) -> TemporalResult { - if source == "Z" { - return Ok(Self::OffsetMinutes(0)); - } - let mut cursor = source.chars().peekable(); - if cursor.peek().is_some_and(is_ascii_sign) { - return parse_offset(&mut cursor); - } else if provider.check_identifier(source) { - return Ok(Self::IanaIdentifier(source.to_owned())); + try_timezone_from_str_with_provider(source, provider) + } + + /// Returns the current `TimeZoneSlot`'s identifier. + pub fn identifier(&self) -> TemporalResult { + match self { + TimeZone::IanaIdentifier(s) => Ok(s.clone()), + TimeZone::OffsetMinutes(m) => { + let sign = if *m < 0 { + Sign::Negative + } else { + Sign::Positive + }; + let hour = (m.abs() / 60) as u8; + let minute = (m.abs() % 60) as u8; + let formattable_offset = FormattableOffset { + sign, + time: FormattableTime { + hour, + minute, + second: 0, + nanosecond: 0, + precision: Precision::Minute, + include_sep: true, + }, + }; + Ok(formattable_offset.to_string()) + } } - Err(TemporalError::range().with_message("Valid time zone was not provided.")) } } +pub fn try_timezone_from_str_with_provider( + source: &str, + provider: &impl TimeZoneProvider, +) -> TemporalResult { + if source == "Z" { + return Ok(TimeZone::OffsetMinutes(0)); + } + let mut cursor = source.chars().peekable(); + if cursor.peek().is_some_and(is_ascii_sign) { + return parse_offset(&mut cursor); + } else if provider.check_identifier(source) { + return Ok(TimeZone::IanaIdentifier(source.to_owned())); + } + Err(TemporalError::range().with_message("Valid time zone was not provided.")) +} + impl Default for TimeZone { fn default() -> Self { Self::IanaIdentifier("UTC".into()) @@ -134,11 +116,9 @@ impl TimeZone { let nanos = self.get_offset_nanos_for(instant.as_i128(), provider)?; IsoDateTime::from_epoch_nanos(&instant.as_i128(), nanos.to_i64().unwrap_or(0)) } -} -impl TimeZone { /// Get the offset for this current `TimeZoneSlot`. - pub fn get_offset_nanos_for( + pub(crate) fn get_offset_nanos_for( &self, utc_epoch: i128, provider: &impl TimeZoneProvider, @@ -154,7 +134,7 @@ impl TimeZone { } } - pub fn get_epoch_nanoseconds_for( + pub(crate) fn get_epoch_nanoseconds_for( &self, iso: IsoDateTime, disambiguation: Disambiguation, @@ -167,7 +147,7 @@ impl TimeZone { } /// Get the possible `Instant`s for this `TimeZoneSlot`. - pub fn get_possible_epoch_ns_for( + pub(crate) fn get_possible_epoch_ns_for( &self, iso: IsoDateTime, provider: &impl TimeZoneProvider, @@ -220,34 +200,6 @@ impl TimeZone { // 5. Return possibleEpochNanoseconds. Ok(possible_nanoseconds) } - - /// Returns the current `TimeZoneSlot`'s identifier. - pub fn identifier(&self) -> TemporalResult { - match self { - TimeZone::IanaIdentifier(s) => Ok(s.clone()), - TimeZone::OffsetMinutes(m) => { - let sign = if *m < 0 { - Sign::Negative - } else { - Sign::Positive - }; - let hour = (m.abs() / 60) as u8; - let minute = (m.abs() % 60) as u8; - let formattable_offset = FormattableOffset { - sign, - time: FormattableTime { - hour, - minute, - second: 0, - nanosecond: 0, - precision: Precision::Minute, - include_sep: true, - }, - }; - Ok(formattable_offset.to_string()) - } - } - } } impl TimeZone { @@ -505,7 +457,7 @@ fn is_ascii_sign(ch: &char) -> bool { *ch == '+' || *ch == '-' } -#[cfg(all(test, feature = "tzdb"))] +#[cfg(all(test, feature = "tzdb", not(feature = "full")))] mod tests { use super::TimeZone; use crate::tzdb::FsTzdbProvider; diff --git a/src/components/year_month.rs b/src/builtins/core/year_month.rs similarity index 98% rename from src/components/year_month.rs rename to src/builtins/core/year_month.rs index c88214399..6133ed541 100644 --- a/src/components/year_month.rs +++ b/src/builtins/core/year_month.rs @@ -6,12 +6,11 @@ use core::str::FromStr; use tinystr::TinyAsciiStr; use crate::{ - components::calendar::Calendar, iso::IsoDate, options::{ArithmeticOverflow, DisplayCalendar}, parsers::{FormattableCalendar, FormattableDate, FormattableYearMonth}, utils::pad_iso_year, - TemporalError, TemporalResult, TemporalUnwrap, + Calendar, TemporalError, TemporalResult, TemporalUnwrap, }; use super::{Duration, PartialDate}; diff --git a/src/components/zoneddatetime.rs b/src/builtins/core/zoneddatetime.rs similarity index 79% rename from src/components/zoneddatetime.rs rename to src/builtins/core/zoneddatetime.rs index 712d31941..7a0d1b55b 100644 --- a/src/components/zoneddatetime.rs +++ b/src/builtins/core/zoneddatetime.rs @@ -6,10 +6,11 @@ use ixdtf::parsers::records::{TimeZoneRecord, UtcOffsetRecordOrZ}; use tinystr::TinyAsciiStr; use crate::{ - components::{ + builtins::core::{ + calendar::Calendar, duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration}, - timezone::{parse_offset, TimeZoneProvider}, - EpochNanoseconds, + timezone::{parse_offset, TimeZone}, + Duration, Instant, PlainDate, PlainDateTime, PlainTime, }, iso::{IsoDate, IsoDateTime, IsoTime}, options::{ @@ -20,14 +21,13 @@ use crate::{ }, parsers::{self, IxdtfStringBuilder}, partial::{PartialDate, PartialTime}, + provider::TimeZoneProvider, rounding::{IncrementRounder, Round}, - temporal_assert, Calendar, Duration, Instant, PlainDate, PlainDateTime, PlainTime, Sign, - TemporalError, TemporalResult, TimeZone, + temporal_assert, + time::EpochNanoseconds, + Sign, TemporalError, TemporalResult, }; -#[cfg(feature = "experimental")] -use crate::components::timezone::TZ_PROVIDER; - /// A struct representing a partial `ZonedDateTime`. #[derive(Debug, Default, Clone, PartialEq)] pub struct PartialZonedDateTime { @@ -434,268 +434,9 @@ impl ZonedDateTime { } } -// ===== Experimental TZ_PROVIDER accessor implementations ===== - -#[cfg(feature = "experimental")] -impl ZonedDateTime { - pub fn year(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.year_with_provider(&*provider) - } - - pub fn month(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.month_with_provider(&*provider) - } - - pub fn month_code(&self) -> TemporalResult> { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.month_code_with_provider(&*provider) - } - - pub fn day(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.day_with_provider(&*provider) - } - - pub fn hour(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.hour_with_provider(&*provider) - } - - pub fn minute(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.minute_with_provider(&*provider) - } - - pub fn second(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.second_with_provider(&*provider) - } - - pub fn millisecond(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.millisecond_with_provider(&*provider) - } - - pub fn microsecond(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.millisecond_with_provider(&*provider) - } - - pub fn nanosecond(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - - self.millisecond_with_provider(&*provider) - } -} - -// ==== Experimental TZ_PROVIDER calendar method implementations ==== - -#[cfg(feature = "experimental")] -impl ZonedDateTime { - pub fn era(&self) -> TemporalResult>> { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.era_with_provider(&*provider) - } - - pub fn era_year(&self) -> TemporalResult> { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.era_year_with_provider(&*provider) - } - - /// Returns the calendar day of week value. - pub fn day_of_week(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.day_of_week_with_provider(&*provider) - } - - /// Returns the calendar day of year value. - pub fn day_of_year(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.day_of_year_with_provider(&*provider) - } - - /// Returns the calendar week of year value. - pub fn week_of_year(&self) -> TemporalResult> { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.week_of_year_with_provider(&*provider) - } - - /// Returns the calendar year of week value. - pub fn year_of_week(&self) -> TemporalResult> { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.year_of_week_with_provider(&*provider) - } - - /// Returns the calendar days in week value. - pub fn days_in_week(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.days_in_week_with_provider(&*provider) - } - - /// Returns the calendar days in month value. - pub fn days_in_month(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.days_in_month_with_provider(&*provider) - } - - /// Returns the calendar days in year value. - pub fn days_in_year(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.days_in_year_with_provider(&*provider) - } - - /// Returns the calendar months in year value. - pub fn months_in_year(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.months_in_year_with_provider(&*provider) - } - - /// Returns returns whether the date in a leap year for the given calendar. - pub fn in_leap_year(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.in_leap_year_with_provider(&*provider) - } -} - -// ==== Experimental TZ_PROVIDER method implementations ==== - -#[cfg(feature = "experimental")] -impl ZonedDateTime { - /// Creates a new `ZonedDateTime` from the current `ZonedDateTime` - /// combined with the provided `TimeZone`. - pub fn with_plain_time(&self, time: PlainTime) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.with_plain_time_and_provider(time, &*provider) - } - - pub fn add( - &self, - duration: &Duration, - overflow: Option, - ) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - - self.add_internal( - duration, - overflow.unwrap_or(ArithmeticOverflow::Constrain), - &*provider, - ) - } - - pub fn subtract( - &self, - duration: &Duration, - overflow: Option, - ) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.add_internal( - &duration.negated(), - overflow.unwrap_or(ArithmeticOverflow::Constrain), - &*provider, - ) - } - - pub fn start_of_day(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.start_of_day_with_provider(&*provider) - } - - pub fn to_plain_date(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.to_plain_date_with_provider(&*provider) - } - - pub fn to_plain_time(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.to_plain_time_with_provider(&*provider) - } - - pub fn to_plain_datetime(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.to_plain_datetime_with_provider(&*provider) - } - - pub fn from_str( - source: &str, - disambiguation: Disambiguation, - offset_option: OffsetDisambiguation, - ) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - Self::from_str_with_provider(source, disambiguation, offset_option, &*provider) - } -} - // ==== HoursInDay accessor method implementation ==== impl ZonedDateTime { - #[cfg(feature = "experimental")] - pub fn hours_in_day(&self) -> TemporalResult { - let provider = TZ_PROVIDER - .lock() - .map_err(|_| TemporalError::general("Unable to acquire lock"))?; - self.hours_in_day_with_provider(&*provider) - } - pub fn hours_in_day_with_provider( &self, provider: &impl TimeZoneProvider, @@ -1294,21 +1035,19 @@ pub(crate) fn nanoseconds_to_formattable_offset_minutes( Ok((sign, hour as u8, minute as u8)) } -#[cfg(all(test, feature = "tzdb"))] +#[cfg(all(test, feature = "tzdb", not(feature = "full")))] mod tests { + use super::ZonedDateTime; use crate::{ options::{DifferenceSettings, Disambiguation, OffsetDisambiguation, TemporalUnit}, partial::{PartialDate, PartialTime, PartialZonedDateTime}, primitive::FiniteF64, tzdb::FsTzdbProvider, - Calendar, TimeZone, ZonedDateTime, + Calendar, TimeZone, }; use core::str::FromStr; use tinystr::tinystr; - #[cfg(all(feature = "experimental", not(target_os = "windows")))] - use crate::Duration; - #[test] fn basic_zdt_test() { let provider = &FsTzdbProvider::default(); @@ -1357,82 +1096,6 @@ mod tests { assert_eq!(zdt_plus_eleven.second_with_provider(provider).unwrap(), 12); } - #[cfg(all(feature = "experimental", not(target_os = "windows")))] - #[test] - fn static_tzdb_zdt_test() { - let nov_30_2023_utc = 1_701_308_952_000_000_000i128; - - let zdt = ZonedDateTime::try_new( - nov_30_2023_utc, - Calendar::from_str("iso8601").unwrap(), - TimeZone::try_from_str("Z").unwrap(), - ) - .unwrap(); - - assert_eq!(zdt.year().unwrap(), 2023); - assert_eq!(zdt.month().unwrap(), 11); - assert_eq!(zdt.day().unwrap(), 30); - assert_eq!(zdt.hour().unwrap(), 1); - assert_eq!(zdt.minute().unwrap(), 49); - assert_eq!(zdt.second().unwrap(), 12); - - let zdt_minus_five = ZonedDateTime::try_new( - nov_30_2023_utc, - Calendar::from_str("iso8601").unwrap(), - TimeZone::try_from_str("America/New_York").unwrap(), - ) - .unwrap(); - - assert_eq!(zdt_minus_five.year().unwrap(), 2023); - assert_eq!(zdt_minus_five.month().unwrap(), 11); - assert_eq!(zdt_minus_five.day().unwrap(), 29); - assert_eq!(zdt_minus_five.hour().unwrap(), 20); - assert_eq!(zdt_minus_five.minute().unwrap(), 49); - assert_eq!(zdt_minus_five.second().unwrap(), 12); - - let zdt_plus_eleven = ZonedDateTime::try_new( - nov_30_2023_utc, - Calendar::from_str("iso8601").unwrap(), - TimeZone::try_from_str("Australia/Sydney").unwrap(), - ) - .unwrap(); - - assert_eq!(zdt_plus_eleven.year().unwrap(), 2023); - assert_eq!(zdt_plus_eleven.month().unwrap(), 11); - assert_eq!(zdt_plus_eleven.day().unwrap(), 30); - assert_eq!(zdt_plus_eleven.hour().unwrap(), 12); - assert_eq!(zdt_plus_eleven.minute().unwrap(), 49); - assert_eq!(zdt_plus_eleven.second().unwrap(), 12); - } - - #[cfg(all(feature = "experimental", not(target_os = "windows")))] - #[test] - fn basic_zdt_add() { - let zdt = - ZonedDateTime::try_new(-560174321098766, Calendar::default(), TimeZone::default()) - .unwrap(); - let d = Duration::new( - 0.into(), - 0.into(), - 0.into(), - 0.into(), - 240.into(), - 0.into(), - 0.into(), - 0.into(), - 0.into(), - 800.into(), - ) - .unwrap(); - // "1970-01-04T12:23:45.678902034+00:00[UTC]" - let expected = - ZonedDateTime::try_new(303825678902034, Calendar::default(), TimeZone::default()) - .unwrap(); - - let result = zdt.add(&d, None).unwrap(); - assert_eq!(result, expected); - } - #[test] fn zdt_from_partial() { let provider = &FsTzdbProvider::default(); diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs new file mode 100644 index 000000000..64d99c62d --- /dev/null +++ b/src/builtins/mod.rs @@ -0,0 +1,10 @@ +pub mod core; + +#[cfg(feature = "full")] +pub(crate) mod native; + +#[cfg(not(feature = "full"))] +pub use core::*; + +#[cfg(feature = "full")] +pub use native::*; diff --git a/src/builtins/native/date.rs b/src/builtins/native/date.rs new file mode 100644 index 000000000..8c74fe8dd --- /dev/null +++ b/src/builtins/native/date.rs @@ -0,0 +1,279 @@ +use crate::builtins::native::PlainTime; +use crate::{ + builtins::core as temporal_core, + options::{ArithmeticOverflow, DifferenceSettings, DisplayCalendar}, + Calendar, TemporalResult, +}; +use alloc::string::String; + +use super::{duration::Duration, PartialDate, PlainDateTime, PlainMonthDay, PlainYearMonth}; +use tinystr::TinyAsciiStr; + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct PlainDate(pub(crate) temporal_core::PlainDate); + +impl core::fmt::Display for PlainDate { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +impl From for PlainDate { + fn from(value: temporal_core::PlainDate) -> Self { + Self(value) + } +} + +impl PlainDate { + /// Creates a new `PlainDate` automatically constraining any values that may be invalid. + pub fn new(year: i32, month: u8, day: u8, calendar: Calendar) -> TemporalResult { + temporal_core::PlainDate::new(year, month, day, calendar).map(Into::into) + } + + /// Creates a new `PlainDate` rejecting any date that may be invalid. + pub fn try_new(year: i32, month: u8, day: u8, calendar: Calendar) -> TemporalResult { + temporal_core::PlainDate::try_new(year, month, day, calendar).map(Into::into) + } + + /// Creates a new `PlainDate` with the specified overflow. + /// + /// This operation is the public facing API to Temporal's `RegulateIsoDate` + #[inline] + pub fn new_with_overflow( + year: i32, + month: u8, + day: u8, + calendar: Calendar, + overflow: ArithmeticOverflow, + ) -> TemporalResult { + temporal_core::PlainDate::new_with_overflow(year, month, day, calendar, overflow) + .map(Into::into) + } + + /// Create a `PlainDate` from a `PartialDate` + /// + /// ```rust + /// use temporal_rs::{PlainDate, partial::PartialDate}; + /// + /// let partial = PartialDate { + /// year: Some(2000), + /// month: Some(13), + /// day: Some(2), + /// ..Default::default() + /// }; + /// + /// let date = PlainDate::from_partial(partial, None).unwrap(); + /// + /// assert_eq!(date.year().unwrap(), 2000); + /// assert_eq!(date.month().unwrap(), 12); + /// assert_eq!(date.day().unwrap(), 2); + /// assert_eq!(date.calendar().identifier(), "iso8601"); + /// + /// ``` + #[inline] + pub fn from_partial( + partial: PartialDate, + overflow: Option, + ) -> TemporalResult { + temporal_core::PlainDate::from_partial(partial, overflow).map(Into::into) + } + + /// Creates a date time with values from a `PartialDate`. + pub fn with( + &self, + partial: PartialDate, + overflow: Option, + ) -> TemporalResult { + self.0.with(partial, overflow).map(Into::into) + } + + /// Creates a new `Date` from the current `Date` and the provided calendar. + pub fn with_calendar(&self, calendar: Calendar) -> TemporalResult { + self.0.with_calendar(calendar).map(Into::into) + } + + #[inline] + #[must_use] + /// Returns this `Date`'s ISO year value. + pub const fn iso_year(&self) -> i32 { + self.0.iso_year() + } + + #[inline] + #[must_use] + /// Returns this `Date`'s ISO month value. + pub const fn iso_month(&self) -> u8 { + self.0.iso_month() + } + + #[inline] + #[must_use] + /// Returns this `Date`'s ISO day value. + pub const fn iso_day(&self) -> u8 { + self.0.iso_day() + } + + #[inline] + #[must_use] + /// Returns a reference to this `Date`'s calendar slot. + pub fn calendar(&self) -> &Calendar { + self.0.calendar() + } + + /// 3.5.7 `IsValidISODate` + /// + /// Checks if the current date is a valid `ISODate`. + #[must_use] + pub fn is_valid(&self) -> bool { + self.0.is_valid() + } + + /// `DaysUntil` + /// + /// Calculates the epoch days between two `Date`s + #[inline] + #[must_use] + pub fn days_until(&self, other: &Self) -> i32 { + self.0.days_until(&other.0) + } + + #[inline] + /// Adds a `Duration` to the current `Date` + pub fn add( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.0.add(&duration.0, overflow).map(Into::into) + } + + #[inline] + /// Subtracts a `Duration` to the current `Date` + pub fn subtract( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.0.subtract(&duration.0, overflow).map(Into::into) + } + + #[inline] + /// Returns a `Duration` representing the time from this `Date` until the other `Date`. + pub fn until(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.until(&other.0, settings).map(Into::into) + } + + #[inline] + /// Returns a `Duration` representing the time passed from this `Date` since the other `Date`. + pub fn since(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.since(&other.0, settings).map(Into::into) + } +} + +// ==== Calendar-derived Public API ==== + +impl PlainDate { + /// Returns the calendar year value. + pub fn year(&self) -> TemporalResult { + self.0.year() + } + + /// Returns the calendar month value. + pub fn month(&self) -> TemporalResult { + self.0.month() + } + + /// Returns the calendar month code value. + pub fn month_code(&self) -> TemporalResult> { + self.0.month_code() + } + + /// Returns the calendar day value. + pub fn day(&self) -> TemporalResult { + self.0.day() + } + + /// Returns the calendar day of week value. + pub fn day_of_week(&self) -> TemporalResult { + self.0.day_of_week() + } + + /// Returns the calendar day of year value. + pub fn day_of_year(&self) -> TemporalResult { + self.0.day_of_year() + } + + /// Returns the calendar week of year value. + pub fn week_of_year(&self) -> TemporalResult> { + self.0.week_of_year() + } + + /// Returns the calendar year of week value. + pub fn year_of_week(&self) -> TemporalResult> { + self.0.year_of_week() + } + + /// Returns the calendar days in week value. + pub fn days_in_week(&self) -> TemporalResult { + self.0.days_in_week() + } + + /// Returns the calendar days in month value. + pub fn days_in_month(&self) -> TemporalResult { + self.0.days_in_month() + } + + /// Returns the calendar days in year value. + pub fn days_in_year(&self) -> TemporalResult { + self.0.days_in_year() + } + + /// Returns the calendar months in year value. + pub fn months_in_year(&self) -> TemporalResult { + self.0.months_in_year() + } + + /// Returns returns whether the date in a leap year for the given calendar. + pub fn in_leap_year(&self) -> TemporalResult { + self.0.in_leap_year() + } + + pub fn era(&self) -> TemporalResult>> { + self.0.era() + } + + pub fn era_year(&self) -> TemporalResult> { + self.0.era_year() + } +} + +// ==== ToX Methods ==== + +impl PlainDate { + /// Converts the current `Date` into a `DateTime` + /// + /// # Notes + /// + /// If no time is provided, then the time will default to midnight. + #[inline] + pub fn to_date_time(&self, time: Option) -> TemporalResult { + self.0.to_date_time(time.map(|t| t.0)).map(Into::into) + } + + /// Converts the current `Date` into a `PlainYearMonth` + #[inline] + pub fn to_year_month(&self) -> TemporalResult { + self.0.to_year_month() + } + + /// Converts the current `Date` into a `PlainMonthDay` + #[inline] + pub fn to_month_day(&self) -> TemporalResult { + self.0.to_month_day() + } + + #[inline] + pub fn to_ixdtf_string(&self, display_calendar: DisplayCalendar) -> String { + self.0.to_ixdtf_string(display_calendar) + } +} diff --git a/src/builtins/native/datetime.rs b/src/builtins/native/datetime.rs new file mode 100644 index 000000000..762385938 --- /dev/null +++ b/src/builtins/native/datetime.rs @@ -0,0 +1,375 @@ +use super::{Duration, PartialDateTime, PlainDate, PlainTime}; +use crate::{ + builtins::core as temporal_core, + options::{ + ArithmeticOverflow, DifferenceSettings, DisplayCalendar, RoundingOptions, + ToStringRoundingOptions, + }, + Calendar, TemporalResult, +}; +use alloc::string::String; +use tinystr::TinyAsciiStr; + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct PlainDateTime(pub(crate) temporal_core::PlainDateTime); + +impl core::fmt::Display for PlainDateTime { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +impl From for PlainDateTime { + fn from(value: temporal_core::PlainDateTime) -> Self { + Self(value) + } +} + +impl PlainDateTime { + /// Creates a new `DateTime`, constraining any arguments that into a valid range. + #[allow(clippy::too_many_arguments)] + pub fn new( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + millisecond: u16, + microsecond: u16, + nanosecond: u16, + calendar: Calendar, + ) -> TemporalResult { + temporal_core::PlainDateTime::new( + year, + month, + day, + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + calendar, + ) + .map(Into::into) + } + + /// Creates a new `DateTime`, rejecting any arguments that are not in a valid range. + #[allow(clippy::too_many_arguments)] + pub fn try_new( + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + millisecond: u16, + microsecond: u16, + nanosecond: u16, + calendar: Calendar, + ) -> TemporalResult { + temporal_core::PlainDateTime::try_new( + year, + month, + day, + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + calendar, + ) + .map(Into::into) + } + + /// Create a `DateTime` from a `Date` and a `Time`. + pub fn from_date_and_time(date: PlainDate, time: PlainTime) -> TemporalResult { + temporal_core::PlainDateTime::from_date_and_time(date.0, time.0).map(Into::into) + } + + /// Creates a `DateTime` from a `PartialDateTime`. + /// + /// ```rust + /// use temporal_rs::{PlainDateTime, partial::{PartialDateTime, PartialTime, PartialDate}}; + /// + /// let date = PartialDate { + /// year: Some(2000), + /// month: Some(13), + /// day: Some(2), + /// ..Default::default() + /// }; + /// + /// let time = PartialTime { + /// hour: Some(4), + /// minute: Some(25), + /// ..Default::default() + /// }; + /// + /// let partial = PartialDateTime { date, time }; + /// + /// let date = PlainDateTime::from_partial(partial, None).unwrap(); + /// + /// assert_eq!(date.year().unwrap(), 2000); + /// assert_eq!(date.month().unwrap(), 12); + /// assert_eq!(date.day().unwrap(), 2); + /// assert_eq!(date.calendar().identifier(), "iso8601"); + /// assert_eq!(date.hour(), 4); + /// assert_eq!(date.minute(), 25); + /// assert_eq!(date.second(), 0); + /// assert_eq!(date.millisecond(), 0); + /// + /// ``` + pub fn from_partial( + partial: PartialDateTime, + overflow: Option, + ) -> TemporalResult { + temporal_core::PlainDateTime::from_partial(partial, overflow).map(Into::into) + } + + /// Creates a new `DateTime` with the fields of a `PartialDateTime`. + /// + /// ```rust + /// use temporal_rs::{Calendar, PlainDateTime, partial::{PartialDateTime, PartialTime, PartialDate}}; + /// + /// let initial = PlainDateTime::try_new(2000, 12, 2, 0,0,0,0,0,0, Calendar::default()).unwrap(); + /// + /// let date = PartialDate { + /// month: Some(5), + /// ..Default::default() + /// }; + /// + /// let time = PartialTime { + /// hour: Some(4), + /// second: Some(30), + /// ..Default::default() + /// }; + /// + /// let partial = PartialDateTime { date, time }; + /// + /// let date = initial.with(partial, None).unwrap(); + /// + /// assert_eq!(date.year().unwrap(), 2000); + /// assert_eq!(date.month().unwrap(), 5); + /// assert_eq!(date.day().unwrap(), 2); + /// assert_eq!(date.calendar().identifier(), "iso8601"); + /// assert_eq!(date.hour(), 4); + /// assert_eq!(date.minute(), 0); + /// assert_eq!(date.second(), 30); + /// assert_eq!(date.millisecond(), 0); + /// + /// ``` + #[inline] + pub fn with( + &self, + partial_datetime: PartialDateTime, + overflow: Option, + ) -> TemporalResult { + self.0.with(partial_datetime, overflow).map(Into::into) + } + + /// Creates a new `DateTime` from the current `DateTime` and the provided `Time`. + pub fn with_time(&self, time: PlainTime) -> TemporalResult { + self.0.with_time(time.0).map(Into::into) + } + + /// Creates a new `DateTime` from the current `DateTime` and a provided `Calendar`. + pub fn with_calendar(&self, calendar: Calendar) -> TemporalResult { + self.0.with_calendar(calendar).map(Into::into) + } + + /// Returns this `Date`'s ISO year value. + #[inline] + #[must_use] + pub const fn iso_year(&self) -> i32 { + self.0.iso_year() + } + + /// Returns this `Date`'s ISO month value. + #[inline] + #[must_use] + pub const fn iso_month(&self) -> u8 { + self.0.iso_month() + } + + /// Returns this `Date`'s ISO day value. + #[inline] + #[must_use] + pub const fn iso_day(&self) -> u8 { + self.0.iso_day() + } + + /// Returns the hour value + #[inline] + #[must_use] + pub fn hour(&self) -> u8 { + self.0.hour() + } + + /// Returns the minute value + #[inline] + #[must_use] + pub fn minute(&self) -> u8 { + self.0.minute() + } + + /// Returns the second value + #[inline] + #[must_use] + pub fn second(&self) -> u8 { + self.0.second() + } + + /// Returns the `millisecond` value + #[inline] + #[must_use] + pub fn millisecond(&self) -> u16 { + self.0.millisecond() + } + + /// Returns the `microsecond` value + #[inline] + #[must_use] + pub fn microsecond(&self) -> u16 { + self.0.microsecond() + } + + /// Returns the `nanosecond` value + #[inline] + #[must_use] + pub fn nanosecond(&self) -> u16 { + self.0.nanosecond() + } + + /// Returns the Calendar value. + #[inline] + #[must_use] + pub fn calendar(&self) -> &Calendar { + self.0.calendar() + } +} + +// ==== Calendar-derived public API ==== + +impl PlainDateTime { + /// Returns the calendar year value. + pub fn year(&self) -> TemporalResult { + self.0.year() + } + + /// Returns the calendar month value. + pub fn month(&self) -> TemporalResult { + self.0.month() + } + + /// Returns the calendar month code value. + pub fn month_code(&self) -> TemporalResult> { + self.0.month_code() + } + + /// Returns the calendar day value. + pub fn day(&self) -> TemporalResult { + self.0.day() + } + + /// Returns the calendar day of week value. + pub fn day_of_week(&self) -> TemporalResult { + self.0.day_of_week() + } + + /// Returns the calendar day of year value. + pub fn day_of_year(&self) -> TemporalResult { + self.0.day_of_year() + } + + /// Returns the calendar week of year value. + pub fn week_of_year(&self) -> TemporalResult> { + self.0.week_of_year() + } + + /// Returns the calendar year of week value. + pub fn year_of_week(&self) -> TemporalResult> { + self.0.year_of_week() + } + + /// Returns the calendar days in week value. + pub fn days_in_week(&self) -> TemporalResult { + self.0.days_in_week() + } + + /// Returns the calendar days in month value. + pub fn days_in_month(&self) -> TemporalResult { + self.0.days_in_month() + } + + /// Returns the calendar days in year value. + pub fn days_in_year(&self) -> TemporalResult { + self.0.days_in_year() + } + + /// Returns the calendar months in year value. + pub fn months_in_year(&self) -> TemporalResult { + self.0.months_in_year() + } + + /// Returns returns whether the date in a leap year for the given calendar. + pub fn in_leap_year(&self) -> TemporalResult { + self.0.in_leap_year() + } + + pub fn era(&self) -> TemporalResult>> { + self.0.era() + } + + pub fn era_year(&self) -> TemporalResult> { + self.0.era_year() + } +} + +impl PlainDateTime { + #[inline] + /// Adds a `Duration` to the current `DateTime`. + pub fn add( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.0.add(&duration.0, overflow).map(Into::into) + } + + #[inline] + /// Subtracts a `Duration` to the current `DateTime`. + pub fn subtract( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.0.subtract(&duration.0, overflow).map(Into::into) + } + + #[inline] + /// Returns a `Duration` representing the period of time from this `DateTime` until the other `DateTime`. + pub fn until(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.until(&other.0, settings).map(Into::into) + } + + #[inline] + /// Returns a `Duration` representing the period of time from this `DateTime` since the other `DateTime`. + pub fn since(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.since(&other.0, settings).map(Into::into) + } + + /// Rounds the current datetime based on provided options. + pub fn round(&self, options: RoundingOptions) -> TemporalResult { + self.0.round(options).map(Into::into) + } + + pub fn to_ixdtf_string( + &self, + options: ToStringRoundingOptions, + display_calendar: DisplayCalendar, + ) -> TemporalResult { + self.0.to_ixdtf_string(options, display_calendar) + } +} diff --git a/src/builtins/native/duration.rs b/src/builtins/native/duration.rs new file mode 100644 index 000000000..09dbcba24 --- /dev/null +++ b/src/builtins/native/duration.rs @@ -0,0 +1,238 @@ +use crate::builtins::core::PartialDuration; +use crate::options::ToStringRoundingOptions; +use crate::{ + builtins::core as temporal_core, + options::{RelativeTo, RoundingOptions}, + primitive::FiniteF64, + Sign, TemporalError, TemporalResult, +}; +use alloc::string::String; + +use super::{timezone::TZ_PROVIDER, DateDuration, TimeDuration}; + +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone)] +pub struct Duration(pub(crate) temporal_core::Duration); + +impl core::fmt::Display for Duration { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str( + &self + .as_temporal_string(ToStringRoundingOptions::default()) + .expect("Default options on a valid Duration should return a string."), + ) + } +} + +impl From for Duration { + fn from(value: temporal_core::Duration) -> Self { + Self(value) + } +} + +impl From for Duration { + fn from(value: DateDuration) -> Self { + Self(value.into()) + } +} + +impl From for Duration { + fn from(value: TimeDuration) -> Self { + Self(value.into()) + } +} + +impl Duration { + /// Creates a new validated `Duration`. + #[allow(clippy::too_many_arguments)] + pub fn new( + years: FiniteF64, + months: FiniteF64, + weeks: FiniteF64, + days: FiniteF64, + hours: FiniteF64, + minutes: FiniteF64, + seconds: FiniteF64, + milliseconds: FiniteF64, + microseconds: FiniteF64, + nanoseconds: FiniteF64, + ) -> TemporalResult { + temporal_core::Duration::new( + years, + months, + weeks, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + ) + .map(Into::into) + } + + /// Creates a `Duration` from a provided `PartialDuration`. + pub fn from_partial_duration(partial: PartialDuration) -> TemporalResult { + temporal_core::Duration::from_partial_duration(partial).map(Into::into) + } +} + +// ==== Public `Duration` Getters/Setters ==== + +impl Duration { + /// Returns a reference to the inner `TimeDuration` + #[inline] + #[must_use] + pub fn time(&self) -> &TimeDuration { + self.0.time() + } + + /// Returns a reference to the inner `DateDuration` + #[inline] + #[must_use] + pub fn date(&self) -> &DateDuration { + self.0.date() + } + + /// Set this `DurationRecord`'s `TimeDuration`. + #[inline] + pub fn set_time_duration(&mut self, time: TimeDuration) { + self.0.set_time_duration(time); + } + + /// Returns the `years` field of duration. + #[inline] + #[must_use] + pub const fn years(&self) -> FiniteF64 { + self.0.years() + } + + /// Returns the `months` field of duration. + #[inline] + #[must_use] + pub const fn months(&self) -> FiniteF64 { + self.0.months() + } + + /// Returns the `weeks` field of duration. + #[inline] + #[must_use] + pub const fn weeks(&self) -> FiniteF64 { + self.0.weeks() + } + + /// Returns the `weeks` field of duration. + #[inline] + #[must_use] + pub const fn days(&self) -> FiniteF64 { + self.0.days() + } + + /// Returns the `hours` field of duration. + #[inline] + #[must_use] + pub const fn hours(&self) -> FiniteF64 { + self.0.hours() + } + + /// Returns the `hours` field of duration. + #[inline] + #[must_use] + pub const fn minutes(&self) -> FiniteF64 { + self.0.minutes() + } + + /// Returns the `seconds` field of duration. + #[inline] + #[must_use] + pub const fn seconds(&self) -> FiniteF64 { + self.0.seconds() + } + + /// Returns the `hours` field of duration. + #[inline] + #[must_use] + pub const fn milliseconds(&self) -> FiniteF64 { + self.0.milliseconds() + } + + /// Returns the `microseconds` field of duration. + #[inline] + #[must_use] + pub const fn microseconds(&self) -> FiniteF64 { + self.0.microseconds() + } + + /// Returns the `nanoseconds` field of duration. + #[inline] + #[must_use] + pub const fn nanoseconds(&self) -> FiniteF64 { + self.0.nanoseconds() + } +} + +// ==== Public Duration methods ==== + +impl Duration { + /// Determines the sign for the current self. + #[inline] + #[must_use] + pub fn sign(&self) -> Sign { + self.0.sign() + } + + /// Returns whether the current `Duration` is zero. + /// + /// Equivalant to `Temporal.Duration.blank()`. + #[inline] + #[must_use] + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } + + /// Returns a negated `Duration` + #[inline] + #[must_use] + pub fn negated(&self) -> Self { + self.0.negated().into() + } + + /// Returns the absolute value of `Duration`. + #[inline] + #[must_use] + pub fn abs(&self) -> Self { + self.0.abs().into() + } + + /// Returns the result of adding a `Duration` to the current `Duration` + #[inline] + pub fn add(&self, other: &Self) -> TemporalResult { + self.0.add(&other.0).map(Into::into) + } + + /// Returns the result of subtracting a `Duration` from the current `Duration` + #[inline] + pub fn subtract(&self, other: &Self) -> TemporalResult { + self.0.subtract(&other.0).map(Into::into) + } + + pub fn round( + &self, + options: RoundingOptions, + relative_to: Option, + ) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .round_with_provider(options, relative_to.map(Into::into), &*provider) + .map(Into::into) + } + + pub fn as_temporal_string(&self, options: ToStringRoundingOptions) -> TemporalResult { + self.0.as_temporal_string(options) + } +} diff --git a/src/components/duration/tests.rs b/src/builtins/native/duration/tests.rs similarity index 80% rename from src/components/duration/tests.rs rename to src/builtins/native/duration/tests.rs index ff0ab0b3d..a85303ac8 100644 --- a/src/components/duration/tests.rs +++ b/src/builtins/native/duration/tests.rs @@ -1,13 +1,15 @@ -#[cfg(feature = "experimental")] +use crate::builtins::native::PlainDate; use crate::{ - components::{calendar::Calendar, PlainDate}, - options::{RoundingIncrement, TemporalRoundingMode}, - TimeZone, + builtins::{core::calendar::Calendar, native::zoneddatetime::ZonedDateTime}, + options::{RelativeTo, RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit}, + primitive::FiniteF64, + DateDuration, TimeDuration, TimeZone, }; +use alloc::vec::Vec; +use core::str::FromStr; -use super::*; +use super::Duration; -#[cfg(feature = "experimental")] fn get_round_result( test_duration: &Duration, relative_to: RelativeTo, @@ -16,6 +18,7 @@ fn get_round_result( test_duration .round(options, Some(relative_to)) .unwrap() + .0 .fields() .iter() .map(|f| f.as_date_value().unwrap()) @@ -23,7 +26,6 @@ fn get_round_result( } // roundingmode-floor.js -#[cfg(feature = "experimental")] #[test] fn basic_positive_floor_rounding_v2() { let test_duration = Duration::new( @@ -92,7 +94,6 @@ fn basic_positive_floor_rounding_v2() { } #[test] -#[cfg(feature = "experimental")] fn basic_negative_floor_rounding_v2() { // Test setup let test_duration = Duration::new( @@ -162,7 +163,6 @@ fn basic_negative_floor_rounding_v2() { } // roundingmode-ceil.js -#[cfg(feature = "experimental")] #[test] fn basic_positive_ceil_rounding() { let test_duration = Duration::new( @@ -230,7 +230,6 @@ fn basic_positive_ceil_rounding() { assert_eq!(&result, &[5, 7, 0, 27, 16, 30, 20, 123, 987, 500],); } -#[cfg(feature = "experimental")] #[test] fn basic_negative_ceil_rounding() { let test_duration = Duration::new( @@ -299,7 +298,6 @@ fn basic_negative_ceil_rounding() { } // roundingmode-expand.js -#[cfg(feature = "experimental")] #[test] fn basic_positive_expand_rounding() { let test_duration = Duration::new( @@ -366,7 +364,6 @@ fn basic_positive_expand_rounding() { assert_eq!(&result, &[5, 7, 0, 27, 16, 30, 20, 123, 987, 500],); } -#[cfg(feature = "experimental")] #[test] fn basic_negative_expand_rounding() { let test_duration = Duration::new( @@ -437,7 +434,6 @@ fn basic_negative_expand_rounding() { } // test262/test/built-ins/Temporal/Duration/prototype/round/roundingincrement-non-integer.js -#[cfg(feature = "experimental")] #[test] fn rounding_increment_non_integer() { let test_duration = Duration::from( @@ -467,7 +463,7 @@ fn rounding_increment_non_integer() { .unwrap(); assert_eq!( - result.fields(), + result.0.fields(), &[ FiniteF64::default(), FiniteF64::default(), @@ -487,7 +483,7 @@ fn rounding_increment_non_integer() { .insert(RoundingIncrement::try_from(1e9 + 0.5).unwrap()); let result = test_duration.round(options, Some(relative_to)).unwrap(); assert_eq!( - result.fields(), + result.0.fields(), &[ FiniteF64::default(), FiniteF64::default(), @@ -603,22 +599,7 @@ fn basic_subtract_duration() { assert_eq!(result.minutes(), 30.0); } -#[test] -fn partial_duration_empty() { - let err = Duration::from_partial_duration(PartialDuration::default()); - assert!(err.is_err()) -} - -#[test] -fn partial_duration_values() { - let mut partial = PartialDuration::default(); - let _ = partial.years.insert(FiniteF64(20.0)); - let result = Duration::from_partial_duration(partial).unwrap(); - assert_eq!(result.years(), 20.0); -} - // days-24-hours-relative-to-zoned-date-time.js -#[cfg(feature = "experimental")] #[test] fn round_relative_to_zoned_datetime() { let duration = Duration::from( @@ -651,171 +632,3 @@ fn round_relative_to_zoned_datetime() { assert_eq!(result.days(), 1.0); assert_eq!(result.hours(), 1.0); } - -#[test] -fn default_duration_string() { - let duration = Duration::default(); - - let options = ToStringRoundingOptions { - precision: Precision::Auto, - smallest_unit: None, - rounding_mode: None, - }; - let result = duration.to_temporal_string(options).unwrap(); - assert_eq!(&result, "PT0S"); - - let options = ToStringRoundingOptions { - precision: Precision::Digit(0), - smallest_unit: None, - rounding_mode: None, - }; - let result = duration.to_temporal_string(options).unwrap(); - assert_eq!(&result, "PT0S"); - - let options = ToStringRoundingOptions { - precision: Precision::Digit(1), - smallest_unit: None, - rounding_mode: None, - }; - let result = duration.to_temporal_string(options).unwrap(); - assert_eq!(&result, "PT0.0S"); - - let options = ToStringRoundingOptions { - precision: Precision::Digit(3), - smallest_unit: None, - rounding_mode: None, - }; - let result = duration.to_temporal_string(options).unwrap(); - assert_eq!(&result, "PT0.000S"); -} - -#[test] -fn duration_to_string_auto_precision() { - let duration = Duration::new( - 1.into(), - 2.into(), - 3.into(), - 4.into(), - 5.into(), - 6.into(), - 7.into(), - FiniteF64::default(), - FiniteF64::default(), - FiniteF64::default(), - ) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - assert_eq!(&result, "P1Y2M3W4DT5H6M7S"); - - let duration = Duration::new( - 1.into(), - 2.into(), - 3.into(), - 4.into(), - 5.into(), - 6.into(), - 7.into(), - 987.into(), - 650.into(), - FiniteF64::default(), - ) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - assert_eq!(&result, "P1Y2M3W4DT5H6M7.98765S"); -} - -#[test] -fn empty_date_duration() { - let duration = Duration::from_partial_duration(PartialDuration { - hours: Some(1.into()), - ..Default::default() - }) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - assert_eq!(&result, "PT1H"); -} - -#[test] -fn negative_fields_to_string() { - let duration = Duration::from_partial_duration(PartialDuration { - years: Some(FiniteF64::from(-1)), - months: Some(FiniteF64::from(-1)), - weeks: Some(FiniteF64::from(-1)), - days: Some(FiniteF64::from(-1)), - hours: Some(FiniteF64::from(-1)), - minutes: Some(FiniteF64::from(-1)), - seconds: Some(FiniteF64::from(-1)), - milliseconds: Some(FiniteF64::from(-1)), - microseconds: Some(FiniteF64::from(-1)), - nanoseconds: Some(FiniteF64::from(-1)), - }) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - assert_eq!(&result, "-P1Y1M1W1DT1H1M1.001001001S"); - - let duration = Duration::from_partial_duration(PartialDuration { - milliseconds: Some(FiniteF64::from(-250)), - ..Default::default() - }) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - assert_eq!(&result, "-PT0.25S"); - - let duration = Duration::from_partial_duration(PartialDuration { - milliseconds: Some(FiniteF64::from(-3500)), - ..Default::default() - }) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - assert_eq!(&result, "-PT3.5S"); - - let duration = Duration::from_partial_duration(PartialDuration { - milliseconds: Some(FiniteF64::from(-3500)), - ..Default::default() - }) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - assert_eq!(&result, "-PT3.5S"); - - let duration = Duration::from_partial_duration(PartialDuration { - weeks: Some(FiniteF64::from(-1)), - days: Some(FiniteF64::from(-1)), - ..Default::default() - }) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - - assert_eq!(&result, "-P1W1D"); -} - -#[test] -fn preserve_precision_loss() { - const MAX_SAFE_INT: f64 = 9_007_199_254_740_991.0; - let duration = Duration::from_partial_duration(PartialDuration { - milliseconds: Some(FiniteF64::try_from(MAX_SAFE_INT).unwrap()), - microseconds: Some(FiniteF64::try_from(MAX_SAFE_INT).unwrap()), - ..Default::default() - }) - .unwrap(); - let result = duration - .to_temporal_string(ToStringRoundingOptions::default()) - .unwrap(); - - assert_eq!(&result, "PT9016206453995.731991S"); -} diff --git a/src/builtins/native/instant.rs b/src/builtins/native/instant.rs new file mode 100644 index 000000000..5a849f0ef --- /dev/null +++ b/src/builtins/native/instant.rs @@ -0,0 +1,110 @@ +use crate::{ + builtins::core as temporal_core, + options::{DifferenceSettings, RoundingOptions, ToStringRoundingOptions}, + time::EpochNanoseconds, + TemporalError, TemporalResult, TimeZone, +}; +use alloc::string::String; + +use super::{duration::Duration, timezone::TZ_PROVIDER, TimeDuration}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Instant(temporal_core::Instant); + +impl From for Instant { + fn from(value: temporal_core::Instant) -> Self { + Self(value) + } +} + +impl core::fmt::Display for Instant { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str( + &self + .as_ixdtf_string(None, ToStringRoundingOptions::default()) + .expect("A valid instant string."), + ) + } +} + +impl Instant { + /// Create a new validated `Instant`. + #[inline] + pub fn try_new(nanoseconds: i128) -> TemporalResult { + Ok(temporal_core::Instant::from(EpochNanoseconds::try_from(nanoseconds)?).into()) + } + + pub fn from_epoch_milliseconds(epoch_milliseconds: i128) -> TemporalResult { + temporal_core::Instant::from_epoch_milliseconds(epoch_milliseconds).map(Into::into) + } + + /// Adds a `Duration` to the current `Instant`, returning an error if the `Duration` + /// contains a `DateDuration`. + #[inline] + pub fn add(&self, duration: Duration) -> TemporalResult { + self.0.add(duration.0).map(Into::into) + } + + /// Adds a `TimeDuration` to `Instant`. + #[inline] + pub fn add_time_duration(&self, duration: &TimeDuration) -> TemporalResult { + self.0.add_time_duration(duration).map(Into::into) + } + + /// Subtract a `Duration` to the current `Instant`, returning an error if the `Duration` + /// contains a `DateDuration`. + #[inline] + pub fn subtract(&self, duration: Duration) -> TemporalResult { + self.0.subtract(duration.0).map(Into::into) + } + + /// Subtracts a `TimeDuration` to `Instant`. + #[inline] + pub fn subtract_time_duration(&self, duration: &TimeDuration) -> TemporalResult { + self.0.subtract_time_duration(duration).map(Into::into) + } + + /// Returns a `TimeDuration` representing the duration since provided `Instant` + #[inline] + pub fn since(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.since(&other.0, settings).map(Into::into) + } + + /// Returns a `TimeDuration` representing the duration until provided `Instant` + #[inline] + pub fn until(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.until(&other.0, settings).map(Into::into) + } + + /// Returns an `Instant` by rounding the current `Instant` according to the provided settings. + pub fn round(&self, options: RoundingOptions) -> TemporalResult { + self.0.round(options).map(Into::into) + } + + /// Returns the `epochMilliseconds` value for this `Instant`. + #[must_use] + pub fn epoch_milliseconds(&self) -> i64 { + self.0.epoch_milliseconds() + } + + /// Returns the `epochNanoseconds` value for this `Instant`. + #[must_use] + pub fn epoch_nanoseconds(&self) -> i128 { + self.0.epoch_nanoseconds() + } + + /// Returns the RFC9557 (IXDTF) string for this `Instant` with the + /// provided options + pub fn as_ixdtf_string( + &self, + timezone: Option<&TimeZone>, + options: ToStringRoundingOptions, + ) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + + self.0 + .as_ixdtf_string_with_provider(timezone, options, &*provider) + } +} diff --git a/src/builtins/native/mod.rs b/src/builtins/native/mod.rs new file mode 100644 index 000000000..331aadb96 --- /dev/null +++ b/src/builtins/native/mod.rs @@ -0,0 +1,32 @@ +//! This module implements native Rust wrappers for the Temporal builtins. + +pub(crate) mod timezone; + +mod date; +mod datetime; +mod duration; +mod instant; +mod now; +pub(crate) mod options; +mod time; +mod zoneddatetime; + +#[doc(inline)] +pub use date::PlainDate; +#[doc(inline)] +pub use datetime::PlainDateTime; +#[doc(inline)] +pub use duration::Duration; +#[doc(inline)] +pub use instant::Instant; +#[doc(inline)] +pub use now::Now; +#[doc(inline)] +pub use time::PlainTime; +#[doc(inline)] +pub use zoneddatetime::ZonedDateTime; + +pub use crate::builtins::core::{ + calendar, DateDuration, PartialDate, PartialDateTime, PartialTime, PartialZonedDateTime, + PlainMonthDay, PlainYearMonth, TimeDuration, +}; diff --git a/src/builtins/native/now.rs b/src/builtins/native/now.rs new file mode 100644 index 000000000..cf76c0128 --- /dev/null +++ b/src/builtins/native/now.rs @@ -0,0 +1,48 @@ +use crate::builtins::native::{PlainDate, PlainDateTime, PlainTime, ZonedDateTime}; +use crate::{builtins::core, Instant, TemporalError, TemporalResult, TimeZone}; +use alloc::string::String; + +use super::timezone::TZ_PROVIDER; + +pub struct Now; + +impl Now { + /// Returns the current instant + pub fn instant() -> TemporalResult { + core::Now::instant().map(Into::into) + } + + /// Returns the current time zone. + pub fn time_zone_id() -> TemporalResult { + core::Now::time_zone_id() + } + + /// Returns the current system time as a `ZonedDateTime` with an ISO8601 calendar. + /// + /// The time zone will be set to either the `TimeZone` if a value is provided, or + /// according to the system timezone if no value is provided. + pub fn zoneddatetime_iso(timezone: Option) -> TemporalResult { + core::Now::zoneddatetime_iso(timezone).map(Into::into) + } + + pub fn plain_datetime_iso(timezone: Option) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + core::Now::plain_datetime_iso_with_provider(timezone, &*provider).map(Into::into) + } + + pub fn plain_date_iso(timezone: Option) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + core::Now::plain_date_iso_with_provider(timezone, &*provider).map(Into::into) + } + + pub fn plain_time_iso(timezone: Option) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + core::Now::plain_time_iso_with_provider(timezone, &*provider).map(Into::into) + } +} diff --git a/src/builtins/native/options.rs b/src/builtins/native/options.rs new file mode 100644 index 000000000..699fc34fc --- /dev/null +++ b/src/builtins/native/options.rs @@ -0,0 +1,52 @@ +use crate::builtins::native::timezone::TZ_PROVIDER; +use crate::TemporalError; +use crate::{builtins::core, TemporalResult}; + +use super::{date::PlainDate, ZonedDateTime}; + +use core::options::RelativeTo as CoreRelativeTo; + +#[derive(Debug, Clone)] +pub enum RelativeTo { + PlainDate(PlainDate), + ZonedDateTime(ZonedDateTime), +} + +impl From for RelativeTo { + fn from(value: PlainDate) -> Self { + Self::PlainDate(value) + } +} + +impl From for RelativeTo { + fn from(value: ZonedDateTime) -> Self { + Self::ZonedDateTime(value) + } +} + +impl From for RelativeTo { + fn from(value: CoreRelativeTo) -> Self { + match value { + CoreRelativeTo::PlainDate(d) => Self::PlainDate(d.into()), + CoreRelativeTo::ZonedDateTime(d) => Self::ZonedDateTime(d.into()), + } + } +} + +impl From for CoreRelativeTo { + fn from(value: RelativeTo) -> Self { + match value { + RelativeTo::PlainDate(d) => CoreRelativeTo::PlainDate(d.0), + RelativeTo::ZonedDateTime(zdt) => CoreRelativeTo::ZonedDateTime(zdt.0), + } + } +} + +impl RelativeTo { + pub fn try_from_str(source: &str) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + core::options::RelativeTo::try_from_str_with_provider(source, &*provider).map(Into::into) + } +} diff --git a/src/builtins/native/time.rs b/src/builtins/native/time.rs new file mode 100644 index 000000000..c71161323 --- /dev/null +++ b/src/builtins/native/time.rs @@ -0,0 +1,233 @@ +use core::fmt::Debug; + +use super::{Duration, PartialTime, TimeDuration}; +use crate::{ + builtins::core as temporal_core, + options::{ + ArithmeticOverflow, DifferenceSettings, TemporalRoundingMode, TemporalUnit, + ToStringRoundingOptions, + }, + TemporalResult, +}; +use alloc::string::String; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct PlainTime(pub(crate) temporal_core::PlainTime); + +impl core::fmt::Display for PlainTime { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +impl From for PlainTime { + fn from(value: temporal_core::PlainTime) -> Self { + Self(value) + } +} + +impl PlainTime { + /// Creates a new `PlainTime`, constraining any field into a valid range. + /// + /// ```rust + /// use temporal_rs::PlainTime; + /// + /// let time = PlainTime::new(23, 59, 59, 999, 999, 999).unwrap(); + /// + /// let constrained_time = PlainTime::new(24, 59, 59, 999, 999, 999).unwrap(); + /// assert_eq!(time, constrained_time); + /// ``` + pub fn new( + hour: u8, + minute: u8, + second: u8, + millisecond: u16, + microsecond: u16, + nanosecond: u16, + ) -> TemporalResult { + temporal_core::PlainTime::new(hour, minute, second, millisecond, microsecond, nanosecond) + .map(Into::into) + } + + /// Creates a new `PlainTime`, rejecting any field that is not in a valid range. + /// + /// ```rust + /// use temporal_rs::PlainTime; + /// + /// let time = PlainTime::try_new(23, 59, 59, 999, 999, 999).unwrap(); + /// + /// let invalid_time = PlainTime::try_new(24, 59, 59, 999, 999, 999); + /// assert!(invalid_time.is_err()); + /// ``` + pub fn try_new( + hour: u8, + minute: u8, + second: u8, + millisecond: u16, + microsecond: u16, + nanosecond: u16, + ) -> TemporalResult { + temporal_core::PlainTime::try_new( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + ) + .map(Into::into) + } + + /// Creates a new `PlainTime` from a `PartialTime`. + /// + /// ```rust + /// use temporal_rs::{partial::PartialTime, PlainTime}; + /// + /// let partial_time = PartialTime { + /// hour: Some(22), + /// ..Default::default() + /// }; + /// + /// let time = PlainTime::from_partial(partial_time, None).unwrap(); + /// + /// assert_eq!(time.hour(), 22); + /// assert_eq!(time.minute(), 0); + /// assert_eq!(time.second(), 0); + /// assert_eq!(time.millisecond(), 0); + /// assert_eq!(time.microsecond(), 0); + /// assert_eq!(time.nanosecond(), 0); + /// + /// ``` + pub fn from_partial( + partial: PartialTime, + overflow: Option, + ) -> TemporalResult { + temporal_core::PlainTime::from_partial(partial, overflow).map(Into::into) + } + + /// Creates a new `PlainTime` using the current `PlainTime` fields as a fallback. + /// + /// ```rust + /// use temporal_rs::{partial::PartialTime, PlainTime}; + /// + /// let partial_time = PartialTime { + /// hour: Some(22), + /// ..Default::default() + /// }; + /// + /// let initial = PlainTime::try_new(15, 30, 12, 123, 456, 789).unwrap(); + /// + /// let time = initial.with(partial_time, None).unwrap(); + /// + /// assert_eq!(time.hour(), 22); + /// assert_eq!(time.minute(), 30); + /// assert_eq!(time.second(), 12); + /// assert_eq!(time.millisecond(), 123); + /// assert_eq!(time.microsecond(), 456); + /// assert_eq!(time.nanosecond(), 789); + /// + /// ``` + pub fn with( + &self, + partial: PartialTime, + overflow: Option, + ) -> TemporalResult { + self.0.with(partial, overflow).map(Into::into) + } + + /// Returns the internal `hour` field. + #[inline] + #[must_use] + pub const fn hour(&self) -> u8 { + self.0.hour() + } + + /// Returns the internal `minute` field. + #[inline] + #[must_use] + pub const fn minute(&self) -> u8 { + self.0.minute() + } + + /// Returns the internal `second` field. + #[inline] + #[must_use] + pub const fn second(&self) -> u8 { + self.0.second() + } + + /// Returns the internal `millisecond` field. + #[inline] + #[must_use] + pub const fn millisecond(&self) -> u16 { + self.0.millisecond() + } + + /// Returns the internal `microsecond` field. + #[inline] + #[must_use] + pub const fn microsecond(&self) -> u16 { + self.0.microsecond() + } + + /// Returns the internal `nanosecond` field. + #[inline] + #[must_use] + pub const fn nanosecond(&self) -> u16 { + self.0.nanosecond() + } + + /// Add a `Duration` to the current `Time`. + pub fn add(&self, duration: &Duration) -> TemporalResult { + self.0.add(&duration.0).map(Into::into) + } + + /// Adds a `TimeDuration` to the current `Time`. + #[inline] + pub fn add_time_duration(&self, duration: &TimeDuration) -> TemporalResult { + self.0.add_time_duration(duration).map(Into::into) + } + + /// Subtract a `Duration` to the current `Time`. + pub fn subtract(&self, duration: &Duration) -> TemporalResult { + self.0.subtract(&duration.0).map(Into::into) + } + + /// Adds a `TimeDuration` to the current `Time`. + #[inline] + pub fn subtract_time_duration(&self, duration: &TimeDuration) -> TemporalResult { + self.0.subtract_time_duration(duration).map(Into::into) + } + + #[inline] + /// Returns the `Duration` until the provided `Time` from the current `Time`. + /// + /// NOTE: `until` assumes the provided other time will occur in the future relative to the current. + pub fn until(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.until(&other.0, settings).map(Into::into) + } + + #[inline] + /// Returns the `Duration` since the provided `Time` from the current `Time`. + /// + /// NOTE: `since` assumes the provided other time is in the past relative to the current. + pub fn since(&self, other: &Self, settings: DifferenceSettings) -> TemporalResult { + self.0.since(&other.0, settings).map(Into::into) + } + + /// Rounds the current `Time` according to provided options. + pub fn round( + &self, + smallest_unit: TemporalUnit, + rounding_increment: Option, + rounding_mode: Option, + ) -> TemporalResult { + self.0 + .round(smallest_unit, rounding_increment, rounding_mode) + .map(Into::into) + } + + pub fn to_ixdtf_string(&self, options: ToStringRoundingOptions) -> TemporalResult { + self.0.as_ixdtf_string(options) + } +} diff --git a/src/builtins/native/timezone.rs b/src/builtins/native/timezone.rs new file mode 100644 index 000000000..f725d9e3b --- /dev/null +++ b/src/builtins/native/timezone.rs @@ -0,0 +1,5 @@ +use crate::tzdb::FsTzdbProvider; +use std::sync::{LazyLock, Mutex}; + +pub static TZ_PROVIDER: LazyLock> = + LazyLock::new(|| Mutex::new(FsTzdbProvider::default())); diff --git a/src/builtins/native/zoneddatetime.rs b/src/builtins/native/zoneddatetime.rs new file mode 100644 index 000000000..7748744dd --- /dev/null +++ b/src/builtins/native/zoneddatetime.rs @@ -0,0 +1,445 @@ +use super::timezone::TZ_PROVIDER; +use crate::{ + builtins::core as temporal_core, + options::{ + ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DisplayOffset, + DisplayTimeZone, OffsetDisambiguation, ToStringRoundingOptions, + }, + Calendar, Duration, PlainDate, PlainDateTime, PlainTime, TemporalError, TemporalResult, + TimeZone, +}; +use alloc::string::String; +use tinystr::TinyAsciiStr; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ZonedDateTime(pub(crate) temporal_core::ZonedDateTime); + +impl core::fmt::Display for ZonedDateTime { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str( + &self + .to_ixdtf_string( + DisplayOffset::Auto, + DisplayTimeZone::Auto, + DisplayCalendar::Auto, + ToStringRoundingOptions::default(), + ) + .expect("A valid ZonedDateTime string with default options."), + ) + } +} + +impl From for ZonedDateTime { + fn from(value: temporal_core::ZonedDateTime) -> Self { + Self(value) + } +} + +impl ZonedDateTime { + #[inline] + pub fn try_new(nanos: i128, calendar: Calendar, tz: TimeZone) -> TemporalResult { + temporal_core::ZonedDateTime::try_new(nanos, calendar, tz).map(Into::into) + } + + pub fn calendar(&self) -> &Calendar { + self.0.calendar() + } + + pub fn timezone(&self) -> &TimeZone { + self.0.timezone() + } +} + +// ===== Experimental TZ_PROVIDER accessor implementations ===== + +impl ZonedDateTime { + pub fn year(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.year_with_provider(&*provider) + } + + pub fn month(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.month_with_provider(&*provider) + } + + pub fn month_code(&self) -> TemporalResult> { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.month_code_with_provider(&*provider) + } + + pub fn day(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.day_with_provider(&*provider) + } + + pub fn hour(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.hour_with_provider(&*provider) + } + + pub fn minute(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.minute_with_provider(&*provider) + } + + pub fn second(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.second_with_provider(&*provider) + } + + pub fn millisecond(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.millisecond_with_provider(&*provider) + } + + pub fn microsecond(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.millisecond_with_provider(&*provider) + } + + pub fn nanosecond(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + + self.0.millisecond_with_provider(&*provider) + } +} + +// ==== Experimental TZ_PROVIDER calendar method implementations ==== + +impl ZonedDateTime { + pub fn era(&self) -> TemporalResult>> { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.era_with_provider(&*provider) + } + + pub fn era_year(&self) -> TemporalResult> { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.era_year_with_provider(&*provider) + } + + /// Returns the calendar day of week value. + pub fn day_of_week(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.day_of_week_with_provider(&*provider) + } + + /// Returns the calendar day of year value. + pub fn day_of_year(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.day_of_year_with_provider(&*provider) + } + + /// Returns the calendar week of year value. + pub fn week_of_year(&self) -> TemporalResult> { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.week_of_year_with_provider(&*provider) + } + + /// Returns the calendar year of week value. + pub fn year_of_week(&self) -> TemporalResult> { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.year_of_week_with_provider(&*provider) + } + + /// Returns the calendar days in week value. + pub fn days_in_week(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.days_in_week_with_provider(&*provider) + } + + /// Returns the calendar days in month value. + pub fn days_in_month(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.days_in_month_with_provider(&*provider) + } + + /// Returns the calendar days in year value. + pub fn days_in_year(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.days_in_year_with_provider(&*provider) + } + + /// Returns the calendar months in year value. + pub fn months_in_year(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.months_in_year_with_provider(&*provider) + } + + /// Returns returns whether the date in a leap year for the given calendar. + pub fn in_leap_year(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.in_leap_year_with_provider(&*provider) + } + + pub fn hours_in_day(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.hours_in_day_with_provider(&*provider) + } +} + +// ==== Experimental TZ_PROVIDER method implementations ==== + +impl ZonedDateTime { + /// Creates a new `ZonedDateTime` from the current `ZonedDateTime` + /// combined with the provided `TimeZone`. + pub fn with_plain_time(&self, time: PlainTime) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .with_plain_time_and_provider(time.0, &*provider) + .map(Into::into) + } + + pub fn add( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .add_with_provider(&duration.0, overflow, &*provider) + .map(Into::into) + } + + pub fn subtract( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .subtract_with_provider(&duration.0, overflow, &*provider) + .map(Into::into) + } + + /// Returns a [`Duration`] representing the period of time from this `ZonedDateTime` since the other `ZonedDateTime`. + pub fn since(&self, other: &Self, options: DifferenceSettings) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .since_with_provider(&other.0, options, &*provider) + .map(Into::into) + } + + /// Returns a [`Duration`] representing the period of time from this `ZonedDateTime` since the other `ZonedDateTime`. + pub fn until(&self, other: &Self, options: DifferenceSettings) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .until_with_provider(&other.0, options, &*provider) + .map(Into::into) + } + + pub fn start_of_day(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .start_of_day_with_provider(&*provider) + .map(Into::into) + } + + /// Creates a new [`PlainDate`] from this `ZonedDateTime`. + pub fn to_plain_date(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .to_plain_date_with_provider(&*provider) + .map(Into::into) + } + + /// Creates a new [`PlainTime`] from this `ZonedDateTime`. + pub fn to_plain_time(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .to_plain_time_with_provider(&*provider) + .map(Into::into) + } + + /// Creates a new [`PlainDateTime`] from this `ZonedDateTime`. + pub fn to_plain_datetime(&self) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0 + .to_plain_datetime_with_provider(&*provider) + .map(Into::into) + } + + /// Returns a RFC9557 (IXDTF) string with the provided options. + pub fn to_ixdtf_string( + &self, + display_offset: DisplayOffset, + display_timezone: DisplayTimeZone, + display_calendar: DisplayCalendar, + options: ToStringRoundingOptions, + ) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + self.0.to_ixdtf_string_with_provider( + display_offset, + display_timezone, + display_calendar, + options, + &*provider, + ) + } + + pub fn from_str( + source: &str, + disambiguation: Disambiguation, + offset_option: OffsetDisambiguation, + ) -> TemporalResult { + let provider = TZ_PROVIDER + .lock() + .map_err(|_| TemporalError::general("Unable to acquire lock"))?; + temporal_core::ZonedDateTime::from_str_with_provider( + source, + disambiguation, + offset_option, + &*provider, + ) + .map(Into::into) + } +} + +mod tests { + #[cfg(not(target_os = "windows"))] + #[test] + fn static_tzdb_zdt_test() { + use super::ZonedDateTime; + use crate::{Calendar, TimeZone}; + use core::str::FromStr; + + let nov_30_2023_utc = 1_701_308_952_000_000_000i128; + + let zdt = ZonedDateTime::try_new( + nov_30_2023_utc, + Calendar::from_str("iso8601").unwrap(), + TimeZone::try_from_str("Z").unwrap(), + ) + .unwrap(); + + assert_eq!(zdt.year().unwrap(), 2023); + assert_eq!(zdt.month().unwrap(), 11); + assert_eq!(zdt.day().unwrap(), 30); + assert_eq!(zdt.hour().unwrap(), 1); + assert_eq!(zdt.minute().unwrap(), 49); + assert_eq!(zdt.second().unwrap(), 12); + + let zdt_minus_five = ZonedDateTime::try_new( + nov_30_2023_utc, + Calendar::from_str("iso8601").unwrap(), + TimeZone::try_from_str("America/New_York").unwrap(), + ) + .unwrap(); + + assert_eq!(zdt_minus_five.year().unwrap(), 2023); + assert_eq!(zdt_minus_five.month().unwrap(), 11); + assert_eq!(zdt_minus_five.day().unwrap(), 29); + assert_eq!(zdt_minus_five.hour().unwrap(), 20); + assert_eq!(zdt_minus_five.minute().unwrap(), 49); + assert_eq!(zdt_minus_five.second().unwrap(), 12); + + let zdt_plus_eleven = ZonedDateTime::try_new( + nov_30_2023_utc, + Calendar::from_str("iso8601").unwrap(), + TimeZone::try_from_str("Australia/Sydney").unwrap(), + ) + .unwrap(); + + assert_eq!(zdt_plus_eleven.year().unwrap(), 2023); + assert_eq!(zdt_plus_eleven.month().unwrap(), 11); + assert_eq!(zdt_plus_eleven.day().unwrap(), 30); + assert_eq!(zdt_plus_eleven.hour().unwrap(), 12); + assert_eq!(zdt_plus_eleven.minute().unwrap(), 49); + assert_eq!(zdt_plus_eleven.second().unwrap(), 12); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn basic_zdt_add() { + use super::ZonedDateTime; + use crate::{Calendar, Duration, TimeZone}; + + let zdt = + ZonedDateTime::try_new(-560174321098766, Calendar::default(), TimeZone::default()) + .unwrap(); + let d = Duration::new( + 0.into(), + 0.into(), + 0.into(), + 0.into(), + 240.into(), + 0.into(), + 0.into(), + 0.into(), + 0.into(), + 800.into(), + ) + .unwrap(); + // "1970-01-04T12:23:45.678902034+00:00[UTC]" + let expected = + ZonedDateTime::try_new(303825678902034, Calendar::default(), TimeZone::default()) + .unwrap(); + + let result = zdt.add(&d, None).unwrap(); + assert_eq!(result, expected); + } +} diff --git a/src/epoch_nanoseconds.rs b/src/epoch_nanoseconds.rs new file mode 100644 index 000000000..063343ab0 --- /dev/null +++ b/src/epoch_nanoseconds.rs @@ -0,0 +1,46 @@ +use num_traits::FromPrimitive; + +use crate::{TemporalError, NS_MAX_INSTANT}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct EpochNanoseconds(pub(crate) i128); + +impl TryFrom for EpochNanoseconds { + type Error = TemporalError; + fn try_from(value: i128) -> Result { + if !is_valid_epoch_nanos(&value) { + return Err(TemporalError::range() + .with_message("Instant nanoseconds are not within a valid epoch range.")); + } + Ok(Self(value)) + } +} + +impl TryFrom for EpochNanoseconds { + type Error = TemporalError; + fn try_from(value: u128) -> Result { + if (NS_MAX_INSTANT as u128) < value { + return Err(TemporalError::range() + .with_message("Instant nanoseconds are not within a valid epoch range.")); + } + Ok(Self(value as i128)) + } +} + +impl TryFrom for EpochNanoseconds { + type Error = TemporalError; + fn try_from(value: f64) -> Result { + let Some(value) = i128::from_f64(value) else { + return Err(TemporalError::range() + .with_message("Instant nanoseconds are not within a valid epoch range.")); + }; + Self::try_from(value) + } +} + +/// Utility for determining if the nanos are within a valid range. +#[inline] +#[must_use] +pub(crate) fn is_valid_epoch_nanos(nanos: &i128) -> bool { + (crate::NS_MIN_INSTANT..=crate::NS_MAX_INSTANT).contains(nanos) +} diff --git a/src/iso.rs b/src/iso.rs index d268b5bf9..e4e53cee7 100644 --- a/src/iso.rs +++ b/src/iso.rs @@ -26,7 +26,7 @@ use alloc::string::ToString; use core::num::NonZeroU128; use crate::{ - components::{ + builtins::core::{ calendar::Calendar, duration::{ normalized::{NormalizedDurationRecord, NormalizedTimeDuration}, diff --git a/src/lib.rs b/src/lib.rs index 2094c49f4..af9d19da9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,8 +51,11 @@ pub mod iso; pub mod options; pub mod parsers; pub mod primitive; +pub mod provider; -pub(crate) mod components; +mod epoch_nanoseconds; + +pub(crate) mod builtins; #[cfg(feature = "now")] mod sys; @@ -83,25 +86,26 @@ pub mod partial { //! //! The partial records are `temporal_rs`'s method of addressing //! `TemporalFields` in the specification. - pub use crate::components::{ - duration::PartialDuration, PartialDate, PartialDateTime, PartialTime, PartialZonedDateTime, + pub use crate::builtins::{ + core::PartialDuration, PartialDate, PartialDateTime, PartialTime, PartialZonedDateTime, }; } // TODO: Potentially bikeshed how `EpochNanoseconds` should be exported. pub mod time { - pub use crate::components::EpochNanoseconds; + pub use crate::epoch_nanoseconds::EpochNanoseconds; } -pub use crate::components::{ - calendar::Calendar, - timezone::{TimeZone, TimeZoneProvider}, - DateDuration, Duration, Instant, PlainDate, PlainDateTime, PlainMonthDay, PlainTime, - PlainYearMonth, TimeDuration, ZonedDateTime, +#[cfg(feature = "full")] +pub use crate::builtins::core as temporal_core; + +pub use crate::builtins::{ + calendar::Calendar, core::timezone::TimeZone, DateDuration, Duration, Instant, PlainDate, + PlainDateTime, PlainMonthDay, PlainTime, PlainYearMonth, TimeDuration, ZonedDateTime, }; #[cfg(feature = "std")] -pub use crate::components::Now; +pub use crate::builtins::Now; /// A library specific trait for unwrapping assertions. pub(crate) trait TemporalUnwrap { diff --git a/src/options/relative_to.rs b/src/options/relative_to.rs index 2f451540b..a2feb9d6b 100644 --- a/src/options/relative_to.rs +++ b/src/options/relative_to.rs @@ -1,135 +1,7 @@ //! RelativeTo rounding option -use alloc::string::String; +#[cfg(not(feature = "full"))] +pub use crate::builtins::core::options::RelativeTo; -use crate::components::{timezone::TimeZoneProvider, zoneddatetime::interpret_isodatetime_offset}; -use crate::iso::{IsoDate, IsoTime}; -use crate::options::{ArithmeticOverflow, Disambiguation, OffsetDisambiguation}; -use crate::parsers::parse_date_time; -use crate::{ - Calendar, PlainDate, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, ZonedDateTime, -}; - -use ixdtf::parsers::records::{TimeZoneRecord, UtcOffsetRecordOrZ}; - -// ==== RelativeTo Object ==== - -#[derive(Debug, Clone)] -pub enum RelativeTo { - PlainDate(PlainDate), - ZonedDateTime(ZonedDateTime), -} - -impl From for RelativeTo { - fn from(value: PlainDate) -> Self { - Self::PlainDate(value) - } -} - -impl From for RelativeTo { - fn from(value: ZonedDateTime) -> Self { - Self::ZonedDateTime(value) - } -} - -impl RelativeTo { - /// Attempts to parse a `ZonedDateTime` string falling back to a `PlainDate` - /// if possible. - /// - /// If the fallback fails or either the `ZonedDateTime` or `PlainDate` - /// is invalid, then an error is returned. - pub fn try_from_str_with_provider( - source: &str, - provider: &impl TimeZoneProvider, - ) -> TemporalResult { - let result = parse_date_time(source)?; - - let Some(annotation) = result.tz else { - let date_record = result.date.temporal_unwrap()?; - - let calendar = result - .calendar - .map(Calendar::from_utf8) - .transpose()? - .unwrap_or_default(); - - return Ok(PlainDate::try_new( - date_record.year, - date_record.month, - date_record.day, - calendar, - )? - .into()); - }; - - let timezone = match annotation.tz { - TimeZoneRecord::Name(s) => { - TimeZone::IanaIdentifier(String::from_utf8_lossy(s).into_owned()) - } - TimeZoneRecord::Offset(offset_record) => { - // NOTE: ixdtf parser restricts minute/second to 0..=60 - let minutes = i16::from(offset_record.hour) * 60 + offset_record.minute as i16; - TimeZone::OffsetMinutes(minutes * i16::from(offset_record.sign as i8)) - } - // TimeZoneRecord is non_exhaustive, but all current branches are matching. - _ => return Err(TemporalError::assert()), - }; - - let (offset_nanos, is_exact) = result - .offset - .map(|record| { - let UtcOffsetRecordOrZ::Offset(offset) = record else { - return (None, true); - }; - let hours_in_ns = i64::from(offset.hour) * 3_600_000_000_000_i64; - let minutes_in_ns = i64::from(offset.minute) * 60_000_000_000_i64; - let seconds_in_ns = i64::from(offset.minute) * 1_000_000_000_i64; - ( - Some( - (hours_in_ns - + minutes_in_ns - + seconds_in_ns - + i64::from(offset.nanosecond)) - * i64::from(offset.sign as i8), - ), - false, - ) - }) - .unwrap_or((None, false)); - - let calendar = result - .calendar - .map(Calendar::from_utf8) - .transpose()? - .unwrap_or_default(); - - let time = result - .time - .map(|time| { - IsoTime::from_components(time.hour, time.minute, time.second, time.nanosecond) - }) - .transpose()?; - - let date = result.date.temporal_unwrap()?; - let iso = IsoDate::new_with_overflow( - date.year, - date.month, - date.day, - ArithmeticOverflow::Constrain, - )?; - - let epoch_ns = interpret_isodatetime_offset( - iso, - time, - is_exact, - offset_nanos, - &timezone, - Disambiguation::Compatible, - OffsetDisambiguation::Reject, - true, - provider, - )?; - - Ok(ZonedDateTime::try_new(epoch_ns.0, calendar, timezone)?.into()) - } -} +#[cfg(feature = "full")] +pub use crate::builtins::native::options::RelativeTo; diff --git a/src/provider.rs b/src/provider.rs new file mode 100644 index 000000000..f1377a779 --- /dev/null +++ b/src/provider.rs @@ -0,0 +1,53 @@ +//! The `TimeZoneProvider` trait. + +use crate::{iso::IsoDateTime, time::EpochNanoseconds, TemporalResult}; +use alloc::vec::Vec; + +/// `TimeZoneOffset` represents the number of seconds to be added to UT in order to determine local time. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TimeZoneOffset { + /// The transition time epoch at which the offset needs to be applied. + pub transition_epoch: Option, + /// The time zone offset in seconds. + pub offset: i64, +} + +// NOTE: It may be a good idea to eventually move this into it's +// own individual crate rather than having it tied directly into `temporal_rs` +/// The `TimeZoneProvider` trait provides methods required for a provider +/// to implement in order to source time zone data from that provider. +pub trait TimeZoneProvider { + fn check_identifier(&self, identifier: &str) -> bool; + + fn get_named_tz_epoch_nanoseconds( + &self, + identifier: &str, + local_datetime: IsoDateTime, + ) -> TemporalResult>; + + fn get_named_tz_offset_nanoseconds( + &self, + identifier: &str, + utc_epoch: i128, + ) -> TemporalResult; +} + +pub struct NeverProvider; + +impl TimeZoneProvider for NeverProvider { + fn check_identifier(&self, _: &str) -> bool { + unimplemented!() + } + + fn get_named_tz_epoch_nanoseconds( + &self, + _: &str, + _: IsoDateTime, + ) -> TemporalResult> { + unimplemented!() + } + + fn get_named_tz_offset_nanoseconds(&self, _: &str, _: i128) -> TemporalResult { + unimplemented!() + } +} diff --git a/src/tzdb.rs b/src/tzdb.rs index eaca7dbe4..00af1fcf9 100644 --- a/src/tzdb.rs +++ b/src/tzdb.rs @@ -47,10 +47,10 @@ use tzif::{ }, }; -use crate::components::timezone::TimeZoneOffset; use crate::{ - components::{timezone::TimeZoneProvider, EpochNanoseconds}, iso::IsoDateTime, + provider::{TimeZoneOffset, TimeZoneProvider}, + time::EpochNanoseconds, utils, TemporalError, TemporalResult, };