Browse Source

Build out ZonedDateTime, TimeZone, and Instant (#3497)

* Build out ZonedDateTime, TimeZone, and Instant

* Post rebase fix on zdt
pull/3507/head
Kevin 12 months ago committed by GitHub
parent
commit
d27555740d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      boa_engine/src/builtins/temporal/time_zone/mod.rs
  2. 12
      boa_engine/src/builtins/temporal/zoned_date_time/mod.rs
  3. 58
      boa_temporal/src/datetime.rs
  4. 94
      boa_temporal/src/instant.rs
  5. 188
      boa_temporal/src/iso.rs
  6. 2
      boa_temporal/src/lib.rs
  7. 40
      boa_temporal/src/time.rs
  8. 116
      boa_temporal/src/tz.rs
  9. 270
      boa_temporal/src/zoneddatetime.rs

60
boa_engine/src/builtins/temporal/time_zone/mod.rs

@ -15,15 +15,14 @@ use crate::{
}; };
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::tz::{TimeZoneSlot, TzProtocol};
/// The `Temporal.TimeZone` object. /// The `Temporal.TimeZone` object.
#[derive(Debug, Clone, Trace, Finalize, JsData)] #[derive(Debug, Clone, Trace, Finalize, JsData)]
// SAFETY: `TimeZone` doesn't contain traceable data. // SAFETY: `TimeZone` doesn't contain traceable data.
#[boa_gc(unsafe_empty_trace)] #[boa_gc(unsafe_empty_trace)]
pub struct TimeZone { pub struct TimeZone {
pub(crate) initialized_temporal_time_zone: bool, slot: TimeZoneSlot,
pub(crate) identifier: String,
pub(crate) offset_nanoseconds: Option<i64>,
} }
impl BuiltInObject for TimeZone { impl BuiltInObject for TimeZone {
@ -133,14 +132,18 @@ impl BuiltInConstructor for TimeZone {
impl TimeZone { impl TimeZone {
// NOTE: id, toJSON, toString currently share the exact same implementation -> Consolidate into one function and define multiple accesors? // 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<JsValue> { pub(crate) fn get_id(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let tz = this let tz = this
.as_object() .as_object()
.and_then(JsObject::downcast_ref::<Self>) .and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| { .ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") 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( pub(crate) fn get_offset_nanoseconds_for(
@ -158,8 +161,6 @@ impl TimeZone {
})?; })?;
// 3. Set instant to ? ToTemporalInstant(instant). // 3. Set instant to ? ToTemporalInstant(instant).
let _i = args.get_or_undefined(0); 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]]). // 4. If timeZone.[[OffsetNanoseconds]] is not undefined, return 𝔽(timeZone.[[OffsetNanoseconds]]).
// 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])). // 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])).
@ -242,7 +243,11 @@ impl TimeZone {
.into()) .into())
} }
pub(crate) fn to_string(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> { pub(crate) fn to_string(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let timeZone be the this value. // 1. Let timeZone be the this value.
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]). // 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
let tz = this let tz = this
@ -252,7 +257,7 @@ impl TimeZone {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") JsNativeError::typ().with_message("this value must be a Temporal.TimeZone")
})?; })?;
// 3. Return timeZone.[[Identifier]]. // 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 = let prototype =
get_prototype_from_constructor(&new_target, StandardConstructors::time_zone, context)?; get_prototype_from_constructor(&new_target, StandardConstructors::time_zone, context)?;
// 3. Let offsetNanosecondsResult be Completion(ParseTimeZoneOffsetString(identifier)). // TODO: Migrate ISO8601 parsing to `boa_temporal`
let offset_nanoseconds_result = parse_timezone_offset_string(&identifier, context); Err(JsNativeError::error()
.with_message("not yet implemented.")
// 4. If offsetNanosecondsResult is an abrupt completion, then .into())
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())
} }
/// Abstract operation `ParseTimeZoneOffsetString ( offsetString )` /// Abstract operation `ParseTimeZoneOffsetString ( offsetString )`

12
boa_engine/src/builtins/temporal/zoned_date_time/mod.rs

@ -9,14 +9,16 @@ use crate::{
}; };
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler; 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. /// 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 { pub struct ZonedDateTime {
nanoseconds: JsBigInt, inner: InnerZdt,
time_zone: JsObject,
calendar: JsObject,
} }
impl BuiltInObject for ZonedDateTime { impl BuiltInObject for ZonedDateTime {

58
boa_temporal/src/datetime.rs

@ -4,6 +4,7 @@ use std::str::FromStr;
use crate::{ use crate::{
calendar::CalendarSlot, calendar::CalendarSlot,
instant::Instant,
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime}, iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime},
options::ArithmeticOverflow, options::ArithmeticOverflow,
parser::parse_date_time, parser::parse_date_time,
@ -36,6 +37,17 @@ impl DateTime {
fn validate_iso(iso: IsoDate) -> bool { fn validate_iso(iso: IsoDate) -> bool {
IsoDateTime::new_unchecked(iso, IsoTime::noon()).is_within_limits() 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<Self> {
let iso = IsoDateTime::from_epoch_nanos(&instant.nanos, offset)?;
Ok(Self { iso, calendar })
}
} }
// ==== Public DateTime API ==== // ==== Public DateTime API ====
@ -79,14 +91,56 @@ impl DateTime {
#[inline] #[inline]
#[must_use] #[must_use]
pub fn iso_date(&self) -> IsoDate { pub fn iso_date(&self) -> IsoDate {
self.iso.iso_date() self.iso.date()
} }
/// Returns the inner `IsoTime` value. /// Returns the inner `IsoTime` value.
#[inline] #[inline]
#[must_use] #[must_use]
pub fn iso_time(&self) -> IsoTime { 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. /// Returns the Calendar value.

94
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<Self> {
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());
}
}

188
boa_temporal/src/iso.rs

@ -16,7 +16,7 @@ use crate::{
}; };
use icu_calendar::{Date as IcuDate, Iso}; use icu_calendar::{Date as IcuDate, Iso};
use num_bigint::BigInt; use num_bigint::BigInt;
use num_traits::cast::FromPrimitive; use num_traits::{cast::FromPrimitive, ToPrimitive};
/// `IsoDateTime` is the Temporal internal representation of /// `IsoDateTime` is the Temporal internal representation of
/// a `DateTime` record /// a `DateTime` record
@ -32,6 +32,73 @@ impl IsoDateTime {
Self { date, time } 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<Self> {
// 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. /// Returns whether the `IsoDateTime` is within valid limits.
pub(crate) fn is_within_limits(&self) -> bool { pub(crate) fn is_within_limits(&self) -> bool {
let Some(ns) = self.to_utc_epoch_nanoseconds(0f64) else { let Some(ns) = self.to_utc_epoch_nanoseconds(0f64) else {
@ -58,11 +125,11 @@ impl IsoDateTime {
BigInt::from_f64(epoch_nanos - offset) BigInt::from_f64(epoch_nanos - offset)
} }
pub(crate) fn iso_date(&self) -> IsoDate { pub(crate) fn date(&self) -> IsoDate {
self.date self.date
} }
pub(crate) fn iso_time(&self) -> IsoTime { pub(crate) fn time(&self) -> IsoTime {
self.time self.time
} }
} }
@ -106,6 +173,7 @@ impl IsoDate {
let m = month.clamp(1, 12); let m = month.clamp(1, 12);
let days_in_month = utils::iso_days_in_month(year, month); let days_in_month = utils::iso_days_in_month(year, month);
let d = day.clamp(1, days_in_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)) Ok(Self::new_unchecked(year, m as u8, d as u8))
} }
ArithmeticOverflow::Reject => { ArithmeticOverflow::Reject => {
@ -209,23 +277,23 @@ impl IsoDate {
/// time slots. /// time slots.
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
pub struct IsoTime { pub struct IsoTime {
pub(crate) hour: i32, // 0..=23 pub(crate) hour: u8, // 0..=23
pub(crate) minute: i32, // 0..=59 pub(crate) minute: u8, // 0..=59
pub(crate) second: i32, // 0..=59 pub(crate) second: u8, // 0..=59
pub(crate) millisecond: i32, // 0..=999 pub(crate) millisecond: u16, // 0..=999
pub(crate) microsecond: i32, // 0..=999 pub(crate) microsecond: u16, // 0..=999
pub(crate) nanosecond: i32, // 0..=999 pub(crate) nanosecond: u16, // 0..=999
} }
impl IsoTime { impl IsoTime {
/// Creates a new `IsoTime` without any validation. /// Creates a new `IsoTime` without any validation.
pub(crate) fn new_unchecked( pub(crate) fn new_unchecked(
hour: i32, hour: u8,
minute: i32, minute: u8,
second: i32, second: u8,
millisecond: i32, millisecond: u16,
microsecond: i32, microsecond: u16,
nanosecond: i32, nanosecond: u16,
) -> Self { ) -> Self {
Self { Self {
hour, hour,
@ -249,22 +317,26 @@ impl IsoTime {
) -> TemporalResult<IsoTime> { ) -> TemporalResult<IsoTime> {
match overflow { match overflow {
ArithmeticOverflow::Constrain => { ArithmeticOverflow::Constrain => {
let h = hour.clamp(0, 23); let h = hour.clamp(0, 23) as u8;
let min = minute.clamp(0, 59); let min = minute.clamp(0, 59) as u8;
let sec = second.clamp(0, 59); let sec = second.clamp(0, 59) as u8;
let milli = millisecond.clamp(0, 999); let milli = millisecond.clamp(0, 999) as u16;
let micro = microsecond.clamp(0, 999); let micro = microsecond.clamp(0, 999) as u16;
let nano = nanosecond.clamp(0, 999); let nano = nanosecond.clamp(0, 999) as u16;
Ok(Self::new_unchecked(h, min, sec, milli, micro, nano)) Ok(Self::new_unchecked(h, min, sec, milli, micro, nano))
} }
ArithmeticOverflow::Reject => { ArithmeticOverflow::Reject => {
// TODO: Invert structure validation and update fields to u16. if !is_valid_time(hour, minute, second, millisecond, microsecond, nanosecond) {
let time =
Self::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond);
if !time.is_valid() {
return Err(TemporalError::range().with_message("IsoTime is not valid")); 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` /// Checks if the time is a valid `IsoTime`
pub(crate) fn is_valid(&self) -> bool { pub(crate) fn is_valid(&self) -> bool {
if !(0..=23).contains(&self.hour) { 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); let days_in_month = utils::iso_days_in_month(year, month);
(1..=days_in_month).contains(&day) (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)
}

2
boa_temporal/src/lib.rs

@ -33,11 +33,13 @@ pub mod datetime;
pub mod duration; pub mod duration;
pub mod error; pub mod error;
pub mod fields; pub mod fields;
pub mod instant;
pub mod iso; pub mod iso;
pub mod month_day; pub mod month_day;
pub mod options; pub mod options;
pub mod parser; pub mod parser;
pub mod time; pub mod time;
pub mod tz;
pub(crate) mod utils; pub(crate) mod utils;
pub mod year_month; pub mod year_month;
pub mod zoneddatetime; pub mod zoneddatetime;

40
boa_temporal/src/time.rs

@ -1,6 +1,6 @@
//! Temporal Time Representation. //! Temporal Time Representation.
use crate::iso::IsoTime; use crate::{iso::IsoTime, options::ArithmeticOverflow, TemporalResult};
/// The Temporal `PlainTime` object. /// The Temporal `PlainTime` object.
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
@ -12,23 +12,39 @@ pub struct Time {
// ==== Private API ==== // ==== Private API ====
impl Time { impl Time {
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoTime) -> Self {
Self { iso }
}
/// Returns true if a valid `Time`.
#[allow(dead_code)] #[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, hour: i32,
minute: i32, minute: i32,
second: i32, second: i32,
millisecond: i32, millisecond: i32,
microsecond: i32, microsecond: i32,
nanosecond: i32, nanosecond: i32,
) -> Self { overflow: ArithmeticOverflow,
Self { ) -> TemporalResult<Self> {
iso: IsoTime::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond), let time = IsoTime::new(
} hour,
} minute,
second,
/// Returns true if a valid `Time`. millisecond,
#[allow(dead_code)] microsecond,
pub(crate) fn is_valid(&self) -> bool { nanosecond,
self.iso.is_valid() overflow,
)?;
Ok(Self::new_unchecked(time))
} }
} }

116
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<dyn TzProtocol>;
}
impl<P> TzProtocolClone for P
where
P: 'static + TzProtocol + Clone,
{
fn clone_box(&self) -> Box<dyn TzProtocol> {
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<BigInt>;
/// Get the possible Instant for this `TimeZone`
fn get_possible_instant_for(&self, context: &mut dyn Any) -> TemporalResult<Vec<Instant>>; // 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<String>, // TODO: ICU4X IANA TimeZone support.
pub(crate) offset: Option<i16>,
}
/// 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<dyn TzProtocol>),
}
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<DateTime> {
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<BigInt> {
// 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<Vec<Instant>> {
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),
}
}
}

270
boa_temporal/src/zoneddatetime.rs

@ -1,7 +1,267 @@
//! The `ZonedDateTime` module. //! The `ZonedDateTime` module.
// NOTE: Mostly serves as a placeholder currently use num_bigint::BigInt;
// until the rest can be implemented. use tinystr::TinyStr4;
/// `TemporalZoneDateTime`
#[derive(Debug, Clone, Copy)] use crate::{calendar::CalendarSlot, instant::Instant, tz::TimeZoneSlot, TemporalResult};
pub struct ZonedDateTime;
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<Self> {
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<i32> {
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<i32> {
self.contextual_year(&mut ())
}
/// Returns the `month` value for this `ZonedDateTime`.
pub fn contextual_month(&self, context: &mut dyn Any) -> TemporalResult<u8> {
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<u8> {
self.contextual_month(&mut ())
}
/// Returns the `monthCode` value for this `ZonedDateTime`.
pub fn contextual_month_code(&self, context: &mut dyn Any) -> TemporalResult<TinyStr4> {
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<TinyStr4> {
self.contextual_month_code(&mut ())
}
/// Returns the `day` value for this `ZonedDateTime`.
pub fn contextual_day(&self, context: &mut dyn Any) -> TemporalResult<u8> {
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<u8> {
self.contextual_day(&mut ())
}
/// Returns the `hour` value for this `ZonedDateTime`.
pub fn contextual_hour(&self, context: &mut dyn Any) -> TemporalResult<u8> {
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<u8> {
self.contextual_hour(&mut ())
}
/// Returns the `minute` value for this `ZonedDateTime`.
pub fn contextual_minute(&self, context: &mut dyn Any) -> TemporalResult<u8> {
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<u8> {
self.contextual_minute(&mut ())
}
/// Returns the `second` value for this `ZonedDateTime`.
pub fn contextual_second(&self, context: &mut dyn Any) -> TemporalResult<u8> {
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<u8> {
self.contextual_second(&mut ())
}
/// Returns the `millisecond` value for this `ZonedDateTime`.
pub fn contextual_millisecond(&self, context: &mut dyn Any) -> TemporalResult<u16> {
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<u16> {
self.contextual_millisecond(&mut ())
}
/// Returns the `microsecond` value for this `ZonedDateTime`.
pub fn contextual_microsecond(&self, context: &mut dyn Any) -> TemporalResult<u16> {
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<u16> {
self.contextual_microsecond(&mut ())
}
/// Returns the `nanosecond` value for this `ZonedDateTime`.
pub fn contextual_nanosecond(&self, context: &mut dyn Any) -> TemporalResult<u16> {
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<u16> {
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);
}
}

Loading…
Cancel
Save