Browse Source

First portion of the Temporal implementation (#3277)

* Started with the Temporal implementation

* Implemented some useful functions

* Updaating some spec references

* Initial work on TimeZone and Instant

* More work completed on Temporal.Duration and Temporal.Instant

* General scaffolding and heavy work on Instant and Duration complete

* ZonedDateTime and Calendar started with further work on duration abstract ops

* Further work on temporal work and clippy fixes

* Post rebase fixes/reverts

* Add BuiltinCalendar and begin IsoCalendar impl

* More work completed on calendar/date/yearmonth/monthday

* Calendar and iso impl close to completion - no datelike parsing

* Initial work on temporal ISO8601 parsing and grammar

* Post rebase fixes and updates

* More on parser/Duration and work through clippy lints

* Fix bug on peek_n and add temporal cfg

* Fix clippy lints on parser tests

* Build out calendar with icu_calendar, add some tests, and misc.

* Fix spec hyperlinks

* Parser clean up and invalid annotations

* Add Duration and Temporal Parsing

* Remove IsoYearMonthRecord

* Post rebase update

* Fix and add to ISO Parser docs

* Parser/ast cleanup and duration refactor/additions

* Review feedback, options update, and duration changes

* Review changes, general cleanup, and post rebase fixes

* Fix time zone parsing issue/test logic

* Clean up parse output nodes

* Apply review feedback and various fixes

* Review feedback and get_option changes

* Review feedback

---------

Co-authored-by: Iban Eguia Moraza <razican@protonmail.ch>
Co-authored-by: José Julián Espina <jedel0124@gmail.com>
pull/3376/head
Kevin 1 year ago committed by GitHub
parent
commit
d281988986
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      boa_ast/Cargo.toml
  2. 2
      boa_ast/src/lib.rs
  3. 113
      boa_ast/src/temporal/mod.rs
  4. 2
      boa_engine/Cargo.toml
  5. 11
      boa_engine/src/bigint.rs
  6. 2
      boa_engine/src/builtins/date/mod.rs
  7. 6
      boa_engine/src/builtins/date/utils.rs
  8. 24
      boa_engine/src/builtins/mod.rs
  9. 47
      boa_engine/src/builtins/options.rs
  10. 367
      boa_engine/src/builtins/temporal/calendar/iso.rs
  11. 2303
      boa_engine/src/builtins/temporal/calendar/mod.rs
  12. 22
      boa_engine/src/builtins/temporal/calendar/tests.rs
  13. 107
      boa_engine/src/builtins/temporal/calendar/utils.rs
  14. 121
      boa_engine/src/builtins/temporal/date_equations.rs
  15. 1039
      boa_engine/src/builtins/temporal/duration/mod.rs
  16. 1820
      boa_engine/src/builtins/temporal/duration/record.rs
  17. 27
      boa_engine/src/builtins/temporal/duration/tests.rs
  18. 587
      boa_engine/src/builtins/temporal/fields.rs
  19. 784
      boa_engine/src/builtins/temporal/instant/mod.rs
  20. 660
      boa_engine/src/builtins/temporal/mod.rs
  21. 188
      boa_engine/src/builtins/temporal/now.rs
  22. 384
      boa_engine/src/builtins/temporal/options.rs
  23. 236
      boa_engine/src/builtins/temporal/plain_date/iso.rs
  24. 567
      boa_engine/src/builtins/temporal/plain_date/mod.rs
  25. 100
      boa_engine/src/builtins/temporal/plain_date_time/iso.rs
  26. 148
      boa_engine/src/builtins/temporal/plain_date_time/mod.rs
  27. 122
      boa_engine/src/builtins/temporal/plain_month_day/mod.rs
  28. 62
      boa_engine/src/builtins/temporal/plain_time/mod.rs
  29. 327
      boa_engine/src/builtins/temporal/plain_year_month/mod.rs
  30. 52
      boa_engine/src/builtins/temporal/tests.rs
  31. 491
      boa_engine/src/builtins/temporal/time_zone/mod.rs
  32. 133
      boa_engine/src/builtins/temporal/zoned_date_time/mod.rs
  33. 214
      boa_engine/src/context/intrinsics.rs
  34. 105
      boa_engine/src/object/jsobject.rs
  35. 394
      boa_engine/src/object/mod.rs
  36. 24
      boa_engine/src/string/common.rs
  37. 1
      boa_parser/Cargo.toml
  38. 1
      boa_parser/src/error/mod.rs
  39. 2
      boa_parser/src/lib.rs
  40. 2
      boa_parser/src/parser/expression/assignment/yield.rs
  41. 4
      boa_parser/src/parser/mod.rs
  42. 205
      boa_parser/src/temporal/annotations.rs
  43. 373
      boa_parser/src/temporal/date_time.rs
  44. 275
      boa_parser/src/temporal/duration.rs
  45. 136
      boa_parser/src/temporal/grammar.rs
  46. 348
      boa_parser/src/temporal/mod.rs
  47. 190
      boa_parser/src/temporal/tests.rs
  48. 146
      boa_parser/src/temporal/time.rs
  49. 263
      boa_parser/src/temporal/time_zone.rs

1
boa_ast/Cargo.toml

@ -13,6 +13,7 @@ rust-version.workspace = true
[features]
serde = ["dep:serde", "boa_interner/serde", "bitflags/serde", "num-bigint/serde"]
arbitrary = ["dep:arbitrary", "boa_interner/arbitrary", "num-bigint/arbitrary"]
experimental = []
[dependencies]
boa_interner.workspace = true

2
boa_ast/src/lib.rs

@ -91,6 +91,8 @@ pub mod operations;
pub mod pattern;
pub mod property;
pub mod statement;
#[cfg(feature = "experimental")]
pub mod temporal;
pub mod visitor;
use boa_interner::{Interner, ToIndentedString, ToInternedString};

113
boa_ast/src/temporal/mod.rs

@ -0,0 +1,113 @@
//! AST nodes for Temporal's implementation of ISO8601 grammar.
/// An ISO Date Node consisting of non-validated date fields and calendar value.
#[derive(Default, Debug)]
pub struct IsoDate {
/// Date Year
pub year: i32,
/// Date Month
pub month: i32,
/// Date Day
pub day: i32,
/// The calendar value.
pub calendar: Option<String>,
}
/// The `IsoTime` node consists of non-validated time fields.
#[derive(Default, Debug, Clone, Copy)]
pub struct IsoTime {
/// An hour value between 0-23
pub hour: u8,
/// A minute value between 0-59
pub minute: u8,
/// A second value between 0-60
pub second: u8,
/// A millisecond value between 0-999
pub millisecond: u16,
/// A microsecond value between 0-999
pub microsecond: u16,
/// A nanosecond value between 0-999
pub nanosecond: u16,
}
impl IsoTime {
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
/// A utility initialization function to create `ISOTime` from the `TimeSpec` components.
pub fn from_components(hour: u8, minute: u8, second: u8, fraction: f64) -> Self {
// Note: Precision on nanoseconds drifts, so opting for round over floor or ceil for now.
// e.g. 0.329402834 becomes 329.402833.999
let millisecond = fraction * 1000.0;
let micros = millisecond.rem_euclid(1.0) * 1000.0;
let nanos = micros.rem_euclid(1.0) * 1000.0;
Self {
hour,
minute,
second,
millisecond: millisecond.floor() as u16,
microsecond: micros.floor() as u16,
nanosecond: nanos.round() as u16,
}
}
}
/// The `IsoDateTime` node output by the ISO parser
#[derive(Default, Debug)]
pub struct IsoDateTime {
/// The `ISODate` record
pub date: IsoDate,
/// The `ISOTime` record
pub time: IsoTime,
/// The `TimeZone` value for this `ISODateTime`
pub tz: Option<TimeZone>,
}
/// `TimeZone` data
#[derive(Default, Debug, Clone)]
pub struct TimeZone {
/// TimeZoneIANAName
pub name: Option<String>,
/// TimeZoneOffset
pub offset: Option<UTCOffset>,
}
/// A full precision `UtcOffset`
#[derive(Debug, Clone, Copy)]
pub struct UTCOffset {
/// The `+`/`-` sign of this `UtcOffset`
pub sign: i8,
/// The hour value of the `UtcOffset`
pub hour: u8,
/// The minute value of the `UtcOffset`.
pub minute: u8,
/// The second value of the `UtcOffset`.
pub second: u8,
/// Any sub second components of the `UTCOffset`
pub fraction: f64,
}
/// An `IsoDuration` Node output by the ISO parser.
#[derive(Debug, Default, Clone, Copy)]
pub struct IsoDuration {
/// Years value.
pub years: i32,
/// Months value.
pub months: i32,
/// Weeks value.
pub weeks: i32,
/// Days value.
pub days: i32,
/// Hours value.
pub hours: i32,
/// Minutes value.
pub minutes: f64,
/// Seconds value.
pub seconds: f64,
/// Milliseconds value.
pub milliseconds: f64,
/// Microseconds value.
pub microseconds: f64,
/// Nanoseconds value.
pub nanoseconds: f64,
}

2
boa_engine/Cargo.toml

@ -47,7 +47,7 @@ trace = []
annex-b = ["boa_parser/annex-b"]
# Enable experimental features, like Stage 3 proposals.
experimental = []
experimental = ["boa_parser/experimental", "dep:icu_calendar"]
[dependencies]
boa_interner.workspace = true

11
boa_engine/src/bigint.rs

@ -225,6 +225,17 @@ impl JsBigInt {
Self::new(x.inner.as_ref().clone().add(y.inner.as_ref()))
}
/// Utility function for performing `+` operation on more than two values.
#[inline]
#[cfg(feature = "experimental")]
pub(crate) fn add_n(values: &[Self]) -> Self {
let mut result = Self::zero();
for big_int in values {
result = Self::add(&result, big_int);
}
result
}
/// Performs the `-` operation.
#[inline]
#[must_use]

2
boa_engine/src/builtins/date/mod.rs

@ -7,7 +7,7 @@
//! [spec]: https://tc39.es/ecma262/#sec-date-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
mod utils;
pub(crate) mod utils;
use utils::{make_date, make_day, make_time, replace_params, time_clip, DateParameters};
#[cfg(test)]

6
boa_engine/src/builtins/date/utils.rs

@ -39,7 +39,7 @@ pub(super) const fn day_from_year(year: i64) -> i64 {
/// Abstract operation [`MakeTime`][spec].
///
/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-maketime
pub(super) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option<i64> {
pub(crate) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option<i64> {
// 1. If hour is not finite or min is not finite or sec is not finite or ms is not finite, return NaN.
// 2. Let h be 𝔽(! ToIntegerOrInfinity(hour)).
// 3. Let m be 𝔽(! ToIntegerOrInfinity(min)).
@ -59,7 +59,7 @@ pub(super) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option<i64> {
/// Abstract operation [`MakeDay`][spec].
///
/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makeday
pub(super) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option<i64> {
pub(crate) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option<i64> {
// 1. If year is not finite or month is not finite or date is not finite, return NaN.
// 2. Let y be 𝔽(! ToIntegerOrInfinity(year)).
// 3. Let m be 𝔽(! ToIntegerOrInfinity(month)).
@ -101,7 +101,7 @@ pub(super) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option<i64>
/// Abstract operation [`MakeDate`][spec].
///
/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makedate
pub(super) fn make_date(day: i64, time: i64) -> Option<i64> {
pub(crate) fn make_date(day: i64, time: i64) -> Option<i64> {
// 1. If day is not finite or time is not finite, return NaN.
// 2. Let tv be day × msPerDay + time.
// 3. If tv is not finite, return NaN.

24
boa_engine/src/builtins/mod.rs

@ -43,6 +43,9 @@ pub mod intl;
#[cfg(any(feature = "intl", feature = "experimental"))]
pub(crate) mod options;
#[cfg(feature = "experimental")]
pub mod temporal;
pub(crate) use self::{
array::Array,
async_function::AsyncFunction,
@ -275,6 +278,22 @@ impl Realm {
intl::segmenter::SegmentIterator::init(self);
intl::PluralRules::init(self);
}
#[cfg(feature = "experimental")]
{
temporal::TimeZone::init(self);
temporal::Temporal::init(self);
temporal::Now::init(self);
temporal::Instant::init(self);
temporal::Duration::init(self);
temporal::PlainDate::init(self);
temporal::PlainTime::init(self);
temporal::PlainDateTime::init(self);
temporal::PlainMonthDay::init(self);
temporal::PlainYearMonth::init(self);
temporal::ZonedDateTime::init(self);
temporal::Calendar::init(self);
}
}
}
@ -374,6 +393,11 @@ pub(crate) fn set_default_global_bindings(context: &mut Context<'_>) -> JsResult
#[cfg(feature = "intl")]
global_binding::<intl::Intl>(context)?;
#[cfg(feature = "experimental")]
{
global_binding::<temporal::Temporal>(context)?;
}
Ok(())
}

47
boa_engine/src/builtins/options.rs

@ -124,6 +124,53 @@ pub(crate) enum RoundingMode {
HalfEven,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UnsignedRoundingMode {
Infinity,
Zero,
HalfInfinity,
HalfZero,
HalfEven,
}
impl RoundingMode {
pub(crate) const fn negate(self) -> Self {
use RoundingMode::{
Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc,
};
match self {
Ceil => Self::Floor,
Floor => Self::Ceil,
HalfCeil => Self::HalfFloor,
HalfFloor => Self::HalfCeil,
Trunc => Self::Trunc,
Expand => Self::Expand,
HalfTrunc => Self::HalfTrunc,
HalfExpand => Self::HalfExpand,
HalfEven => Self::HalfEven,
}
}
pub(crate) const fn get_unsigned_round_mode(self, is_negative: bool) -> UnsignedRoundingMode {
use RoundingMode::{
Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc,
};
match self {
Ceil if !is_negative => UnsignedRoundingMode::Infinity,
Ceil => UnsignedRoundingMode::Zero,
Floor if !is_negative => UnsignedRoundingMode::Zero,
Floor | Trunc | Expand => UnsignedRoundingMode::Infinity,
HalfCeil if !is_negative => UnsignedRoundingMode::HalfInfinity,
HalfCeil | HalfTrunc => UnsignedRoundingMode::HalfZero,
HalfFloor if !is_negative => UnsignedRoundingMode::HalfZero,
HalfFloor | HalfExpand => UnsignedRoundingMode::HalfInfinity,
HalfEven => UnsignedRoundingMode::HalfEven,
}
}
}
#[derive(Debug)]
pub(crate) struct ParseRoundingModeError;

367
boa_engine/src/builtins/temporal/calendar/iso.rs

@ -0,0 +1,367 @@
//! Implementation of the "iso8601" `BuiltinCalendar`.
use crate::{
builtins::temporal::{
self, create_temporal_date,
date_equations::mathematical_days_in_year,
options::{ArithmeticOverflow, TemporalUnit},
plain_date::iso::IsoDateRecord,
},
js_string,
property::PropertyKey,
string::utf16,
Context, JsNativeError, JsResult, JsString, JsValue,
};
use super::BuiltinCalendar;
use icu_calendar::{
iso::Iso,
week::{RelativeUnit, WeekCalculator},
Calendar, Date,
};
pub(crate) struct IsoCalendar;
impl BuiltinCalendar for IsoCalendar {
/// Temporal 15.8.2.1 `Temporal.prototype.dateFromFields( fields [, options])` - Supercedes 12.5.4
///
/// This is a basic implementation for an iso8601 calendar's `dateFromFields` method.
fn date_from_fields(
&self,
fields: &mut temporal::TemporalFields,
overflow: ArithmeticOverflow,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// NOTE: we are in ISO by default here.
// a. Perform ? ISOResolveMonth(fields).
// b. Let result be ? ISODateFromFields(fields, overflow).
fields.iso_resolve_month()?;
// Extra: handle reulating/overflow until implemented on `icu_calendar`
fields.regulate(overflow)?;
let date = Date::try_new_iso_date(
fields.year().unwrap_or(0),
fields.month().unwrap_or(250) as u8,
fields.day().unwrap_or(250) as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
// 9. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
Ok(create_temporal_date(
IsoDateRecord::from_date_iso(date),
js_string!("iso8601").into(),
None,
context,
)?
.into())
}
/// 12.5.5 `Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] )`
///
/// This is a basic implementation for an iso8601 calendar's `yearMonthFromFields` method.
fn year_month_from_fields(
&self,
fields: &mut temporal::TemporalFields,
overflow: ArithmeticOverflow,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 9. If calendar.[[Identifier]] is "iso8601", then
// a. Perform ? ISOResolveMonth(fields).
fields.iso_resolve_month()?;
// b. Let result be ? ISOYearMonthFromFields(fields, overflow).
fields.regulate_year_month(overflow);
let result = Date::try_new_iso_date(
fields.year().unwrap_or(0),
fields.month().unwrap_or(250) as u8,
fields.day().unwrap_or(20) as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
// 10. Return ? CreateTemporalYearMonth(result.[[Year]], result.[[Month]], "iso8601", result.[[ReferenceISODay]]).
temporal::create_temporal_year_month(
IsoDateRecord::from_date_iso(result),
js_string!("iso8601").into(),
None,
context,
)
}
/// 12.5.6 `Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] )`
///
/// This is a basic implementation for an iso8601 calendar's `monthDayFromFields` method.
fn month_day_from_fields(
&self,
fields: &mut temporal::TemporalFields,
overflow: ArithmeticOverflow,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 8. Perform ? ISOResolveMonth(fields).
fields.iso_resolve_month()?;
fields.regulate(overflow)?;
// TODO: double check error mapping is correct for specifcation/test262.
// 9. Let result be ? ISOMonthDayFromFields(fields, overflow).
let result = Date::try_new_iso_date(
fields.year().unwrap_or(1972),
fields.month().unwrap_or(250) as u8,
fields.day().unwrap_or(250) as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
// 10. Return ? CreateTemporalMonthDay(result.[[Month]], result.[[Day]], "iso8601", result.[[ReferenceISOYear]]).
temporal::create_temporal_month_day(
IsoDateRecord::from_date_iso(result),
js_string!("iso8601").into(),
None,
context,
)
}
/// 12.5.7 `Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] )`
///
/// Below implements the basic implementation for an iso8601 calendar's `dateAdd` method.
fn date_add(
&self,
_date: &temporal::PlainDate,
_duration: &temporal::duration::DurationRecord,
_overflow: ArithmeticOverflow,
_context: &mut Context<'_>,
) -> JsResult<JsValue> {
// TODO: Not stable on `ICU4X`. Implement once completed.
Err(JsNativeError::range()
.with_message("feature not implemented.")
.into())
// 9. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], balanceResult.[[Days]], overflow).
// 10. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
}
/// 12.5.8 `Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] )`
///
/// Below implements the basic implementation for an iso8601 calendar's `dateUntil` method.
fn date_until(
&self,
_one: &temporal::PlainDate,
_two: &temporal::PlainDate,
_largest_unit: TemporalUnit,
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// TODO: Not stable on `ICU4X`. Implement once completed.
Err(JsNativeError::range()
.with_message("Feature not yet implemented.")
.into())
// 9. Let result be DifferenceISODate(one.[[ISOYear]], one.[[ISOMonth]], one.[[ISODay]], two.[[ISOYear]], two.[[ISOMonth]], two.[[ISODay]], largestUnit).
// 10. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], 0, 0, 0, 0, 0, 0).
}
/// `Temporal.Calendar.prototype.era( dateLike )` for iso8601 calendar.
fn era(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
// Returns undefined on iso8601.
Ok(JsValue::undefined())
}
/// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar.
fn era_year(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
// Returns undefined on iso8601.
Ok(JsValue::undefined())
}
/// Returns the `year` for the `Iso` calendar.
fn year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(date.year().number.into())
}
/// Returns the `month` for the `Iso` calendar.
fn month(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(date.month().ordinal.into())
}
/// Returns the `monthCode` for the `Iso` calendar.
fn month_code(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(JsString::from(date.month().code.to_string()).into())
}
/// Returns the `day` for the `Iso` calendar.
fn day(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(date.day_of_month().0.into())
}
/// Returns the `dayOfWeek` for the `Iso` calendar.
fn day_of_week(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok((date.day_of_week() as u8).into())
}
/// Returns the `dayOfYear` for the `Iso` calendar.
fn day_of_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(i32::from(date.day_of_year_info().day_of_year).into())
}
/// Returns the `weekOfYear` for the `Iso` calendar.
fn week_of_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
// TODO: Determine `ICU4X` equivalent.
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
let week_calculator = WeekCalculator::default();
let week_of = date
.week_of_year(&week_calculator)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(week_of.week.into())
}
/// Returns the `yearOfWeek` for the `Iso` calendar.
fn year_of_week(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
// TODO: Determine `ICU4X` equivalent.
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
let week_calculator = WeekCalculator::default();
let week_of = date
.week_of_year(&week_calculator)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
match week_of.unit {
RelativeUnit::Previous => Ok((date.year().number - 1).into()),
RelativeUnit::Current => Ok(date.year().number.into()),
RelativeUnit::Next => Ok((date.year().number + 1).into()),
}
}
/// Returns the `daysInWeek` value for the `Iso` calendar.
fn days_in_week(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
Ok(7.into())
}
/// Returns the `daysInMonth` value for the `Iso` calendar.
fn days_in_month(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(date.days_in_month().into())
}
/// Returns the `daysInYear` value for the `Iso` calendar.
fn days_in_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
let date = Date::try_new_iso_date(
date_like.year(),
date_like.month() as u8,
date_like.day() as u8,
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
Ok(date.days_in_year().into())
}
/// Return the amount of months in an ISO Calendar.
fn months_in_year(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
Ok(12.into())
}
/// Returns whether provided date is in a leap year according to this calendar.
fn in_leap_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> {
// `ICU4X`'s `CalendarArithmetic` is currently private.
if mathematical_days_in_year(date_like.year()) == 366 {
return Ok(true.into());
}
Ok(false.into())
}
// Resolve the fields for the iso calendar.
fn resolve_fields(&self, fields: &mut temporal::TemporalFields, _: &str) -> JsResult<()> {
fields.iso_resolve_month()?;
Ok(())
}
/// Returns the ISO field descriptors, which is not called for the iso8601 calendar.
fn field_descriptors(&self, _: &[String]) -> Vec<(String, bool)> {
// NOTE(potential improvement): look into implementing field descriptors and call
// ISO like any other calendar?
// Field descriptors is unused on ISO8601.
unreachable!()
}
/// Returns the `CalendarFieldKeysToIgnore` implementation for ISO.
fn field_keys_to_ignore(&self, additional_keys: Vec<PropertyKey>) -> Vec<PropertyKey> {
let mut result = Vec::new();
for key in additional_keys {
let key_string = key.to_string();
result.push(key);
if key_string.as_str() == "month" {
result.push(utf16!("monthCode").into());
} else if key_string.as_str() == "monthCode" {
result.push(utf16!("month").into());
}
}
result
}
// NOTE: This is currently not a name that is compliant with
// the Temporal proposal. For debugging purposes only.
/// Returns the debug name.
fn debug_name(&self) -> &str {
Iso.debug_name()
}
}

2303
boa_engine/src/builtins/temporal/calendar/mod.rs

File diff suppressed because it is too large Load Diff

22
boa_engine/src/builtins/temporal/calendar/tests.rs

@ -0,0 +1,22 @@
use crate::{js_string, run_test_actions, TestAction};
#[test]
fn calendar_constructor() {
// TODO: Add other BuiltinCalendars
run_test_actions([TestAction::assert_eq(
"new Temporal.Calendar('iso8601').id",
js_string!("iso8601"),
)]);
}
#[test]
fn calendar_methods() {
run_test_actions([
TestAction::run("let iso = new Temporal.Calendar('iso8601');"),
TestAction::assert_eq("iso.inLeapYear('2020-11-20')", true),
TestAction::assert_eq("iso.daysInYear('2020-11-20')", 366),
TestAction::assert_eq("iso.daysInYear('2021-11-20')", 365),
TestAction::assert_eq("iso.monthsInYear('2021-11-20')", 12),
TestAction::assert_eq("iso.daysInWeek('2021-11-20')", 7),
]);
}

107
boa_engine/src/builtins/temporal/calendar/utils.rs

@ -0,0 +1,107 @@
//! Calendar utility calculations
// TODO: determine if any of the below are needed.
use crate::builtins::temporal::{self, date_equations, plain_date::iso::IsoDateRecord};
use crate::JsString;
/// 12.2.31 `ISODaysInMonth ( year, month )`
pub(crate) fn iso_days_in_month(year: i32, month: i32) -> i32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
28 + temporal::date_equations::mathematical_in_leap_year(
temporal::date_equations::epoch_time_for_year(year),
)
}
_ => unreachable!("an invalid month value is an implementation error."),
}
}
/// 12.2.32 `ToISOWeekOfYear ( year, month, day )`
///
/// Takes an `[[IsoYear]]`, `[[IsoMonth]]`, and `[[IsoDay]]` and returns a (week, year) record.
#[allow(unused)]
pub(crate) fn to_iso_week_of_year(year: i32, month: i32, day: i32) -> (i32, i32) {
// Function constants
// 2. Let wednesday be 3.
// 3. Let thursday be 4.
// 4. Let friday be 5.
// 5. Let saturday be 6.
// 6. Let daysInWeek be 7.
// 7. Let maxWeekNumber be 53.
let day_of_year = to_iso_day_of_year(year, month, day);
let day_of_week = to_iso_day_of_week(year, month, day);
let week = (day_of_week + 7 - day_of_week + 3) / 7;
if week < 1 {
let first_day_of_year = to_iso_day_of_week(year, 1, 1);
if first_day_of_year == 5 {
return (53, year - 1);
} else if first_day_of_year == 6
&& date_equations::mathematical_in_leap_year(date_equations::epoch_time_for_year(
year - 1,
)) == 1
{
return (52, year - 1);
}
return (52, year - 1);
} else if week == 53 {
let days_in_year = date_equations::mathematical_days_in_year(year);
let days_later_in_year = days_in_year - day_of_year;
let days_after_thursday = 4 - day_of_week;
if days_later_in_year < days_after_thursday {
return (1, year - 1);
}
}
(week, year)
}
/// 12.2.33 `ISOMonthCode ( month )`
#[allow(unused)]
fn iso_month_code(month: i32) -> JsString {
// TODO: optimize
if month < 10 {
JsString::from(format!("M0{month}"))
} else {
JsString::from(format!("M{month}"))
}
}
// 12.2.34 `ISOResolveMonth ( fields )`
// Note: currently implemented on TemporalFields -> implement in this mod?
// 12.2.35 ISODateFromFields ( fields, overflow )
// Note: implemented on IsoDateRecord.
// 12.2.36 ISOYearMonthFromFields ( fields, overflow )
// TODO: implement on a IsoYearMonthRecord
// 12.2.37 ISOMonthDayFromFields ( fields, overflow )
// TODO: implement as method on IsoDateRecord.
// 12.2.38 IsoFieldKeysToIgnore
// TODO: determine usefulness.
/// 12.2.39 `ToISODayOfYear ( year, month, day )`
#[allow(unused)]
fn to_iso_day_of_year(year: i32, month: i32, day: i32) -> i32 {
// TODO: update fn parameter to take IsoDateRecord.
let iso = IsoDateRecord::new(year, month - 1, day);
let epoch_days = iso.as_epoch_days();
date_equations::epoch_time_to_day_in_year(temporal::epoch_days_to_epoch_ms(epoch_days, 0)) + 1
}
/// 12.2.40 `ToISODayOfWeek ( year, month, day )`
#[allow(unused)]
pub(crate) fn to_iso_day_of_week(year: i32, month: i32, day: i32) -> i32 {
let iso = IsoDateRecord::new(year, month - 1, day);
let epoch_days = iso.as_epoch_days();
let day_of_week =
date_equations::epoch_time_to_week_day(temporal::epoch_days_to_epoch_ms(epoch_days, 0));
if day_of_week == 0 {
return 7;
}
day_of_week
}

121
boa_engine/src/builtins/temporal/date_equations.rs

@ -0,0 +1,121 @@
//! This file represents all equations listed under section 13.4 of the [Temporal Specification][spec]
//!
//! [spec]: https://tc39.es/proposal-temporal/#sec-date-equations
use std::ops::Mul;
pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 {
(t / f64::from(super::MS_PER_DAY)).floor() as i32
}
pub(crate) fn mathematical_days_in_year(y: i32) -> i32 {
if y % 4 != 0 {
365
} else if y % 4 == 0 && y % 100 != 0 {
366
} else if y % 100 == 0 && y % 400 != 0 {
365
} else {
// Assert that y is divisble by 400 to ensure we are returning the correct result.
assert_eq!(y % 400, 0);
366
}
}
pub(crate) fn epoch_day_number_for_year(y: f64) -> f64 {
365.0f64.mul_add(y - 1970.0, ((y - 1969.0) / 4.0).floor()) - ((y - 1901.0) / 100.0).floor()
+ ((y - 1601.0) / 400.0).floor()
}
pub(crate) fn epoch_time_for_year(y: i32) -> f64 {
f64::from(super::MS_PER_DAY) * epoch_day_number_for_year(f64::from(y))
}
// NOTE: The below returns the epoch years (years since 1970). The spec
// appears to assume the below returns with the epoch applied.
pub(crate) fn epoch_time_to_epoch_year(t: f64) -> i32 {
// roughly calculate the largest possible year given the time t,
// then check and refine the year.
let day_count = epoch_time_to_day_number(t);
let mut year = day_count / 365;
loop {
if epoch_time_for_year(year) <= t {
break;
}
year -= 1;
}
year + 1970
}
/// Returns either 1 (true) or 0 (false)
pub(crate) fn mathematical_in_leap_year(t: f64) -> i32 {
mathematical_days_in_year(epoch_time_to_epoch_year(t)) - 365
}
pub(crate) fn epoch_time_to_month_in_year(t: f64) -> i32 {
const DAYS: [i32; 11] = [30, 58, 89, 120, 150, 181, 212, 242, 272, 303, 333];
const LEAP_DAYS: [i32; 11] = [30, 59, 90, 121, 151, 182, 213, 242, 272, 303, 334];
let in_leap_year = mathematical_in_leap_year(t) == 1;
let day = epoch_time_to_day_in_year(t);
let result = if in_leap_year {
LEAP_DAYS.binary_search(&day)
} else {
DAYS.binary_search(&day)
};
match result {
Ok(i) | Err(i) => i as i32,
}
}
pub(crate) fn epoch_time_for_month_given_year(m: i32, y: i32) -> f64 {
let leap_day = mathematical_days_in_year(y) - 365;
let days = match m {
0 => 1,
1 => 31,
2 => 59 + leap_day,
3 => 90 + leap_day,
4 => 121 + leap_day,
5 => 151 + leap_day,
6 => 182 + leap_day,
7 => 213 + leap_day,
8 => 243 + leap_day,
9 => 273 + leap_day,
10 => 304 + leap_day,
11 => 334 + leap_day,
_ => unreachable!(),
};
(super::NS_PER_DAY as f64).mul(f64::from(days))
}
pub(crate) fn epoch_time_to_date(t: f64) -> i32 {
const OFFSETS: [i16; 12] = [
1, -30, -58, -89, -119, -150, -180, -211, -242, -272, -303, -333,
];
let day_in_year = epoch_time_to_day_in_year(t);
let in_leap_year = mathematical_in_leap_year(t);
let month = epoch_time_to_month_in_year(t);
// Cast from i32 to usize should be safe as the return must be 0-11
let mut date = day_in_year + i32::from(OFFSETS[month as usize]);
if month >= 2 {
date -= in_leap_year;
}
date
}
pub(crate) fn epoch_time_to_day_in_year(t: f64) -> i32 {
epoch_time_to_day_number(t)
- (epoch_day_number_for_year(f64::from(epoch_time_to_epoch_year(t))) as i32)
}
pub(crate) fn epoch_time_to_week_day(t: f64) -> i32 {
(epoch_time_to_day_number(t) + 4) % 7
}

1039
boa_engine/src/builtins/temporal/duration/mod.rs

File diff suppressed because it is too large Load Diff

1820
boa_engine/src/builtins/temporal/duration/record.rs

File diff suppressed because it is too large Load Diff

27
boa_engine/src/builtins/temporal/duration/tests.rs

@ -0,0 +1,27 @@
use crate::{run_test_actions, TestAction};
#[test]
fn duration_constructor() {
run_test_actions([
TestAction::run("let dur = new Temporal.Duration(1, 1, 0, 1)"),
TestAction::assert_eq("dur.years", 1),
TestAction::assert_eq("dur.months", 1),
TestAction::assert_eq("dur.weeks", 0),
TestAction::assert_eq("dur.days", 1),
TestAction::assert_eq("dur.milliseconds", 0),
]);
}
#[test]
fn duration_abs() {
run_test_actions([
TestAction::run("let dur = new Temporal.Duration(-1, -1, 0, -1)"),
TestAction::assert_eq("dur.sign", -1),
TestAction::run("let abs = dur.abs()"),
TestAction::assert_eq("abs.years", 1),
TestAction::assert_eq("abs.months", 1),
TestAction::assert_eq("abs.weeks", 0),
TestAction::assert_eq("abs.days", 1),
TestAction::assert_eq("abs.milliseconds", 0),
]);
}

587
boa_engine/src/builtins/temporal/fields.rs

@ -0,0 +1,587 @@
//! A Rust native implementation of the `fields` object used in `Temporal`.
use crate::{
js_string, property::PropertyKey, value::PreferredType, Context, JsNativeError, JsObject,
JsResult, JsString, JsValue,
};
use super::options::ArithmeticOverflow;
use bitflags::bitflags;
use rustc_hash::FxHashSet;
bitflags! {
#[derive(Debug, PartialEq, Eq)]
pub struct FieldMap: u16 {
const YEAR = 0b0000_0000_0000_0001;
const MONTH = 0b0000_0000_0000_0010;
const MONTH_CODE = 0b0000_0000_0000_0100;
const DAY = 0b0000_0000_0000_1000;
const HOUR = 0b0000_0000_0001_0000;
const MINUTE = 0b0000_0000_0010_0000;
const SECOND = 0b0000_0000_0100_0000;
const MILLISECOND = 0b0000_0000_1000_0000;
const MICROSECOND = 0b0000_0001_0000_0000;
const NANOSECOND = 0b0000_0010_0000_0000;
const OFFSET = 0b0000_0100_0000_0000;
const ERA = 0b0000_1000_0000_0000;
const ERA_YEAR = 0b0001_0000_0000_0000;
const TIME_ZONE = 0b0010_0000_0000_0000;
}
}
/// The temporal fields are laid out in the Temporal proposal under section 13.46 `PrepareTemporalFields`
/// with conversion and defaults laid out by Table 17 (displayed below).
///
/// `TemporalFields` is meant to act as a native Rust implementation
/// of the fields.
///
///
/// ## Table 17: Temporal field requirements
///
/// | Property | Conversion | Default |
/// | -------------|---------------------------------|------------|
/// | "year" | `ToIntegerWithTruncation` | undefined |
/// | "month" | `ToPositiveIntegerWithTruncation` | undefined |
/// | "monthCode" | `ToPrimitiveAndRequireString` | undefined |
/// | "day" | `ToPositiveIntegerWithTruncation` | undefined |
/// | "hour" | `ToIntegerWithTruncation` | +0𝔽 |
/// | "minute" | `ToIntegerWithTruncation` | +0𝔽 |
/// | "second" | `ToIntegerWithTruncation` | +0𝔽 |
/// | "millisecond"| `ToIntegerWithTruncation` | +0𝔽 |
/// | "microsecond"| `ToIntegerWithTruncation` | +0𝔽 |
/// | "nanosecond" | `ToIntegerWithTruncation` | +0𝔽 |
/// | "offset" | `ToPrimitiveAndRequireString` | undefined |
/// | "era" | `ToPrimitiveAndRequireString` | undefined |
/// | "eraYear" | `ToIntegerWithTruncation` | undefined |
/// | "timeZone" | | undefined |
///
#[derive(Debug)]
pub(crate) struct TemporalFields {
bit_map: FieldMap,
year: Option<i32>,
month: Option<i32>,
month_code: Option<JsString>, // TODO: Switch to icu compatible value.
day: Option<i32>,
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
offset: Option<JsString>,
era: Option<JsString>, // TODO: switch to icu compatible value.
era_year: Option<i32>, // TODO: switch to icu compatible value.
time_zone: Option<JsString>, // TODO: figure out the identifier for TimeZone.
}
impl Default for TemporalFields {
fn default() -> Self {
Self {
bit_map: FieldMap::empty(),
year: None,
month: None,
month_code: None,
day: None,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
nanosecond: 0,
offset: None,
era: None,
era_year: None,
time_zone: None,
}
}
}
impl TemporalFields {
pub(crate) const fn year(&self) -> Option<i32> {
self.year
}
pub(crate) const fn month(&self) -> Option<i32> {
self.month
}
pub(crate) const fn day(&self) -> Option<i32> {
self.day
}
}
impl TemporalFields {
#[inline]
fn set_field_value(
&mut self,
field: &str,
value: &JsValue,
context: &mut Context<'_>,
) -> JsResult<()> {
match field {
"year" => self.set_year(value, context)?,
"month" => self.set_month(value, context)?,
"monthCode" => self.set_month_code(value, context)?,
"day" => self.set_day(value, context)?,
"hour" => self.set_hour(value, context)?,
"minute" => self.set_minute(value, context)?,
"second" => self.set_second(value, context)?,
"millisecond" => self.set_milli(value, context)?,
"microsecond" => self.set_micro(value, context)?,
"nanosecond" => self.set_nano(value, context)?,
"offset" => self.set_offset(value, context)?,
"era" => self.set_era(value, context)?,
"eraYear" => self.set_era_year(value, context)?,
"timeZone" => self.set_time_zone(value),
_ => unreachable!(),
}
Ok(())
}
#[inline]
fn set_year(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let y = super::to_integer_with_truncation(value, context)?;
self.year = Some(y);
self.bit_map.set(FieldMap::YEAR, true);
Ok(())
}
#[inline]
fn set_month(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let mo = super::to_positive_integer_with_trunc(value, context)?;
self.year = Some(mo);
self.bit_map.set(FieldMap::MONTH, true);
Ok(())
}
#[inline]
fn set_month_code(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let mc = value.to_primitive(context, PreferredType::String)?;
if let Some(string) = mc.as_string() {
self.month_code = Some(string.clone());
} else {
return Err(JsNativeError::typ()
.with_message("ToPrimativeAndRequireString must be of type String.")
.into());
}
self.bit_map.set(FieldMap::MONTH_CODE, true);
Ok(())
}
#[inline]
fn set_day(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let d = super::to_positive_integer_with_trunc(value, context)?;
self.day = Some(d);
self.bit_map.set(FieldMap::DAY, true);
Ok(())
}
#[inline]
fn set_hour(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let h = super::to_integer_with_truncation(value, context)?;
self.hour = h;
self.bit_map.set(FieldMap::HOUR, true);
Ok(())
}
#[inline]
fn set_minute(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let m = super::to_integer_with_truncation(value, context)?;
self.minute = m;
self.bit_map.set(FieldMap::MINUTE, true);
Ok(())
}
#[inline]
fn set_second(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let sec = super::to_integer_with_truncation(value, context)?;
self.second = sec;
self.bit_map.set(FieldMap::SECOND, true);
Ok(())
}
#[inline]
fn set_milli(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let milli = super::to_integer_with_truncation(value, context)?;
self.millisecond = milli;
self.bit_map.set(FieldMap::MILLISECOND, true);
Ok(())
}
#[inline]
fn set_micro(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let micro = super::to_integer_with_truncation(value, context)?;
self.microsecond = micro;
self.bit_map.set(FieldMap::MICROSECOND, true);
Ok(())
}
#[inline]
fn set_nano(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let nano = super::to_integer_with_truncation(value, context)?;
self.nanosecond = nano;
self.bit_map.set(FieldMap::NANOSECOND, true);
Ok(())
}
#[inline]
fn set_offset(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let mc = value.to_primitive(context, PreferredType::String)?;
if let Some(string) = mc.as_string() {
self.offset = Some(string.clone());
} else {
return Err(JsNativeError::typ()
.with_message("ToPrimativeAndRequireString must be of type String.")
.into());
}
self.bit_map.set(FieldMap::OFFSET, true);
Ok(())
}
#[inline]
fn set_era(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let mc = value.to_primitive(context, PreferredType::String)?;
if let Some(string) = mc.as_string() {
self.era = Some(string.clone());
} else {
return Err(JsNativeError::typ()
.with_message("ToPrimativeAndRequireString must be of type String.")
.into());
}
self.bit_map.set(FieldMap::ERA, true);
Ok(())
}
#[inline]
fn set_era_year(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> {
let ey = super::to_integer_with_truncation(value, context)?;
self.era_year = Some(ey);
self.bit_map.set(FieldMap::ERA_YEAR, true);
Ok(())
}
#[inline]
fn set_time_zone(&mut self, value: &JsValue) {
let tz = value.as_string().cloned();
self.time_zone = tz;
self.bit_map.set(FieldMap::TIME_ZONE, true);
}
}
impl TemporalFields {
// TODO: Shift to JsString or utf16 over String.
/// A method for creating a Native representation for `TemporalFields` from
/// a `JsObject`.
///
/// This is the equivalant to Abstract Operation 13.46 `PrepareTemporalFields`
pub(crate) fn from_js_object(
fields: &JsObject,
field_names: &mut Vec<String>,
required_fields: &mut Vec<String>, // None when Partial
extended_fields: Option<Vec<(String, bool)>>,
partial: bool,
dup_behaviour: Option<JsString>,
context: &mut Context<'_>,
) -> JsResult<Self> {
// 1. If duplicateBehaviour is not present, set duplicateBehaviour to throw.
let dup_option = dup_behaviour.unwrap_or_else(|| js_string!("throw"));
// 2. Let result be OrdinaryObjectCreate(null).
let mut result = Self::default();
// 3. Let any be false.
let mut any = false;
// 4. If extraFieldDescriptors is present, then
if let Some(extra_fields) = extended_fields {
for (field_name, required) in extra_fields {
// a. For each Calendar Field Descriptor Record desc of extraFieldDescriptors, do
// i. Assert: fieldNames does not contain desc.[[Property]].
// ii. Append desc.[[Property]] to fieldNames.
field_names.push(field_name.clone());
// iii. If desc.[[Required]] is true and requiredFields is a List, then
if required && !partial {
// 1. Append desc.[[Property]] to requiredFields.
required_fields.push(field_name);
}
}
}
// 5. Let sortedFieldNames be SortStringListByCodeUnit(fieldNames).
// 6. Let previousProperty be undefined.
let mut dups_map = FxHashSet::default();
// 7. For each property name property of sortedFieldNames, do
for field in &*field_names {
// a. If property is one of "constructor" or "__proto__", then
if field.as_str() == "constructor" || field.as_str() == "__proto__" {
// i. Throw a RangeError exception.
return Err(JsNativeError::range()
.with_message("constructor or proto is out of field range.")
.into());
}
let new_value = dups_map.insert(field);
// b. If property is not equal to previousProperty, then
if new_value {
// i. Let value be ? Get(fields, property).
let value =
fields.get(PropertyKey::from(JsString::from(field.clone())), context)?;
// ii. If value is not undefined, then
if !value.is_undefined() {
// 1. Set any to true.
any = true;
// 2. If property is in the Property column of Table 17 and there is a Conversion value in the same row, then
// a. Let Conversion be the Conversion value of the same row.
// b. If Conversion is ToIntegerWithTruncation, then
// i. Set value to ? ToIntegerWithTruncation(value).
// ii. Set value to 𝔽(value).
// c. Else if Conversion is ToPositiveIntegerWithTruncation, then
// i. Set value to ? ToPositiveIntegerWithTruncation(value).
// ii. Set value to 𝔽(value).
// d. Else,
// i. Assert: Conversion is ToPrimitiveAndRequireString.
// ii. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings.
// iii. Set value to ? ToPrimitive(value, string).
// iv. If value is not a String, throw a TypeError exception.
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
result.set_field_value(field, &value, context)?;
// iii. Else if requiredFields is a List, then
} else if !partial {
// 1. If requiredFields contains property, then
if required_fields.contains(field) {
// a. Throw a TypeError exception.
return Err(JsNativeError::typ()
.with_message("A required TemporalField was not provided.")
.into());
}
// NOTE: Values set to a default on init.
// 2. If property is in the Property column of Table 17, then
// a. Set value to the corresponding Default value of the same row.
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
}
// c. Else if duplicateBehaviour is throw, then
} else if dup_option.to_std_string_escaped() == "throw" {
// i. Throw a RangeError exception.
return Err(JsNativeError::range()
.with_message("Cannot have a duplicate field")
.into());
}
// d. Set previousProperty to property.
}
// 8. If requiredFields is partial and any is false, then
if partial && !any {
// a. Throw a TypeError exception.
return Err(JsNativeError::range()
.with_message("requiredFields cannot be partial when any is false")
.into());
}
// 9. Return result.
Ok(result)
}
/// Convert a `TemporalFields` struct into a `JsObject`.
pub(crate) fn as_object(&self, context: &mut Context<'_>) -> JsResult<JsObject> {
let obj = JsObject::with_null_proto();
for bit in self.bit_map.iter() {
match bit {
FieldMap::YEAR => {
obj.create_data_property_or_throw(
js_string!("year"),
self.year.map_or(JsValue::undefined(), JsValue::from),
context,
)?;
}
FieldMap::MONTH => {
obj.create_data_property_or_throw(
js_string!("month"),
self.month.map_or(JsValue::undefined(), JsValue::from),
context,
)?;
}
FieldMap::MONTH_CODE => {
obj.create_data_property_or_throw(
js_string!("monthCode"),
self.month_code
.as_ref()
.map_or(JsValue::undefined(), |f| f.clone().into()),
context,
)?;
}
FieldMap::DAY => {
obj.create_data_property(
js_string!("day"),
self.day().map_or(JsValue::undefined(), JsValue::from),
context,
)?;
}
FieldMap::HOUR => {
obj.create_data_property(js_string!("hour"), self.hour, context)?;
}
FieldMap::MINUTE => {
obj.create_data_property(js_string!("minute"), self.minute, context)?;
}
FieldMap::SECOND => {
obj.create_data_property_or_throw(js_string!("second"), self.second, context)?;
}
FieldMap::MILLISECOND => {
obj.create_data_property_or_throw(
js_string!("millisecond"),
self.millisecond,
context,
)?;
}
FieldMap::MICROSECOND => {
obj.create_data_property_or_throw(
js_string!("microsecond"),
self.microsecond,
context,
)?;
}
FieldMap::NANOSECOND => {
obj.create_data_property_or_throw(
js_string!("nanosecond"),
self.nanosecond,
context,
)?;
}
FieldMap::OFFSET => {
obj.create_data_property_or_throw(
js_string!("offset"),
self.offset
.as_ref()
.map_or(JsValue::undefined(), |s| s.clone().into()),
context,
)?;
}
FieldMap::ERA => {
obj.create_data_property_or_throw(
js_string!("era"),
self.era
.as_ref()
.map_or(JsValue::undefined(), |s| s.clone().into()),
context,
)?;
}
FieldMap::ERA_YEAR => {
obj.create_data_property_or_throw(
js_string!("eraYear"),
self.era_year.map_or(JsValue::undefined(), JsValue::from),
context,
)?;
}
FieldMap::TIME_ZONE => {
obj.create_data_property_or_throw(
js_string!("timeZone"),
self.time_zone
.as_ref()
.map_or(JsValue::undefined(), |s| s.clone().into()),
context,
)?;
}
_ => unreachable!(),
}
}
Ok(obj)
}
// Note placeholder until overflow is implemented on `ICU4x`'s Date<Iso>.
/// A function to regulate the current `TemporalFields` according to the overflow value
pub(crate) fn regulate(&mut self, overflow: ArithmeticOverflow) -> JsResult<()> {
if let (Some(year), Some(month), Some(day)) = (self.year(), self.month(), self.day()) {
match overflow {
ArithmeticOverflow::Constrain => {
let m = month.clamp(1, 12);
let days_in_month = super::calendar::utils::iso_days_in_month(year, month);
let d = day.clamp(1, days_in_month);
self.month = Some(m);
self.day = Some(d);
}
ArithmeticOverflow::Reject => {
return Err(JsNativeError::range()
.with_message("TemporalFields is out of a valid range.")
.into())
}
}
}
Ok(())
}
pub(crate) fn regulate_year_month(&mut self, overflow: ArithmeticOverflow) {
match self.month {
Some(month) if overflow == ArithmeticOverflow::Constrain => {
let m = month.clamp(1, 12);
self.month = Some(m);
}
_ => {}
}
}
/// Resolve the month and monthCode on this `TemporalFields`.
pub(crate) fn iso_resolve_month(&mut self) -> JsResult<()> {
if self.month_code.is_none() {
if self.month.is_some() {
return Ok(());
}
return Err(JsNativeError::range()
.with_message("month and MonthCode values cannot both be undefined.")
.into());
}
let unresolved_month_code = self
.month_code
.as_ref()
.expect("monthCode must exist at this point.");
let month_code_integer = month_code_to_integer(unresolved_month_code)?;
let new_month = match self.month {
Some(month) if month != month_code_integer => {
return Err(JsNativeError::range()
.with_message("month and monthCode cannot be resolved.")
.into())
}
_ => month_code_integer,
};
self.month = Some(new_month);
Ok(())
}
}
fn month_code_to_integer(mc: &JsString) -> JsResult<i32> {
match mc.to_std_string_escaped().as_str() {
"M01" => Ok(1),
"M02" => Ok(2),
"M03" => Ok(3),
"M04" => Ok(4),
"M05" => Ok(5),
"M06" => Ok(6),
"M07" => Ok(7),
"M08" => Ok(8),
"M09" => Ok(9),
"M10" => Ok(10),
"M11" => Ok(11),
"M12" => Ok(12),
"M13" => Ok(13),
_ => Err(JsNativeError::range()
.with_message("monthCode is not within the valid values.")
.into()),
}
}

784
boa_engine/src/builtins/temporal/instant/mod.rs

@ -0,0 +1,784 @@
//! Boa's implementation of ECMAScript's `Temporal.Instant` builtin object.
#![allow(dead_code)]
use crate::{
builtins::{
options::{get_option, get_options_object, RoundingMode},
temporal::{
duration::{DateDuration, TimeDuration},
options::{
get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup,
},
},
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
object::{internal_methods::get_prototype_from_constructor, ObjectData},
property::Attribute,
realm::Realm,
string::{common::StaticJsStrings, utf16},
Context, JsArgs, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
use super::{duration, ns_max_instant, ns_min_instant, MIS_PER_DAY, MS_PER_DAY, NS_PER_DAY};
const NANOSECONDS_PER_SECOND: i64 = 10_000_000_000;
const NANOSECONDS_PER_MINUTE: i64 = 600_000_000_000;
const NANOSECONDS_PER_HOUR: i64 = 36_000_000_000_000;
/// The `Temporal.Instant` object.
#[derive(Debug, Clone)]
pub struct Instant {
pub(crate) nanoseconds: JsBigInt,
}
impl BuiltInObject for Instant {
const NAME: JsString = StaticJsStrings::INSTANT;
}
impl IntrinsicObject for Instant {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
let get_seconds = BuiltInBuilder::callable(realm, Self::get_epoc_seconds)
.name(js_string!("get epochSeconds"))
.build();
let get_millis = BuiltInBuilder::callable(realm, Self::get_epoc_milliseconds)
.name(js_string!("get epochMilliseconds"))
.build();
let get_micros = BuiltInBuilder::callable(realm, Self::get_epoc_microseconds)
.name(js_string!("get epochMicroseconds"))
.build();
let get_nanos = BuiltInBuilder::callable(realm, Self::get_epoc_nanoseconds)
.name(js_string!("get epochNanoseconds"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.accessor(
utf16!("epochSeconds"),
Some(get_seconds),
None,
Attribute::default(),
)
.accessor(
utf16!("epochMilliseconds"),
Some(get_millis),
None,
Attribute::default(),
)
.accessor(
utf16!("epochMicroseconds"),
Some(get_micros),
None,
Attribute::default(),
)
.accessor(
utf16!("epochNanoseconds"),
Some(get_nanos),
None,
Attribute::default(),
)
.method(Self::add, js_string!("add"), 1)
.method(Self::subtract, js_string!("subtract"), 1)
.method(Self::until, js_string!("until"), 2)
.method(Self::since, js_string!("since"), 2)
.method(Self::round, js_string!("round"), 1)
.method(Self::equals, js_string!("equals"), 1)
.method(Self::to_zoned_date_time, js_string!("toZonedDateTime"), 1)
.method(
Self::to_zoned_date_time_iso,
js_string!("toZonedDateTimeISO"),
1,
)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for Instant {
const LENGTH: usize = 1;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::instant;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, then
if new_target.is_undefined() {
// a. Throw a TypeError exception.
return Err(JsNativeError::typ()
.with_message("Temporal.Instant new target cannot be undefined.")
.into());
};
// 2. Let epochNanoseconds be ? ToBigInt(epochNanoseconds).
let epoch_nanos = args.get_or_undefined(0).to_bigint(context)?;
// 3. If ! IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception.
if !is_valid_epoch_nanos(&epoch_nanos) {
return Err(JsNativeError::range()
.with_message("Temporal.Instant must have a valid epochNanoseconds.")
.into());
};
// 4. Return ? CreateTemporalInstant(epochNanoseconds, NewTarget).
create_temporal_instant(epoch_nanos, Some(new_target.clone()), context)
}
}
// -- Instant method implementations --
impl Instant {
/// 8.3.3 get Temporal.Instant.prototype.epochSeconds
pub(crate) fn get_epoc_seconds(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Let ns be instant.[[Nanoseconds]].
let ns = &instant.nanoseconds;
// 4. Let s be floor(ℝ(ns) / 10e9).
let s = (ns.to_f64() / 10e9).floor();
// 5. Return 𝔽(s).
Ok(s.into())
}
/// 8.3.4 get Temporal.Instant.prototype.epochMilliseconds
pub(crate) fn get_epoc_milliseconds(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Let ns be instant.[[Nanoseconds]].
let ns = &instant.nanoseconds;
// 4. Let ms be floor(ℝ(ns) / 106).
let ms = (ns.to_f64() / 10e6).floor();
// 5. Return 𝔽(ms).
Ok(ms.into())
}
/// 8.3.5 get Temporal.Instant.prototype.epochMicroseconds
pub(crate) fn get_epoc_microseconds(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Let ns be instant.[[Nanoseconds]].
let ns = &instant.nanoseconds;
// 4. Let µs be floor(ℝ(ns) / 103).
let micro_s = (ns.to_f64() / 10e3).floor();
// 5. Return ℤ(µs).
let big_int = JsBigInt::try_from(micro_s).map_err(|_| {
JsNativeError::typ().with_message("Could not convert microseconds to JsBigInt value")
})?;
Ok(big_int.into())
}
/// 8.3.6 get Temporal.Instant.prototype.epochNanoseconds
pub(crate) fn get_epoc_nanoseconds(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Let ns be instant.[[Nanoseconds]].
let ns = &instant.nanoseconds;
// 4. Return ns.
Ok(ns.clone().into())
}
/// 8.3.7 `Temporal.Instant.prototype.add ( temporalDurationLike )`
pub(crate) fn add(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Return ? AddDurationToOrSubtractDurationFromInstant(add, instant, temporalDurationLike).
let temporal_duration_like = args.get_or_undefined(0);
add_or_subtract_duration_from_instant(true, instant, temporal_duration_like, context)
}
/// 8.3.8 `Temporal.Instant.prototype.subtract ( temporalDurationLike )`
pub(crate) fn subtract(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Return ? AddDurationToOrSubtractDurationFromInstant(subtract, instant, temporalDurationLike).
let temporal_duration_like = args.get_or_undefined(0);
add_or_subtract_duration_from_instant(false, instant, temporal_duration_like, context)
}
/// 8.3.9 `Temporal.Instant.prototype.until ( other [ , options ] )`
pub(crate) fn until(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Return ? DifferenceTemporalInstant(until, instant, other, options).
let other = args.get_or_undefined(0);
let option = args.get_or_undefined(1);
diff_temporal_instant(true, instant, other, option, context)
}
/// 8.3.10 `Temporal.Instant.prototype.since ( other [ , options ] )`
pub(crate) fn since(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Return ? DifferenceTemporalInstant(since, instant, other, options).
let other = args.get_or_undefined(0);
let option = args.get_or_undefined(1);
diff_temporal_instant(false, instant, other, option, context)
}
/// 8.3.11 `Temporal.Instant.prototype.round ( roundTo )`
pub(crate) fn round(
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
let round_to = args.get_or_undefined(0);
// 3. If roundTo is undefined, then
if round_to.is_undefined() {
// a. Throw a TypeError exception.
return Err(JsNativeError::typ()
.with_message("roundTo cannot be undefined.")
.into());
};
// 4. If Type(roundTo) is String, then
let round_to = if round_to.is_string() {
// a. Let paramString be roundTo.
let param_string = round_to
.as_string()
.expect("roundTo is confirmed to be a string here.");
// b. Set roundTo to OrdinaryObjectCreate(null).
let new_round_to = JsObject::with_null_proto();
// c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit"), paramString).
new_round_to.create_data_property_or_throw(
utf16!("smallestUnit"),
param_string.clone(),
context,
)?;
new_round_to
// 5. Else,
} else {
// a. Set roundTo to ? GetOptionsObject(roundTo).
get_options_object(round_to)?
};
// 6. NOTE: The following steps read options and perform independent validation in
// alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode").
// 7. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo).
let rounding_increment = get_temporal_rounding_increment(&round_to, context)?;
// 8. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand").
let rounding_mode =
get_option(&round_to, utf16!("roundingMode"), context)?.unwrap_or_default();
// 9. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit"), time, required).
let smallest_unit = get_temporal_unit(
&round_to,
utf16!("smallestUnit"),
TemporalUnitGroup::Time,
None,
context,
)?
.ok_or_else(|| JsNativeError::range().with_message("smallestUnit cannot be undefined."))?;
let maximum = match smallest_unit {
// 10. If smallestUnit is "hour"), then
// a. Let maximum be HoursPerDay.
TemporalUnit::Hour => 24,
// 11. Else if smallestUnit is "minute"), then
// a. Let maximum be MinutesPerHour × HoursPerDay.
TemporalUnit::Minute => 14400,
// 12. Else if smallestUnit is "second"), then
// a. Let maximum be SecondsPerMinute × MinutesPerHour × HoursPerDay.
TemporalUnit::Second => 86400,
// 13. Else if smallestUnit is "millisecond"), then
// a. Let maximum be ℝ(msPerDay).
TemporalUnit::Millisecond => i64::from(MS_PER_DAY),
// 14. Else if smallestUnit is "microsecond"), then
// a. Let maximum be 10^3 × ℝ(msPerDay).
TemporalUnit::Microsecond => MIS_PER_DAY,
// 15. Else,
// a. Assert: smallestUnit is "nanosecond".
// b. Let maximum be nsPerDay.
TemporalUnit::Nanosecond => NS_PER_DAY,
// unreachable here functions as 15.a.
_ => unreachable!(),
};
// 16. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, true).
super::validate_temporal_rounding_increment(rounding_increment, maximum as f64, true)?;
// 17. Let roundedNs be RoundTemporalInstant(instant.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode).
let rounded_ns = round_temporal_instant(
&instant.nanoseconds,
rounding_increment,
smallest_unit,
rounding_mode,
)?;
// 18. Return ! CreateTemporalInstant(roundedNs).
create_temporal_instant(rounded_ns, None, context)
}
/// 8.3.12 `Temporal.Instant.prototype.equals ( other )`
pub(crate) fn equals(
this: &JsValue,
args: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let instant be the this value.
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
// 4. If instant.[[Nanoseconds]] ≠ other.[[Nanoseconds]], return false.
// 5. Return true.
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message("this value of Instant must be an object.")
})?;
let instant = o.as_instant().ok_or_else(|| {
JsNativeError::typ().with_message("the this object must be an instant object.")
})?;
// 3. Set other to ? ToTemporalInstant(other).
let other = args.get_or_undefined(0);
let other_instant = to_temporal_instant(other)?;
if instant.nanoseconds != other_instant.nanoseconds {
return Ok(false.into());
}
Ok(true.into())
}
/// 8.3.17 `Temporal.Instant.prototype.toZonedDateTime ( item )`
pub(crate) fn to_zoned_date_time(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// TODO: Complete
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// 8.3.18 `Temporal.Instant.prototype.toZonedDateTimeISO ( timeZone )`
pub(crate) fn to_zoned_date_time_iso(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// TODO Complete
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
}
// -- Instant Abstract Operations --
/// 8.5.1 `IsValidEpochNanoseconds ( epochNanoseconds )`
#[inline]
fn is_valid_epoch_nanos(epoch_nanos: &JsBigInt) -> bool {
// 1. Assert: Type(epochNanoseconds) is BigInt.
// 2. If ℝ(epochNanoseconds) < nsMinInstant or ℝ(epochNanoseconds) > nsMaxInstant, then
if epoch_nanos.to_f64() < ns_min_instant().to_f64()
|| epoch_nanos.to_f64() > ns_max_instant().to_f64()
{
// a. Return false.
return false;
}
// 3. Return true.
true
}
/// 8.5.2 `CreateTemporalInstant ( epochNanoseconds [ , newTarget ] )`
#[inline]
fn create_temporal_instant(
epoch_nanos: JsBigInt,
new_target: Option<JsValue>,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Assert: ! IsValidEpochNanoseconds(epochNanoseconds) is true.
assert!(is_valid_epoch_nanos(&epoch_nanos));
// 2. If newTarget is not present, set newTarget to %Temporal.Instant%.
let new_target = new_target.unwrap_or_else(|| {
context
.realm()
.intrinsics()
.constructors()
.instant()
.constructor()
.into()
});
// 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Instant.prototype%"), « [[InitializedTemporalInstant]], [[Nanoseconds]] »).
let proto =
get_prototype_from_constructor(&new_target, StandardConstructors::instant, context)?;
// 4. Set object.[[Nanoseconds]] to epochNanoseconds.
let obj = JsObject::from_proto_and_data(
proto,
ObjectData::instant(Instant {
nanoseconds: epoch_nanos,
}),
);
// 5. Return object.
Ok(obj.into())
}
/// 8.5.3 `ToTemporalInstant ( item )`
#[inline]
fn to_temporal_instant(_: &JsValue) -> JsResult<Instant> {
// TODO: Need to implement parsing.
Err(JsNativeError::error()
.with_message("Instant parsing is not yet implemented.")
.into())
}
/// 8.5.6 `AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds )`
#[inline]
fn add_instant(
epoch_nanos: &JsBigInt,
hours: i32,
minutes: i32,
seconds: i32,
millis: i32,
micros: i32,
nanos: i32,
) -> JsResult<JsBigInt> {
let result = JsBigInt::add_n(&[
JsBigInt::mul(
&JsBigInt::from(hours),
&JsBigInt::from(NANOSECONDS_PER_HOUR),
),
JsBigInt::mul(
&JsBigInt::from(minutes),
&JsBigInt::from(NANOSECONDS_PER_MINUTE),
),
JsBigInt::mul(
&JsBigInt::from(seconds),
&JsBigInt::from(NANOSECONDS_PER_SECOND),
),
JsBigInt::mul(&JsBigInt::from(millis), &JsBigInt::from(10_000_000_i32)),
JsBigInt::mul(&JsBigInt::from(micros), &JsBigInt::from(1000_i32)),
JsBigInt::add(&JsBigInt::from(nanos), epoch_nanos),
]);
if !is_valid_epoch_nanos(&result) {
return Err(JsNativeError::range()
.with_message("result is not a valid epoch nanosecond value.")
.into());
}
Ok(result)
}
/// 8.5.7 `DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, roundingMode )`
#[inline]
fn diff_instant(
ns1: &JsBigInt,
ns2: &JsBigInt,
rounding_increment: f64,
smallest_unit: TemporalUnit,
largest_unit: TemporalUnit,
rounding_mode: RoundingMode,
context: &mut Context<'_>,
) -> JsResult<duration::DurationRecord> {
// 1. Let difference be ℝ(ns2) - ℝ(ns1).
let difference = JsBigInt::sub(ns1, ns2);
// 2. Let nanoseconds be remainder(difference, 1000).
let nanoseconds = JsBigInt::rem(&difference, &JsBigInt::from(1000));
// 3. Let microseconds be remainder(truncate(difference / 1000), 1000).
let truncated_micro = JsBigInt::try_from((&difference.to_f64() / 1000_f64).trunc())
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
let microseconds = JsBigInt::rem(&truncated_micro, &JsBigInt::from(1000));
// 4. Let milliseconds be remainder(truncate(difference / 106), 1000).
let truncated_milli = JsBigInt::try_from((&difference.to_f64() / 1_000_000_f64).trunc())
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
let milliseconds = JsBigInt::rem(&truncated_milli, &JsBigInt::from(1000));
// 5. Let seconds be truncate(difference / 109).
let seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc();
// 6. Let roundResult be ! RoundDuration(0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, roundingIncrement, smallestUnit, largestUnit, roundingMode).
let mut roundable_duration = duration::DurationRecord::new(
DateDuration::default(),
TimeDuration::new(
0.0,
0.0,
seconds,
milliseconds.to_f64(),
microseconds.to_f64(),
nanoseconds.to_f64(),
),
);
let _rem = roundable_duration.round_duration(
rounding_increment,
smallest_unit,
rounding_mode,
None,
context,
)?;
// 7. Assert: roundResult.[[Days]] is 0.
assert_eq!(roundable_duration.days() as i32, 0);
// 8. Return ! BalanceTimeDuration(0, roundResult.[[Hours]], roundResult.[[Minutes]],
// roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]],
// roundResult.[[Nanoseconds]], largestUnit).
roundable_duration.balance_time_duration(largest_unit, None)?;
Ok(roundable_duration)
}
/// 8.5.8 `RoundTemporalInstant ( ns, increment, unit, roundingMode )`
#[inline]
fn round_temporal_instant(
ns: &JsBigInt,
increment: f64,
unit: TemporalUnit,
rounding_mode: RoundingMode,
) -> JsResult<JsBigInt> {
let increment_ns = match unit {
// 1. If unit is "hour"), then
TemporalUnit::Hour => {
// a. Let incrementNs be increment × 3.6 × 10^12.
increment as i64 * NANOSECONDS_PER_HOUR
}
// 2. Else if unit is "minute"), then
TemporalUnit::Minute => {
// a. Let incrementNs be increment × 6 × 10^10.
increment as i64 * NANOSECONDS_PER_MINUTE
}
// 3. Else if unit is "second"), then
TemporalUnit::Second => {
// a. Let incrementNs be increment × 10^9.
increment as i64 * NANOSECONDS_PER_SECOND
}
// 4. Else if unit is "millisecond"), then
TemporalUnit::Millisecond => {
// a. Let incrementNs be increment × 10^6.
increment as i64 * 1_000_000
}
// 5. Else if unit is "microsecond"), then
TemporalUnit::Microsecond => {
// a. Let incrementNs be increment × 10^3.
increment as i64 * 1000
}
// 6. Else,
TemporalUnit::Nanosecond => {
// NOTE: We shouldn't have to assert here as `unreachable` asserts instead.
// a. Assert: unit is "nanosecond".
// b. Let incrementNs be increment.
increment as i64
}
_ => unreachable!(),
};
// 7. Return ℤ(RoundNumberToIncrementAsIfPositive(ℝ(ns), incrementNs, roundingMode)).
super::round_to_increment_as_if_positive(ns, increment_ns, rounding_mode)
}
/// 8.5.10 `DifferenceTemporalInstant ( operation, instant, other, options )`
#[inline]
fn diff_temporal_instant(
op: bool,
instant: &Instant,
other: &JsValue,
options: &JsValue,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If operation is since, let sign be -1. Otherwise, let sign be 1.
let sign = if op { 1_f64 } else { -1_f64 };
// 2. Set other to ? ToTemporalInstant(other).
let other = to_temporal_instant(other)?;
// 3. Let resolvedOptions be ? CopyOptions(options).
let resolved_options =
super::snapshot_own_properties(&get_options_object(options)?, None, None, context)?;
// 4. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, time, « », "nanosecond"), "second").
let settings = super::get_diff_settings(
op,
&resolved_options,
TemporalUnitGroup::Time,
&[],
TemporalUnit::Nanosecond,
TemporalUnit::Second,
context,
)?;
// 5. Let result be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[LargestUnit]], settings.[[RoundingMode]]).
let result = diff_instant(
&instant.nanoseconds,
&other.nanoseconds,
settings.3,
settings.0,
settings.1,
settings.2,
context,
)?;
// 6. Return ! CreateTemporalDuration(0, 0, 0, 0, sign × result.[[Hours]], sign × result.[[Minutes]], sign × result.[[Seconds]], sign × result.[[Milliseconds]], sign × result.[[Microseconds]], sign × result.[[Nanoseconds]]).
Ok(duration::create_temporal_duration(
duration::DurationRecord::new(
DateDuration::default(),
TimeDuration::new(
sign * result.hours(),
sign * result.minutes(),
sign * result.seconds(),
sign * result.milliseconds(),
sign * result.microseconds(),
sign * result.nanoseconds(),
),
),
None,
context,
)?
.into())
}
/// 8.5.11 `AddDurationToOrSubtractDurationFromInstant ( operation, instant, temporalDurationLike )`
#[inline]
fn add_or_subtract_duration_from_instant(
op: bool,
instant: &Instant,
temporal_duration_like: &JsValue,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If operation is subtract, let sign be -1. Otherwise, let sign be 1.
let sign = if op { 1 } else { -1 };
// 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike).
let duration = super::to_temporal_duration_record(temporal_duration_like)?;
// 3. If duration.[[Days]] is not 0, throw a RangeError exception.
if duration.days() != 0_f64 {
return Err(JsNativeError::range()
.with_message("DurationDays cannot be 0")
.into());
}
// 4. If duration.[[Months]] is not 0, throw a RangeError exception.
if duration.months() != 0_f64 {
return Err(JsNativeError::range()
.with_message("DurationMonths cannot be 0")
.into());
}
// 5. If duration.[[Weeks]] is not 0, throw a RangeError exception.
if duration.weeks() != 0_f64 {
return Err(JsNativeError::range()
.with_message("DurationWeeks cannot be 0")
.into());
}
// 6. If duration.[[Years]] is not 0, throw a RangeError exception.
if duration.years() != 0_f64 {
return Err(JsNativeError::range()
.with_message("DurationYears cannot be 0")
.into());
}
// 7. Let ns be ? AddInstant(instant.[[Nanoseconds]], sign × duration.[[Hours]],
// sign × duration.[[Minutes]], sign × duration.[[Seconds]], sign × duration.[[Milliseconds]],
// sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]).
let new = add_instant(
&instant.nanoseconds,
sign * duration.hours() as i32,
sign * duration.minutes() as i32,
sign * duration.seconds() as i32,
sign * duration.milliseconds() as i32,
sign * duration.microseconds() as i32,
sign * duration.nanoseconds() as i32,
)?;
// 8. Return ! CreateTemporalInstant(ns).
create_temporal_instant(new, None, context)
}

660
boa_engine/src/builtins/temporal/mod.rs

@ -0,0 +1,660 @@
//! The ECMAScript `Temporal` stage 3 built-in implementation.
//!
//! More information:
//!
//! [spec]: https://tc39.es/proposal-temporal/
mod calendar;
mod date_equations;
mod duration;
mod fields;
mod instant;
mod now;
mod options;
mod plain_date;
mod plain_date_time;
mod plain_month_day;
mod plain_time;
mod plain_year_month;
mod time_zone;
mod zoned_date_time;
#[cfg(feature = "experimental")]
#[cfg(test)]
mod tests;
pub(crate) use fields::TemporalFields;
use self::options::{
get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup,
};
pub use self::{
calendar::*, duration::*, instant::*, now::*, plain_date::*, plain_date_time::*,
plain_month_day::*, plain_time::*, plain_year_month::*, time_zone::*, zoned_date_time::*,
};
use crate::{
builtins::{
iterable::IteratorRecord,
options::{get_option, RoundingMode, UnsignedRoundingMode},
BuiltInBuilder, BuiltInObject, IntrinsicObject,
},
context::intrinsics::Intrinsics,
js_string,
property::Attribute,
realm::Realm,
string::{common::StaticJsStrings, utf16},
value::Type,
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
// Relavant numeric constants
/// Nanoseconds per day constant: 8.64e+13
pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000;
/// Microseconds per day constant: 8.64e+10
pub(crate) const MIS_PER_DAY: i64 = 8_640_000_000;
/// Milliseconds per day constant: 8.64e+7
pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000;
pub(crate) fn ns_max_instant() -> JsBigInt {
JsBigInt::from(i128::from(NS_PER_DAY) * 100_000_000_i128)
}
pub(crate) fn ns_min_instant() -> JsBigInt {
JsBigInt::from(i128::from(NS_PER_DAY) * -100_000_000_i128)
}
// An enum representing common fields across `Temporal` objects.
#[allow(unused)]
pub(crate) enum DateTimeValues {
Year,
Month,
MonthCode,
Week,
Day,
Hour,
Minute,
Second,
Millisecond,
Microsecond,
Nanosecond,
}
/// The [`Temporal`][spec] builtin object.
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-objects
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Temporal;
impl BuiltInObject for Temporal {
const NAME: JsString = StaticJsStrings::TEMPORAL;
}
impl IntrinsicObject for Temporal {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
BuiltInBuilder::with_intrinsic::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("Now"),
realm.intrinsics().objects().now(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("Calendar"),
realm.intrinsics().constructors().calendar().constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("Duration"),
realm.intrinsics().constructors().duration().constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("Instant"),
realm.intrinsics().constructors().instant().constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("PlainDate"),
realm.intrinsics().constructors().plain_date().constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("PlainDateTime"),
realm
.intrinsics()
.constructors()
.plain_date_time()
.constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("PlainMonthDay"),
realm
.intrinsics()
.constructors()
.plain_month_day()
.constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("PlainTime"),
realm.intrinsics().constructors().plain_time().constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("PlainYearMonth"),
realm
.intrinsics()
.constructors()
.plain_year_month()
.constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("TimeZone"),
realm.intrinsics().constructors().time_zone().constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
js_string!("ZonedDateTime"),
realm
.intrinsics()
.constructors()
.zoned_date_time()
.constructor(),
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics.objects().temporal()
}
}
// -- Temporal Abstract Operations --
/// Abstract operation `ToZeroPaddedDecimalString ( n, minLength )`
///
/// The abstract operation `ToZeroPaddedDecimalString` takes arguments `n` (a non-negative integer)
/// and `minLength` (a non-negative integer) and returns a String.
fn to_zero_padded_decimal_string(n: u64, min_length: usize) -> String {
format!("{n:0min_length$}")
}
/// Abstract Operation 13.1 [`IteratorToListOfType`][proposal]
///
/// [proposal]: https://tc39.es/proposal-temporal/#sec-iteratortolistoftype
pub(crate) fn iterator_to_list_of_types(
iterator: &mut IteratorRecord,
element_types: &[Type],
context: &mut Context<'_>,
) -> JsResult<Vec<JsValue>> {
// 1. Let values be a new empty List.
let mut values = Vec::new();
// 2. Let next be true.
// 3. Repeat, while next is not false,
// a. Set next to ? IteratorStep(iteratorRecord).
// b. If next is not false, then
while iterator.step(context)? {
// i. Let nextValue be ? IteratorValue(next).
let next_value = iterator.value(context)?;
// ii. If Type(nextValue) is not an element of elementTypes, then
if element_types.contains(&next_value.get_type()) {
// 1. Let completion be ThrowCompletion(a newly created TypeError object).
let completion = JsNativeError::typ()
.with_message("IteratorNext is not within allowed type values.");
// NOTE: The below should return as we are forcing a ThrowCompletion.
// 2. Return ? IteratorClose(iteratorRecord, completion).
let _never = iterator.close(Err(completion.into()), context)?;
}
// iii. Append nextValue to the end of the List values.
values.push(next_value);
}
// 4. Return values.
Ok(values)
}
/// 13.2 `ISODateToEpochDays ( year, month, date )`
// Note: implemented on IsoDateRecord.
// Abstract Operation 13.3 `EpochDaysToEpochMs`
pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: i32) -> f64 {
f64::from(day).mul_add(f64::from(MS_PER_DAY), f64::from(time))
}
// 13.4 Date Equations
// implemented in temporal/date_equations.rs
// Abstract Operation 13.5 `GetOptionsObject ( options )`
// Implemented in builtin/options.rs
// 13.6 `GetOption ( options, property, type, values, default )`
// Implemented in builtin/options.rs
/// 13.7 `ToTemporalOverflow (options)`
// Now implemented in temporal/options.rs
/// 13.10 `ToTemporalRoundingMode ( normalizedOptions, fallback )`
// Now implemented in builtin/options.rs
// 13.11 `NegateTemporalRoundingMode ( roundingMode )`
// Now implemented in builtin/options.rs
// 13.16 `ToTemporalRoundingIncrement ( normalizedOptions )`
// Now implemented in temporal/options.rs
/// 13.17 `ValidateTemporalRoundingIncrement ( increment, dividend, inclusive )`
#[inline]
pub(crate) fn validate_temporal_rounding_increment(
increment: f64,
dividend: f64,
inclusive: bool,
) -> JsResult<()> {
// 1. If inclusive is true, then
let maximum = if inclusive {
// a. Let maximum be dividend.
dividend
// 2. Else,
} else {
// a. Assert: dividend > 1.
assert!(dividend > 1.0);
// b. Let maximum be dividend - 1.
dividend - 1.0
};
// 3. If increment > maximum, throw a RangeError exception.
if increment > maximum {
return Err(JsNativeError::range()
.with_message("increment is exceeds the range of the allowed maximum.")
.into());
}
// 4. If dividend modulo increment ≠ 0, then
if dividend % increment != 0.0 {
// a. Throw a RangeError exception.
return Err(JsNativeError::range()
.with_message("Temporal rounding increment is not valid.")
.into());
}
// 5. Return unused.
Ok(())
}
/// 13.21 `ToRelativeTemporalObject ( options )`
pub(crate) fn to_relative_temporal_object(
_options: &JsObject,
_context: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::range()
.with_message("not yet implemented.")
.into())
}
// 13.22 `LargerOfTwoTemporalUnits ( u1, u2 )`
// use core::cmp::max
// 13.23 `MaximumTemporalDurationRoundingIncrement ( unit )`
// Implemented on TemporalUnit in temporal/options.rs
// 13.26 `GetUnsignedRoundingMode ( roundingMode, isNegative )`
// Implemented on RoundingMode in builtins/options.rs
/// 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )`
#[inline]
fn apply_unsigned_rounding_mode(
x: f64,
r1: f64,
r2: f64,
unsigned_rounding_mode: UnsignedRoundingMode,
) -> f64 {
// 1. If x is equal to r1, return r1.
if (x - r1).abs() == 0.0 {
return r1;
};
// 2. Assert: r1 < x < r2.
assert!(r1 < x && x < r2);
// 3. Assert: unsignedRoundingMode is not undefined.
// 4. If unsignedRoundingMode is zero, return r1.
if unsigned_rounding_mode == UnsignedRoundingMode::Zero {
return r1;
};
// 5. If unsignedRoundingMode is infinity, return r2.
if unsigned_rounding_mode == UnsignedRoundingMode::Infinity {
return r2;
};
// 6. Let d1 be x – r1.
let d1 = x - r1;
// 7. Let d2 be r2 – x.
let d2 = r2 - x;
// 8. If d1 < d2, return r1.
if d1 < d2 {
return r1;
}
// 9. If d2 < d1, return r2.
if d2 < d1 {
return r2;
}
// 10. Assert: d1 is equal to d2.
assert!((d1 - d2).abs() == 0.0);
// 11. If unsignedRoundingMode is half-zero, return r1.
if unsigned_rounding_mode == UnsignedRoundingMode::HalfZero {
return r1;
};
// 12. If unsignedRoundingMode is half-infinity, return r2.
if unsigned_rounding_mode == UnsignedRoundingMode::HalfInfinity {
return r2;
};
// 13. Assert: unsignedRoundingMode is half-even.
assert!(unsigned_rounding_mode == UnsignedRoundingMode::HalfEven);
// 14. Let cardinality be (r1 / (r2 – r1)) modulo 2.
let cardinality = (r1 / (r2 - r1)) % 2.0;
// 15. If cardinality is 0, return r1.
if cardinality == 0.0 {
return r1;
}
// 16. Return r2.
r2
}
/// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )`
pub(crate) fn round_number_to_increment(
x: f64,
increment: f64,
rounding_mode: RoundingMode,
) -> f64 {
// 1. Let quotient be x / increment.
let mut quotient = x / increment;
// 2. If quotient < 0, then
let is_negative = if quotient < 0_f64 {
// a. Let isNegative be true.
// b. Set quotient to -quotient.
quotient = -quotient;
true
// 3. Else,
} else {
// a. Let isNegative be false.
false
};
// 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative).
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative);
// 5. Let r1 be the largest integer such that r1 ≤ quotient.
let r1 = quotient.ceil();
// 6. Let r2 be the smallest integer such that r2 > quotient.
let r2 = quotient.floor();
// 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
let mut rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode);
// 8. If isNegative is true, set rounded to -rounded.
if is_negative {
rounded = -rounded;
};
// 9. Return rounded × increment.
rounded * increment
}
/// 13.29 `RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode )`
#[inline]
pub(crate) fn round_to_increment_as_if_positive(
ns: &JsBigInt,
increment: i64,
rounding_mode: RoundingMode,
) -> JsResult<JsBigInt> {
// 1. Let quotient be x / increment.
let q = ns.to_f64() / increment as f64;
// 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false).
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false);
// 3. Let r1 be the largest integer such that r1 ≤ quotient.
let r1 = q.trunc();
// 4. Let r2 be the smallest integer such that r2 > quotient.
let r2 = q.trunc() + 1.0;
// 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
let rounded = apply_unsigned_rounding_mode(q, r1, r2, unsigned_rounding_mode);
// 6. Return rounded × increment.
let rounded = JsBigInt::try_from(rounded)
.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?;
Ok(JsBigInt::mul(&rounded, &JsBigInt::from(increment)))
}
/// 13.43 `ToPositiveIntegerWithTruncation ( argument )`
#[inline]
pub(crate) fn to_positive_integer_with_trunc(
value: &JsValue,
context: &mut Context<'_>,
) -> JsResult<i32> {
// 1. Let integer be ? ToIntegerWithTruncation(argument).
let int = to_integer_with_truncation(value, context)?;
// 2. If integer ≤ 0, throw a RangeError exception.
if int <= 0 {
return Err(JsNativeError::range()
.with_message("value is not a positive integer")
.into());
}
// 3. Return integer.
Ok(int)
}
/// 13.44 `ToIntegerWithTruncation ( argument )`
#[inline]
pub(crate) fn to_integer_with_truncation(
value: &JsValue,
context: &mut Context<'_>,
) -> JsResult<i32> {
// 1. Let number be ? ToNumber(argument).
let number = value.to_number(context)?;
// 2. If number is NaN, +∞𝔽 or -∞𝔽, throw a RangeError exception.
if number.is_nan() || number.is_infinite() {
return Err(JsNativeError::range()
.with_message("truncation target must be an integer.")
.into());
}
// 3. Return truncate(ℝ(number)).
Ok(number.trunc() as i32)
}
/// Abstract operation 13.45 `ToIntegerIfIntegral( argument )`
#[inline]
pub(crate) fn to_integer_if_integral(arg: &JsValue, context: &mut Context<'_>) -> JsResult<i32> {
// 1. Let number be ? ToNumber(argument).
// 2. If IsIntegralNumber(number) is false, throw a RangeError exception.
// 3. Return ℝ(number).
if !arg.is_integer() {
return Err(JsNativeError::range()
.with_message("value to convert is not an integral number.")
.into());
}
arg.to_i32(context)
}
// 13.46 `PrepareTemporalFields ( fields, fieldNames, requiredFields [ , duplicateBehaviour ] )`
// See fields.rs
// NOTE: op -> true == until | false == since
/// 13.47 `GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )`
#[inline]
pub(crate) fn get_diff_settings(
op: bool,
options: &JsObject,
unit_group: TemporalUnitGroup,
disallowed_units: &[TemporalUnit],
fallback_smallest_unit: TemporalUnit,
smallest_largest_default_unit: TemporalUnit,
context: &mut Context<'_>,
) -> JsResult<(TemporalUnit, TemporalUnit, RoundingMode, f64)> {
// 1. NOTE: The following steps read options and perform independent validation in alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode").
// 2. Let largestUnit be ? GetTemporalUnit(options, "largestUnit", unitGroup, "auto").
let mut largest_unit =
get_temporal_unit(options, utf16!("largestUnit"), unit_group, None, context)?
.unwrap_or(TemporalUnit::Auto);
// 3. If disallowedUnits contains largestUnit, throw a RangeError exception.
if disallowed_units.contains(&largest_unit) {
return Err(JsNativeError::range()
.with_message("largestUnit is not an allowed unit.")
.into());
}
// 4. Let roundingIncrement be ? ToTemporalRoundingIncrement(options).
let rounding_increment = get_temporal_rounding_increment(options, context)?;
// 5. Let roundingMode be ? ToTemporalRoundingMode(options, "trunc").
let mut rounding_mode =
get_option(options, utf16!("roundingMode"), context)?.unwrap_or(RoundingMode::Trunc);
// 6. If operation is since, then
if !op {
// a. Set roundingMode to ! NegateTemporalRoundingMode(roundingMode).
rounding_mode = rounding_mode.negate();
}
// 7. Let smallestUnit be ? GetTemporalUnit(options, "smallestUnit", unitGroup, fallbackSmallestUnit).
let smallest_unit =
get_temporal_unit(options, utf16!("smallestUnit"), unit_group, None, context)?
.unwrap_or(fallback_smallest_unit);
// 8. If disallowedUnits contains smallestUnit, throw a RangeError exception.
if disallowed_units.contains(&smallest_unit) {
return Err(JsNativeError::range()
.with_message("smallestUnit is not an allowed unit.")
.into());
}
// 9. Let defaultLargestUnit be ! LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit).
let default_largest_unit = core::cmp::max(smallest_largest_default_unit, smallest_unit);
// 10. If largestUnit is "auto", set largestUnit to defaultLargestUnit.
if largest_unit == TemporalUnit::Auto {
largest_unit = default_largest_unit;
}
// 11. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception.
if largest_unit != core::cmp::max(largest_unit, smallest_unit) {
return Err(JsNativeError::range()
.with_message("largestUnit must be larger than smallestUnit")
.into());
}
// 12. Let maximum be ! MaximumTemporalDurationRoundingIncrement(smallestUnit).
let maximum = smallest_unit.to_maximum_rounding_increment();
// 13. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
if let Some(max) = maximum {
validate_temporal_rounding_increment(rounding_increment, f64::from(max), false)?;
}
// 14. Return the Record { [[SmallestUnit]]: smallestUnit, [[LargestUnit]]: largestUnit, [[RoundingMode]]: roundingMode, [[RoundingIncrement]]: roundingIncrement, }.
Ok((
smallest_unit,
largest_unit,
rounding_mode,
rounding_increment,
))
}
// NOTE: used for MergeFields methods. Potentially can be omitted in favor of `TemporalFields`.
/// 14.6 `CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] )`
pub(crate) fn copy_data_properties(
target: &JsObject,
source: &JsValue,
excluded_keys: &Vec<JsString>,
excluded_values: Option<&Vec<JsValue>>,
context: &mut Context<'_>,
) -> JsResult<()> {
// 1. If source is undefined or null, return unused.
if source.is_null_or_undefined() {
return Ok(());
}
// 2. Let from be ! ToObject(source).
let from = source.to_object(context)?;
// 3. Let keys be ? from.[[OwnPropertyKeys]]().
let keys = from.__own_property_keys__(context)?;
// 4. For each element nextKey of keys, do
for next_key in keys {
// a. Let excluded be false.
let mut excluded = false;
// b. For each element e of excludedItemsexcludedKeys, do
for e in excluded_keys {
// i. If SameValue(e, nextKey) is true, then
if next_key.to_string() == e.to_std_string_escaped() {
// 1. Set excluded to true.
excluded = true;
}
}
// c. If excluded is false, then
if !excluded {
// i. Let desc be ? from.[[GetOwnProperty]](nextKey).
let desc = from.__get_own_property__(&next_key, context)?;
// ii. If desc is not undefined and desc.[[Enumerable]] is true, then
match desc {
Some(d)
if d.enumerable()
.expect("enumerable field must be set per spec.") =>
{
// 1. Let propValue be ? Get(from, nextKey).
let prop_value = from.get(next_key.clone(), context)?;
// 2. If excludedValues is present, then
if let Some(values) = excluded_values {
// a. For each element e of excludedValues, do
for e in values {
// i. If SameValue(e, propValue) is true, then
if JsValue::same_value(e, &prop_value) {
// i. Set excluded to true.
excluded = true;
}
}
}
// 3. PerformIf excluded is false, perform ! CreateDataPropertyOrThrow(target, nextKey, propValue).
if !excluded {
target.create_data_property_or_throw(next_key, prop_value, context)?;
}
}
_ => {}
}
}
}
// 5. Return unused.
Ok(())
}
// Note: Deviates from Proposal spec -> proto appears to be always null across the specification.
/// 14.7 `SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] )`
fn snapshot_own_properties(
source: &JsObject,
excluded_keys: Option<Vec<JsString>>,
excluded_values: Option<Vec<JsValue>>,
context: &mut Context<'_>,
) -> JsResult<JsObject> {
// 1. Let copy be OrdinaryObjectCreate(proto).
let copy = JsObject::with_null_proto();
// 2. If excludedKeys is not present, set excludedKeys to « ».
let keys = excluded_keys.unwrap_or_default();
// 3. If excludedValues is not present, set excludedValues to « ».
let values = excluded_values.unwrap_or_default();
// 4. Perform ? CopyDataProperties(copy, source, excludedKeys, excludedValues).
copy_data_properties(&copy, &source.clone().into(), &keys, Some(&values), context)?;
// 5. Return copy.
Ok(copy)
}

188
boa_engine/src/builtins/temporal/now.rs

@ -0,0 +1,188 @@
//! Boa's implementation of `Temporal.Now` ECMAScript Builtin object.
use crate::{
builtins::{
temporal::{create_temporal_time_zone, default_time_zone},
BuiltInBuilder, BuiltInObject, IntrinsicObject,
},
context::intrinsics::Intrinsics,
js_string,
property::Attribute,
realm::Realm,
string::common::StaticJsStrings,
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
use super::{ns_max_instant, ns_min_instant};
use std::time::SystemTime;
/// JavaScript `Temporal.Now` object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Now;
impl IntrinsicObject for Now {
/// Initializes the `Temporal.Now` object.
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
// is an ordinary object.
// has a [[Prototype]] internal slot whose value is %Object.prototype%.
// is not a function object.
// does not have a [[Construct]] internal method; it cannot be used as a constructor with the new operator.
// does not have a [[Call]] internal method; it cannot be invoked as a function.
BuiltInBuilder::with_intrinsic::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_method(Self::time_zone_id, js_string!("timeZoneId"), 0)
.static_method(Self::instant, js_string!("instant"), 0)
.static_method(Self::plain_date_time, js_string!("plainDateTime"), 2)
.static_method(Self::plain_date_time_iso, js_string!("plainDateTimeISO"), 1)
.static_method(Self::zoned_date_time, js_string!("zonedDateTime"), 2)
.static_method(Self::zoned_date_time_iso, js_string!("zonedDateTimeISO"), 1)
.static_method(Self::plain_date, js_string!("plainDate"), 2)
.static_method(Self::plain_date_iso, js_string!("plainDateISO"), 1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics.objects().now()
}
}
impl BuiltInObject for Now {
const NAME: JsString = StaticJsStrings::NOW;
}
impl Now {
/// `Temporal.Now.timeZoneId ( )`
///
/// More information:
/// - [ECMAScript specififcation][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal.now.timezone
#[allow(clippy::unnecessary_wraps)]
fn time_zone_id(
_: &JsValue,
_args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Return ! SystemTimeZone().
Ok(system_time_zone(context).expect("retrieving the system timezone must not fail"))
}
/// `Temporal.Now.instant()`
fn instant(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// `Temporal.Now.plainDateTime()`
fn plain_date_time(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// `Temporal.Now.plainDateTimeISO`
fn plain_date_time_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// `Temporal.Now.zonedDateTime`
fn zoned_date_time(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// `Temporal.Now.zonedDateTimeISO`
fn zoned_date_time_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// `Temporal.Now.plainDate()`
fn plain_date(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// `Temporal.Now.plainDateISO`
fn plain_date_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
}
// -- Temporal.Now abstract operations --
/// 2.3.1 `HostSystemUTCEpochNanoseconds ( global )`
fn host_system_utc_epoch_nanoseconds() -> JsResult<JsBigInt> {
// TODO: Implement `SystemTime::now()` calls for `no_std`
let epoch_nanos = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| JsNativeError::range().with_message(e.to_string()))?
.as_nanos();
Ok(clamp_epoc_nanos(JsBigInt::from(epoch_nanos)))
}
fn clamp_epoc_nanos(ns: JsBigInt) -> JsBigInt {
let max = ns_max_instant();
let min = ns_min_instant();
ns.clamp(min, max)
}
/// 2.3.2 `SystemUTCEpochMilliseconds`
#[allow(unused)]
fn system_utc_epoch_millis() -> JsResult<f64> {
let now = host_system_utc_epoch_nanoseconds()?;
Ok(now.to_f64().div_euclid(1_000_000_f64).floor())
}
/// 2.3.3 `SystemUTCEpochNanoseconds`
#[allow(unused)]
fn system_utc_epoch_nanos() -> JsResult<JsBigInt> {
host_system_utc_epoch_nanoseconds()
}
/// `SystemInstant`
#[allow(unused)]
fn system_instant() {
todo!()
}
/// `SystemDateTime`
#[allow(unused)]
fn system_date_time() {
todo!()
}
/// `SystemZonedDateTime`
#[allow(unused)]
fn system_zoned_date_time() {
todo!()
}
/// Abstract operation `SystemTimeZone ( )`
///
/// More information:
/// - [ECMAScript specififcation][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-systemtimezone
#[allow(unused)]
fn system_time_zone(context: &mut Context<'_>) -> JsResult<JsValue> {
// 1. Let identifier be ! DefaultTimeZone().
let identifier = default_time_zone(context);
// 2. Return ! CreateTemporalTimeZone(identifier).
create_temporal_time_zone(identifier, None, context)
}

384
boa_engine/src/builtins/temporal/options.rs

@ -0,0 +1,384 @@
//! Temporal Option types.
// Implementation Note:
//
// The below Option types are adapted from the types laid out by
// the Temporal proposal's polyfill types that can be found at the
// below link.
//
// https://github.com/tc39/proposal-temporal/blob/main/polyfill/index.d.ts
use crate::{
builtins::options::{get_option, ParsableOptionType},
js_string, Context, JsNativeError, JsObject, JsResult,
};
use std::{fmt, str::FromStr};
// TODO: Expand docs on the below options.
#[inline]
pub(crate) fn get_temporal_rounding_increment(
options: &JsObject,
context: &mut Context<'_>,
) -> JsResult<f64> {
// 1. Let increment be ? GetOption(normalizedOptions, "roundingIncrement", "number", undefined, 1𝔽).
let value = options.get(js_string!("roundingIncrement"), context)?;
let increment = if value.is_undefined() {
1.0
} else {
value.to_number(context)?
};
// 2. If increment is not finite, throw a RangeError exception.
if !increment.is_finite() {
return Err(JsNativeError::range()
.with_message("rounding increment was out of range.")
.into());
}
// 3. Let integerIncrement be truncate(ℝ(increment)).
let integer_increment = increment.trunc();
// 4. If integerIncrement < 1 or integerIncrement > 10^9, throw a RangeError exception.
if (1.0..=1_000_000_000.0).contains(&integer_increment) {
return Err(JsNativeError::range()
.with_message("rounding increment was out of range.")
.into());
}
// 5. Return integerIncrement.
Ok(integer_increment)
}
/// Gets the `TemporalUnit` from an options object.
#[inline]
pub(crate) fn get_temporal_unit(
options: &JsObject,
key: &[u16],
unit_group: TemporalUnitGroup,
extra_values: Option<Vec<TemporalUnit>>,
context: &mut Context<'_>,
) -> JsResult<Option<TemporalUnit>> {
let extra = extra_values.unwrap_or_default();
let mut unit_values = unit_group.group();
unit_values.extend(extra);
let unit = get_option(options, key, context)?;
if let Some(u) = &unit {
if !unit_values.contains(u) {
return Err(JsNativeError::range()
.with_message("TemporalUnit was not part of the valid UnitGroup.")
.into());
}
}
Ok(unit)
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum TemporalUnitGroup {
Date,
Time,
DateTime,
}
impl TemporalUnitGroup {
fn group(self) -> Vec<TemporalUnit> {
use TemporalUnitGroup::{Date, DateTime, Time};
match self {
Date => date_units().collect(),
Time => time_units().collect(),
DateTime => datetime_units().collect(),
}
}
}
fn time_units() -> impl Iterator<Item = TemporalUnit> {
[
TemporalUnit::Hour,
TemporalUnit::Minute,
TemporalUnit::Second,
TemporalUnit::Millisecond,
TemporalUnit::Microsecond,
TemporalUnit::Nanosecond,
]
.iter()
.copied()
}
fn date_units() -> impl Iterator<Item = TemporalUnit> {
[
TemporalUnit::Year,
TemporalUnit::Month,
TemporalUnit::Week,
TemporalUnit::Day,
]
.iter()
.copied()
}
fn datetime_units() -> impl Iterator<Item = TemporalUnit> {
date_units().chain(time_units())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum TemporalUnit {
Auto = 0,
Nanosecond,
Microsecond,
Millisecond,
Second,
Minute,
Hour,
Day,
Week,
Month,
Year,
}
impl TemporalUnit {
pub(crate) fn to_maximum_rounding_increment(self) -> Option<u16> {
use TemporalUnit::{
Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week,
Year,
};
// 1. If unit is "year", "month", "week", or "day", then
// a. Return undefined.
// 2. If unit is "hour", then
// a. Return 24.
// 3. If unit is "minute" or "second", then
// a. Return 60.
// 4. Assert: unit is one of "millisecond", "microsecond", or "nanosecond".
// 5. Return 1000.
match self {
Year | Month | Week | Day => None,
Hour => Some(24),
Minute | Second => Some(60),
Millisecond | Microsecond | Nanosecond => Some(1000),
Auto => unreachable!(),
}
}
}
#[derive(Debug)]
pub(crate) struct ParseTemporalUnitError;
impl fmt::Display for ParseTemporalUnitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not a valid TemporalUnit")
}
}
impl FromStr for TemporalUnit {
type Err = ParseTemporalUnitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"auto" => Ok(Self::Auto),
"year" | "years" => Ok(Self::Year),
"month" | "months" => Ok(Self::Month),
"week" | "weeks" => Ok(Self::Week),
"day" | "days" => Ok(Self::Day),
"hour" | "hours" => Ok(Self::Hour),
"minute" | "minutes" => Ok(Self::Minute),
"second" | "seconds" => Ok(Self::Second),
"millisecond" | "milliseconds" => Ok(Self::Millisecond),
"microsecond" | "microseconds" => Ok(Self::Microsecond),
"nanosecond" | "nanoseconds" => Ok(Self::Nanosecond),
_ => Err(ParseTemporalUnitError),
}
}
}
impl ParsableOptionType for TemporalUnit {}
impl fmt::Display for TemporalUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Auto => "auto",
Self::Year => "constrain",
Self::Month => "month",
Self::Week => "week",
Self::Day => "day",
Self::Hour => "hour",
Self::Minute => "minute",
Self::Second => "second",
Self::Millisecond => "millsecond",
Self::Microsecond => "microsecond",
Self::Nanosecond => "nanosecond",
}
.fmt(f)
}
}
/// `ArithmeticOverflow` can also be used as an
/// assignment overflow and consists of the "constrain"
/// and "reject" options.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ArithmeticOverflow {
Constrain,
Reject,
}
#[derive(Debug)]
pub(crate) struct ParseArithmeticOverflowError;
impl fmt::Display for ParseArithmeticOverflowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not a valid overflow value")
}
}
impl FromStr for ArithmeticOverflow {
type Err = ParseArithmeticOverflowError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"constrain" => Ok(Self::Constrain),
"reject" => Ok(Self::Reject),
_ => Err(ParseArithmeticOverflowError),
}
}
}
impl ParsableOptionType for ArithmeticOverflow {}
impl fmt::Display for ArithmeticOverflow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Constrain => "constrain",
Self::Reject => "reject",
}
.fmt(f)
}
}
/// `Duration` overflow options.
pub(crate) enum DurationOverflow {
Constrain,
Balance,
}
#[derive(Debug)]
pub(crate) struct ParseDurationOverflowError;
impl fmt::Display for ParseDurationOverflowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not a valid duration overflow value")
}
}
impl FromStr for DurationOverflow {
type Err = ParseDurationOverflowError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"constrain" => Ok(Self::Constrain),
"balance" => Ok(Self::Balance),
_ => Err(ParseDurationOverflowError),
}
}
}
impl ParsableOptionType for DurationOverflow {}
impl fmt::Display for DurationOverflow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Constrain => "constrain",
Self::Balance => "balance",
}
.fmt(f)
}
}
/// The disambiguation options for an instant.
pub(crate) enum InstantDisambiguation {
Compatible,
Earlier,
Later,
Reject,
}
#[derive(Debug)]
pub(crate) struct ParseInstantDisambiguationError;
impl fmt::Display for ParseInstantDisambiguationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not a valid instant disambiguation value")
}
}
impl FromStr for InstantDisambiguation {
type Err = ParseInstantDisambiguationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"compatible" => Ok(Self::Compatible),
"earlier" => Ok(Self::Earlier),
"later" => Ok(Self::Later),
"reject" => Ok(Self::Reject),
_ => Err(ParseInstantDisambiguationError),
}
}
}
impl ParsableOptionType for InstantDisambiguation {}
impl fmt::Display for InstantDisambiguation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Compatible => "compatible",
Self::Earlier => "earlier",
Self::Later => "later",
Self::Reject => "reject",
}
.fmt(f)
}
}
/// Offset disambiguation options.
pub(crate) enum OffsetDisambiguation {
Use,
Prefer,
Ignore,
Reject,
}
#[derive(Debug)]
pub(crate) struct ParseOffsetDisambiguationError;
impl fmt::Display for ParseOffsetDisambiguationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("provided string was not a valid offset disambiguation value")
}
}
impl FromStr for OffsetDisambiguation {
type Err = ParseOffsetDisambiguationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"use" => Ok(Self::Use),
"prefer" => Ok(Self::Prefer),
"ignore" => Ok(Self::Ignore),
"reject" => Ok(Self::Reject),
_ => Err(ParseOffsetDisambiguationError),
}
}
}
impl ParsableOptionType for OffsetDisambiguation {}
impl fmt::Display for OffsetDisambiguation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Use => "use",
Self::Prefer => "prefer",
Self::Ignore => "ignore",
Self::Reject => "reject",
}
.fmt(f)
}
}

236
boa_engine/src/builtins/temporal/plain_date/iso.rs

@ -0,0 +1,236 @@
//! An `IsoDateRecord` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
use crate::{
builtins::temporal::{self, TemporalFields},
JsNativeError, JsResult, JsString,
};
use icu_calendar::{Date, Iso};
// TODO: Move ISODateRecord to a more generalized location.
// TODO: Determine whether month/day should be u8 or i32.
/// `IsoDateRecord` serves as an record for the `[[ISOYear]]`, `[[ISOMonth]]`,
/// and `[[ISODay]]` internal fields.
///
/// These fields are used for the `Temporal.PlainDate` object, the
/// `Temporal.YearMonth` object, and the `Temporal.MonthDay` object.
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct IsoDateRecord {
year: i32,
month: i32,
day: i32,
}
// TODO: determine whether the below is neccessary.
impl IsoDateRecord {
pub(crate) const fn year(&self) -> i32 {
self.year
}
pub(crate) const fn month(&self) -> i32 {
self.month
}
pub(crate) const fn day(&self) -> i32 {
self.day
}
}
impl IsoDateRecord {
// TODO: look into using Date<Iso> across the board...TBD.
/// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date<Iso>` struct.
pub(crate) fn from_date_iso(date: Date<Iso>) -> Self {
Self {
year: date.year().number,
month: date.month().ordinal as i32,
day: i32::from(date.days_in_month()),
}
}
}
impl IsoDateRecord {
/// 3.5.2 `CreateISODateRecord`
pub(crate) const fn new(year: i32, month: i32, day: i32) -> Self {
Self { year, month, day }
}
/// 3.5.6 `RegulateISODate`
pub(crate) fn from_unregulated(
year: i32,
month: i32,
day: i32,
overflow: &JsString,
) -> JsResult<Self> {
match overflow.to_std_string_escaped().as_str() {
"constrain" => {
let m = month.clamp(1, 12);
let days_in_month = temporal::calendar::utils::iso_days_in_month(year, month);
let d = day.clamp(1, days_in_month);
Ok(Self::new(year, m, d))
}
"reject" => {
let date = Self::new(year, month, day);
if !date.is_valid() {
return Err(JsNativeError::range()
.with_message("not a valid ISO date.")
.into());
}
Ok(date)
}
_ => unreachable!(),
}
}
/// 12.2.35 `ISODateFromFields ( fields, overflow )`
///
/// Note: fields.month must be resolved prior to using `from_temporal_fields`
pub(crate) fn from_temporal_fields(
fields: &TemporalFields,
overflow: &JsString,
) -> JsResult<Self> {
Self::from_unregulated(
fields.year().expect("Cannot fail per spec"),
fields.month().expect("cannot fail after resolution"),
fields.day().expect("cannot fail per spec"),
overflow,
)
}
/// Create a Month-Day record from a `TemporalFields` object.
pub(crate) fn month_day_from_temporal_fields(
fields: &TemporalFields,
overflow: &JsString,
) -> JsResult<Self> {
match fields.year() {
Some(year) => Self::from_unregulated(
year,
fields.month().expect("month must exist."),
fields.day().expect("cannot fail per spec"),
overflow,
),
None => Self::from_unregulated(
1972,
fields.month().expect("cannot fail per spec"),
fields.day().expect("cannot fail per spec."),
overflow,
),
}
}
/// Within `YearMonth` valid limits
pub(crate) const fn within_year_month_limits(&self) -> bool {
if self.year < -271_821 || self.year > 275_760 {
return false;
}
if self.year == -271_821 && self.month < 4 {
return false;
}
if self.year == 275_760 && self.month > 9 {
return true;
}
true
}
/// 3.5.5 `DifferenceISODate`
pub(crate) fn diff_iso_date(
&self,
o: &Self,
largest_unit: &JsString,
) -> JsResult<temporal::duration::DurationRecord> {
debug_assert!(self.is_valid());
// TODO: Implement on `ICU4X`.
Err(JsNativeError::range()
.with_message("not yet implemented.")
.into())
}
/// 3.5.7 `IsValidISODate`
pub(crate) fn is_valid(&self) -> bool {
if self.month < 1 || self.month > 12 {
return false;
}
let days_in_month = temporal::calendar::utils::iso_days_in_month(self.year, self.month);
if self.day < 1 || self.day > days_in_month {
return false;
}
true
}
/// 13.2 `IsoDateToEpochDays`
pub(crate) fn as_epoch_days(&self) -> i32 {
// 1. Let resolvedYear be year + floor(month / 12).
let resolved_year = self.year + (f64::from(self.month) / 12_f64).floor() as i32;
// 2. Let resolvedMonth be month modulo 12.
let resolved_month = self.month % 12;
// 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1.
let year_t = temporal::date_equations::epoch_time_for_year(resolved_year);
let month_t = temporal::date_equations::epoch_time_for_month_given_year(
resolved_month,
resolved_year,
);
// 4. Return EpochTimeToDayNumber(t) + date - 1.
temporal::date_equations::epoch_time_to_day_number(year_t + month_t) + self.day - 1
}
// NOTE: Implementing as mut self so balance is applied to self, but TBD.
/// 3.5.8 `BalanceIsoDate`
pub(crate) fn balance(&mut self) {
let epoch_days = self.as_epoch_days();
let ms = temporal::epoch_days_to_epoch_ms(epoch_days, 0);
// Balance current values
self.year = temporal::date_equations::epoch_time_to_epoch_year(ms);
self.month = temporal::date_equations::epoch_time_to_month_in_year(ms);
self.day = temporal::date_equations::epoch_time_to_date(ms);
}
// NOTE: Used in AddISODate only, so could possibly be deleted in the future.
/// 9.5.4 `BalanceISOYearMonth ( year, month )`
pub(crate) fn balance_year_month(&mut self) {
self.year += (self.month - 1) / 12;
self.month = ((self.month - 1) % 12) + 1;
}
/// 3.5.11 `AddISODate ( year, month, day, years, months, weeks, days, overflow )`
pub(crate) fn add_iso_date(
&self,
years: i32,
months: i32,
weeks: i32,
days: i32,
overflow: &JsString,
) -> JsResult<Self> {
// 1. Assert: year, month, day, years, months, weeks, and days are integers.
// 2. Assert: overflow is either "constrain" or "reject".
let mut intermediate = Self::new(self.year + years, self.month + months, 0);
// 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months).
intermediate.balance_year_month();
// 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow).
let mut new_date = Self::from_unregulated(
intermediate.year(),
intermediate.month(),
self.day,
overflow,
)?;
// 5. Set days to days + 7 × weeks.
// 6. Let d be intermediate.[[Day]] + days.
let additional_days = days + (weeks * 7);
new_date.day += additional_days;
// 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
new_date.balance();
Ok(new_date)
}
}

567
boa_engine/src/builtins/temporal/plain_date/mod.rs

@ -0,0 +1,567 @@
//! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object.
#![allow(dead_code, unused_variables)]
use crate::{
builtins::{
options::{get_option, get_options_object},
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
object::{internal_methods::get_prototype_from_constructor, ObjectData},
property::Attribute,
realm::Realm,
string::{common::StaticJsStrings, utf16},
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_parser::temporal::{IsoCursor, TemporalDateTimeString};
use boa_profiler::Profiler;
use super::{options::ArithmeticOverflow, plain_date::iso::IsoDateRecord, plain_date_time};
pub(crate) mod iso;
/// The `Temporal.PlainDate` object.
#[derive(Debug, Clone)]
pub struct PlainDate {
pub(crate) inner: IsoDateRecord,
pub(crate) calendar: JsValue, // Calendar can probably be stored as a JsObject.
}
impl BuiltInObject for PlainDate {
const NAME: JsString = StaticJsStrings::PLAIN_DATE;
}
impl IntrinsicObject for PlainDate {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
let get_calendar_id = BuiltInBuilder::callable(realm, Self::get_calendar_id)
.name(js_string!("get calendarId"))
.build();
let get_year = BuiltInBuilder::callable(realm, Self::get_year)
.name(js_string!("get year"))
.build();
let get_month = BuiltInBuilder::callable(realm, Self::get_month)
.name(js_string!("get month"))
.build();
let get_month_code = BuiltInBuilder::callable(realm, Self::get_month_code)
.name(js_string!("get monthCode"))
.build();
let get_day = BuiltInBuilder::callable(realm, Self::get_day)
.name(js_string!("get day"))
.build();
let get_day_of_week = BuiltInBuilder::callable(realm, Self::get_day_of_week)
.name(js_string!("get dayOfWeek"))
.build();
let get_day_of_year = BuiltInBuilder::callable(realm, Self::get_day_of_year)
.name(js_string!("get dayOfYear"))
.build();
let get_week_of_year = BuiltInBuilder::callable(realm, Self::get_week_of_year)
.name(js_string!("get weekOfYear"))
.build();
let get_year_of_week = BuiltInBuilder::callable(realm, Self::get_year_of_week)
.name(js_string!("get yearOfWeek"))
.build();
let get_days_in_week = BuiltInBuilder::callable(realm, Self::get_days_in_week)
.name(js_string!("get daysInWeek"))
.build();
let get_days_in_month = BuiltInBuilder::callable(realm, Self::get_days_in_month)
.name(js_string!("get daysInMonth"))
.build();
let get_days_in_year = BuiltInBuilder::callable(realm, Self::get_days_in_year)
.name(js_string!("get daysInYear"))
.build();
let get_months_in_year = BuiltInBuilder::callable(realm, Self::get_months_in_year)
.name(js_string!("get monthsInYear"))
.build();
let get_in_leap_year = BuiltInBuilder::callable(realm, Self::get_in_leap_year)
.name(js_string!("get inLeapYear"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.accessor(
utf16!("calendarId"),
Some(get_calendar_id),
None,
Attribute::default(),
)
.accessor(utf16!("year"), Some(get_year), None, Attribute::default())
.accessor(utf16!("month"), Some(get_month), None, Attribute::default())
.accessor(
utf16!("monthCode"),
Some(get_month_code),
None,
Attribute::default(),
)
.accessor(utf16!("day"), Some(get_day), None, Attribute::default())
.accessor(
utf16!("dayOfWeek"),
Some(get_day_of_week),
None,
Attribute::default(),
)
.accessor(
utf16!("dayOfYear"),
Some(get_day_of_year),
None,
Attribute::default(),
)
.accessor(
utf16!("weekOfYear"),
Some(get_week_of_year),
None,
Attribute::default(),
)
.accessor(
utf16!("yearOfWeek"),
Some(get_year_of_week),
None,
Attribute::default(),
)
.accessor(
utf16!("daysInWeek"),
Some(get_days_in_week),
None,
Attribute::default(),
)
.accessor(
utf16!("daysInMonth"),
Some(get_days_in_month),
None,
Attribute::default(),
)
.accessor(
utf16!("daysInYear"),
Some(get_days_in_year),
None,
Attribute::default(),
)
.accessor(
utf16!("monthsInYear"),
Some(get_months_in_year),
None,
Attribute::default(),
)
.accessor(
utf16!("inLeapYear"),
Some(get_in_leap_year),
None,
Attribute::default(),
)
.method(Self::to_plain_year_month, js_string!("toPlainYearMonth"), 0)
.method(Self::to_plain_month_day, js_string!("toPlainMonthDay"), 0)
.method(Self::get_iso_fields, js_string!("getISOFields"), 0)
.method(Self::get_calendar, js_string!("getCalendar"), 0)
.method(Self::add, js_string!("add"), 2)
.method(Self::subtract, js_string!("subtract"), 2)
.method(Self::with, js_string!("with"), 2)
.method(Self::with_calendar, js_string!("withCalendar"), 1)
.method(Self::until, js_string!("until"), 2)
.method(Self::since, js_string!("since"), 2)
.method(Self::equals, js_string!("equals"), 1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for PlainDate {
const LENGTH: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::plain_date;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
if new_target.is_undefined() {
return Err(JsNativeError::typ()
.with_message("NewTarget cannot be undefined.")
.into());
};
let iso_year = super::to_integer_with_truncation(args.get_or_undefined(0), context)?;
let iso_month = super::to_integer_with_truncation(args.get_or_undefined(1), context)?;
let iso_day = super::to_integer_with_truncation(args.get_or_undefined(2), context)?;
let default_calendar = JsValue::from(js_string!("iso8601"));
let calendar_like = args.get(3).unwrap_or(&default_calendar);
let iso = IsoDateRecord::new(iso_year, iso_month, iso_day);
Ok(create_temporal_date(iso, calendar_like.clone(), Some(new_target), context)?.into())
}
}
// -- `PlainDate` getter methods --
impl PlainDate {
fn get_calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn get_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_month(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_month_code(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_day_of_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_day_of_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_week_of_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_year_of_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_days_in_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_days_in_month(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_days_in_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_months_in_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_in_leap_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
}
// ==== `PlainDate.prototype` method implementation ====
impl PlainDate {
fn to_plain_year_month(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn to_plain_month_day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_iso_fields(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_calendar(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn add(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn subtract(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn with(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn with_calendar(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn until(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn since(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn equals(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
}
// -- `PlainDate` Abstract Operations --
// 3.5.2 `CreateIsoDateRecord`
// Implemented on `IsoDateRecord`
/// 3.5.3 `CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] )`
pub(crate) fn create_temporal_date(
iso_date: IsoDateRecord,
calendar: JsValue,
new_target: Option<&JsValue>,
context: &mut Context<'_>,
) -> JsResult<JsObject> {
// 1. If IsValidISODate(isoYear, isoMonth, isoDay) is false, throw a RangeError exception.
if !iso_date.is_valid() {
return Err(JsNativeError::range()
.with_message("Date is not a valid ISO date.")
.into());
};
let iso_date_time = plain_date_time::iso::IsoDateTimeRecord::default()
.with_date(iso_date.year(), iso_date.month(), iso_date.day())
.with_time(12, 0, 0, 0, 0, 0);
// 2. If ISODateTimeWithinLimits(isoYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception.
if iso_date_time.is_valid() {
return Err(JsNativeError::range()
.with_message("Date is not within ISO date time limits.")
.into());
}
// 3. If newTarget is not present, set newTarget to %Temporal.PlainDate%.
let new_target = if let Some(new_target) = new_target {
new_target.clone()
} else {
context
.realm()
.intrinsics()
.constructors()
.plain_date()
.constructor()
.into()
};
// 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainDate.prototype%", « [[InitializedTemporalDate]], [[ISOYear]], [[ISOMonth]], [[ISODay]], [[Calendar]] »).
let prototype =
get_prototype_from_constructor(&new_target, StandardConstructors::plain_date, context)?;
// 5. Set object.[[ISOYear]] to isoYear.
// 6. Set object.[[ISOMonth]] to isoMonth.
// 7. Set object.[[ISODay]] to isoDay.
// 8. Set object.[[Calendar]] to calendar.
let obj = JsObject::from_proto_and_data(
prototype,
ObjectData::plain_date(PlainDate {
inner: iso_date,
calendar,
}),
);
// 9. Return object.
Ok(obj)
}
/// 3.5.4 `ToTemporalDate ( item [ , options ] )`
///
/// Converts an ambiguous `JsValue` into a `PlainDate`
pub(crate) fn to_temporal_date(
item: &JsValue,
options: Option<JsValue>,
context: &mut Context<'_>,
) -> JsResult<PlainDate> {
// 1. If options is not present, set options to undefined.
let options = options.unwrap_or(JsValue::undefined());
// 2. Assert: Type(options) is Object or Undefined.
// 3. If options is not undefined, set options to ? SnapshotOwnProperties(? GetOptionsObject(options), null).
let options_obj = get_options_object(&options)?;
// 4. If Type(item) is Object, then
if let Some(object) = item.as_object() {
// a. If item has an [[InitializedTemporalDate]] internal slot, then
if object.is_plain_date() {
// i. Return item.
let obj = object.borrow();
let date = obj.as_plain_date().expect("obj must be a PlainDate.");
return Ok(PlainDate {
inner: date.inner,
calendar: date.calendar.clone(),
});
// b. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then
} else if object.is_zoned_date_time() {
return Err(JsNativeError::range()
.with_message("ZonedDateTime not yet implemented.")
.into());
// i. Perform ? ToTemporalOverflow(options).
// ii. Let instant be ! CreateTemporalInstant(item.[[Nanoseconds]]).
// iii. Let plainDateTime be ? GetPlainDateTimeFor(item.[[TimeZone]], instant, item.[[Calendar]]).
// iv. Return ! CreateTemporalDate(plainDateTime.[[ISOYear]], plainDateTime.[[ISOMonth]], plainDateTime.[[ISODay]], plainDateTime.[[Calendar]]).
// c. If item has an [[InitializedTemporalDateTime]] internal slot, then
} else if object.is_plain_date_time() {
// i. Perform ? ToTemporalOverflow(options).
let _o = get_option(&options_obj, utf16!("overflow"), context)?
.unwrap_or(ArithmeticOverflow::Constrain);
let obj = object.borrow();
let date_time = obj
.as_plain_date_time()
.expect("obj must be a PlainDateTime");
let iso = date_time.inner.iso_date();
let calendar = date_time.calendar.clone();
drop(obj);
// ii. Return ! CreateTemporalDate(item.[[ISOYear]], item.[[ISOMonth]], item.[[ISODay]], item.[[Calendar]]).
return Ok(PlainDate {
inner: iso,
calendar,
});
}
// d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item).
// e. Let fieldNames be ? CalendarFields(calendar, « "day", "month", "monthCode", "year" »).
// f. Let fields be ? PrepareTemporalFields(item, fieldNames, «»).
// g. Return ? CalendarDateFromFields(calendar, fields, options).
return Err(JsNativeError::error()
.with_message("CalendarDateFields not yet implemented.")
.into());
}
// 5. If item is not a String, throw a TypeError exception.
match item {
JsValue::String(s) => {
// 6. Let result be ? ParseTemporalDateString(item).
let result = TemporalDateTimeString::parse(
false,
&mut IsoCursor::new(&s.to_std_string_escaped()),
)
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
// 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true.
// 8. Let calendar be result.[[Calendar]].
// 9. If calendar is undefined, set calendar to "iso8601".
let identifier = result
.date
.calendar
.map_or_else(|| js_string!("iso8601"), JsString::from);
// 10. If IsBuiltinCalendar(calendar) is false, throw a RangeError exception.
if !super::calendar::is_builtin_calendar(&identifier) {
return Err(JsNativeError::range()
.with_message("not a valid calendar identifier.")
.into());
}
// TODO: impl to ASCII-lowercase on JsStirng
// 11. Set calendar to the ASCII-lowercase of calendar.
// 12. Perform ? ToTemporalOverflow(options).
let _result = get_option(&options_obj, utf16!("overflow"), context)?
.unwrap_or(ArithmeticOverflow::Constrain);
// 13. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar).
Ok(PlainDate {
inner: IsoDateRecord::new(result.date.year, result.date.month, result.date.day),
calendar: identifier.into(),
})
}
_ => Err(JsNativeError::typ()
.with_message("ToTemporalDate item must be an object or string.")
.into()),
}
}
// 3.5.5. DifferenceIsoDate
// Implemented on IsoDateRecord.
// 3.5.6 RegulateIsoDate
// Implemented on IsoDateRecord.
// 3.5.7 IsValidIsoDate
// Implemented on IsoDateRecord.
// 3.5.8 BalanceIsoDate
// Implemented on IsoDateRecord.
// 3.5.11 AddISODate ( year, month, day, years, months, weeks, days, overflow )
// Implemented on IsoDateRecord

100
boa_engine/src/builtins/temporal/plain_date_time/iso.rs

@ -0,0 +1,100 @@
use crate::{
builtins::{
date::utils,
temporal::{self, plain_date::iso::IsoDateRecord},
},
JsBigInt,
};
#[derive(Default, Debug, Clone)]
pub(crate) struct IsoDateTimeRecord {
iso_date: IsoDateRecord,
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
}
impl IsoDateTimeRecord {
pub(crate) const fn iso_date(&self) -> IsoDateRecord {
self.iso_date
}
}
// ==== `IsoDateTimeRecord` methods ====
impl IsoDateTimeRecord {
pub(crate) const fn with_date(mut self, year: i32, month: i32, day: i32) -> Self {
let iso_date = IsoDateRecord::new(year, month, day);
self.iso_date = iso_date;
self
}
pub(crate) const fn with_time(
mut self,
hour: i32,
minute: i32,
second: i32,
ms: i32,
mis: i32,
ns: i32,
) -> Self {
self.hour = hour;
self.minute = minute;
self.second = second;
self.millisecond = ms;
self.microsecond = mis;
self.nanosecond = ns;
self
}
/// 5.5.1 `ISODateTimeWithinLimits ( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond )`
pub(crate) fn is_valid(&self) -> bool {
self.iso_date.is_valid();
let ns = self.get_utc_epoch_ns(None).to_f64();
if ns <= temporal::ns_min_instant().to_f64() - (temporal::NS_PER_DAY as f64)
|| ns >= temporal::ns_max_instant().to_f64() + (temporal::NS_PER_DAY as f64)
{
return false;
}
true
}
/// 14.8.1 `GetUTCEpochNanoseconds`
pub(crate) fn get_utc_epoch_ns(&self, offset_ns: Option<i64>) -> JsBigInt {
let day = utils::make_day(
i64::from(self.iso_date.year()),
i64::from(self.iso_date.month()),
i64::from(self.iso_date.day()),
)
.unwrap_or_default();
let time = utils::make_time(
i64::from(self.hour),
i64::from(self.minute),
i64::from(self.second),
i64::from(self.millisecond),
)
.unwrap_or_default();
let ms = utils::make_date(day, time).unwrap_or_default();
let epoch_ns = match offset_ns {
Some(offset) if offset != 0 => {
let ns = (ms * 1_000_000_i64)
+ (i64::from(self.microsecond) * 1_000_i64)
+ i64::from(self.nanosecond);
ns - offset
}
_ => {
(ms * 1_000_000_i64)
+ (i64::from(self.microsecond) * 1_000_i64)
+ i64::from(self.nanosecond)
}
};
JsBigInt::from(epoch_ns)
}
}

148
boa_engine/src/builtins/temporal/plain_date_time/mod.rs

@ -0,0 +1,148 @@
//! Boa's implementation of the ECMAScript `Temporal.PlainDateTime` builtin object.
#![allow(dead_code, unused_variables)]
use crate::{
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
property::Attribute,
realm::Realm,
string::common::StaticJsStrings,
Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
use self::iso::IsoDateTimeRecord;
pub(crate) mod iso;
/// The `Temporal.PlainDateTime` object.
#[derive(Debug, Clone)]
pub struct PlainDateTime {
pub(crate) inner: IsoDateTimeRecord,
pub(crate) calendar: JsValue,
}
impl BuiltInObject for PlainDateTime {
const NAME: JsString = StaticJsStrings::PLAIN_DATETIME;
}
impl IntrinsicObject for PlainDateTime {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for PlainDateTime {
const LENGTH: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::plain_date_time;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::range()
.with_message("Not yet implemented.")
.into())
}
}
// ==== `PlainDateTime` Accessor Properties ====
impl PlainDateTime {
fn calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn month(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn month_code(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn hour(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn minute(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn second(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn millisecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn microsecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn nanosecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn era(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
fn era_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("calendars not yet implemented.")
.into())
}
}
// ==== `PlainDateTime` Abstract Operations` ====
// See `IsoDateTimeRecord`

122
boa_engine/src/builtins/temporal/plain_month_day/mod.rs

@ -0,0 +1,122 @@
//! Boa's implementation of the ECMAScript `Temporal.PlainMonthDay` builtin object.
#![allow(dead_code, unused_variables)]
use crate::{
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
object::{internal_methods::get_prototype_from_constructor, ObjectData},
property::Attribute,
realm::Realm,
string::common::StaticJsStrings,
Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
use super::{plain_date::iso::IsoDateRecord, plain_date_time::iso::IsoDateTimeRecord};
/// The `Temporal.PlainMonthDay` object.
#[derive(Debug, Clone)]
pub struct PlainMonthDay {
pub(crate) inner: IsoDateRecord,
pub(crate) calendar: JsValue,
}
impl BuiltInObject for PlainMonthDay {
const NAME: JsString = StaticJsStrings::PLAIN_MD;
}
impl IntrinsicObject for PlainMonthDay {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for PlainMonthDay {
const LENGTH: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::plain_month_day;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::range()
.with_message("Not yet implemented.")
.into())
}
}
// ==== `PlainMonthDay` Abstract Operations ====
pub(crate) fn create_temporal_month_day(
iso: IsoDateRecord,
calendar: JsValue,
new_target: Option<&JsValue>,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If IsValidISODate(referenceISOYear, isoMonth, isoDay) is false, throw a RangeError exception.
if iso.is_valid() {
return Err(JsNativeError::range()
.with_message("PlainMonthDay is not a valid ISO date.")
.into());
}
// 2. If ISODateTimeWithinLimits(referenceISOYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception.
let iso_date_time = IsoDateTimeRecord::default()
.with_date(iso.year(), iso.month(), iso.day())
.with_time(12, 0, 0, 0, 0, 0);
if !iso_date_time.is_valid() {
return Err(JsNativeError::range()
.with_message("PlainMonthDay is not a valid ISO date time.")
.into());
}
// 3. If newTarget is not present, set newTarget to %Temporal.PlainMonthDay%.
let new_target = if let Some(target) = new_target {
target.clone()
} else {
context
.realm()
.intrinsics()
.constructors()
.plain_month_day()
.constructor()
.into()
};
// 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainMonthDay.prototype%", « [[InitializedTemporalMonthDay]], [[ISOMonth]], [[ISODay]], [[ISOYear]], [[Calendar]] »).
let proto = get_prototype_from_constructor(
&new_target,
StandardConstructors::plain_month_day,
context,
)?;
// 5. Set object.[[ISOMonth]] to isoMonth.
// 6. Set object.[[ISODay]] to isoDay.
// 7. Set object.[[Calendar]] to calendar.
// 8. Set object.[[ISOYear]] to referenceISOYear.
let obj = JsObject::from_proto_and_data(
proto,
ObjectData::plain_month_day(PlainMonthDay {
inner: iso,
calendar,
}),
);
// 9. Return object.
Ok(obj.into())
}

62
boa_engine/src/builtins/temporal/plain_time/mod.rs

@ -0,0 +1,62 @@
//! Boa's implementation of the ECMAScript `Temporal.PlainTime` builtin object.
#![allow(dead_code, unused_variables)]
use crate::{
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
property::Attribute,
realm::Realm,
string::common::StaticJsStrings,
Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
/// The `Temporal.PlainTime` object.
#[derive(Debug, Clone, Copy)]
pub struct PlainTime {
iso_hour: i32, // integer between 0-23
iso_minute: i32, // integer between 0-59
iso_second: i32, // integer between 0-59
iso_millisecond: i32, // integer between 0-999
iso_microsecond: i32, // integer between 0-999
iso_nanosecond: i32, // integer between 0-999
}
impl BuiltInObject for PlainTime {
const NAME: JsString = StaticJsStrings::PLAIN_TIME;
}
impl IntrinsicObject for PlainTime {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for PlainTime {
const LENGTH: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::plain_time;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::range()
.with_message("Not yet implemented.")
.into())
}
}

327
boa_engine/src/builtins/temporal/plain_year_month/mod.rs

@ -0,0 +1,327 @@
//! Boa's implementation of the `Temporal.PlainYearMonth` builtin object.
use crate::{
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
object::{internal_methods::get_prototype_from_constructor, ObjectData},
property::Attribute,
realm::Realm,
string::{common::StaticJsStrings, utf16},
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
use super::plain_date::iso::IsoDateRecord;
/// The `Temporal.PlainYearMonth` object.
#[derive(Debug, Clone)]
pub struct PlainYearMonth {
pub(crate) inner: IsoDateRecord,
pub(crate) calendar: JsValue,
}
impl BuiltInObject for PlainYearMonth {
const NAME: JsString = StaticJsStrings::PLAIN_YM;
}
impl IntrinsicObject for PlainYearMonth {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
let get_calendar_id = BuiltInBuilder::callable(realm, Self::get_calendar_id)
.name(js_string!("get calendarId"))
.build();
let get_year = BuiltInBuilder::callable(realm, Self::get_year)
.name(js_string!("get year"))
.build();
let get_month = BuiltInBuilder::callable(realm, Self::get_month)
.name(js_string!("get month"))
.build();
let get_month_code = BuiltInBuilder::callable(realm, Self::get_month_code)
.name(js_string!("get monthCode"))
.build();
let get_days_in_month = BuiltInBuilder::callable(realm, Self::get_days_in_month)
.name(js_string!("get daysInMonth"))
.build();
let get_days_in_year = BuiltInBuilder::callable(realm, Self::get_days_in_year)
.name(js_string!("get daysInYear"))
.build();
let get_months_in_year = BuiltInBuilder::callable(realm, Self::get_months_in_year)
.name(js_string!("get monthsInYear"))
.build();
let get_in_leap_year = BuiltInBuilder::callable(realm, Self::get_in_leap_year)
.name(js_string!("get inLeapYear"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.accessor(
utf16!("calendarId"),
Some(get_calendar_id),
None,
Attribute::default(),
)
.accessor(utf16!("year"), Some(get_year), None, Attribute::default())
.accessor(utf16!("month"), Some(get_month), None, Attribute::default())
.accessor(
utf16!("monthCode"),
Some(get_month_code),
None,
Attribute::default(),
)
.accessor(
utf16!("daysInMonth"),
Some(get_days_in_month),
None,
Attribute::default(),
)
.accessor(
utf16!("daysInYear"),
Some(get_days_in_year),
None,
Attribute::default(),
)
.accessor(
utf16!("monthsInYear"),
Some(get_months_in_year),
None,
Attribute::default(),
)
.accessor(
utf16!("inLeapYear"),
Some(get_in_leap_year),
None,
Attribute::default(),
)
.method(Self::with, js_string!("with"), 2)
.method(Self::add, js_string!("add"), 2)
.method(Self::subtract, js_string!("subtract"), 2)
.method(Self::until, js_string!("until"), 2)
.method(Self::since, js_string!("since"), 2)
.method(Self::equals, js_string!("equals"), 1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for PlainYearMonth {
const LENGTH: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::plain_year_month;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, then
if new_target.is_undefined() {
// a. Throw a TypeError exception.
return Err(JsNativeError::typ()
.with_message("NewTarget cannot be undefined when constructing a PlainYearMonth.")
.into());
}
let day = args.get_or_undefined(3);
// 2. If referenceISODay is undefined, then
let ref_day = if day.is_undefined() {
// a. Set referenceISODay to 1𝔽.
1
} else {
// 6. Let ref be ? ToIntegerWithTruncation(referenceISODay).
super::to_integer_with_truncation(day, context)?
};
// 3. Let y be ? ToIntegerWithTruncation(isoYear).
let y = super::to_integer_with_truncation(args.get_or_undefined(0), context)?;
// 4. Let m be ? ToIntegerWithTruncation(isoMonth).
let m = super::to_integer_with_truncation(args.get_or_undefined(1), context)?;
// TODO: calendar handling.
// 5. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601").
// 7. Return ? CreateTemporalYearMonth(y, m, calendar, ref, NewTarget).
let record = IsoDateRecord::new(y, m, ref_day);
create_temporal_year_month(
record,
JsValue::from(js_string!("iso8601")),
Some(new_target),
context,
)
}
}
// ==== `PlainYearMonth` Accessor Implementations ====
impl PlainYearMonth {
fn get_calendar_id(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_year(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_month(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_month_code(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_days_in_year(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_days_in_month(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_months_in_year(
_this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
fn get_in_leap_year(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
}
// ==== `PlainYearMonth` Method Implementations ====
impl PlainYearMonth {
fn with(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::typ()
.with_message("not yet implemented.")
.into())
}
fn add(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::typ()
.with_message("not yet implemented.")
.into())
}
fn subtract(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::typ()
.with_message("not yet implemented.")
.into())
}
fn until(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::typ()
.with_message("not yet implemented.")
.into())
}
fn since(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::typ()
.with_message("not yet implemented.")
.into())
}
fn equals(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> {
Err(JsNativeError::typ()
.with_message("not yet implemented.")
.into())
}
}
// ==== Abstract Operations ====
// 9.5.2 `RegulateISOYearMonth ( year, month, overflow )`
// Implemented on `TemporalFields`.
// 9.5.5 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )`
pub(crate) fn create_temporal_year_month(
year_month_record: IsoDateRecord,
calendar: JsValue,
new_target: Option<&JsValue>,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If IsValidISODate(isoYear, isoMonth, referenceISODay) is false, throw a RangeError exception.
if !year_month_record.is_valid() {
return Err(JsNativeError::range()
.with_message("PlainYearMonth values are not a valid ISO date.")
.into());
}
// 2. If ! ISOYearMonthWithinLimits(isoYear, isoMonth) is false, throw a RangeError exception.
if year_month_record.within_year_month_limits() {
return Err(JsNativeError::range()
.with_message("PlainYearMonth values are not a valid ISO date.")
.into());
}
// 3. If newTarget is not present, set newTarget to %Temporal.PlainYearMonth%.
let new_target = if let Some(target) = new_target {
target.clone()
} else {
context
.realm()
.intrinsics()
.constructors()
.plain_year_month()
.constructor()
.into()
};
// 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainYearMonth.prototype%", « [[InitializedTemporalYearMonth]], [[ISOYear]], [[ISOMonth]], [[ISODay]], [[Calendar]] »).
let proto = get_prototype_from_constructor(
&new_target,
StandardConstructors::plain_year_month,
context,
)?;
// 5. Set object.[[ISOYear]] to isoYear.
// 6. Set object.[[ISOMonth]] to isoMonth.
// 7. Set object.[[Calendar]] to calendar.
// 8. Set object.[[ISODay]] to referenceISODay.
let obj = JsObject::from_proto_and_data(
proto,
ObjectData::plain_year_month(PlainYearMonth {
inner: year_month_record,
calendar,
}),
);
// 9. Return object.
Ok(obj.into())
}

52
boa_engine/src/builtins/temporal/tests.rs

@ -0,0 +1,52 @@
use super::date_equations::{epoch_time_to_month_in_year, mathematical_in_leap_year};
use crate::{js_string, run_test_actions, JsValue, TestAction};
// Temporal Object tests.
#[test]
fn temporal_object() {
// Temporal Builtin tests.
run_test_actions([
TestAction::assert_eq(
"Object.prototype.toString.call(Temporal)",
js_string!("[object Temporal]"),
),
TestAction::assert_eq("String(Temporal)", js_string!("[object Temporal]")),
TestAction::assert_eq("Object.keys(Temporal).length === 0", true),
]);
}
#[test]
fn now_object() {
// Now Builtin tests.
run_test_actions([
TestAction::assert_eq("Object.isExtensible(Temporal.Now)", true),
TestAction::assert_eq(
"Object.prototype.toString.call(Temporal.Now)",
js_string!("[object Temporal.Now]"),
),
TestAction::assert_eq(
"Object.getPrototypeOf(Temporal.Now) === Object.prototype",
true,
),
TestAction::assert_eq("Temporal.Now.prototype", JsValue::undefined()),
]);
}
// Date Equations
#[test]
fn time_to_month() {
let oct_2023 = 1_696_459_917_000_f64;
let mar_1_2020 = 1_583_020_800_000_f64;
let feb_29_2020 = 1_582_934_400_000_f64;
let mar_1_2021 = 1_614_556_800_000_f64;
assert_eq!(epoch_time_to_month_in_year(oct_2023), 9);
assert_eq!(epoch_time_to_month_in_year(mar_1_2020), 2);
assert_eq!(mathematical_in_leap_year(mar_1_2020), 1);
assert_eq!(epoch_time_to_month_in_year(feb_29_2020), 1);
assert_eq!(mathematical_in_leap_year(feb_29_2020), 1);
assert_eq!(epoch_time_to_month_in_year(mar_1_2021), 2);
assert_eq!(mathematical_in_leap_year(mar_1_2021), 0);
}

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

@ -0,0 +1,491 @@
#![allow(dead_code)]
use crate::{
builtins::{
temporal::to_zero_padded_decimal_string, BuiltInBuilder, BuiltInConstructor, BuiltInObject,
IntrinsicObject,
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
object::{internal_methods::get_prototype_from_constructor, ObjectData, CONSTRUCTOR},
property::Attribute,
realm::Realm,
string::{common::StaticJsStrings, utf16},
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
/// 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>,
}
impl BuiltInObject for TimeZone {
const NAME: JsString = StaticJsStrings::TIMEZONE;
}
impl IntrinsicObject for TimeZone {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
let get_id = BuiltInBuilder::callable(realm, Self::get_id)
.name(js_string!("get Id"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.method(
Self::get_offset_nanoseconds_for,
js_string!("getOffsetNanosecondsFor"),
1,
)
.method(
Self::get_offset_string_for,
js_string!("getOffsetStringFor"),
1,
)
.method(
Self::get_plain_date_time_for,
js_string!("getPlainDateTimeFor"),
2,
)
.method(Self::get_instant_for, js_string!("getInstantFor"), 2)
.method(
Self::get_possible_instants_for,
js_string!("getPossibleInstantFor"),
1,
)
.method(
Self::get_next_transition,
js_string!("getNextTransition"),
1,
)
.method(
Self::get_previous_transition,
js_string!("getPreviousTransition"),
1,
)
.method(Self::to_string, js_string!("toString"), 0)
.method(Self::to_string, js_string!("toJSON"), 0)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.static_property(
CONSTRUCTOR,
realm.intrinsics().constructors().time_zone().prototype(),
Attribute::default(),
)
.accessor(utf16!("id"), Some(get_id), None, Attribute::default())
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for TimeZone {
const LENGTH: usize = 1;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::time_zone;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If NewTarget is undefined, then
// 1a. Throw a TypeError exception.
if new_target.is_undefined() {
return Err(JsNativeError::typ()
.with_message("newTarget cannot be undefined for Temporal.TimeZone constructor")
.into());
};
// 2. Set identifier to ? ToString(identifier).
let identifier = args.get_or_undefined(0);
if identifier.is_undefined() {
return Err(JsNativeError::range()
.with_message("Temporal.TimeZone must be called with a valid initializer")
.into());
}
// 3. If IsTimeZoneOffsetString(identifier) is false, then
// a. If IsAvailableTimeZoneName(identifier) is false, then
// i. Throw a RangeError exception.
// b. Set identifier to ! CanonicalizeTimeZoneName(identifier).
// 4. Return ? CreateTemporalTimeZone(identifier, NewTarget).
create_temporal_time_zone(
identifier.to_string(context)?.to_std_string_escaped(),
Some(new_target.clone()),
context,
)
}
}
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> {
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())
}
pub(crate) fn get_offset_nanoseconds_for(
this: &JsValue,
args: &[JsValue],
_: &mut Context<'_>,
) -> 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")
})?;
// 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]])).
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
pub(crate) fn get_offset_string_for(
this: &JsValue,
args: &[JsValue],
_: &mut Context<'_>,
) -> 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")
})?;
// 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)?;
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
// 4. Return ? GetOffsetStringFor(timeZone, instant).
}
pub(crate) fn get_plain_date_time_for(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
pub(crate) fn get_instant_for(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
pub(crate) fn get_possible_instants_for(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
pub(crate) fn get_next_transition(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
pub(crate) fn get_previous_transition(
_: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
pub(crate) fn to_string(
this: &JsValue,
_: &[JsValue],
_: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. Let timeZone be the this value.
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
let o = this.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone")
})?;
let o = o.borrow();
let tz = o.as_time_zone().ok_or_else(|| {
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone")
})?;
// 3. Return timeZone.[[Identifier]].
Ok(JsString::from(tz.identifier.clone()).into())
}
}
// -- TimeZone Abstract Operations --
/// Abstract operation `DefaultTimeZone ( )`
///
/// The abstract operation `DefaultTimeZone` takes no arguments. It returns a String value
/// representing the host environment's current time zone, which is either a valid (11.1.1) and
/// canonicalized (11.1.2) time zone name, or an offset conforming to the syntax of a
/// `TimeZoneNumericUTCOffset`.
///
/// An ECMAScript implementation that includes the ECMA-402 Internationalization API must implement
/// the `DefaultTimeZone` abstract operation as specified in the ECMA-402 specification.
///
/// More information:
/// - [ECMAScript specififcation][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-defaulttimezone
#[allow(unused)]
pub(super) fn default_time_zone(context: &mut Context<'_>) -> String {
// The minimum implementation of DefaultTimeZone for ECMAScript implementations that do not
// include the ECMA-402 API, supporting only the "UTC" time zone, performs the following steps
// when called:
// 1. Return "UTC".
"UTC".to_owned()
// TO-DO: full, system-aware implementation (and intl feature)
}
/// Abstract operation `CreateTemporalTimeZone ( identifier [ , newTarget ] )`
///
/// More information:
/// - [ECMAScript specififcation][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-createtemporaltimezone
#[allow(clippy::needless_pass_by_value, unused)]
pub(super) fn create_temporal_time_zone(
identifier: String,
new_target: Option<JsValue>,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If newTarget is not present, set newTarget to %Temporal.TimeZone%.
let new_target = new_target.unwrap_or_else(|| {
context
.realm()
.intrinsics()
.constructors()
.time_zone()
.prototype()
.into()
});
// 2. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.TimeZone.prototype%", « [[InitializedTemporalTimeZone]], [[Identifier]], [[OffsetNanoseconds]] »).
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())
}
/// Abstract operation `ParseTimeZoneOffsetString ( offsetString )`
///
/// The abstract operation `ParseTimeZoneOffsetString` takes argument `offsetString` (a String). It
/// parses the argument as a numeric UTC offset string and returns a signed integer representing
/// that offset as a number of nanoseconds.
///
/// More information:
/// - [ECMAScript specififcation][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring
#[allow(clippy::unnecessary_wraps, unused)]
fn parse_timezone_offset_string(offset_string: &str, context: &mut Context<'_>) -> JsResult<i64> {
use boa_parser::temporal::{IsoCursor, TemporalTimeZoneString};
// 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset).
let parse_result = TemporalTimeZoneString::parse(&mut IsoCursor::new(offset_string))?;
// 2. Assert: parseResult is not a List of errors.
// 3. Assert: parseResult contains a TemporalSign Parse Node.
let Some(utc_offset) = parse_result.offset else {
return Err(JsNativeError::typ()
.with_message("Offset string was not a valid offset")
.into());
};
// 4. Let parsedSign be the source text matched by the TemporalSign Parse Node contained within
// parseResult.
// 5. If parsedSign is the single code point U+002D (HYPHEN-MINUS) or U+2212 (MINUS SIGN), then
let sign = utc_offset.sign;
// a. Let sign be -1.
// 6. Else,
// a. Let sign be 1.
// 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed
// values is guaranteed to be a sufficiently short string of decimal digits.
// 8. Assert: parseResult contains an Hour Parse Node.
// 9. Let parsedHours be the source text matched by the Hour Parse Node contained within parseResult.
let parsed_hours = utc_offset.hour;
// 10. Let hours be ℝ(StringToNumber(CodePointsToString(parsedHours))).
// 11. If parseResult does not contain a MinuteSecond Parse Node, then
// a. Let minutes be 0.
// 12. Else,
// a. Let parsedMinutes be the source text matched by the first MinuteSecond Parse Node contained within parseResult.
// b. Let minutes be ℝ(StringToNumber(CodePointsToString(parsedMinutes))).
// 13. If parseResult does not contain two MinuteSecond Parse Nodes, then
// a. Let seconds be 0.
// 14. Else,
// a. Let parsedSeconds be the source text matched by the second MinuteSecond Parse Node contained within parseResult.
// b. Let seconds be ℝ(StringToNumber(CodePointsToString(parsedSeconds))).
// 15. If parseResult does not contain a TemporalDecimalFraction Parse Node, then
// a. Let nanoseconds be 0.
// 16. Else,
// a. Let parsedFraction be the source text matched by the TemporalDecimalFraction Parse Node contained within parseResult.
// b. Let fraction be the string-concatenation of CodePointsToString(parsedFraction) and "000000000".
// c. Let nanosecondsString be the substring of fraction from 1 to 10.
// d. Let nanoseconds be ℝ(StringToNumber(nanosecondsString)).
// 17. Return sign × (((hours × 60 + minutes) × 60 + seconds) × 10^9 + nanoseconds).
Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
}
/// Abstract operation `FormatTimeZoneOffsetString ( offsetNanoseconds )`
fn format_time_zone_offset_string(offset_nanoseconds: i64) -> String {
// 1. Assert: offsetNanoseconds is an integer.
// 2. If offsetNanoseconds ≥ 0, let sign be "+"; otherwise, let sign be "-".
let sign = if offset_nanoseconds >= 0 { "+" } else { "-" };
// 3. Let offsetNanoseconds be abs(offsetNanoseconds).
let offset_nanoseconds = offset_nanoseconds.unsigned_abs();
// 4. Let nanoseconds be offsetNanoseconds modulo 10^9.
let nanoseconds = offset_nanoseconds % 1_000_000_000;
// 5. Let seconds be floor(offsetNanoseconds / 10^9) modulo 60.
let seconds = (offset_nanoseconds / 1_000_000_000) % 60;
// 6. Let minutes be floor(offsetNanoseconds / (6 × 10^10)) modulo 60.
let minutes = (offset_nanoseconds / 60_000_000_000) % 60;
// 7. Let hours be floor(offsetNanoseconds / (3.6 × 1012)).
let hours = (offset_nanoseconds / 3_600_000_000_000) % 60;
// 8. Let h be ToZeroPaddedDecimalString(hours, 2).
let h = to_zero_padded_decimal_string(hours, 2);
// 9. Let m be ToZeroPaddedDecimalString(minutes, 2).
let m = to_zero_padded_decimal_string(minutes, 2);
// 10. Let s be ToZeroPaddedDecimalString(seconds, 2).
let s = to_zero_padded_decimal_string(seconds, 2);
// 11. If nanoseconds ≠ 0, then
let post = if nanoseconds != 0 {
// a. Let fraction be ToZeroPaddedDecimalString(nanoseconds, 9).
let fraction = to_zero_padded_decimal_string(nanoseconds, 9);
// b. Set fraction to the longest possible substring of fraction starting at position 0 and not ending with the code unit 0x0030 (DIGIT ZERO).
let fraction = fraction.trim_end_matches('0');
// c. Let post be the string-concatenation of the code unit 0x003A (COLON), s, the code unit 0x002E (FULL STOP), and fraction.
format!(":{s}.{fraction}")
} else if seconds != 0 {
// 12. Else if seconds ≠ 0, then
// a. Let post be the string-concatenation of the code unit 0x003A (COLON) and s.
format!(":{s}")
} else {
// 13. Else,
// a. Let post be the empty String.
String::new()
};
// 14. Return the string-concatenation of sign, h, the code unit 0x003A (COLON), m, and post.
format!("{sign}{h}:{m}{post}")
}
/// Abstract operation `CanonicalizeTimeZoneName ( timeZone )`
///
/// The abstract operation `CanonicalizeTimeZoneName` takes argument `timeZone` (a String that is a
/// valid time zone name as verified by `IsAvailableTimeZoneName`). It returns the canonical and
/// case-regularized form of `timeZone`.
fn canonicalize_time_zone_name(time_zone: &str) -> String {
// The minimum implementation of CanonicalizeTimeZoneName for ECMAScript implementations that
// do not include local political rules for any time zones performs the following steps when
// called:
// 1. Assert: timeZone is an ASCII-case-insensitive match for "UTC".
assert!(time_zone.to_ascii_uppercase() == "UTC");
// 2. Return "UTC".
"UTC".to_owned()
}

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

@ -0,0 +1,133 @@
#![allow(dead_code, unused_variables)]
use crate::{
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
property::Attribute,
realm::Realm,
string::common::StaticJsStrings,
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
};
use boa_profiler::Profiler;
/// The `Temporal.ZonedDateTime` object.
#[derive(Debug, Clone)]
pub struct ZonedDateTime {
nanoseconds: JsBigInt,
time_zone: JsObject,
calendar: JsObject,
}
impl BuiltInObject for ZonedDateTime {
const NAME: JsString = StaticJsStrings::ZONED_DT;
}
impl IntrinsicObject for ZonedDateTime {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInConstructor for ZonedDateTime {
const LENGTH: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::zoned_date_time;
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// TODO: Implement ZonedDateTime.
Err(JsNativeError::error()
.with_message("%ZonedDateTime% not yet implemented.")
.into())
}
}
// -- ZonedDateTime Abstract Operations --
///6.5.5 `AddZonedDateTime ( epochNanoseconds, timeZone, calendar, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , options ] )`
pub(crate) fn add_zoned_date_time(
epoch_nanos: &JsBigInt,
time_zone: &JsObject,
calendar: &JsObject,
duration: super::duration::DurationRecord,
options: Option<&JsObject>,
) -> JsResult<JsBigInt> {
// 1. If options is not present, set options to undefined.
// 2. Assert: Type(options) is Object or Undefined.
// 3. If years = 0, months = 0, weeks = 0, and days = 0, then
// a. Return ? AddInstant(epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
// 4. Let instant be ! CreateTemporalInstant(epochNanoseconds).
// 5. Let temporalDateTime be ? GetPlainDateTimeFor(timeZone, instant, calendar).
// 6. Let datePart be ! CreateTemporalDate(temporalDateTime.[[ISOYear]], temporalDateTime.[[ISOMonth]], temporalDateTime.[[ISODay]], calendar).
// 7. Let dateDuration be ! CreateTemporalDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0).
// 8. Let addedDate be ? CalendarDateAdd(calendar, datePart, dateDuration, options).
// 9. Let intermediateDateTime be ? CreateTemporalDateTime(addedDate.[[ISOYear]], addedDate.[[ISOMonth]], addedDate.[[ISODay]], temporalDateTime.[[ISOHour]], temporalDateTime.[[ISOMinute]], temporalDateTime.[[ISOSecond]], temporalDateTime.[[ISOMillisecond]], temporalDateTime.[[ISOMicrosecond]], temporalDateTime.[[ISONanosecond]], calendar).
// 10. Let intermediateInstant be ? GetInstantFor(timeZone, intermediateDateTime, "compatible").
// 11. Return ? AddInstant(intermediateInstant.[[Nanoseconds]], hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
Err(JsNativeError::error()
.with_message("%ZonedDateTime% not yet implemented.")
.into())
}
/// 6.5.7 `NanosecondsToDays ( nanoseconds, relativeTo )`
pub(crate) fn nanoseconds_to_days(
nanoseconds: f64,
relative_to: &JsValue,
) -> JsResult<(i32, i32, i32)> {
// 1. Let dayLengthNs be nsPerDay.
// 2. If nanoseconds = 0, then
// a. Return the Record { [[Days]]: 0, [[Nanoseconds]]: 0, [[DayLength]]: dayLengthNs }.
// 3. If nanoseconds < 0, let sign be -1; else, let sign be 1.
// 4. If Type(relativeTo) is not Object or relativeTo does not have an [[InitializedTemporalZonedDateTime]] internal slot, then
// a. Return the Record { [[Days]]: truncate(nanoseconds / dayLengthNs), [[Nanoseconds]]: (abs(nanoseconds) modulo dayLengthNs) × sign, [[DayLength]]: dayLengthNs }.
// 5. Let startNs be ℝ(relativeTo.[[Nanoseconds]]).
// 6. Let startInstant be ! CreateTemporalInstant(ℤ(startNs)).
// 7. Let startDateTime be ? GetPlainDateTimeFor(relativeTo.[[TimeZone]], startInstant, relativeTo.[[Calendar]]).
// 8. Let endNs be startNs + nanoseconds.
// 9. If ! IsValidEpochNanoseconds(ℤ(endNs)) is false, throw a RangeError exception.
// 10. Let endInstant be ! CreateTemporalInstant(ℤ(endNs)).
// 11. Let endDateTime be ? GetPlainDateTimeFor(relativeTo.[[TimeZone]], endInstant, relativeTo.[[Calendar]]).
// 12. Let dateDifference be ? DifferenceISODateTime(startDateTime.[[ISOYear]], startDateTime.[[ISOMonth]], startDateTime.[[ISODay]], startDateTime.[[ISOHour]], startDateTime.[[ISOMinute]], startDateTime.[[ISOSecond]], startDateTime.[[ISOMillisecond]], startDateTime.[[ISOMicrosecond]], startDateTime.[[ISONanosecond]], endDateTime.[[ISOYear]], endDateTime.[[ISOMonth]], endDateTime.[[ISODay]], endDateTime.[[ISOHour]], endDateTime.[[ISOMinute]], endDateTime.[[ISOSecond]], endDateTime.[[ISOMillisecond]], endDateTime.[[ISOMicrosecond]], endDateTime.[[ISONanosecond]], relativeTo.[[Calendar]], "day", OrdinaryObjectCreate(null)).
// 13. Let days be dateDifference.[[Days]].
// 14. Let intermediateNs be ℝ(? AddZonedDateTime(ℤ(startNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
// 15. If sign is 1, then
// a. Repeat, while days > 0 and intermediateNs > endNs,
// i. Set days to days - 1.
// ii. Set intermediateNs to ℝ(? AddZonedDateTime(ℤ(startNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
// 16. Set nanoseconds to endNs - intermediateNs.
// 17. Let done be false.
// 18. Repeat, while done is false,
// a. Let oneDayFartherNs be ℝ(? AddZonedDateTime(ℤ(intermediateNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, sign, 0, 0, 0, 0, 0, 0)).
// b. Set dayLengthNs to oneDayFartherNs - intermediateNs.
// c. If (nanoseconds - dayLengthNs) × sign ≥ 0, then
// i. Set nanoseconds to nanoseconds - dayLengthNs.
// ii. Set intermediateNs to oneDayFartherNs.
// iii. Set days to days + sign.
// d. Else,
// i. Set done to true.
// 19. If days < 0 and sign = 1, throw a RangeError exception.
// 20. If days > 0 and sign = -1, throw a RangeError exception.
// 21. If nanoseconds < 0, then
// a. Assert: sign is -1.
// 22. If nanoseconds > 0 and sign = -1, throw a RangeError exception.
// 23. Assert: The inequality abs(nanoseconds) < abs(dayLengthNs) holds.
// 24. Return the Record { [[Days]]: days, [[Nanoseconds]]: nanoseconds, [[DayLength]]: abs(dayLengthNs) }.
Err(JsNativeError::error()
.with_message("%ZonedDateTime% not yet implemented.")
.into())
}

214
boa_engine/src/context/intrinsics.rs

@ -166,6 +166,26 @@ pub struct StandardConstructors {
segmenter: StandardConstructor,
#[cfg(feature = "intl")]
plural_rules: StandardConstructor,
#[cfg(feature = "experimental")]
instant: StandardConstructor,
#[cfg(feature = "experimental")]
plain_date_time: StandardConstructor,
#[cfg(feature = "experimental")]
plain_date: StandardConstructor,
#[cfg(feature = "experimental")]
plain_time: StandardConstructor,
#[cfg(feature = "experimental")]
plain_year_month: StandardConstructor,
#[cfg(feature = "experimental")]
plain_month_day: StandardConstructor,
#[cfg(feature = "experimental")]
time_zone: StandardConstructor,
#[cfg(feature = "experimental")]
duration: StandardConstructor,
#[cfg(feature = "experimental")]
zoned_date_time: StandardConstructor,
#[cfg(feature = "experimental")]
calendar: StandardConstructor,
}
impl Default for StandardConstructors {
@ -242,6 +262,26 @@ impl Default for StandardConstructors {
segmenter: StandardConstructor::default(),
#[cfg(feature = "intl")]
plural_rules: StandardConstructor::default(),
#[cfg(feature = "experimental")]
instant: StandardConstructor::default(),
#[cfg(feature = "experimental")]
plain_date_time: StandardConstructor::default(),
#[cfg(feature = "experimental")]
plain_date: StandardConstructor::default(),
#[cfg(feature = "experimental")]
plain_time: StandardConstructor::default(),
#[cfg(feature = "experimental")]
plain_year_month: StandardConstructor::default(),
#[cfg(feature = "experimental")]
plain_month_day: StandardConstructor::default(),
#[cfg(feature = "experimental")]
time_zone: StandardConstructor::default(),
#[cfg(feature = "experimental")]
duration: StandardConstructor::default(),
#[cfg(feature = "experimental")]
zoned_date_time: StandardConstructor::default(),
#[cfg(feature = "experimental")]
calendar: StandardConstructor::default(),
}
}
}
@ -827,6 +867,136 @@ impl StandardConstructors {
pub const fn plural_rules(&self) -> &StandardConstructor {
&self.plural_rules
}
/// Returns the `Temporal.Instant` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-instant-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn instant(&self) -> &StandardConstructor {
&self.instant
}
/// Returns the `Temporal.PlainDateTime` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plaindatetime-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn plain_date_time(&self) -> &StandardConstructor {
&self.plain_date_time
}
/// Returns the `Temporal.PlainDate` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plaindate-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn plain_date(&self) -> &StandardConstructor {
&self.plain_date
}
/// Returns the `Temporal.PlainTime` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plaintime-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn plain_time(&self) -> &StandardConstructor {
&self.plain_time
}
/// Returns the `Temporal.PlainYearMonth` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plainyearmonth-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn plain_year_month(&self) -> &StandardConstructor {
&self.plain_year_month
}
/// Returns the `Temporal.PlainMonthDay` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn plain_month_day(&self) -> &StandardConstructor {
&self.plain_month_day
}
/// Returns the `Temporal.TimeZone` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-timezone-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn time_zone(&self) -> &StandardConstructor {
&self.time_zone
}
/// Returns the `Temporal.Duration` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-duration-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn duration(&self) -> &StandardConstructor {
&self.duration
}
/// Returns the `Temporal.ZonedDateTime` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-zoneddatetime-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn zoned_date_time(&self) -> &StandardConstructor {
&self.zoned_date_time
}
/// Returns the `Temporal.Calendar` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-calendar-constructor
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn calendar(&self) -> &StandardConstructor {
&self.calendar
}
}
/// Cached intrinsic objects
@ -892,6 +1062,14 @@ pub struct IntrinsicObjects {
/// [`%SegmentsPrototype%`](https://tc39.es/ecma402/#sec-%segmentsprototype%-object)
#[cfg(feature = "intl")]
segments_prototype: JsObject,
/// [`%Temporal%`](https://tc39.es/proposal-temporal/#sec-temporal-objects)
#[cfg(feature = "experimental")]
temporal: JsObject,
/// [`%Temporal.Now%`](https://tc39.es/proposal-temporal/#sec-temporal-now-object)
#[cfg(feature = "experimental")]
now: JsObject,
}
impl Default for IntrinsicObjects {
@ -920,6 +1098,10 @@ impl Default for IntrinsicObjects {
intl: JsObject::default(),
#[cfg(feature = "intl")]
segments_prototype: JsObject::default(),
#[cfg(feature = "experimental")]
temporal: JsObject::default(),
#[cfg(feature = "experimental")]
now: JsObject::default(),
}
}
}
@ -980,12 +1162,14 @@ impl IntrinsicObjects {
/// Gets the [`%eval%`][spec] intrinsic function.
///
/// [spec]: https://tc39.es/ecma262/#sec-eval-x
#[inline]
#[must_use]
pub fn eval(&self) -> JsFunction {
self.eval.clone()
}
/// Gets the URI intrinsic functions.
#[inline]
#[must_use]
pub const fn uri_functions(&self) -> &UriFunctions {
&self.uri_functions
@ -994,6 +1178,7 @@ impl IntrinsicObjects {
/// Gets the [`%Reflect%`][spec] intrinsic object.
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect
#[inline]
#[must_use]
pub fn reflect(&self) -> JsObject {
self.reflect.clone()
@ -1002,6 +1187,7 @@ impl IntrinsicObjects {
/// Gets the [`%Math%`][spec] intrinsic object.
///
/// [spec]: https://tc39.es/ecma262/#sec-math
#[inline]
#[must_use]
pub fn math(&self) -> JsObject {
self.math.clone()
@ -1010,6 +1196,7 @@ impl IntrinsicObjects {
/// Gets the [`%JSON%`][spec] intrinsic object.
///
/// [spec]: https://tc39.es/ecma262/#sec-json
#[inline]
#[must_use]
pub fn json(&self) -> JsObject {
self.json.clone()
@ -1018,6 +1205,7 @@ impl IntrinsicObjects {
/// Gets the [`%isFinite%`][spec] intrinsic function.
///
/// [spec]: https://tc39.es/ecma262/#sec-isfinite-number
#[inline]
#[must_use]
pub fn is_finite(&self) -> JsFunction {
self.is_finite.clone()
@ -1026,6 +1214,7 @@ impl IntrinsicObjects {
/// Gets the [`%isNaN%`][spec] intrinsic function.
///
/// [spec]: https://tc39.es/ecma262/#sec-isnan-number
#[inline]
#[must_use]
pub fn is_nan(&self) -> JsFunction {
self.is_nan.clone()
@ -1034,6 +1223,7 @@ impl IntrinsicObjects {
/// Gets the [`%parseFloat%`][spec] intrinsic function.
///
/// [spec]: https://tc39.es/ecma262/#sec-parsefloat-string
#[inline]
#[must_use]
pub fn parse_float(&self) -> JsFunction {
self.parse_float.clone()
@ -1042,6 +1232,7 @@ impl IntrinsicObjects {
/// Gets the [`%parseInt%`][spec] intrinsic function.
///
/// [spec]: https://tc39.es/ecma262/#sec-parseint-string-radix
#[inline]
#[must_use]
pub fn parse_int(&self) -> JsFunction {
self.parse_int.clone()
@ -1052,6 +1243,7 @@ impl IntrinsicObjects {
/// [spec]: https://tc39.es/ecma262/#sec-escape-string
#[must_use]
#[cfg(feature = "annex-b")]
#[inline]
pub fn escape(&self) -> JsFunction {
self.escape.clone()
}
@ -1061,6 +1253,7 @@ impl IntrinsicObjects {
/// [spec]: https://tc39.es/ecma262/#sec-unescape-string
#[must_use]
#[cfg(feature = "annex-b")]
#[inline]
pub fn unescape(&self) -> JsFunction {
self.unescape.clone()
}
@ -1070,6 +1263,7 @@ impl IntrinsicObjects {
/// [spec]: https://tc39.es/ecma402/#intl-object
#[must_use]
#[cfg(feature = "intl")]
#[inline]
pub fn intl(&self) -> JsObject {
self.intl.clone()
}
@ -1082,6 +1276,26 @@ impl IntrinsicObjects {
pub fn segments_prototype(&self) -> JsObject {
self.segments_prototype.clone()
}
/// Gets the [`%Temporal%`][spec] intrinsic object.
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-objects
#[cfg(feature = "experimental")]
#[must_use]
#[inline]
pub fn temporal(&self) -> JsObject {
self.temporal.clone()
}
/// Gets the [`%Temporal.Now%`][spec] intrinsic object.
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-now-object
#[cfg(feature = "experimental")]
#[must_use]
#[inline]
pub fn now(&self) -> JsObject {
self.now.clone()
}
}
/// Contains commonly used [`ObjectTemplate`]s.

105
boa_engine/src/object/jsobject.rs

@ -75,6 +75,7 @@ impl JsObject {
}),
}
}
/// Creates a new ordinary object with its prototype set to the `Object` prototype.
///
/// This is equivalent to calling the specification's abstract operation
@ -735,6 +736,110 @@ impl JsObject {
self.borrow().is_ordinary()
}
/// Checks if current object is a `Temporal.Duration` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_duration(&self) -> bool {
self.borrow().is_duration()
}
/// Checks if current object is a `Temporal.TimeZone` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_time_zone(&self) -> bool {
self.borrow().is_time_zone()
}
/// Checks if current object is a `Temporal.PlainDateTime` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_plain_date_time(&self) -> bool {
self.borrow().is_plain_date_time()
}
/// Checks if current object is a `Temporal.PlainDate` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_plain_date(&self) -> bool {
self.borrow().is_plain_date()
}
/// Checks if current object is a `Temporal.PlainYearMonth` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_plain_year_month(&self) -> bool {
self.borrow().is_plain_year_month()
}
/// Checks if current object is a `Temporal.PlainMonthDay` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_plain_month_day(&self) -> bool {
self.borrow().is_plain_month_day()
}
/// Checks if current object is a `Temporal.ZonedDateTime` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_zoned_date_time(&self) -> bool {
self.borrow().is_zoned_date_time()
}
/// Checks if current object is a `Temporal.Calendar` object.
///
/// # Panics
///
/// Panics if the object is currently mutably borrowed.
#[inline]
#[must_use]
#[track_caller]
#[cfg(feature = "experimental")]
pub fn is_calendar(&self) -> bool {
self.borrow().is_calendar()
}
/// Checks if it's a proxy object.
///
/// # Panics

394
boa_engine/src/object/mod.rs

@ -38,6 +38,11 @@ use crate::builtins::intl::{
plural_rules::PluralRules,
segmenter::{SegmentIterator, Segmenter, Segments},
};
#[cfg(feature = "experimental")]
use crate::builtins::temporal::{
Calendar, Duration, Instant, PlainDate, PlainDateTime, PlainMonthDay, PlainTime,
PlainYearMonth, TimeZone, ZonedDateTime,
};
use crate::{
builtins::{
array::ArrayIterator,
@ -443,10 +448,49 @@ pub enum ObjectKind {
/// The `Segment Iterator` object kind.
#[cfg(feature = "intl")]
SegmentIterator(SegmentIterator),
/// The `PluralRules` object kind.
#[cfg(feature = "intl")]
PluralRules(PluralRules),
/// The `Temporal.Instant` object kind.
#[cfg(feature = "experimental")]
Instant(Instant),
/// The `Temporal.PlainDateTime` object kind.
#[cfg(feature = "experimental")]
PlainDateTime(PlainDateTime),
/// The `Temporal.PlainDate` object kind.
#[cfg(feature = "experimental")]
PlainDate(PlainDate),
/// The `Temporal.PlainTime` object kind.
#[cfg(feature = "experimental")]
PlainTime(PlainTime),
/// The `Temporal.PlainYearMonth` object kind.
#[cfg(feature = "experimental")]
PlainYearMonth(PlainYearMonth),
/// The `Temporal.PlainMonthDay` object kind.
#[cfg(feature = "experimental")]
PlainMonthDay(PlainMonthDay),
/// The `Temporal.TimeZone` object kind.
#[cfg(feature = "experimental")]
TimeZone(TimeZone),
/// The `Temporal.Duration` object kind.
#[cfg(feature = "experimental")]
Duration(Duration),
/// The `Temporal.ZonedDateTime` object kind.
#[cfg(feature = "experimental")]
ZonedDateTime(ZonedDateTime),
/// The `Temporal.Calendar` object kind.
#[cfg(feature = "experimental")]
Calendar(Calendar),
}
unsafe impl Trace for ObjectKind {
@ -504,6 +548,17 @@ unsafe impl Trace for ObjectKind {
| Self::Global
| Self::Number(_)
| Self::Symbol(_) => {}
#[cfg(feature = "experimental")]
Self::Instant(_)
| Self::PlainDateTime(_)
| Self::PlainDate(_)
| Self::PlainTime(_)
| Self::PlainYearMonth(_)
| Self::PlainMonthDay(_)
| Self::TimeZone(_)
| Self::Calendar(_)
| Self::Duration(_)
| Self::ZonedDateTime(_) => {}
}
}}
}
@ -958,6 +1013,105 @@ impl ObjectData {
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `Instant` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn instant(instant: Instant) -> Self {
Self {
kind: ObjectKind::Instant(instant),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `PlainDateTime` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn plain_date_time(date_time: PlainDateTime) -> Self {
Self {
kind: ObjectKind::PlainDateTime(date_time),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `PlainDate` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn plain_date(date: PlainDate) -> Self {
Self {
kind: ObjectKind::PlainDate(date),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `PlainTime` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn plain_time(time: PlainTime) -> Self {
Self {
kind: ObjectKind::PlainTime(time),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `PlainYearMonth` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn plain_year_month(year_month: PlainYearMonth) -> Self {
Self {
kind: ObjectKind::PlainYearMonth(year_month),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `PlainMonthDay` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn plain_month_day(month_day: PlainMonthDay) -> Self {
Self {
kind: ObjectKind::PlainMonthDay(month_day),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `TimeZone` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn time_zone(time_zone: TimeZone) -> Self {
Self {
kind: ObjectKind::TimeZone(time_zone),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `Duration` object data
#[cfg(feature = "experimental")]
#[must_use]
pub fn duration(duration: Duration) -> Self {
Self {
kind: ObjectKind::Duration(duration),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `ZonedDateTime` object data.
#[cfg(feature = "experimental")]
#[must_use]
pub fn zoned_date_time(zoned_date_time: ZonedDateTime) -> Self {
Self {
kind: ObjectKind::ZonedDateTime(zoned_date_time),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
/// Create the `Calendar` object data.
#[cfg(feature = "experimental")]
#[must_use]
pub fn calendar(calendar: Calendar) -> Self {
Self {
kind: ObjectKind::Calendar(calendar),
internal_methods: &ORDINARY_INTERNAL_METHODS,
}
}
}
impl Debug for ObjectKind {
@ -1017,6 +1171,26 @@ impl Debug for ObjectKind {
Self::SegmentIterator(_) => "SegmentIterator",
#[cfg(feature = "intl")]
Self::PluralRules(_) => "PluralRules",
#[cfg(feature = "experimental")]
Self::Instant(_) => "Instant",
#[cfg(feature = "experimental")]
Self::PlainDateTime(_) => "PlainDateTime",
#[cfg(feature = "experimental")]
Self::PlainDate(_) => "PlainDate",
#[cfg(feature = "experimental")]
Self::PlainTime(_) => "PlainTime",
#[cfg(feature = "experimental")]
Self::PlainYearMonth(_) => "PlainYearMonth",
#[cfg(feature = "experimental")]
Self::PlainMonthDay(_) => "PlainMonthDay",
#[cfg(feature = "experimental")]
Self::TimeZone(_) => "TimeZone",
#[cfg(feature = "experimental")]
Self::Duration(_) => "Duration",
#[cfg(feature = "experimental")]
Self::ZonedDateTime(_) => "ZonedDateTime",
#[cfg(feature = "experimental")]
Self::Calendar(_) => "Calendar",
})
}
}
@ -1955,6 +2129,224 @@ impl Object {
}
}
/// Gets the `TimeZone` data if the object is a `Temporal.TimeZone`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_time_zone(&self) -> Option<&TimeZone> {
match self.kind {
ObjectKind::TimeZone(ref tz) => Some(tz),
_ => None,
}
}
/// Checks if the object is a `TimeZone` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_time_zone(&self) -> bool {
matches!(self.kind, ObjectKind::TimeZone(_))
}
/// Gets a mutable reference to `Instant` data if the object is a `Temporal.Instant`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub fn as_instant_mut(&mut self) -> Option<&mut Instant> {
match &mut self.kind {
ObjectKind::Instant(instant) => Some(instant),
_ => None,
}
}
/// Gets the `Instant` data if the object is a `Temporal.Instant`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_instant(&self) -> Option<&Instant> {
match &self.kind {
ObjectKind::Instant(instant) => Some(instant),
_ => None,
}
}
/// Checks if the object is a `Duration` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_duration(&self) -> bool {
matches!(self.kind, ObjectKind::Duration(_))
}
/// Gets a mutable reference to `Duration` data if the object is a `Temporal.Duration`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub fn as_duration_mut(&mut self) -> Option<&mut Duration> {
match &mut self.kind {
ObjectKind::Duration(dur) => Some(dur),
_ => None,
}
}
/// Gets the `Duration` data if the object is a `Temporal.Duration`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_duration(&self) -> Option<&Duration> {
match &self.kind {
ObjectKind::Duration(dur) => Some(dur),
_ => None,
}
}
/// Checks if object is a `PlainDateTime` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_plain_date_time(&self) -> bool {
matches!(self.kind, ObjectKind::PlainDateTime(_))
}
/// Gets a reference to `PlainDateTime` data if the object is a `Temporal.PlainDateTime`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_plain_date_time(&self) -> Option<&PlainDateTime> {
match &self.kind {
ObjectKind::PlainDateTime(date) => Some(date),
_ => None,
}
}
/// Checks if object is a `PlainDate` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_plain_date(&self) -> bool {
matches!(self.kind, ObjectKind::PlainDate(_))
}
/// Gets a mutable reference to `PlainDate` data if the object is a `Temporal.PlainDate`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub fn as_plain_date_mut(&mut self) -> Option<&mut PlainDate> {
match &mut self.kind {
ObjectKind::PlainDate(date) => Some(date),
_ => None,
}
}
/// Gets the `PlainDate` data if the object is a `Temporal.PlainDate`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_plain_date(&self) -> Option<&PlainDate> {
match &self.kind {
ObjectKind::PlainDate(date) => Some(date),
_ => None,
}
}
/// Checks if object is a `PlainYearMonth` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_plain_year_month(&self) -> bool {
matches!(self.kind, ObjectKind::PlainYearMonth(_))
}
/// Gets a mutable reference to `PlainYearMonth` data if the object is a `Temporal.PlainYearMonth`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub fn as_plain_year_month_mut(&mut self) -> Option<&mut PlainYearMonth> {
match &mut self.kind {
ObjectKind::PlainYearMonth(year_month) => Some(year_month),
_ => None,
}
}
/// Gets the `PlainYearMonth` data if the object is a `Temporal.PlainYearMonth`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_plain_year_month(&self) -> Option<&PlainYearMonth> {
match &self.kind {
ObjectKind::PlainYearMonth(ym) => Some(ym),
_ => None,
}
}
/// Checks if object is a `PlainMonthDay` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_plain_month_day(&self) -> bool {
matches!(self.kind, ObjectKind::PlainMonthDay(_))
}
/// Gets the `PlainMonthDay` data if the object is a `Temporal.PlainMonthDay`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_plain_month_day(&self) -> Option<&PlainMonthDay> {
match &self.kind {
ObjectKind::PlainMonthDay(md) => Some(md),
_ => None,
}
}
/// Gets a mutable reference to `PlainMonthDay` data if the object is a `Temporal.PlainMonthDay`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub fn as_plain_month_day_mut(&mut self) -> Option<&mut PlainMonthDay> {
match &mut self.kind {
ObjectKind::PlainMonthDay(month_day) => Some(month_day),
_ => None,
}
}
/// Gets the `PlainDate` data if the object is a `Temporal.PlainDate`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_zoned_date_time(&self) -> Option<&ZonedDateTime> {
match &self.kind {
ObjectKind::ZonedDateTime(zdt) => Some(zdt),
_ => None,
}
}
/// Checks if the object is a `ZonedDateTime` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_zoned_date_time(&self) -> bool {
matches!(self.kind, ObjectKind::ZonedDateTime(_))
}
/// Checks if the object is a `Calendar` object.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn is_calendar(&self) -> bool {
matches!(self.kind, ObjectKind::Calendar(_))
}
/// Gets the `Calendar` data if the object is a `Temporal.Calendar`.
#[inline]
#[must_use]
#[cfg(feature = "experimental")]
pub const fn as_calendar(&self) -> Option<&Calendar> {
match &self.kind {
ObjectKind::Calendar(calendar) => Some(calendar),
_ => None,
}
}
/// Return `true` if it is a native object and the native type is `T`.
#[must_use]
pub fn is<T>(&self) -> bool

24
boa_engine/src/string/common.rs

@ -173,6 +173,18 @@ impl StaticJsStrings {
(WEAK_REF, "WeakRef"),
(WEAK_MAP, "WeakMap"),
(WEAK_SET, "WeakSet"),
(TEMPORAL, "Temporal"),
(NOW, "Temporal.Now"),
(INSTANT, "Temporal.Instant"),
(DURATION, "Temporal.Duration"),
(PLAIN_DATE, "Temporal.PlainDate"),
(PLAIN_DATETIME, "Temporal.PlainDateTime"),
(PLAIN_TIME, "Temporal.PlainTime"),
(PLAIN_YM, "Temporal.PlainYearMonth"),
(PLAIN_MD, "Temporal.PlainMonthDay"),
(CALENDAR, "Temporal.Calendar"),
(TIMEZONE, "Temporal.TimeZone"),
(ZONED_DT, "Temporal.ZonedDateTime"),
}
}
@ -301,6 +313,18 @@ const RAW_STATICS: &[&[u16]] = &[
utf16!("WeakRef"),
utf16!("WeakMap"),
utf16!("WeakSet"),
utf16!("Temporal"),
utf16!("Temporal.Now"),
utf16!("Temporal.Instant"),
utf16!("Temporal.Duration"),
utf16!("Temporal.Calendar"),
utf16!("Temporal.PlainDate"),
utf16!("Temporal.PlainDateTime"),
utf16!("Temporal.PlainMonthDay"),
utf16!("Temporal.PlainYearMonth"),
utf16!("Temporal.PlainTime"),
utf16!("Temporal.TimeZone"),
utf16!("Temporal.ZonedDateTime"),
// Misc
utf16!(","),
utf16!(":"),

1
boa_parser/Cargo.toml

@ -25,3 +25,4 @@ icu_properties.workspace = true
[features]
annex-b = []
experimental = ["boa_ast/experimental"]

1
boa_parser/src/error/mod.rs

@ -10,6 +10,7 @@ use std::fmt;
/// Result of a parsing operation.
pub type ParseResult<T> = Result<T, Error>;
/// Adds context to a parser error.
pub(crate) trait ErrorContext {
/// Sets the context of the error, if possible.
fn set_context(self, context: &'static str) -> Self;

2
boa_parser/src/lib.rs

@ -80,6 +80,8 @@ pub mod error;
pub mod lexer;
pub mod parser;
mod source;
#[cfg(feature = "experimental")]
pub mod temporal;
pub use error::Error;
pub use lexer::Lexer;

2
boa_parser/src/parser/expression/assignment/yield.rs

@ -10,7 +10,7 @@
use super::AssignmentExpression;
use crate::{
lexer::TokenKind,
parser::{AllowAwait, AllowIn, Cursor, OrAbrupt, ParseResult, TokenParser},
parser::{cursor::Cursor, AllowAwait, AllowIn, OrAbrupt, ParseResult, TokenParser},
};
use boa_ast::{expression::Yield, Expression, Keyword, Punctuator};
use boa_interner::Interner;

4
boa_parser/src/parser/mod.rs

@ -46,6 +46,10 @@ where
/// Parses the token stream using the current parser.
///
/// This method needs to be provided by the implementor type.
///
/// # Errors
///
/// It will fail if the cursor is not placed at the beginning of the expected non-terminal.
fn parse(self, cursor: &mut Cursor<R>, interner: &mut Interner) -> ParseResult<Self::Output>;
}

205
boa_parser/src/temporal/annotations.rs

@ -0,0 +1,205 @@
/// Parsing for Temporal's `Annotations`.
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
temporal::{
grammar::{
is_a_key_char, is_a_key_leading_char, is_annotation_close,
is_annotation_key_value_separator, is_annotation_value_component, is_critical_flag,
},
time_zone,
time_zone::TimeZoneAnnotation,
IsoCursor,
},
};
use boa_ast::{Position, Span};
use super::grammar::{is_annotation_open, is_hyphen};
/// A `KeyValueAnnotation` Parse Node.
#[derive(Debug, Clone)]
pub(crate) struct KeyValueAnnotation {
/// An `Annotation`'s Key.
pub(crate) key: String,
/// An `Annotation`'s value.
pub(crate) value: String,
/// Whether the annotation was flagged as critical.
pub(crate) critical: bool,
}
/// Strictly a Parsing Intermediary for the checking the common annotation backing.
pub(crate) struct AnnotationSet {
pub(crate) tz: Option<TimeZoneAnnotation>,
pub(crate) calendar: Option<String>,
}
/// Parse a `TimeZoneAnnotation` `Annotations` set
pub(crate) fn parse_annotation_set(
zoned: bool,
cursor: &mut IsoCursor,
) -> ParseResult<AnnotationSet> {
// Parse the first annotation.
let tz_annotation = time_zone::parse_ambiguous_tz_annotation(cursor)?;
if tz_annotation.is_none() && zoned {
return Err(Error::unexpected(
"Annotation",
Span::new(
Position::new(1, cursor.pos() + 1),
Position::new(1, cursor.pos() + 2),
),
"iso8601 ZonedDateTime requires a TimeZoneAnnotation.",
));
}
// Parse any `Annotations`
let annotations = cursor.check_or(false, is_annotation_open);
if annotations {
let annotations = parse_annotations(cursor)?;
return Ok(AnnotationSet {
tz: tz_annotation,
calendar: annotations.calendar,
});
}
Ok(AnnotationSet {
tz: tz_annotation,
calendar: None,
})
}
/// An internal crate type to house any recognized annotations that are found.
#[derive(Default)]
pub(crate) struct RecognizedAnnotations {
pub(crate) calendar: Option<String>,
}
/// Parse any number of `KeyValueAnnotation`s
pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult<RecognizedAnnotations> {
let mut annotations = RecognizedAnnotations::default();
let mut calendar_crit = false;
while cursor.check_or(false, is_annotation_open) {
let start = Position::new(1, cursor.pos() + 1);
let kv = parse_kv_annotation(cursor)?;
if &kv.key == "u-ca" {
if annotations.calendar.is_none() {
annotations.calendar = Some(kv.value);
calendar_crit = kv.critical;
continue;
}
if calendar_crit || kv.critical {
return Err(Error::general(
"Cannot have critical flag with duplicate calendar annotations",
start,
));
}
} else if kv.critical {
return Err(Error::general("Unrecognized critical annotation.", start));
}
}
Ok(annotations)
}
/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair.
fn parse_kv_annotation(cursor: &mut IsoCursor) -> ParseResult<KeyValueAnnotation> {
debug_assert!(cursor.check_or(false, is_annotation_open));
let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?;
let (leading_char, critical) = if is_critical_flag(potential_critical) {
(cursor.next().ok_or_else(|| Error::AbruptEnd)?, true)
} else {
(potential_critical, false)
};
if !is_a_key_leading_char(leading_char) {
return Err(LexError::syntax(
"Invalid AnnotationKey leading character",
Position::new(1, cursor.pos() + 1),
)
.into());
}
// Parse AnnotationKey.
let annotation_key = parse_annotation_key(cursor)?;
debug_assert!(cursor.check_or(false, is_annotation_key_value_separator));
// Advance past the '=' character.
cursor.advance();
// Parse AnnotationValue.
let annotation_value = parse_annotation_value(cursor)?;
// Assert that we are at the annotation close and advance cursor past annotation to close.
debug_assert!(cursor.check_or(false, is_annotation_close));
cursor.advance();
Ok(KeyValueAnnotation {
key: annotation_key,
value: annotation_value,
critical,
})
}
/// Parse an `AnnotationKey`.
fn parse_annotation_key(cursor: &mut IsoCursor) -> ParseResult<String> {
let key_start = cursor.pos();
while let Some(potential_key_char) = cursor.next() {
// End of key.
if is_annotation_key_value_separator(potential_key_char) {
// Return found key
return Ok(cursor.slice(key_start, cursor.pos()));
}
if !is_a_key_char(potential_key_char) {
return Err(LexError::syntax(
"Invalid AnnotationKey Character",
Position::new(1, cursor.pos() + 1),
)
.into());
}
}
Err(Error::AbruptEnd)
}
/// Parse an `AnnotationValue`.
fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult<String> {
let value_start = cursor.pos();
while let Some(potential_value_char) = cursor.next() {
if is_annotation_close(potential_value_char) {
// Return the determined AnnotationValue.
return Ok(cursor.slice(value_start, cursor.pos()));
}
if is_hyphen(potential_value_char) {
if !cursor
.peek_n(1)
.map_or(false, is_annotation_value_component)
{
return Err(LexError::syntax(
"Missing AttributeValueComponent after '-'",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance();
continue;
}
if !is_annotation_value_component(potential_value_char) {
return Err(LexError::syntax(
"Invalid character in AnnotationValue",
Position::new(1, value_start + cursor.pos() + 1),
)
.into());
}
}
Err(Error::AbruptEnd)
}

373
boa_parser/src/temporal/date_time.rs

@ -0,0 +1,373 @@
//! Parsing for Temporal's ISO8601 `Date` and `DateTime`.
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
temporal::{
annotations,
grammar::{is_date_time_separator, is_sign, is_utc_designator},
time,
time::TimeSpec,
time_zone, IsoCursor, IsoParseRecord,
},
};
use boa_ast::{temporal::TimeZone, Position, Span};
use super::grammar::{is_annotation_open, is_hyphen};
#[derive(Debug, Default, Clone)]
/// A `DateTime` Parse Node that contains the date, time, and offset info.
pub(crate) struct DateTimeRecord {
/// Date
pub(crate) date: DateRecord,
/// Time
pub(crate) time: Option<TimeSpec>,
/// Tz Offset
pub(crate) time_zone: Option<TimeZone>,
}
#[derive(Default, Debug, Clone, Copy)]
/// The record of a parsed date.
pub(crate) struct DateRecord {
/// Date Year
pub(crate) year: i32,
/// Date Month
pub(crate) month: i32,
/// Date Day
pub(crate) day: i32,
}
/// This function handles parsing for [`AnnotatedDateTime`][datetime],
/// [`AnnotatedDateTimeTimeRequred`][time], and
/// [`TemporalInstantString.`][instant] according to the requirements
/// provided via Spec.
///
/// [datetime]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime
/// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired
/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
pub(crate) fn parse_annotated_date_time(
zoned: bool,
time_required: bool,
utc_required: bool,
cursor: &mut IsoCursor,
) -> ParseResult<IsoParseRecord> {
let date_time = parse_date_time(time_required, utc_required, cursor)?;
// Peek Annotation presence
// Throw error if annotation does not exist and zoned is true, else return.
let annotation_check = cursor.check_or(false, is_annotation_open);
if !annotation_check {
if zoned {
return Err(Error::expected(
["TimeZoneAnnotation".into()],
"No Annotation",
Span::new(
Position::new(1, cursor.pos() + 1),
Position::new(1, cursor.pos() + 1),
),
"iso8601 grammar",
));
}
return Ok(IsoParseRecord {
date: date_time.date,
time: date_time.time,
tz: date_time.time_zone,
calendar: None,
});
}
let mut tz = TimeZone::default();
if let Some(tz_info) = date_time.time_zone {
tz = tz_info;
}
let annotation_set = annotations::parse_annotation_set(zoned, cursor)?;
if let Some(annotated_tz) = annotation_set.tz {
tz = annotated_tz.tz;
}
let tz = if tz.name.is_some() || tz.offset.is_some() {
Some(tz)
} else {
None
};
Ok(IsoParseRecord {
date: date_time.date,
time: date_time.time,
tz,
calendar: annotation_set.calendar,
})
}
/// Parses a `DateTime` record.
fn parse_date_time(
time_required: bool,
utc_required: bool,
cursor: &mut IsoCursor,
) -> ParseResult<DateTimeRecord> {
let date = parse_date(cursor)?;
// If there is no `DateTimeSeparator`, return date early.
if !cursor.check_or(false, is_date_time_separator) {
if time_required {
return Err(Error::general(
"Missing a required TimeSpec.",
Position::new(1, cursor.pos() + 1),
));
}
return Ok(DateTimeRecord {
date,
time: None,
time_zone: None,
});
}
cursor.advance();
let time = time::parse_time_spec(cursor)?;
let time_zone = if cursor
.check(|ch| is_sign(ch) || is_utc_designator(ch))
.unwrap_or(false)
{
Some(time_zone::parse_date_time_utc(cursor)?)
} else {
if utc_required {
return Err(Error::general(
"DateTimeUTCOffset is required.",
Position::new(1, cursor.pos() + 1),
));
}
None
};
Ok(DateTimeRecord {
date,
time: Some(time),
time_zone,
})
}
/// Parses `Date` record.
fn parse_date(cursor: &mut IsoCursor) -> ParseResult<DateRecord> {
let year = parse_date_year(cursor)?;
let divided = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?;
if divided {
cursor.advance();
}
let month = parse_date_month(cursor)?;
if cursor.check_or(false, is_hyphen) {
if !divided {
return Err(LexError::syntax(
"Invalid date separator",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance();
}
let day = parse_date_day(cursor)?;
Ok(DateRecord { year, month, day })
}
/// Determines if the string can be parsed as a `DateSpecYearMonth`.
pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult<bool> {
let mut ym_peek = if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) {
7
} else {
4
};
if cursor
.peek_n(ym_peek)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?
{
ym_peek += 1;
}
ym_peek += 2;
if cursor.peek_n(ym_peek).map_or(true, is_annotation_open) {
Ok(true)
} else {
Ok(false)
}
}
/// Parses a `DateSpecYearMonth`
pub(crate) fn parse_year_month(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> {
let year = parse_date_year(cursor)?;
if cursor.check_or(false, is_hyphen) {
cursor.advance();
}
let month = parse_date_month(cursor)?;
Ok((year, month))
}
/// Determines if the string can be parsed as a `DateSpecYearMonth`.
pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult<bool> {
let mut md_peek = if cursor
.peek_n(1)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?
{
4
} else {
2
};
if cursor
.peek_n(md_peek)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?
{
md_peek += 1;
}
md_peek += 2;
if cursor.peek_n(md_peek).map_or(true, is_annotation_open) {
Ok(true)
} else {
Ok(false)
}
}
/// Parses a `DateSpecMonthDay`
pub(crate) fn parse_month_day(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> {
let dash_one = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?;
let dash_two = cursor
.peek_n(1)
.map(is_hyphen)
.ok_or_else(|| Error::AbruptEnd)?;
if dash_two && dash_one {
cursor.advance_n(2);
} else if dash_two && !dash_one {
return Err(LexError::syntax(
"MonthDay requires two dashes",
Position::new(1, cursor.pos()),
)
.into());
}
let month = parse_date_month(cursor)?;
if cursor.check_or(false, is_hyphen) {
cursor.advance();
}
let day = parse_date_day(cursor)?;
Ok((month, day))
}
// ==== Unit Parsers ====
fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult<i32> {
if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) {
let year_start = cursor.pos();
let sign = if cursor.check_or(false, |ch| ch == '+') {
1
} else {
-1
};
cursor.advance();
for _ in 0..6 {
let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?;
if !year_digit.is_ascii_digit() {
return Err(Error::lex(LexError::syntax(
"DateYear must contain digit",
Position::new(1, cursor.pos() + 1),
)));
}
cursor.advance();
}
let year_string = cursor.slice(year_start + 1, cursor.pos());
let year_value = year_string
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, year_start + 1)))?;
// 13.30.1 Static Semantics: Early Errors
//
// It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000).
if sign == -1 && year_value == 0 {
return Err(Error::lex(LexError::syntax(
"Cannot have negative 0 years.",
Position::new(1, year_start + 1),
)));
}
return Ok(sign * year_value);
}
let year_start = cursor.pos();
for _ in 0..4 {
let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?;
if !year_digit.is_ascii_digit() {
return Err(LexError::syntax(
"DateYear must contain digit",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance();
}
let year_string = cursor.slice(year_start, cursor.pos());
let year_value = year_string
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?;
Ok(year_value)
}
fn parse_date_month(cursor: &mut IsoCursor) -> ParseResult<i32> {
let month_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?;
if !(1..=12).contains(&month_value) {
return Err(LexError::syntax(
"DateMonth must be in a range of 1-12",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance_n(2);
Ok(month_value)
}
fn parse_date_day(cursor: &mut IsoCursor) -> ParseResult<i32> {
let day_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<i32>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?;
if !(1..=31).contains(&day_value) {
return Err(LexError::syntax(
"DateDay must be in a range of 1-31",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance_n(2);
Ok(day_value)
}

275
boa_parser/src/temporal/duration.rs

@ -0,0 +1,275 @@
use boa_ast::Position;
use crate::{
error::{Error, ParseResult},
temporal::{
grammar::{
is_day_designator, is_decimal_separator, is_duration_designator, is_hour_designator,
is_minute_designator, is_month_designator, is_second_designator, is_sign,
is_time_designator, is_week_designator, is_year_designator,
},
time::parse_fraction,
IsoCursor,
},
};
/// A ISO8601 `DurationRecord` Parse Node.
#[derive(Debug, Clone, Copy)]
pub(crate) struct DurationParseRecord {
/// Duration Sign
pub(crate) sign: bool,
/// A `DateDuration` record.
pub(crate) date: DateDuration,
/// A `TimeDuration` record.
pub(crate) time: TimeDuration,
}
/// A `DateDuration` Parse Node.
#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct DateDuration {
/// Years value.
pub(crate) years: i32,
/// Months value.
pub(crate) months: i32,
/// Weeks value.
pub(crate) weeks: i32,
/// Days value.
pub(crate) days: i32,
}
/// A `TimeDuration` Parse Node
#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct TimeDuration {
/// Hours value.
pub(crate) hours: i32,
/// Hours fraction value.
pub(crate) fhours: f64,
/// Minutes value with fraction.
pub(crate) minutes: i32,
/// Minutes fraction value.
pub(crate) fminutes: f64,
/// Seconds value with fraction.
pub(crate) seconds: i32,
/// Seconds fraction value,
pub(crate) fseconds: f64,
}
pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult<DurationParseRecord> {
let sign = if cursor.check(is_sign).ok_or_else(|| Error::AbruptEnd)? {
let sign = cursor.check_or(false, |ch| ch == '+');
cursor.advance();
sign
} else {
true
};
if !cursor
.check(is_duration_designator)
.ok_or_else(|| Error::AbruptEnd)?
{
return Err(Error::general(
"DurationString missing DurationDesignator.",
Position::new(1, cursor.pos() + 1),
));
}
cursor.advance();
let date = if cursor.check_or(false, is_time_designator) {
Some(DateDuration::default())
} else {
Some(parse_date_duration(cursor)?)
};
let time = if cursor.check_or(false, is_time_designator) {
cursor.advance();
Some(parse_time_duration(cursor)?)
} else {
None
};
if cursor.peek().is_some() {
return Err(Error::general(
"Unrecognized value in DurationString.",
Position::new(1, cursor.pos()),
));
}
Ok(DurationParseRecord {
sign,
date: date.unwrap_or_default(),
time: time.unwrap_or_default(),
})
}
#[derive(PartialEq, PartialOrd, Eq, Ord)]
enum DateUnit {
None = 0,
Year,
Month,
Week,
Day,
}
pub(crate) fn parse_date_duration(cursor: &mut IsoCursor) -> ParseResult<DateDuration> {
let mut date = DateDuration::default();
let mut previous_unit = DateUnit::None;
while cursor.check_or(false, |ch| ch.is_ascii_digit()) {
let digit_start = cursor.pos();
while cursor.check_or(false, |ch| ch.is_ascii_digit()) {
cursor.advance();
}
let value = cursor
.slice(digit_start, cursor.pos())
.parse::<i32>()
.map_err(|err| {
Error::general(err.to_string(), Position::new(digit_start, cursor.pos()))
})?;
match cursor.peek() {
Some(ch) if is_year_designator(ch) => {
if previous_unit > DateUnit::Year {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
}
date.years = value;
previous_unit = DateUnit::Year;
}
Some(ch) if is_month_designator(ch) => {
if previous_unit > DateUnit::Month {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
}
date.months = value;
previous_unit = DateUnit::Month;
}
Some(ch) if is_week_designator(ch) => {
if previous_unit > DateUnit::Week {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
}
date.weeks = value;
previous_unit = DateUnit::Week;
}
Some(ch) if is_day_designator(ch) => {
if previous_unit > DateUnit::Day {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
}
date.days = value;
previous_unit = DateUnit::Day;
}
Some(_) | None => return Err(Error::AbruptEnd),
}
cursor.advance();
}
Ok(date)
}
#[derive(PartialEq, PartialOrd, Eq, Ord)]
enum TimeUnit {
None = 0,
Hour,
Minute,
Second,
}
pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult<TimeDuration> {
let mut time = TimeDuration::default();
if !cursor.check_or(false, |ch| ch.is_ascii()) {
return Err(Error::general(
"No time values provided after TimeDesignator.",
Position::new(1, cursor.pos()),
));
}
let mut previous_unit = TimeUnit::None;
let mut fraction_present = false;
while cursor.check_or(false, |ch| ch.is_ascii_digit()) {
let digit_start = cursor.pos();
while cursor.check_or(false, |ch| ch.is_ascii_digit()) {
cursor.advance();
}
let value = cursor
.slice(digit_start, cursor.pos())
.parse::<i32>()
.map_err(|err| {
Error::general(err.to_string(), Position::new(digit_start, cursor.pos()))
})?;
let fraction = if cursor.check_or(false, is_decimal_separator) {
fraction_present = true;
parse_fraction(cursor)?
} else {
0.0
};
match cursor.peek() {
Some(ch) if is_hour_designator(ch) => {
if previous_unit > TimeUnit::Hour {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
}
time.hours = value;
time.fhours = fraction;
previous_unit = TimeUnit::Hour;
}
Some(ch) if is_minute_designator(ch) => {
if previous_unit > TimeUnit::Minute {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
}
time.minutes = value;
time.fminutes = fraction;
previous_unit = TimeUnit::Minute;
}
Some(ch) if is_second_designator(ch) => {
if previous_unit > TimeUnit::Second {
return Err(Error::general(
"Not a valid DateDuration order",
Position::new(1, cursor.pos()),
));
}
time.seconds = value;
time.fseconds = fraction;
previous_unit = TimeUnit::Second;
}
Some(_) | None => return Err(Error::AbruptEnd),
}
cursor.advance();
if fraction_present {
if cursor.check_or(false, |ch| ch.is_ascii_digit()) {
return Err(Error::general(
"Invalid TimeDuration continuation after FractionPart.",
Position::new(1, cursor.pos()),
));
}
break;
}
}
Ok(time)
}

136
boa_parser/src/temporal/grammar.rs

@ -0,0 +1,136 @@
//! ISO8601 specific grammar checks.
/// Checks if char is a `AKeyLeadingChar`.
#[inline]
pub(crate) const fn is_a_key_leading_char(ch: char) -> bool {
ch.is_ascii_lowercase() || ch == '_'
}
/// Checks if char is an `AKeyChar`.
#[inline]
pub(crate) const fn is_a_key_char(ch: char) -> bool {
is_a_key_leading_char(ch) || ch.is_ascii_digit() || ch == '-'
}
/// Checks if char is an `AnnotationValueComponent`.
pub(crate) const fn is_annotation_value_component(ch: char) -> bool {
ch.is_ascii_digit() || ch.is_ascii_alphabetic()
}
/// Checks if char is a `TZLeadingChar`.
#[inline]
pub(crate) const fn is_tz_leading_char(ch: char) -> bool {
ch.is_ascii_alphabetic() || ch == '_' || ch == '.'
}
/// Checks if char is a `TZChar`.
#[inline]
pub(crate) const fn is_tz_char(ch: char) -> bool {
is_tz_leading_char(ch) || ch.is_ascii_digit() || ch == '-' || ch == '+'
}
/// Checks if char is a `TimeZoneIANAName` Separator.
pub(crate) const fn is_tz_name_separator(ch: char) -> bool {
ch == '/'
}
/// Checks if char is an ascii sign.
pub(crate) const fn is_ascii_sign(ch: char) -> bool {
ch == '+' || ch == '-'
}
/// Checks if char is an ascii sign or U+2212
pub(crate) const fn is_sign(ch: char) -> bool {
is_ascii_sign(ch) || ch == '\u{2212}'
}
/// Checks if char is a `TimeSeparator`.
pub(crate) const fn is_time_separator(ch: char) -> bool {
ch == ':'
}
/// Checks if char is a `TimeDesignator`.
pub(crate) const fn is_time_designator(ch: char) -> bool {
ch == 'T' || ch == 't'
}
/// Checks if char is a `DateTimeSeparator`.
pub(crate) const fn is_date_time_separator(ch: char) -> bool {
is_time_designator(ch) || ch == '\u{0020}'
}
/// Checks if char is a `UtcDesignator`.
pub(crate) const fn is_utc_designator(ch: char) -> bool {
ch == 'Z' || ch == 'z'
}
/// Checks if char is a `DurationDesignator`.
pub(crate) const fn is_duration_designator(ch: char) -> bool {
ch == 'P' || ch == 'p'
}
/// Checks if char is a `YearDesignator`.
pub(crate) const fn is_year_designator(ch: char) -> bool {
ch == 'Y' || ch == 'y'
}
/// Checks if char is a `MonthsDesignator`.
pub(crate) const fn is_month_designator(ch: char) -> bool {
ch == 'M' || ch == 'm'
}
/// Checks if char is a `WeekDesignator`.
pub(crate) const fn is_week_designator(ch: char) -> bool {
ch == 'W' || ch == 'w'
}
/// Checks if char is a `DayDesignator`.
pub(crate) const fn is_day_designator(ch: char) -> bool {
ch == 'D' || ch == 'd'
}
/// checks if char is a `DayDesignator`.
pub(crate) const fn is_hour_designator(ch: char) -> bool {
ch == 'H' || ch == 'h'
}
/// Checks if char is a `MinuteDesignator`.
pub(crate) const fn is_minute_designator(ch: char) -> bool {
is_month_designator(ch)
}
/// checks if char is a `SecondDesignator`.
pub(crate) const fn is_second_designator(ch: char) -> bool {
ch == 'S' || ch == 's'
}
/// Checks if char is a `DecimalSeparator`.
pub(crate) const fn is_decimal_separator(ch: char) -> bool {
ch == '.' || ch == ','
}
/// Checks if char is an `AnnotationOpen`.
pub(crate) const fn is_annotation_open(ch: char) -> bool {
ch == '['
}
/// Checks if char is an `AnnotationClose`.
pub(crate) const fn is_annotation_close(ch: char) -> bool {
ch == ']'
}
/// Checks if char is an `CriticalFlag`.
pub(crate) const fn is_critical_flag(ch: char) -> bool {
ch == '!'
}
/// Checks if char is the `AnnotationKeyValueSeparator`.
pub(crate) const fn is_annotation_key_value_separator(ch: char) -> bool {
ch == '='
}
/// Checks if char is a hyphen. Hyphens are used as a Date separator
/// and as a `AttributeValueComponent` separator.
pub(crate) const fn is_hyphen(ch: char) -> bool {
ch == '-'
}

348
boa_parser/src/temporal/mod.rs

@ -0,0 +1,348 @@
//! Implementation of Iso8601 grammar lexing/parsing
use crate::error::ParseResult;
mod annotations;
mod date_time;
mod duration;
mod grammar;
mod time;
mod time_zone;
use boa_ast::temporal::{IsoDate, IsoDateTime, IsoDuration, IsoTime, TimeZone};
use date_time::DateRecord;
use time::TimeSpec;
#[cfg(feature = "experimental")]
#[cfg(test)]
mod tests;
// TODO: optimize where possible.
/// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions.
///
/// `IsoParseRecord` is converted into the ISO AST Nodes.
#[derive(Default, Debug)]
pub(crate) struct IsoParseRecord {
/// Parsed Date Record
pub(crate) date: DateRecord,
/// Parsed Time
pub(crate) time: Option<TimeSpec>,
/// Parsed `TimeZone` data (UTCOffset | IANA name)
pub(crate) tz: Option<TimeZone>,
/// The parsed calendar value.
pub(crate) calendar: Option<String>,
}
/// Parse a [`TemporalDateTimeString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString
#[derive(Debug, Clone, Copy)]
pub struct TemporalDateTimeString;
impl TemporalDateTimeString {
/// Parses a targeted string as a `DateTime`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(zoned: bool, cursor: &mut IsoCursor) -> ParseResult<IsoDateTime> {
let parse_record = date_time::parse_annotated_date_time(zoned, false, false, cursor)?;
let date = IsoDate {
year: parse_record.date.year,
month: parse_record.date.month,
day: parse_record.date.day,
calendar: parse_record.calendar,
};
let time = parse_record.time.map_or_else(IsoTime::default, |time| {
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction)
});
Ok(IsoDateTime {
date,
time,
tz: parse_record.tz,
})
}
}
/// Parse a [`TemporalTimeZoneString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalTimeZoneString
#[derive(Debug, Clone, Copy)]
pub struct TemporalTimeZoneString;
impl TemporalTimeZoneString {
/// Parses a targeted string as a `TimeZone`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
time_zone::parse_time_zone(cursor)
}
}
/// Parse a [`TemporalYearMonthString`][proposal]
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalYearMonthString
#[derive(Debug, Clone, Copy)]
pub struct TemporalYearMonthString;
impl TemporalYearMonthString {
/// Parses a targeted string as a `YearMonth`
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDate> {
if date_time::peek_year_month(cursor)? {
let ym = date_time::parse_year_month(cursor)?;
let calendar = if cursor.check_or(false, |ch| ch == '[') {
let set = annotations::parse_annotation_set(false, cursor)?;
set.calendar
} else {
None
};
return Ok(IsoDate {
year: ym.0,
month: ym.1,
day: 0,
calendar,
});
}
let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?;
Ok(IsoDate {
year: parse_record.date.year,
month: parse_record.date.month,
day: parse_record.date.day,
calendar: parse_record.calendar,
})
}
}
/// Parse a [`TemporalMonthDayString`][proposal]
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalMonthDayString
#[derive(Debug, Clone, Copy)]
pub struct TemporalMonthDayString;
impl TemporalMonthDayString {
/// Parses a targeted string as a `MonthDay`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDate> {
if date_time::peek_month_day(cursor)? {
let md = date_time::parse_month_day(cursor)?;
let calendar = if cursor.check_or(false, |ch| ch == '[') {
let set = annotations::parse_annotation_set(false, cursor)?;
set.calendar
} else {
None
};
return Ok(IsoDate {
year: 0,
month: md.0,
day: md.1,
calendar,
});
}
let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?;
Ok(IsoDate {
year: parse_record.date.year,
month: parse_record.date.month,
day: parse_record.date.day,
calendar: parse_record.calendar,
})
}
}
/// Parser for a [`TemporalInstantString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
#[derive(Debug, Clone, Copy)]
pub struct TemporalInstantString;
impl TemporalInstantString {
/// Parses a targeted string as an `Instant`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDateTime> {
let parse_record = date_time::parse_annotated_date_time(false, true, true, cursor)?;
let date = IsoDate {
year: parse_record.date.year,
month: parse_record.date.month,
day: parse_record.date.day,
calendar: parse_record.calendar,
};
let time = parse_record.time.map_or_else(IsoTime::default, |time| {
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction)
});
Ok(IsoDateTime {
date,
time,
tz: parse_record.tz,
})
}
}
// TODO: implement TemporalTimeString.
/// Parser for a [`TemporalDurationString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDurationString
#[derive(Debug, Clone, Copy)]
pub struct TemporalDurationString;
impl TemporalDurationString {
/// Parses a targeted string as a `Duration`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDuration> {
let parse_record = duration::parse_duration(cursor)?;
let minutes = if parse_record.time.fhours > 0.0 {
parse_record.time.fhours * 60.0
} else {
f64::from(parse_record.time.minutes)
};
let seconds = if parse_record.time.fminutes > 0.0 {
parse_record.time.fminutes * 60.0
} else if parse_record.time.seconds > 0 {
f64::from(parse_record.time.seconds)
} else {
minutes.rem_euclid(1.0) * 60.0
};
let milliseconds = if parse_record.time.fseconds > 0.0 {
parse_record.time.fseconds * 1000.0
} else {
seconds.rem_euclid(1.0) * 1000.0
};
let micro = milliseconds.rem_euclid(1.0) * 1000.0;
let nano = micro.rem_euclid(1.0) * 1000.0;
let sign = if parse_record.sign { 1 } else { -1 };
Ok(IsoDuration {
years: parse_record.date.years * sign,
months: parse_record.date.months * sign,
weeks: parse_record.date.weeks * sign,
days: parse_record.date.days * sign,
hours: parse_record.time.hours * sign,
minutes: minutes.floor() * f64::from(sign),
seconds: seconds.floor() * f64::from(sign),
milliseconds: milliseconds.floor() * f64::from(sign),
microseconds: micro.floor() * f64::from(sign),
nanoseconds: nano.floor() * f64::from(sign),
})
}
}
// ==== Mini cursor implementation for Iso8601 targets ====
/// `IsoCursor` is a small cursor implementation for parsing Iso8601 grammar.
#[derive(Debug)]
pub struct IsoCursor {
pos: u32,
source: Vec<char>,
}
impl IsoCursor {
/// Create a new cursor from a source `String` value.
#[must_use]
pub fn new(source: &str) -> Self {
Self {
pos: 0,
source: source.chars().collect(),
}
}
/// Returns a string value from a slice of the cursor.
fn slice(&self, start: u32, end: u32) -> String {
self.source[start as usize..end as usize].iter().collect()
}
/// Get current position
const fn pos(&self) -> u32 {
self.pos
}
/// Peek the value at the current position.
fn peek(&self) -> Option<char> {
if (self.pos as usize) < self.source.len() {
Some(self.source[self.pos as usize])
} else {
None
}
}
/// Peek the value at n len from current.
fn peek_n(&self, n: u32) -> Option<char> {
let target = (self.pos + n) as usize;
if target < self.source.len() {
Some(self.source[target])
} else {
None
}
}
/// Returns boolean if current position passes check.
fn check<F>(&self, f: F) -> Option<bool>
where
F: FnOnce(char) -> bool,
{
self.peek().map(f)
}
/// Returns boolean if current position passes check or default if None.
fn check_or<F>(&self, default: bool, f: F) -> bool
where
F: FnOnce(char) -> bool,
{
self.peek().map_or(default, f)
}
/// Advances the cursor's position and returns the new character.
fn next(&mut self) -> Option<char> {
self.advance();
self.peek()
}
/// Advances the cursor's position by 1.
fn advance(&mut self) {
self.pos += 1;
}
/// Advances the cursor's position by `n`.
fn advance_n(&mut self, n: u32) {
self.pos += n;
}
}

190
boa_parser/src/temporal/tests.rs

@ -0,0 +1,190 @@
use super::{
IsoCursor, TemporalDateTimeString, TemporalDurationString, TemporalInstantString,
TemporalMonthDayString, TemporalYearMonthString,
};
#[test]
fn temporal_parser_basic() {
let basic = "20201108";
let basic_separated = "2020-11-08";
let basic_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap();
let sep_result =
TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic_separated)).unwrap();
assert_eq!(basic_result.date.year, 2020);
assert_eq!(basic_result.date.month, 11);
assert_eq!(basic_result.date.day, 8);
assert_eq!(basic_result.date.year, sep_result.date.year);
assert_eq!(basic_result.date.month, sep_result.date.month);
assert_eq!(basic_result.date.day, sep_result.date.day);
}
#[test]
#[allow(clippy::cast_possible_truncation)]
fn temporal_date_time_max() {
// Fractions not accurate, but for testing purposes.
let date_time =
"+002020-11-08T12:28:32.329402834[!America/Argentina/ComodRivadavia][!u-ca=iso8601]";
let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(date_time)).unwrap();
let time_results = &result.time;
assert_eq!(time_results.hour, 12);
assert_eq!(time_results.minute, 28);
assert_eq!(time_results.second, 32);
assert_eq!(time_results.millisecond, 329);
assert_eq!(time_results.microsecond, 402);
assert_eq!(time_results.nanosecond, 834);
let tz = &result.tz.unwrap();
// OffsetSubMinute is Empty when TimeZoneIdentifier is present.
assert!(&tz.offset.is_none());
let tz_name = &tz.name.clone().unwrap();
assert_eq!(tz_name, "America/Argentina/ComodRivadavia");
assert_eq!(&result.date.calendar, &Some("iso8601".to_string()));
}
#[test]
fn temporal_year_parsing() {
let long = "+002020-11-08";
let bad_year = "-000000-11-08";
let result_good = TemporalDateTimeString::parse(false, &mut IsoCursor::new(long)).unwrap();
assert_eq!(result_good.date.year, 2020);
let err_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(bad_year));
assert!(err_result.is_err());
}
#[test]
fn temporal_annotated_date_time() {
let basic = "2020-11-08[America/Argentina/ComodRivadavia][u-ca=iso8601][foo=bar]";
let omitted = "+0020201108[u-ca=iso8601][f-1a2b=a0sa-2l4s]";
let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap();
let tz = &result.tz.unwrap().name.unwrap();
assert_eq!(tz, "America/Argentina/ComodRivadavia");
assert_eq!(&result.date.calendar, &Some("iso8601".to_string()));
let omit_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(omitted)).unwrap();
assert!(&omit_result.tz.is_none());
assert_eq!(&omit_result.date.calendar, &Some("iso8601".to_string()));
}
#[test]
fn temporal_year_month() {
let possible_year_months = &[
"+002020-11",
"2020-11[u-ca=iso8601]",
"+00202011",
"202011[u-ca=iso8601]",
];
for ym in possible_year_months {
let result = TemporalYearMonthString::parse(&mut IsoCursor::new(ym)).unwrap();
assert_eq!(result.year, 2020);
assert_eq!(result.month, 11);
if let Some(calendar) = result.calendar {
assert_eq!(calendar, "iso8601");
}
}
}
#[test]
fn temporal_month_day() {
let possible_month_day = ["11-07", "1107[+04:00]", "--11-07", "--1107[+04:00]"];
for md in possible_month_day {
let result = TemporalMonthDayString::parse(&mut IsoCursor::new(md)).unwrap();
assert_eq!(result.month, 11);
assert_eq!(result.day, 7);
}
}
#[test]
fn temporal_invalid_annotations() {
let invalid_annotations = [
"2020-11-11[!u-ca=iso8601][u-ca=iso8601]",
"2020-11-11[u-ca=iso8601][!u-ca=iso8601]",
"2020-11-11[u-ca=iso8601][!rip=this-invalid-annotation]",
];
for invalid in invalid_annotations {
let err_result = TemporalMonthDayString::parse(&mut IsoCursor::new(invalid));
assert!(err_result.is_err());
}
}
#[test]
fn temporal_valid_instant_strings() {
let instants = [
"1970-01-01T00:00+00:00[!Africa/Abidjan]",
"1970-01-01T00:00+00:00[UTC]",
"1970-01-01T00:00Z[!Europe/Vienna]",
];
for test in instants {
let result = TemporalInstantString::parse(&mut IsoCursor::new(test));
assert!(result.is_ok());
}
}
#[test]
#[allow(clippy::cast_possible_truncation)]
fn temporal_duration_parsing() {
let durations = [
"p1y1m1dt1h1m1s",
"P1Y1M1W1DT1H1M1.1S",
"-P1Y1M1W1DT1H1M1.123456789S",
"-P1Y3wT0,5H",
];
for dur in durations {
let ok_result = TemporalDurationString::parse(&mut IsoCursor::new(dur));
assert!(ok_result.is_ok());
}
let sub = durations[2];
let sub_second = TemporalDurationString::parse(&mut IsoCursor::new(sub)).unwrap();
assert_eq!(sub_second.milliseconds, -123.0);
assert_eq!(sub_second.microseconds, -456.0);
assert_eq!(sub_second.nanoseconds, -789.0);
let dur = durations[3];
let test_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)).unwrap();
assert_eq!(test_result.years, -1);
assert_eq!(test_result.weeks, -3);
assert_eq!(test_result.minutes, -30.0);
}
#[test]
fn temporal_invalid_durations() {
let invalids = [
"P1Y1M1W0,5D",
"P1Y1M1W1DT1H1M1.123456789123S",
"+PT",
"P1Y1M1W1DT1H0.5M0.5S",
];
for test in invalids {
let err = TemporalDurationString::parse(&mut IsoCursor::new(test));
assert!(err.is_err());
}
}

146
boa_parser/src/temporal/time.rs

@ -0,0 +1,146 @@
//! Parsing of ISO8601 Time Values
use super::{
grammar::{is_decimal_separator, is_time_separator},
IsoCursor,
};
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
};
/// Parsed Time info
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct TimeSpec {
/// An hour
pub(crate) hour: u8,
/// A minute value
pub(crate) minute: u8,
/// A second value.
pub(crate) second: u8,
/// A floating point number representing the sub-second values
pub(crate) fraction: f64,
}
use boa_ast::Position;
/// Parse `TimeSpec`
pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult<TimeSpec> {
let hour = parse_hour(cursor)?;
let mut separator = false;
if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) {
if cursor.check_or(false, is_time_separator) {
separator = true;
cursor.advance();
}
} else {
return Ok(TimeSpec {
hour,
minute: 0,
second: 0,
fraction: 0.0,
});
}
let minute = parse_minute_second(cursor, false)?;
if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) {
let is_time_separator = cursor.check_or(false, is_time_separator);
if separator && is_time_separator {
cursor.advance();
} else if is_time_separator {
return Err(
LexError::syntax("Invalid TimeSeparator", Position::new(1, cursor.pos())).into(),
);
}
} else {
return Ok(TimeSpec {
hour,
minute,
second: 0,
fraction: 0.0,
});
}
let second = parse_minute_second(cursor, true)?;
let fraction = if cursor.check_or(false, is_decimal_separator) {
parse_fraction(cursor)?
} else {
0.0
};
Ok(TimeSpec {
hour,
minute,
second,
fraction,
})
}
pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult<u8> {
let hour_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<u8>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?;
if !(0..=23).contains(&hour_value) {
return Err(LexError::syntax(
"Hour must be in a range of 0-23",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance_n(2);
Ok(hour_value)
}
// NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`.
/// Parse `MinuteSecond`
pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> ParseResult<u8> {
let min_sec_value = cursor
.slice(cursor.pos(), cursor.pos() + 2)
.parse::<u8>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?;
let valid_range = if inclusive { 0..=60 } else { 0..=59 };
if !valid_range.contains(&min_sec_value) {
return Err(LexError::syntax(
"MinuteSecond must be in a range of 0-59",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance_n(2);
Ok(min_sec_value)
}
/// Parse a `Fraction` value
///
/// This is primarily used in ISO8601 to add percision past
/// a second.
pub(crate) fn parse_fraction(cursor: &mut IsoCursor) -> ParseResult<f64> {
// Decimal is skipped by next call.
let mut fraction_components = Vec::from(['.']);
while let Some(ch) = cursor.next() {
if !ch.is_ascii_digit() {
if fraction_components.len() > 10 {
return Err(Error::general(
"Fraction exceeds 9 DecimalDigits",
Position::new(1, cursor.pos() - 1),
));
}
let fraction_value = fraction_components
.iter()
.collect::<String>()
.parse::<f64>()
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() - 1)))?;
return Ok(fraction_value);
}
fraction_components.push(ch);
}
Err(Error::AbruptEnd)
}

263
boa_parser/src/temporal/time_zone.rs

@ -0,0 +1,263 @@
//! ISO8601 parsing for Time Zone and Offset data.
use super::{
grammar::{
is_a_key_char, is_a_key_leading_char, is_annotation_close,
is_annotation_key_value_separator, is_annotation_open, is_critical_flag,
is_decimal_separator, is_sign, is_time_separator, is_tz_char, is_tz_leading_char,
is_tz_name_separator, is_utc_designator,
},
time::{parse_fraction, parse_hour, parse_minute_second},
IsoCursor,
};
use crate::{
error::{Error, ParseResult},
lexer::Error as LexError,
};
use boa_ast::{
temporal::{TimeZone, UTCOffset},
Position,
};
/// A `TimeZoneAnnotation`.
#[derive(Debug, Clone)]
#[allow(unused)]
pub(crate) struct TimeZoneAnnotation {
/// Critical Flag for the annotation.
pub(crate) critical: bool,
/// TimeZone Data
pub(crate) tz: TimeZone,
}
// ==== Time Zone Annotation Parsing ====
pub(crate) fn parse_ambiguous_tz_annotation(
cursor: &mut IsoCursor,
) -> ParseResult<Option<TimeZoneAnnotation>> {
// Peek position + 1 to check for critical flag.
let mut current_peek = 1;
let critical = cursor
.peek_n(current_peek)
.map(is_critical_flag)
.ok_or_else(|| Error::AbruptEnd)?;
// Advance cursor if critical flag present.
if critical {
current_peek += 1;
}
let leading_char = cursor
.peek_n(current_peek)
.ok_or_else(|| Error::AbruptEnd)?;
if is_tz_leading_char(leading_char) || is_sign(leading_char) {
// Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`.
if is_a_key_leading_char(leading_char) {
let mut peek_pos = current_peek + 1;
while let Some(ch) = cursor.peek_n(peek_pos) {
if is_tz_name_separator(ch) || (is_tz_char(ch) && !is_a_key_char(ch)) {
let tz = parse_tz_annotation(cursor)?;
return Ok(Some(tz));
} else if is_annotation_key_value_separator(ch)
|| (is_a_key_char(ch) && !is_tz_char(ch))
{
return Ok(None);
} else if is_annotation_close(ch) {
return Err(LexError::syntax(
"Invalid Annotation",
Position::new(1, peek_pos + 1),
)
.into());
}
peek_pos += 1;
}
return Err(Error::AbruptEnd);
}
let tz = parse_tz_annotation(cursor)?;
return Ok(Some(tz));
}
if is_a_key_leading_char(leading_char) {
return Ok(None);
};
Err(Error::lex(LexError::syntax(
"Unexpected character in ambiguous annotation.",
Position::new(1, cursor.pos() + 1),
)))
}
fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult<TimeZoneAnnotation> {
debug_assert!(is_annotation_open(cursor.peek().expect("annotation start")));
let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?;
let critical = is_critical_flag(potential_critical);
if critical {
cursor.advance();
}
let tz = parse_time_zone(cursor)?;
if !cursor.check_or(false, is_annotation_close) {
return Err(LexError::syntax(
"Invalid TimeZoneAnnotation.",
Position::new(1, cursor.pos() + 1),
)
.into());
}
cursor.advance();
Ok(TimeZoneAnnotation { critical, tz })
}
/// Parses the [`TimeZoneIdentifier`][tz] node.
///
/// [tz]: https://tc39.es/proposal-temporal/#prod-TimeZoneIdentifier
pub(crate) fn parse_time_zone(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
let is_iana = cursor
.check(is_tz_leading_char)
.ok_or_else(|| Error::AbruptEnd)?;
let is_offset = cursor.check_or(false, is_sign);
if is_iana {
return parse_tz_iana_name(cursor);
} else if is_offset {
let offset = parse_utc_offset_minute_precision(cursor)?;
return Ok(TimeZone {
name: None,
offset: Some(offset),
});
}
Err(LexError::syntax(
"Invalid leading character for a TimeZoneIdentifier",
Position::new(1, cursor.pos() + 1),
)
.into())
}
/// Parse a `TimeZoneIANAName` Parse Node
fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
let tz_name_start = cursor.pos();
while let Some(potential_value_char) = cursor.next() {
if is_tz_name_separator(potential_value_char) {
if !cursor.peek_n(1).map_or(false, is_tz_char) {
return Err(LexError::syntax(
"Missing TimeZoneIANANameComponent after '/'",
Position::new(1, cursor.pos() + 2),
)
.into());
}
continue;
}
if !is_tz_char(potential_value_char) {
// Return the valid TimeZoneIANAName
return Ok(TimeZone {
name: Some(cursor.slice(tz_name_start, cursor.pos())),
offset: None,
});
}
}
Err(Error::AbruptEnd)
}
// ==== Utc Offset Parsing ====
/// Parse a full precision `UtcOffset`
pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult<TimeZone> {
if cursor.check_or(false, is_utc_designator) {
cursor.advance();
return Ok(TimeZone {
name: Some("UTC".to_owned()),
offset: None,
});
}
let separated = cursor.peek_n(3).map_or(false, is_time_separator);
let mut utc_to_minute = parse_utc_offset_minute_precision(cursor)?;
if cursor.check_or(false, is_time_separator) {
if !separated {
return Err(LexError::syntax(
"Unexpected TimeSeparator",
Position::new(1, cursor.pos()),
)
.into());
}
cursor.advance();
}
// Return early on None or next char an AnnotationOpen.
if cursor.check_or(true, is_annotation_open) {
return Ok(TimeZone {
name: None,
offset: Some(utc_to_minute),
});
}
// If `UtcOffsetWithSubMinuteComponents`, continue parsing.
utc_to_minute.second = parse_minute_second(cursor, true)?;
let sub_second = if cursor.check_or(false, is_decimal_separator) {
parse_fraction(cursor)?
} else {
0.0
};
utc_to_minute.fraction = sub_second;
Ok(TimeZone {
name: None,
offset: Some(utc_to_minute),
})
}
/// Parse an `UtcOffsetMinutePrecision` node
pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut IsoCursor) -> ParseResult<UTCOffset> {
let sign = if let Some(ch) = cursor.next() {
if ch == '+' {
1_i8
} else {
-1_i8
}
} else {
return Err(Error::AbruptEnd);
};
let hour = parse_hour(cursor)?;
// If at the end of the utc, then return.
if cursor
.check(|ch| !(ch.is_ascii_digit() || is_time_separator(ch)))
.ok_or_else(|| Error::AbruptEnd)?
{
return Ok(UTCOffset {
sign,
hour,
minute: 0,
second: 0,
fraction: 0.0,
});
}
// Advance cursor beyond any TimeSeparator
if cursor.check_or(false, is_time_separator) {
cursor.advance();
}
let minute = parse_minute_second(cursor, false)?;
Ok(UTCOffset {
sign,
hour,
minute,
second: 0,
fraction: 0.0,
})
}
Loading…
Cancel
Save