Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build out ZonedDateTime, TimeZone, and Instant #3497

Merged
merged 2 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 18 additions & 42 deletions boa_engine/src/builtins/temporal/time_zone/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i64>,
slot: TimeZoneSlot,
}

impl BuiltInObject for TimeZone {
Expand Down Expand Up @@ -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<JsValue> {
pub(crate) fn get_id(
this: &JsValue,
_: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let tz = this
.as_object()
.and_then(JsObject::downcast_ref::<Self>)
.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(
Expand All @@ -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]])).
Expand Down Expand Up @@ -242,7 +243,11 @@ impl TimeZone {
.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.
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
let tz = this
Expand All @@ -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())
}
}

Expand Down Expand Up @@ -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 )`
Expand Down
12 changes: 7 additions & 5 deletions boa_engine/src/builtins/temporal/zoned_date_time/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
58 changes: 56 additions & 2 deletions boa_temporal/src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Self> {
let iso = IsoDateTime::from_epoch_nanos(&instant.nanos, offset)?;
Ok(Self { iso, calendar })
}
}

// ==== Public DateTime API ====
Expand Down Expand Up @@ -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.
Expand Down
94 changes: 94 additions & 0 deletions boa_temporal/src/instant.rs
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading
Loading