Skip to content

Commit

Permalink
Build out ZonedDateTime, TimeZone, and Instant
Browse files Browse the repository at this point in the history
  • Loading branch information
nekevss committed Dec 4, 2023
1 parent e51e628 commit f3b4cfa
Show file tree
Hide file tree
Showing 9 changed files with 751 additions and 104 deletions.
80 changes: 26 additions & 54 deletions boa_engine/src/builtins/temporal/time_zone/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ use crate::{
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
object::{internal_methods::get_prototype_from_constructor, ObjectData, CONSTRUCTOR},
object::{internal_methods::get_prototype_from_constructor, CONSTRUCTOR},
property::Attribute,
realm::Realm,
string::{common::StaticJsStrings, utf16},
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
use boa_temporal::tz::{TimeZoneSlot, TzProtocol};

/// The `Temporal.TimeZone` object.
#[derive(Debug, Clone)]
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 @@ -129,15 +128,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 o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone")
})?;
let tz = o.as_time_zone().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 @@ -147,20 +149,15 @@ impl TimeZone {
) -> JsResult<JsValue> {
// 1. Let timeZone be the this value.
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
let _tz = this
.as_object()
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone")
})?
.borrow()
.as_time_zone()
.ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone")
})?;
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone")
})?;
let _tz = o.as_time_zone().ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.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 @@ -247,7 +244,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 o = this.as_object().ok_or_else(|| {
Expand All @@ -258,7 +259,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 @@ -317,39 +318,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,
ObjectData::time_zone(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
8 changes: 4 additions & 4 deletions boa_engine/src/builtins/temporal/zoned_date_time/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ use crate::{
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
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)]
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 @@ -2,6 +2,7 @@

use crate::{
calendar::CalendarSlot,
instant::Instant,
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime},
options::ArithmeticOverflow,
TemporalResult,
Expand Down Expand Up @@ -33,6 +34,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 @@ -76,14 +88,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
93 changes: 93 additions & 0 deletions boa_temporal/src/instant.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! 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]
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

0 comments on commit f3b4cfa

Please sign in to comment.