From d27555740d071441400d880236554c7b0e18f563 Mon Sep 17 00:00:00 2001 From: Kevin <46825870+nekevss@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:40:31 -0500 Subject: [PATCH] Build out ZonedDateTime, TimeZone, and Instant (#3497) * Build out ZonedDateTime, TimeZone, and Instant * Post rebase fix on zdt --- .../src/builtins/temporal/time_zone/mod.rs | 60 ++-- .../builtins/temporal/zoned_date_time/mod.rs | 12 +- boa_temporal/src/datetime.rs | 58 +++- boa_temporal/src/instant.rs | 94 ++++++ boa_temporal/src/iso.rs | 188 ++++++++++-- boa_temporal/src/lib.rs | 2 + boa_temporal/src/time.rs | 40 ++- boa_temporal/src/tz.rs | 116 ++++++++ boa_temporal/src/zoneddatetime.rs | 270 +++++++++++++++++- 9 files changed, 747 insertions(+), 93 deletions(-) create mode 100644 boa_temporal/src/instant.rs create mode 100644 boa_temporal/src/tz.rs diff --git a/boa_engine/src/builtins/temporal/time_zone/mod.rs b/boa_engine/src/builtins/temporal/time_zone/mod.rs index 6407dc76ec..09a841b3d1 100644 --- a/boa_engine/src/builtins/temporal/time_zone/mod.rs +++ b/boa_engine/src/builtins/temporal/time_zone/mod.rs @@ -15,15 +15,14 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; +use boa_temporal::tz::{TimeZoneSlot, TzProtocol}; /// The `Temporal.TimeZone` object. #[derive(Debug, Clone, Trace, Finalize, JsData)] // SAFETY: `TimeZone` doesn't contain traceable data. #[boa_gc(unsafe_empty_trace)] pub struct TimeZone { - pub(crate) initialized_temporal_time_zone: bool, - pub(crate) identifier: String, - pub(crate) offset_nanoseconds: Option, + slot: TimeZoneSlot, } impl BuiltInObject for TimeZone { @@ -133,14 +132,18 @@ impl BuiltInConstructor for TimeZone { impl TimeZone { // NOTE: id, toJSON, toString currently share the exact same implementation -> Consolidate into one function and define multiple accesors? - pub(crate) fn get_id(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + pub(crate) fn get_id( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { let tz = this .as_object() .and_then(JsObject::downcast_ref::) .ok_or_else(|| { JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") })?; - Ok(JsString::from(tz.identifier.clone()).into()) + Ok(JsString::from(tz.slot.id(context)).into()) } pub(crate) fn get_offset_nanoseconds_for( @@ -158,8 +161,6 @@ impl TimeZone { })?; // 3. Set instant to ? ToTemporalInstant(instant). let _i = args.get_or_undefined(0); - // TODO: to_temporal_instant is abstract operation for Temporal.Instant objects. - // let instant = to_temporal_instant(i)?; // 4. If timeZone.[[OffsetNanoseconds]] is not undefined, return 𝔽(timeZone.[[OffsetNanoseconds]]). // 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])). @@ -242,7 +243,11 @@ impl TimeZone { .into()) } - pub(crate) fn to_string(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { // 1. Let timeZone be the this value. // 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]). let tz = this @@ -252,7 +257,7 @@ impl TimeZone { JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") })?; // 3. Return timeZone.[[Identifier]]. - Ok(JsString::from(tz.identifier.clone()).into()) + Ok(JsString::from(tz.slot.id(context)).into()) } } @@ -311,39 +316,10 @@ pub(super) fn create_temporal_time_zone( let prototype = get_prototype_from_constructor(&new_target, StandardConstructors::time_zone, context)?; - // 3. Let offsetNanosecondsResult be Completion(ParseTimeZoneOffsetString(identifier)). - let offset_nanoseconds_result = parse_timezone_offset_string(&identifier, context); - - // 4. If offsetNanosecondsResult is an abrupt completion, then - let (identifier, offset_nanoseconds) = if let Ok(offset_nanoseconds) = offset_nanoseconds_result - { - // Switched conditions for more idiomatic rust code structuring - // 5. Else, - // a. Set object.[[Identifier]] to ! FormatTimeZoneOffsetString(offsetNanosecondsResult.[[Value]]). - // b. Set object.[[OffsetNanoseconds]] to offsetNanosecondsResult.[[Value]]. - ( - format_time_zone_offset_string(offset_nanoseconds), - Some(offset_nanoseconds), - ) - } else { - // a. Assert: ! CanonicalizeTimeZoneName(identifier) is identifier. - assert_eq!(canonicalize_time_zone_name(&identifier), identifier); - - // b. Set object.[[Identifier]] to identifier. - // c. Set object.[[OffsetNanoseconds]] to undefined. - (identifier, None) - }; - - // 6. Return object. - let object = JsObject::from_proto_and_data( - prototype, - TimeZone { - initialized_temporal_time_zone: false, - identifier, - offset_nanoseconds, - }, - ); - Ok(object.into()) + // TODO: Migrate ISO8601 parsing to `boa_temporal` + Err(JsNativeError::error() + .with_message("not yet implemented.") + .into()) } /// Abstract operation `ParseTimeZoneOffsetString ( offsetString )` diff --git a/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs b/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs index 27d54787ce..ad097fbb79 100644 --- a/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs +++ b/boa_engine/src/builtins/temporal/zoned_date_time/mod.rs @@ -9,14 +9,16 @@ use crate::{ }; use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; -use boa_temporal::duration::Duration as TemporalDuration; +use boa_temporal::{ + duration::Duration as TemporalDuration, zoneddatetime::ZonedDateTime as InnerZdt, +}; /// The `Temporal.ZonedDateTime` object. -#[derive(Debug, Clone, Trace, Finalize, JsData)] +#[derive(Debug, Clone, Finalize, Trace, JsData)] +// SAFETY: ZonedDateTime does not contain any traceable types. +#[boa_gc(unsafe_empty_trace)] pub struct ZonedDateTime { - nanoseconds: JsBigInt, - time_zone: JsObject, - calendar: JsObject, + inner: InnerZdt, } impl BuiltInObject for ZonedDateTime { diff --git a/boa_temporal/src/datetime.rs b/boa_temporal/src/datetime.rs index 02c9d63669..8aab3e84ed 100644 --- a/boa_temporal/src/datetime.rs +++ b/boa_temporal/src/datetime.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use crate::{ calendar::CalendarSlot, + instant::Instant, iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime}, options::ArithmeticOverflow, parser::parse_date_time, @@ -36,6 +37,17 @@ impl DateTime { fn validate_iso(iso: IsoDate) -> bool { IsoDateTime::new_unchecked(iso, IsoTime::noon()).is_within_limits() } + + /// Create a new `DateTime` from an `Instant`. + #[inline] + pub(crate) fn from_instant( + instant: &Instant, + offset: f64, + calendar: CalendarSlot, + ) -> TemporalResult { + let iso = IsoDateTime::from_epoch_nanos(&instant.nanos, offset)?; + Ok(Self { iso, calendar }) + } } // ==== Public DateTime API ==== @@ -79,14 +91,56 @@ impl DateTime { #[inline] #[must_use] pub fn iso_date(&self) -> IsoDate { - self.iso.iso_date() + self.iso.date() } /// Returns the inner `IsoTime` value. #[inline] #[must_use] pub fn iso_time(&self) -> IsoTime { - self.iso.iso_time() + self.iso.time() + } + + /// Returns the hour value + #[inline] + #[must_use] + pub fn hours(&self) -> u8 { + self.iso.time().hour + } + + /// Returns the minute value + #[inline] + #[must_use] + pub fn minutes(&self) -> u8 { + self.iso.time().minute + } + + /// Returns the second value + #[inline] + #[must_use] + pub fn seconds(&self) -> u8 { + self.iso.time().second + } + + /// Returns the `millisecond` value + #[inline] + #[must_use] + pub fn milliseconds(&self) -> u16 { + self.iso.time().millisecond + } + + /// Returns the `microsecond` value + #[inline] + #[must_use] + pub fn microseconds(&self) -> u16 { + self.iso.time().microsecond + } + + /// Returns the `nanosecond` value + #[inline] + #[must_use] + pub fn nanoseconds(&self) -> u16 { + self.iso.time().nanosecond } /// Returns the Calendar value. diff --git a/boa_temporal/src/instant.rs b/boa_temporal/src/instant.rs new file mode 100644 index 0000000000..82f645aa18 --- /dev/null +++ b/boa_temporal/src/instant.rs @@ -0,0 +1,94 @@ +//! An implementation of the Temporal Instant. + +use crate::{TemporalError, TemporalResult}; + +use num_bigint::BigInt; +use num_traits::ToPrimitive; + +/// A Temporal Instant +#[derive(Debug, Clone)] +pub struct Instant { + pub(crate) nanos: BigInt, +} + +// ==== Public API ==== + +impl Instant { + /// Create a new validated `Instant`. + #[inline] + pub fn new(nanos: BigInt) -> TemporalResult { + if !is_valid_epoch_nanos(&nanos) { + return Err(TemporalError::range() + .with_message("Instant nanoseconds are not within a valid epoch range.")); + } + Ok(Self { nanos }) + } + + /// Returns the `epochSeconds` value for this `Instant`. + #[must_use] + pub fn epoch_seconds(&self) -> f64 { + (&self.nanos / BigInt::from(1_000_000_000)) + .to_f64() + .expect("A validated Instant should be within a valid f64") + .floor() + } + + /// Returns the `epochMilliseconds` value for this `Instant`. + #[must_use] + pub fn epoch_milliseconds(&self) -> f64 { + (&self.nanos / BigInt::from(1_000_000)) + .to_f64() + .expect("A validated Instant should be within a valid f64") + .floor() + } + + /// Returns the `epochMicroseconds` value for this `Instant`. + #[must_use] + pub fn epoch_microseconds(&self) -> f64 { + (&self.nanos / BigInt::from(1_000)) + .to_f64() + .expect("A validated Instant should be within a valid f64") + .floor() + } + + /// Returns the `epochNanoseconds` value for this `Instant`. + #[must_use] + pub fn epoch_nanoseconds(&self) -> f64 { + self.nanos + .to_f64() + .expect("A validated Instant should be within a valid f64") + } +} + +/// Utility for determining if the nanos are within a valid range. +#[inline] +#[must_use] +pub(crate) fn is_valid_epoch_nanos(nanos: &BigInt) -> bool { + nanos <= &BigInt::from(crate::NS_MAX_INSTANT) && nanos >= &BigInt::from(crate::NS_MIN_INSTANT) +} + +#[cfg(test)] +mod tests { + use crate::{instant::Instant, NS_MAX_INSTANT, NS_MIN_INSTANT}; + use num_bigint::BigInt; + use num_traits::ToPrimitive; + + #[test] + #[allow(clippy::float_cmp)] + fn max_and_minimum_instant_bounds() { + // This test is primarily to assert that the `expect` in the epoch methods is valid. + let max = BigInt::from(NS_MAX_INSTANT); + let min = BigInt::from(NS_MIN_INSTANT); + let max_instant = Instant::new(max.clone()).unwrap(); + let min_instant = Instant::new(min.clone()).unwrap(); + + assert_eq!(max_instant.epoch_nanoseconds(), max.to_f64().unwrap()); + assert_eq!(min_instant.epoch_nanoseconds(), min.to_f64().unwrap()); + + let max_plus_one = BigInt::from(NS_MAX_INSTANT + 1); + let min_minus_one = BigInt::from(NS_MIN_INSTANT - 1); + + assert!(Instant::new(max_plus_one).is_err()); + assert!(Instant::new(min_minus_one).is_err()); + } +} diff --git a/boa_temporal/src/iso.rs b/boa_temporal/src/iso.rs index 32760bd176..73813677c8 100644 --- a/boa_temporal/src/iso.rs +++ b/boa_temporal/src/iso.rs @@ -16,7 +16,7 @@ use crate::{ }; use icu_calendar::{Date as IcuDate, Iso}; use num_bigint::BigInt; -use num_traits::cast::FromPrimitive; +use num_traits::{cast::FromPrimitive, ToPrimitive}; /// `IsoDateTime` is the Temporal internal representation of /// a `DateTime` record @@ -32,6 +32,73 @@ impl IsoDateTime { Self { date, time } } + // NOTE: The below assumes that nanos is from an `Instant` and thus in a valid range. -> Needs validation. + /// Creates an `IsoDateTime` from a `BigInt` of epochNanoseconds. + pub(crate) fn from_epoch_nanos(nanos: &BigInt, offset: f64) -> TemporalResult { + // Skip the assert as nanos should be validated by Instant. + // TODO: Determine whether value needs to be validated as integral. + // Get the component ISO parts + let mathematical_nanos = nanos.to_f64().ok_or_else(|| { + TemporalError::range().with_message("nanos was not within a valid range.") + })?; + + // 2. Let remainderNs be epochNanoseconds modulo 10^6. + let remainder_nanos = mathematical_nanos % 1_000_000f64; + + // 3. Let epochMilliseconds be 𝔽((epochNanoseconds - remainderNs) / 10^6). + let epoch_millis = ((mathematical_nanos - remainder_nanos) / 1_000_000f64).floor(); + + let year = utils::epoch_time_to_epoch_year(epoch_millis); + let month = utils::epoch_time_to_month_in_year(epoch_millis) + 1; + let day = utils::epoch_time_to_date(epoch_millis); + + // 7. Let hour be ℝ(! HourFromTime(epochMilliseconds)). + let hour = (epoch_millis / 3_600_000f64).floor() % 24f64; + // 8. Let minute be ℝ(! MinFromTime(epochMilliseconds)). + let minute = (epoch_millis / 60_000f64).floor() % 60f64; + // 9. Let second be ℝ(! SecFromTime(epochMilliseconds)). + let second = (epoch_millis / 1000f64).floor() % 60f64; + // 10. Let millisecond be ℝ(! msFromTime(epochMilliseconds)). + let millis = (epoch_millis % 1000f64).floor() % 1000f64; + + // 11. Let microsecond be floor(remainderNs / 1000). + let micros = (remainder_nanos / 1000f64).floor(); + // 12. Assert: microsecond < 1000. + debug_assert!(micros < 1000f64); + // 13. Let nanosecond be remainderNs modulo 1000. + let nanos = (remainder_nanos % 1000f64).floor(); + + Ok(Self::balance( + year, + i32::from(month), + i32::from(day), + hour, + minute, + second, + millis, + micros, + nanos + offset, + )) + } + + #[allow(clippy::too_many_arguments)] + fn balance( + year: i32, + month: i32, + day: i32, + hour: f64, + minute: f64, + second: f64, + millisecond: f64, + microsecond: f64, + nanosecond: f64, + ) -> Self { + let (overflow_day, time) = + IsoTime::balance(hour, minute, second, millisecond, microsecond, nanosecond); + let date = IsoDate::balance(year, month, day + overflow_day); + Self::new_unchecked(date, time) + } + /// Returns whether the `IsoDateTime` is within valid limits. pub(crate) fn is_within_limits(&self) -> bool { let Some(ns) = self.to_utc_epoch_nanoseconds(0f64) else { @@ -58,11 +125,11 @@ impl IsoDateTime { BigInt::from_f64(epoch_nanos - offset) } - pub(crate) fn iso_date(&self) -> IsoDate { + pub(crate) fn date(&self) -> IsoDate { self.date } - pub(crate) fn iso_time(&self) -> IsoTime { + pub(crate) fn time(&self) -> IsoTime { self.time } } @@ -106,6 +173,7 @@ impl IsoDate { let m = month.clamp(1, 12); let days_in_month = utils::iso_days_in_month(year, month); let d = day.clamp(1, days_in_month); + // NOTE: Values are clamped in a u8 range. Ok(Self::new_unchecked(year, m as u8, d as u8)) } ArithmeticOverflow::Reject => { @@ -209,23 +277,23 @@ impl IsoDate { /// time slots. #[derive(Debug, Default, Clone, Copy)] pub struct IsoTime { - pub(crate) hour: i32, // 0..=23 - pub(crate) minute: i32, // 0..=59 - pub(crate) second: i32, // 0..=59 - pub(crate) millisecond: i32, // 0..=999 - pub(crate) microsecond: i32, // 0..=999 - pub(crate) nanosecond: i32, // 0..=999 + pub(crate) hour: u8, // 0..=23 + pub(crate) minute: u8, // 0..=59 + pub(crate) second: u8, // 0..=59 + pub(crate) millisecond: u16, // 0..=999 + pub(crate) microsecond: u16, // 0..=999 + pub(crate) nanosecond: u16, // 0..=999 } impl IsoTime { /// Creates a new `IsoTime` without any validation. pub(crate) fn new_unchecked( - hour: i32, - minute: i32, - second: i32, - millisecond: i32, - microsecond: i32, - nanosecond: i32, + hour: u8, + minute: u8, + second: u8, + millisecond: u16, + microsecond: u16, + nanosecond: u16, ) -> Self { Self { hour, @@ -249,22 +317,26 @@ impl IsoTime { ) -> TemporalResult { match overflow { ArithmeticOverflow::Constrain => { - let h = hour.clamp(0, 23); - let min = minute.clamp(0, 59); - let sec = second.clamp(0, 59); - let milli = millisecond.clamp(0, 999); - let micro = microsecond.clamp(0, 999); - let nano = nanosecond.clamp(0, 999); + let h = hour.clamp(0, 23) as u8; + let min = minute.clamp(0, 59) as u8; + let sec = second.clamp(0, 59) as u8; + let milli = millisecond.clamp(0, 999) as u16; + let micro = microsecond.clamp(0, 999) as u16; + let nano = nanosecond.clamp(0, 999) as u16; Ok(Self::new_unchecked(h, min, sec, milli, micro, nano)) } ArithmeticOverflow::Reject => { - // TODO: Invert structure validation and update fields to u16. - let time = - Self::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond); - if !time.is_valid() { + if !is_valid_time(hour, minute, second, millisecond, microsecond, nanosecond) { return Err(TemporalError::range().with_message("IsoTime is not valid")); - } - Ok(time) + }; + Ok(Self::new_unchecked( + hour as u8, + minute as u8, + second as u8, + millisecond as u16, + microsecond as u16, + nanosecond as u16, + )) } } } @@ -303,6 +375,53 @@ impl IsoTime { ) } + // NOTE(nekevss): f64 is needed here as values could exceed i32 when input. + /// Balances and creates a new `IsoTime` with `day` overflow from the provided values. + pub(crate) fn balance( + hour: f64, + minute: f64, + second: f64, + millisecond: f64, + microsecond: f64, + nanosecond: f64, + ) -> (i32, Self) { + // 1. Set microsecond to microsecond + floor(nanosecond / 1000). + let mut mis = microsecond + (nanosecond / 1000f64).floor(); + // 2. Set nanosecond to nanosecond modulo 1000. + let ns = nanosecond % 1000f64; + // 3. Set millisecond to millisecond + floor(microsecond / 1000). + let mut ms = millisecond + (mis / 1000f64).floor(); + // 4. Set microsecond to microsecond modulo 1000. + mis = mis.rem_euclid(1000f64); + // 5. Set second to second + floor(millisecond / 1000). + let mut secs = second + (ms / 1000f64).floor(); + // 6. Set millisecond to millisecond modulo 1000. + ms = ms.rem_euclid(1000f64); + // 7. Set minute to minute + floor(second / 60). + let mut minutes = minute + (secs / 60f64).floor(); + // 8. Set second to second modulo 60. + secs = secs.rem_euclid(60f64); + // 9. Set hour to hour + floor(minute / 60). + let mut hours = hour + (minutes / 60f64).floor(); + // 10. Set minute to minute modulo 60. + minutes = minutes.rem_euclid(60f64); + // 11. Let days be floor(hour / 24). + let days = (hours / 24f64).floor(); + // 12. Set hour to hour modulo 24. + hours = hours.rem_euclid(24f64); + + let time = Self::new_unchecked( + hours as u8, + minutes as u8, + secs as u8, + ms as u16, + mis as u16, + ns as u16, + ); + + (days as i32, time) + } + /// Checks if the time is a valid `IsoTime` pub(crate) fn is_valid(&self) -> bool { if !(0..=23).contains(&self.hour) { @@ -361,3 +480,18 @@ fn is_valid_date(year: i32, month: i32, day: i32) -> bool { let days_in_month = utils::iso_days_in_month(year, month); (1..=days_in_month).contains(&day) } + +#[inline] +fn is_valid_time(hour: i32, minute: i32, second: i32, ms: i32, mis: i32, ns: i32) -> bool { + if !(0..=23).contains(&hour) { + return false; + } + + let min_sec = 0..=59; + if !min_sec.contains(&minute) || !min_sec.contains(&second) { + return false; + } + + let sub_second = 0..=999; + sub_second.contains(&ms) && sub_second.contains(&mis) && sub_second.contains(&ns) +} diff --git a/boa_temporal/src/lib.rs b/boa_temporal/src/lib.rs index eec77841d9..d9dac9b4cb 100644 --- a/boa_temporal/src/lib.rs +++ b/boa_temporal/src/lib.rs @@ -33,11 +33,13 @@ pub mod datetime; pub mod duration; pub mod error; pub mod fields; +pub mod instant; pub mod iso; pub mod month_day; pub mod options; pub mod parser; pub mod time; +pub mod tz; pub(crate) mod utils; pub mod year_month; pub mod zoneddatetime; diff --git a/boa_temporal/src/time.rs b/boa_temporal/src/time.rs index f618c3ed0b..6c1fb3a9d7 100644 --- a/boa_temporal/src/time.rs +++ b/boa_temporal/src/time.rs @@ -1,6 +1,6 @@ //! Temporal Time Representation. -use crate::iso::IsoTime; +use crate::{iso::IsoTime, options::ArithmeticOverflow, TemporalResult}; /// The Temporal `PlainTime` object. #[derive(Debug, Default, Clone, Copy)] @@ -12,23 +12,39 @@ pub struct Time { // ==== Private API ==== impl Time { + #[inline] + #[must_use] + pub(crate) fn new_unchecked(iso: IsoTime) -> Self { + Self { iso } + } + + /// Returns true if a valid `Time`. #[allow(dead_code)] - pub(crate) fn new_unchecked( + pub(crate) fn is_valid(&self) -> bool { + self.iso.is_valid() + } +} + +impl Time { + /// Creates a new `IsoTime` value. + pub fn new( hour: i32, minute: i32, second: i32, millisecond: i32, microsecond: i32, nanosecond: i32, - ) -> Self { - Self { - iso: IsoTime::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond), - } - } - - /// Returns true if a valid `Time`. - #[allow(dead_code)] - pub(crate) fn is_valid(&self) -> bool { - self.iso.is_valid() + overflow: ArithmeticOverflow, + ) -> TemporalResult { + let time = IsoTime::new( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + overflow, + )?; + Ok(Self::new_unchecked(time)) } } diff --git a/boa_temporal/src/tz.rs b/boa_temporal/src/tz.rs new file mode 100644 index 0000000000..701f268ff0 --- /dev/null +++ b/boa_temporal/src/tz.rs @@ -0,0 +1,116 @@ +//! This module implements the Temporal `TimeZone` and components. + +use std::any::Any; + +use num_bigint::BigInt; +use num_traits::ToPrimitive; + +use crate::{ + calendar::CalendarSlot, datetime::DateTime, instant::Instant, TemporalError, TemporalResult, +}; + +/// Any object that implements the `TzProtocol` must implement the below methods/properties. +pub const TIME_ZONE_PROPERTIES: [&str; 3] = + ["getOffsetNanosecondsFor", "getPossibleInstantsFor", "id"]; + +/// A clonable `TzProtocol` +pub trait TzProtocolClone { + /// Clones the current `TimeZoneProtocol`. + fn clone_box(&self) -> Box; +} + +impl

TzProtocolClone for P +where + P: 'static + TzProtocol + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// The Time Zone Protocol that must be implemented for time zones. +pub trait TzProtocol: TzProtocolClone { + /// Get the Offset nanoseconds for this `TimeZone` + fn get_offset_nanos_for(&self, context: &mut dyn Any) -> TemporalResult; + /// Get the possible Instant for this `TimeZone` + fn get_possible_instant_for(&self, context: &mut dyn Any) -> TemporalResult>; // TODO: Implement Instant + /// Get the `TimeZone`'s identifier. + fn id(&self, context: &mut dyn Any) -> String; +} + +/// A Temporal `TimeZone`. +#[derive(Debug, Clone)] +#[allow(unused)] +pub struct TimeZone { + pub(crate) iana: Option, // TODO: ICU4X IANA TimeZone support. + pub(crate) offset: Option, +} + +/// The `TimeZoneSlot` represents a `[[TimeZone]]` internal slot value. +pub enum TimeZoneSlot { + /// A native `TimeZone` representation. + Tz(TimeZone), + /// A Custom `TimeZone` that implements the `TzProtocol`. + Protocol(Box), +} + +impl core::fmt::Debug for TimeZoneSlot { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Tz(tz) => write!(f, "{tz:?}"), + Self::Protocol(_) => write!(f, "TzProtocol"), + } + } +} + +impl Clone for TimeZoneSlot { + fn clone(&self) -> Self { + match self { + Self::Tz(tz) => Self::Tz(tz.clone()), + Self::Protocol(p) => Self::Protocol(p.clone_box()), + } + } +} + +impl TimeZoneSlot { + pub(crate) fn get_datetime_for( + &self, + instant: &Instant, + calendar: &CalendarSlot, + context: &mut dyn Any, + ) -> TemporalResult { + let nanos = self.get_offset_nanos_for(context)?; + DateTime::from_instant(instant, nanos.to_f64().unwrap_or(0.0), calendar.clone()) + } +} + +impl TzProtocol for TimeZoneSlot { + fn get_offset_nanos_for(&self, context: &mut dyn Any) -> TemporalResult { + // 1. Let timeZone be the this value. + // 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]). + // 3. Set instant to ? ToTemporalInstant(instant). + match self { + Self::Tz(tz) => { + // 4. If timeZone.[[OffsetMinutes]] is not empty, return 𝔽(timeZone.[[OffsetMinutes]] × (60 × 10^9)). + if let Some(offset) = &tz.offset { + return Ok(BigInt::from(i64::from(*offset) * 60_000_000_000i64)); + } + // 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])). + Err(TemporalError::range().with_message("IANA TimeZone names not yet implemented.")) + } + // Call any custom implemented TimeZone. + Self::Protocol(p) => p.get_offset_nanos_for(context), + } + } + + fn get_possible_instant_for(&self, _context: &mut dyn Any) -> TemporalResult> { + Err(TemporalError::general("Not yet implemented.")) + } + + fn id(&self, context: &mut dyn Any) -> String { + match self { + Self::Tz(_) => todo!("implement tz.to_string"), + Self::Protocol(tz) => tz.id(context), + } + } +} diff --git a/boa_temporal/src/zoneddatetime.rs b/boa_temporal/src/zoneddatetime.rs index 6fd3f2c94a..b55405e142 100644 --- a/boa_temporal/src/zoneddatetime.rs +++ b/boa_temporal/src/zoneddatetime.rs @@ -1,7 +1,267 @@ //! The `ZonedDateTime` module. -// NOTE: Mostly serves as a placeholder currently -// until the rest can be implemented. -/// `TemporalZoneDateTime` -#[derive(Debug, Clone, Copy)] -pub struct ZonedDateTime; +use num_bigint::BigInt; +use tinystr::TinyStr4; + +use crate::{calendar::CalendarSlot, instant::Instant, tz::TimeZoneSlot, TemporalResult}; + +use core::any::Any; + +/// Temporal's `ZonedDateTime` object. +#[derive(Debug, Clone)] +pub struct ZonedDateTime { + instant: Instant, + calendar: CalendarSlot, + tz: TimeZoneSlot, +} + +// ==== Private API ==== + +impl ZonedDateTime { + /// Creates a `ZonedDateTime` without validating the input. + #[inline] + #[must_use] + pub(crate) fn new_unchecked( + instant: Instant, + calendar: CalendarSlot, + tz: TimeZoneSlot, + ) -> Self { + Self { + instant, + calendar, + tz, + } + } +} + +// ==== Public API ==== + +impl ZonedDateTime { + /// Creates a new valid `ZonedDateTime`. + #[inline] + pub fn new(nanos: BigInt, calendar: CalendarSlot, tz: TimeZoneSlot) -> TemporalResult { + let instant = Instant::new(nanos)?; + Ok(Self::new_unchecked(instant, calendar, tz)) + } + + /// Returns the `ZonedDateTime`'s Calendar identifier. + #[inline] + #[must_use] + pub fn calendar_id(&self) -> String { + // TODO: Implement Identifier method on `CalendarSlot` + String::from("Not yet implemented.") + } + + /// Returns the `epochSeconds` value of this `ZonedDateTime`. + #[must_use] + pub fn epoch_seconds(&self) -> f64 { + self.instant.epoch_seconds() + } + + /// Returns the `epochMilliseconds` value of this `ZonedDateTime`. + #[must_use] + pub fn epoch_milliseconds(&self) -> f64 { + self.instant.epoch_milliseconds() + } + + /// Returns the `epochMicroseconds` value of this `ZonedDateTime`. + #[must_use] + pub fn epoch_microseconds(&self) -> f64 { + self.instant.epoch_microseconds() + } + + /// Returns the `epochNanoseconds` value of this `ZonedDateTime`. + #[must_use] + pub fn epoch_nanoseconds(&self) -> f64 { + self.instant.epoch_nanoseconds() + } +} + +// ==== Context based API ==== + +impl ZonedDateTime { + /// Returns the `year` value for this `ZonedDateTime`. + #[inline] + pub fn contextual_year(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + self.calendar + .year(&crate::calendar::CalendarDateLike::DateTime(dt), context) + } + + /// Returns the `year` value for this `ZonedDateTime`. + #[inline] + pub fn year(&self) -> TemporalResult { + self.contextual_year(&mut ()) + } + + /// Returns the `month` value for this `ZonedDateTime`. + pub fn contextual_month(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + self.calendar + .month(&crate::calendar::CalendarDateLike::DateTime(dt), context) + } + + /// Returns the `month` value for this `ZonedDateTime`. + #[inline] + pub fn month(&self) -> TemporalResult { + self.contextual_month(&mut ()) + } + + /// Returns the `monthCode` value for this `ZonedDateTime`. + pub fn contextual_month_code(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + self.calendar + .month_code(&crate::calendar::CalendarDateLike::DateTime(dt), context) + } + + /// Returns the `monthCode` value for this `ZonedDateTime`. + #[inline] + pub fn month_code(&self) -> TemporalResult { + self.contextual_month_code(&mut ()) + } + + /// Returns the `day` value for this `ZonedDateTime`. + pub fn contextual_day(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + self.calendar + .day(&crate::calendar::CalendarDateLike::DateTime(dt), context) + } + + /// Returns the `day` value for this `ZonedDateTime`. + pub fn day(&self) -> TemporalResult { + self.contextual_day(&mut ()) + } + + /// Returns the `hour` value for this `ZonedDateTime`. + pub fn contextual_hour(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + Ok(dt.hours()) + } + + /// Returns the `hour` value for this `ZonedDateTime`. + pub fn hour(&self) -> TemporalResult { + self.contextual_hour(&mut ()) + } + + /// Returns the `minute` value for this `ZonedDateTime`. + pub fn contextual_minute(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + Ok(dt.minutes()) + } + + /// Returns the `minute` value for this `ZonedDateTime`. + pub fn minute(&self) -> TemporalResult { + self.contextual_minute(&mut ()) + } + + /// Returns the `second` value for this `ZonedDateTime`. + pub fn contextual_second(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + Ok(dt.seconds()) + } + + /// Returns the `second` value for this `ZonedDateTime`. + pub fn second(&self) -> TemporalResult { + self.contextual_second(&mut ()) + } + + /// Returns the `millisecond` value for this `ZonedDateTime`. + pub fn contextual_millisecond(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + Ok(dt.milliseconds()) + } + + /// Returns the `millisecond` value for this `ZonedDateTime`. + pub fn millisecond(&self) -> TemporalResult { + self.contextual_millisecond(&mut ()) + } + + /// Returns the `microsecond` value for this `ZonedDateTime`. + pub fn contextual_microsecond(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + Ok(dt.milliseconds()) + } + + /// Returns the `microsecond` value for this `ZonedDateTime`. + pub fn microsecond(&self) -> TemporalResult { + self.contextual_microsecond(&mut ()) + } + + /// Returns the `nanosecond` value for this `ZonedDateTime`. + pub fn contextual_nanosecond(&self, context: &mut dyn Any) -> TemporalResult { + let dt = self + .tz + .get_datetime_for(&self.instant, &self.calendar, context)?; + Ok(dt.nanoseconds()) + } + + /// Returns the `nanosecond` value for this `ZonedDateTime`. + pub fn nanosecond(&self) -> TemporalResult { + self.contextual_nanosecond(&mut ()) + } +} + +#[cfg(test)] +mod tests { + use crate::tz::TimeZone; + use num_bigint::BigInt; + + use super::{CalendarSlot, TimeZoneSlot, ZonedDateTime}; + + #[test] + fn basic_zdt_test() { + let nov_30_2023_utc = BigInt::from(1_701_308_952_000_000_000i64); + + let zdt = ZonedDateTime::new( + nov_30_2023_utc.clone(), + CalendarSlot::Identifier("iso8601".to_owned()), + TimeZoneSlot::Tz(TimeZone { + iana: None, + offset: Some(0), + }), + ) + .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::new( + nov_30_2023_utc, + CalendarSlot::Identifier("iso8601".to_owned()), + TimeZoneSlot::Tz(TimeZone { + iana: None, + offset: Some(-300), + }), + ) + .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); + } +}