Browse Source

Migrate `Temporal` to its own crate. (#3461)

* Update round and total method

* Begin Temporal crate migration

* Add dyn Any context and some small changes

* General completion and clean up work of migration

* Finish up clean up and linting of draft

* Post-rebase update and a couple changes

* Rename and some cleanup

* Remove migrated record file

* Apply Review
pull/3498/head
Kevin 11 months ago committed by GitHub
parent
commit
e51e628127
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      Cargo.lock
  2. 2
      Cargo.toml
  3. 1
      boa_engine/Cargo.toml
  4. 343
      boa_engine/src/builtins/temporal/calendar/iso.rs
  5. 1633
      boa_engine/src/builtins/temporal/calendar/mod.rs
  6. 883
      boa_engine/src/builtins/temporal/calendar/object.rs
  7. 39
      boa_engine/src/builtins/temporal/calendar/tests.rs
  8. 107
      boa_engine/src/builtins/temporal/calendar/utils.rs
  9. 121
      boa_engine/src/builtins/temporal/date_equations.rs
  10. 327
      boa_engine/src/builtins/temporal/duration/mod.rs
  11. 20
      boa_engine/src/builtins/temporal/error.rs
  12. 691
      boa_engine/src/builtins/temporal/fields.rs
  13. 111
      boa_engine/src/builtins/temporal/instant/mod.rs
  14. 15
      boa_engine/src/builtins/temporal/mod.rs
  15. 257
      boa_engine/src/builtins/temporal/options.rs
  16. 236
      boa_engine/src/builtins/temporal/plain_date/iso.rs
  17. 238
      boa_engine/src/builtins/temporal/plain_date/mod.rs
  18. 100
      boa_engine/src/builtins/temporal/plain_date_time/iso.rs
  19. 17
      boa_engine/src/builtins/temporal/plain_date_time/mod.rs
  20. 31
      boa_engine/src/builtins/temporal/plain_month_day/mod.rs
  21. 52
      boa_engine/src/builtins/temporal/plain_year_month/mod.rs
  22. 17
      boa_engine/src/builtins/temporal/tests.rs
  23. 3
      boa_engine/src/builtins/temporal/zoned_date_time/mod.rs
  24. 23
      boa_temporal/Cargo.toml
  25. 11
      boa_temporal/README.md
  26. 579
      boa_temporal/src/calendar.rs
  27. 285
      boa_temporal/src/calendar/iso.rs
  28. 220
      boa_temporal/src/date.rs
  29. 95
      boa_temporal/src/datetime.rs
  30. 1500
      boa_temporal/src/duration.rs
  31. 98
      boa_temporal/src/error.rs
  32. 487
      boa_temporal/src/fields.rs
  33. 341
      boa_temporal/src/iso.rs
  34. 61
      boa_temporal/src/lib.rs
  35. 51
      boa_temporal/src/month_day.rs
  36. 413
      boa_temporal/src/options.rs
  37. 34
      boa_temporal/src/time.rs
  38. 284
      boa_temporal/src/utils.rs
  39. 53
      boa_temporal/src/year_month.rs
  40. 7
      boa_temporal/src/zoneddatetime.rs

13
Cargo.lock generated

@ -441,6 +441,7 @@ dependencies = [
"boa_macros", "boa_macros",
"boa_parser", "boa_parser",
"boa_profiler", "boa_profiler",
"boa_temporal",
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"chrono", "chrono",
@ -606,6 +607,18 @@ dependencies = [
"textwrap", "textwrap",
] ]
[[package]]
name = "boa_temporal"
version = "0.17.0"
dependencies = [
"bitflags 2.4.1",
"icu_calendar",
"num-bigint",
"num-traits",
"rustc-hash",
"tinystr",
]
[[package]] [[package]]
name = "boa_tester" name = "boa_tester"
version = "0.17.0" version = "0.17.0"

2
Cargo.toml

@ -13,6 +13,7 @@ members = [
"boa_parser", "boa_parser",
"boa_profiler", "boa_profiler",
"boa_runtime", "boa_runtime",
"boa_temporal",
"boa_tester", "boa_tester",
"boa_wasm", "boa_wasm",
] ]
@ -38,6 +39,7 @@ boa_macros = { version = "~0.17.0", path = "boa_macros" }
boa_parser = { version = "~0.17.0", path = "boa_parser" } boa_parser = { version = "~0.17.0", path = "boa_parser" }
boa_profiler = { version = "~0.17.0", path = "boa_profiler" } boa_profiler = { version = "~0.17.0", path = "boa_profiler" }
boa_runtime = { version = "~0.17.0", path = "boa_runtime" } boa_runtime = { version = "~0.17.0", path = "boa_runtime" }
boa_temporal = {version = "~0.17.0", path = "boa_temporal" }
# Shared deps # Shared deps
arbitrary = "1" arbitrary = "1"

1
boa_engine/Cargo.toml

@ -62,6 +62,7 @@ boa_profiler.workspace = true
boa_macros.workspace = true boa_macros.workspace = true
boa_ast.workspace = true boa_ast.workspace = true
boa_parser.workspace = true boa_parser.workspace = true
boa_temporal = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] } serde = { workspace = true, features = ["derive", "rc"] }
serde_json.workspace = true serde_json.workspace = true
rand = "0.8.5" rand = "0.8.5"

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

@ -1,343 +0,0 @@
//! Implementation of the "iso8601" `BuiltinCalendar`.
use crate::{
builtins::temporal::{
self,
date_equations::mathematical_days_in_year,
duration::DurationRecord,
options::{ArithmeticOverflow, TemporalUnit},
plain_date::iso::IsoDateRecord,
},
js_string, JsNativeError, JsResult, JsString,
};
use super::{BuiltinCalendar, FieldsType};
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,
) -> JsResult<IsoDateRecord> {
// 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(IsoDateRecord::from_date_iso(date))
}
/// 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,
) -> JsResult<IsoDateRecord> {
// 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]]).
Ok(IsoDateRecord::from_date_iso(result))
}
/// 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,
) -> JsResult<IsoDateRecord> {
// 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]]).
Ok(IsoDateRecord::from_date_iso(result))
}
/// 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: &DurationRecord,
_overflow: ArithmeticOverflow,
) -> JsResult<IsoDateRecord> {
// 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,
) -> JsResult<DurationRecord> {
// 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) -> JsResult<Option<JsString>> {
// Returns undefined on iso8601.
Ok(None)
}
/// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar.
fn era_year(&self, _: &IsoDateRecord) -> JsResult<Option<i32>> {
// Returns undefined on iso8601.
Ok(None)
}
/// Returns the `year` for the `Iso` calendar.
fn year(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
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)
}
/// Returns the `month` for the `Iso` calendar.
fn month(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
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 as i32)
}
/// Returns the `monthCode` for the `Iso` calendar.
fn month_code(&self, date_like: &IsoDateRecord) -> JsResult<JsString> {
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()))
}
/// Returns the `day` for the `Iso` calendar.
fn day(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
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 as i32)
}
/// Returns the `dayOfWeek` for the `Iso` calendar.
fn day_of_week(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
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 i32)
}
/// Returns the `dayOfYear` for the `Iso` calendar.
fn day_of_year(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
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))
}
/// Returns the `weekOfYear` for the `Iso` calendar.
fn week_of_year(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
// 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(i32::from(week_of.week))
}
/// Returns the `yearOfWeek` for the `Iso` calendar.
fn year_of_week(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
// 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),
RelativeUnit::Current => Ok(date.year().number),
RelativeUnit::Next => Ok(date.year().number + 1),
}
}
/// Returns the `daysInWeek` value for the `Iso` calendar.
fn days_in_week(&self, _: &IsoDateRecord) -> JsResult<i32> {
Ok(7)
}
/// Returns the `daysInMonth` value for the `Iso` calendar.
fn days_in_month(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
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.days_in_month()))
}
/// Returns the `daysInYear` value for the `Iso` calendar.
fn days_in_year(&self, date_like: &IsoDateRecord) -> JsResult<i32> {
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.days_in_year()))
}
/// Return the amount of months in an ISO Calendar.
fn months_in_year(&self, _: &IsoDateRecord) -> JsResult<i32> {
Ok(12)
}
/// Returns whether provided date is in a leap year according to this calendar.
fn in_leap_year(&self, date_like: &IsoDateRecord) -> JsResult<bool> {
// `ICU4X`'s `CalendarArithmetic` is currently private.
if mathematical_days_in_year(date_like.year()) == 366 {
return Ok(true);
}
Ok(false)
}
// Resolve the fields for the iso calendar.
fn resolve_fields(&self, fields: &mut temporal::TemporalFields, _: FieldsType) -> JsResult<()> {
fields.iso_resolve_month()?;
Ok(())
}
/// Returns the ISO field descriptors, which is not called for the iso8601 calendar.
fn field_descriptors(&self, _: FieldsType) -> Vec<(JsString, 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<JsString>) -> Vec<JsString> {
let mut result = Vec::new();
for key in &additional_keys {
result.push(key.clone());
if key.to_std_string_escaped().as_str() == "month" {
result.push(js_string!("monthCode"));
} else if key.to_std_string_escaped().as_str() == "monthCode" {
result.push(js_string!("month"));
}
}
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()
}
}

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

File diff suppressed because it is too large Load Diff

883
boa_engine/src/builtins/temporal/calendar/object.rs

@ -0,0 +1,883 @@
//! Boa's implementation of a user-defined Anonymous Calendar.
use crate::{
builtins::temporal::{plain_date, plain_month_day, plain_year_month},
property::PropertyKey,
Context, JsObject, JsString, JsValue,
};
use std::any::Any;
use boa_macros::utf16;
use boa_temporal::{
calendar::{CalendarDateLike, CalendarProtocol},
date::Date,
duration::Duration,
error::TemporalError,
fields::TemporalFields,
month_day::MonthDay,
options::ArithmeticOverflow,
year_month::YearMonth,
TemporalResult, TinyAsciiStr,
};
use num_traits::ToPrimitive;
/// A user-defined, custom calendar that is only known at runtime
/// and executed at runtime.
///
/// A user-defined calendar implements all of the `CalendarProtocolMethods`
/// and therefore satisfies the requirements to be used as a calendar.
#[derive(Debug, Clone)]
pub(crate) struct CustomRuntimeCalendar {
calendar: JsObject,
}
impl CustomRuntimeCalendar {
pub(crate) fn new(calendar: &JsObject) -> Self {
Self {
calendar: calendar.clone(),
}
}
}
impl CalendarProtocol for CustomRuntimeCalendar {
fn date_from_fields(
&self,
fields: &mut TemporalFields,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<Date> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let method = self
.calendar
.get(utf16!("dateFromFields"), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let fields = JsObject::from_temporal_fields(fields, context)
.map_err(|e| TemporalError::general(e.to_string()))?;
let overflow_obj = JsObject::with_null_proto();
overflow_obj
.create_data_property_or_throw(
utf16!("overflow"),
JsString::from(overflow.to_string()),
context,
)
.map_err(|e| TemporalError::general(e.to_string()))?;
let value = method
.as_callable()
.ok_or_else(|| {
TemporalError::general("dateFromFields must be implemented as a callable method.")
})?
.call(
&self.calendar.clone().into(),
&[fields.into(), overflow_obj.into()],
context,
)
.map_err(|e| TemporalError::general(e.to_string()))?;
let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| {
TemporalError::r#type()
.with_message("datefromFields must return a valid PlainDate object.")
})?;
let pd = obj.as_plain_date().ok_or_else(|| {
TemporalError::r#type().with_message("Object returned was not a PlainDate")
})?;
Ok(pd.inner.clone())
}
fn year_month_from_fields(
&self,
fields: &mut TemporalFields,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<YearMonth> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let method = self
.calendar
.get(utf16!("yearMonthFromFields"), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let fields = JsObject::from_temporal_fields(fields, context)
.map_err(|e| TemporalError::general(e.to_string()))?;
let overflow_obj = JsObject::with_null_proto();
overflow_obj
.create_data_property_or_throw(
utf16!("overflow"),
JsString::from(overflow.to_string()),
context,
)
.map_err(|e| TemporalError::general(e.to_string()))?;
let value = method
.as_callable()
.ok_or_else(|| {
TemporalError::general(
"yearMonthFromFields must be implemented as a callable method.",
)
})?
.call(
&self.calendar.clone().into(),
&[fields.into(), overflow_obj.into()],
context,
)
.map_err(|e| TemporalError::general(e.to_string()))?;
let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| {
TemporalError::r#type()
.with_message("yearMonthFromFields must return a valid PlainYearMonth object.")
})?;
let ym = obj.as_plain_year_month().ok_or_else(|| {
TemporalError::r#type().with_message("Object returned was not a PlainDate")
})?;
Ok(ym.inner.clone())
}
fn month_day_from_fields(
&self,
fields: &mut TemporalFields,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<MonthDay> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let method = self
.calendar
.get(utf16!("yearMonthFromFields"), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let fields = JsObject::from_temporal_fields(fields, context)
.map_err(|e| TemporalError::general(e.to_string()))?;
let overflow_obj = JsObject::with_null_proto();
overflow_obj
.create_data_property_or_throw(
utf16!("overflow"),
JsString::from(overflow.to_string()),
context,
)
.map_err(|e| TemporalError::general(e.to_string()))?;
let value = method
.as_callable()
.ok_or_else(|| {
TemporalError::general(
"yearMonthFromFields must be implemented as a callable method.",
)
})?
.call(
&self.calendar.clone().into(),
&[fields.into(), overflow_obj.into()],
context,
)
.map_err(|e| TemporalError::general(e.to_string()))?;
let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| {
TemporalError::r#type()
.with_message("yearMonthFromFields must return a valid PlainYearMonth object.")
})?;
let md = obj.as_plain_month_day().ok_or_else(|| {
TemporalError::r#type().with_message("Object returned was not a PlainDate")
})?;
Ok(md.inner.clone())
}
fn date_add(
&self,
_date: &Date,
_duration: &Duration,
_overflow: ArithmeticOverflow,
_context: &mut dyn Any,
) -> TemporalResult<Date> {
// TODO
Err(TemporalError::general("Not yet implemented."))
}
fn date_until(
&self,
_one: &Date,
_two: &Date,
_largest_unit: boa_temporal::options::TemporalUnit,
_context: &mut dyn Any,
) -> TemporalResult<Duration> {
// TODO
Err(TemporalError::general("Not yet implemented."))
}
fn era(
&self,
_: &CalendarDateLike,
_: &mut dyn Any,
) -> TemporalResult<Option<TinyAsciiStr<8>>> {
// Return undefined as custom calendars do not implement -> Currently.
Ok(None)
}
fn era_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<Option<i32>> {
// Return undefined as custom calendars do not implement -> Currently.
Ok(None)
}
fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<i32> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("year")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("year must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("year return must be integral."));
}
if number < 1f64 {
return Err(TemporalError::r#type().with_message("year return must be larger than 1."));
}
let result = number
.to_i32()
.ok_or_else(|| TemporalError::range().with_message("year exceeded a valid range."))?;
Ok(result)
}
fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("month")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("month must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("month return must be integral."));
}
if number < 1f64 {
return Err(TemporalError::r#type().with_message("month return must be larger than 1."));
}
let result = number
.to_u8()
.ok_or_else(|| TemporalError::range().with_message("month exceeded a valid range."))?;
Ok(result)
}
fn month_code(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<TinyAsciiStr<4>> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("monthCode")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
let JsValue::String(result) = val else {
return Err(TemporalError::r#type().with_message("monthCode return must be a String."));
};
let result = TinyAsciiStr::<4>::from_str(&result.to_std_string_escaped())
.map_err(|_| TemporalError::general("Unexpected monthCode value."))?;
Ok(result)
}
fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("day")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("day must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("day return must be integral."));
}
if number < 1f64 {
return Err(TemporalError::r#type().with_message("day return must be larger than 1."));
}
let result = number
.to_u8()
.ok_or_else(|| TemporalError::range().with_message("day exceeded a valid range."))?;
Ok(result)
}
fn day_of_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("dayOfWeek")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("DayOfWeek must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("DayOfWeek return must be integral."));
}
if number < 1f64 {
return Err(
TemporalError::r#type().with_message("DayOfWeek return must be larger than 1.")
);
}
let result = number.to_u16().ok_or_else(|| {
TemporalError::range().with_message("DayOfWeek exceeded valid range.")
})?;
Ok(result)
}
fn day_of_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("dayOfYear")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("dayOfYear must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("dayOfYear return must be integral."));
}
if number < 1f64 {
return Err(
TemporalError::r#type().with_message("dayOfYear return must be larger than 1.")
);
}
let result = number.to_u16().ok_or_else(|| {
TemporalError::range().with_message("dayOfYear exceeded valid range.")
})?;
Ok(result)
}
fn week_of_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("weekOfYear")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("weekOfYear must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("weekOfYear return must be integral."));
}
if number < 1f64 {
return Err(
TemporalError::r#type().with_message("weekOfYear return must be larger than 1.")
);
}
let result = number.to_u16().ok_or_else(|| {
TemporalError::range().with_message("weekOfYear exceeded valid range.")
})?;
Ok(result)
}
fn year_of_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<i32> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("yearOfWeek")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("yearOfWeek must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("yearOfWeek return must be integral."));
}
let result = number.to_i32().ok_or_else(|| {
TemporalError::range().with_message("yearOfWeek exceeded valid range.")
})?;
Ok(result)
}
fn days_in_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("daysInWeek")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("daysInWeek must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("daysInWeek return must be integral."));
}
if number < 1f64 {
return Err(
TemporalError::r#type().with_message("daysInWeek return must be larger than 1.")
);
}
let result = number.to_u16().ok_or_else(|| {
TemporalError::range().with_message("daysInWeek exceeded valid range.")
})?;
Ok(result)
}
fn days_in_month(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("daysInMonth")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("daysInMonth must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(
TemporalError::r#type().with_message("daysInMonth return must be integral.")
);
}
if number < 1f64 {
return Err(
TemporalError::r#type().with_message("daysInMonth return must be larger than 1.")
);
}
let result = number.to_u16().ok_or_else(|| {
TemporalError::range().with_message("daysInMonth exceeded valid range.")
})?;
Ok(result)
}
fn days_in_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("daysInYear")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("daysInYear must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(TemporalError::r#type().with_message("daysInYear return must be integral."));
}
if number < 1f64 {
return Err(
TemporalError::r#type().with_message("daysInYear return must be larger than 1.")
);
}
let result = number.to_u16().ok_or_else(|| {
TemporalError::range().with_message("monthsInYear exceeded valid range.")
})?;
Ok(result)
}
fn months_in_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("monthsInYear")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
// Validate the return value.
// 3. If Type(result) is not Number, throw a TypeError exception.
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
// 5. If result < 1𝔽, throw a RangeError exception.
// 6. Return ℝ(result).
let Some(number) = val.as_number() else {
return Err(TemporalError::r#type().with_message("monthsInYear must return a number."));
};
if !number.is_finite() || number.fract() != 0.0 {
return Err(
TemporalError::r#type().with_message("monthsInYear return must be integral.")
);
}
if number < 1f64 {
return Err(
TemporalError::r#type().with_message("monthsInYear return must be larger than 1.")
);
}
let result = number.to_u16().ok_or_else(|| {
TemporalError::range().with_message("monthsInYear exceeded valid range.")
})?;
Ok(result)
}
fn in_leap_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<bool> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let date_like = date_like_to_object(date_like, context)?;
let method = self
.calendar
.get(PropertyKey::from(utf16!("inLeapYear")), context)
.expect("method must exist on a object that implements the CalendarProtocol.");
let val = method
.as_callable()
.expect("is method")
.call(&method, &[date_like], context)
.map_err(|err| TemporalError::general(err.to_string()))?;
let JsValue::Boolean(result) = val else {
return Err(
TemporalError::r#type().with_message("inLeapYear must return a valid boolean.")
);
};
Ok(result)
}
// TODO: Determine fate of fn fields()
fn field_descriptors(
&self,
_: boa_temporal::calendar::CalendarFieldsType,
) -> Vec<(String, bool)> {
Vec::default()
}
fn field_keys_to_ignore(&self, _: Vec<String>) -> Vec<String> {
Vec::default()
}
fn resolve_fields(
&self,
_: &mut TemporalFields,
_: boa_temporal::calendar::CalendarFieldsType,
) -> TemporalResult<()> {
Ok(())
}
fn identifier(&self, context: &mut dyn Any) -> TemporalResult<String> {
let context = context
.downcast_mut::<Context>()
.expect("Context was not provided for a CustomCalendar.");
let identifier = self
.calendar
.__get__(
&PropertyKey::from(utf16!("id")),
JsValue::undefined(),
context,
)
.expect("method must exist on a object that implements the CalendarProtocol.");
let JsValue::String(s) = identifier else {
return Err(TemporalError::range().with_message("Identifier was not a string"));
};
Ok(s.to_std_string_escaped())
}
}
/// Utility function for converting `Temporal`'s `CalendarDateLike` to it's `Boa` specific `JsObject`.
pub(crate) fn date_like_to_object(
date_like: &CalendarDateLike,
context: &mut Context,
) -> TemporalResult<JsValue> {
match date_like {
CalendarDateLike::Date(d) => plain_date::create_temporal_date(d.clone(), None, context)
.map_err(|e| TemporalError::general(e.to_string()))
.map(Into::into),
CalendarDateLike::DateTime(_dt) => {
todo!()
}
CalendarDateLike::MonthDay(md) => {
plain_month_day::create_temporal_month_day(md.clone(), None, context)
.map_err(|e| TemporalError::general(e.to_string()))
}
CalendarDateLike::YearMonth(ym) => {
plain_year_month::create_temporal_year_month(ym.clone(), None, context)
.map_err(|e| TemporalError::general(e.to_string()))
}
}
}

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

@ -20,3 +20,42 @@ fn calendar_methods() {
TestAction::assert_eq("iso.daysInWeek('2021-11-20')", 7), TestAction::assert_eq("iso.daysInWeek('2021-11-20')", 7),
]); ]);
} }
#[test]
fn run_custom_calendar() {
run_test_actions([
TestAction::run(
r#"const custom = {
dateAdd() {},
dateFromFields() {},
dateUntil() {},
day() {},
dayOfWeek() {},
dayOfYear() {},
daysInMonth() { return 14 },
daysInWeek() {return 6},
daysInYear() {return 360},
fields() {},
id: "custom-calendar",
inLeapYear() {},
mergeFields() {},
month() {},
monthCode() {},
monthDayFromFields() {},
monthsInYear() {},
weekOfYear() {},
year() {},
yearMonthFromFields() {},
yearOfWeek() {},
};
let cal = Temporal.Calendar.from(custom);
let date = "1972-05-01";
"#,
),
TestAction::assert_eq("cal.id", js_string!("custom-calendar")),
TestAction::assert_eq("cal.daysInMonth(date)", 14),
TestAction::assert_eq("cal.daysInWeek(date)", 6),
TestAction::assert_eq("cal.daysInYear(date)", 360),
]);
}

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

@ -1,107 +0,0 @@
//! 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

@ -1,121 +0,0 @@
//! 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
}

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

@ -15,22 +15,17 @@ use crate::{
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{duration::Duration as InnerDuration, options::TemporalUnit};
use super::{ use super::{
options::{ options::{get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup},
get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, plain_date::PlainDate,
},
plain_date::{self, PlainDate},
to_integer_if_integral, DateTimeValues, PlainDateTime, to_integer_if_integral, DateTimeValues, PlainDateTime,
}; };
mod record;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub(crate) use record::{DateDuration, DurationRecord, TimeDuration};
/// The `Temporal.Duration` object. /// The `Temporal.Duration` object.
/// ///
/// Per [spec], `Duration` records are float64-representable integers /// Per [spec], `Duration` records are float64-representable integers
@ -38,7 +33,13 @@ pub(crate) use record::{DateDuration, DurationRecord, TimeDuration};
/// [spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances /// [spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Duration { pub struct Duration {
pub(crate) inner: DurationRecord, pub(crate) inner: InnerDuration,
}
impl Duration {
pub(crate) fn new(inner: InnerDuration) -> Self {
Self { inner }
}
} }
impl BuiltInObject for Duration { impl BuiltInObject for Duration {
@ -241,17 +242,18 @@ impl BuiltInConstructor for Duration {
.map_or(Ok(0), |ns| to_integer_if_integral(ns, context))?, .map_or(Ok(0), |ns| to_integer_if_integral(ns, context))?,
); );
let record = DurationRecord::new( let record = InnerDuration::new(
DateDuration::new(years, months, weeks, days), years,
TimeDuration::new( months,
hours, weeks,
minutes, days,
seconds, hours,
milliseconds, minutes,
microseconds, seconds,
nanoseconds, milliseconds,
), microseconds,
); nanoseconds,
)?;
// 12. Return ? CreateTemporalDuration(y, mo, w, d, h, m, s, ms, mis, ns, NewTarget). // 12. Return ? CreateTemporalDuration(y, mo, w, d, h, m, s, ms, mis, ns, NewTarget).
create_temporal_duration(record, Some(new_target), context).map(Into::into) create_temporal_duration(record, Some(new_target), context).map(Into::into)
@ -270,17 +272,19 @@ impl Duration {
JsNativeError::typ().with_message("the this object must be a Duration object.") JsNativeError::typ().with_message("the this object must be a Duration object.")
})?; })?;
let inner = &duration.inner;
match field { match field {
DateTimeValues::Year => Ok(JsValue::Rational(duration.inner.years())), DateTimeValues::Year => Ok(JsValue::Rational(inner.date().years())),
DateTimeValues::Month => Ok(JsValue::Rational(duration.inner.months())), DateTimeValues::Month => Ok(JsValue::Rational(inner.date().months())),
DateTimeValues::Week => Ok(JsValue::Rational(duration.inner.weeks())), DateTimeValues::Week => Ok(JsValue::Rational(inner.date().weeks())),
DateTimeValues::Day => Ok(JsValue::Rational(duration.inner.days())), DateTimeValues::Day => Ok(JsValue::Rational(inner.date().days())),
DateTimeValues::Hour => Ok(JsValue::Rational(duration.inner.hours())), DateTimeValues::Hour => Ok(JsValue::Rational(inner.time().hours())),
DateTimeValues::Minute => Ok(JsValue::Rational(duration.inner.minutes())), DateTimeValues::Minute => Ok(JsValue::Rational(inner.time().minutes())),
DateTimeValues::Second => Ok(JsValue::Rational(duration.inner.seconds())), DateTimeValues::Second => Ok(JsValue::Rational(inner.time().seconds())),
DateTimeValues::Millisecond => Ok(JsValue::Rational(duration.inner.milliseconds())), DateTimeValues::Millisecond => Ok(JsValue::Rational(inner.time().milliseconds())),
DateTimeValues::Microsecond => Ok(JsValue::Rational(duration.inner.microseconds())), DateTimeValues::Microsecond => Ok(JsValue::Rational(inner.time().microseconds())),
DateTimeValues::Nanosecond => Ok(JsValue::Rational(duration.inner.nanoseconds())), DateTimeValues::Nanosecond => Ok(JsValue::Rational(inner.time().nanoseconds())),
DateTimeValues::MonthCode => unreachable!( DateTimeValues::MonthCode => unreachable!(
"Any other DateTimeValue fields on Duration would be an implementation error." "Any other DateTimeValue fields on Duration would be an implementation error."
), ),
@ -399,122 +403,123 @@ impl Duration {
// 3. Let temporalDurationLike be ? ToTemporalPartialDurationRecord(temporalDurationLike). // 3. Let temporalDurationLike be ? ToTemporalPartialDurationRecord(temporalDurationLike).
let temporal_duration_like = let temporal_duration_like =
DurationRecord::from_partial_js_object(args.get_or_undefined(0), context)?; to_temporal_partial_duration(args.get_or_undefined(0), context)?;
// 4. If temporalDurationLike.[[Years]] is not undefined, then // 4. If temporalDurationLike.[[Years]] is not undefined, then
// a. Let years be temporalDurationLike.[[Years]]. // a. Let years be temporalDurationLike.[[Years]].
// 5. Else, // 5. Else,
// a. Let years be duration.[[Years]]. // a. Let years be duration.[[Years]].
let years = if temporal_duration_like.years().is_nan() { let years = if temporal_duration_like.date().years().is_nan() {
duration.inner.years() duration.inner.date().years()
} else { } else {
temporal_duration_like.years() temporal_duration_like.date().years()
}; };
// 6. If temporalDurationLike.[[Months]] is not undefined, then // 6. If temporalDurationLike.[[Months]] is not undefined, then
// a. Let months be temporalDurationLike.[[Months]]. // a. Let months be temporalDurationLike.[[Months]].
// 7. Else, // 7. Else,
// a. Let months be duration.[[Months]]. // a. Let months be duration.[[Months]].
let months = if temporal_duration_like.months().is_nan() { let months = if temporal_duration_like.date().months().is_nan() {
duration.inner.months() duration.inner.date().months()
} else { } else {
temporal_duration_like.months() temporal_duration_like.date().months()
}; };
// 8. If temporalDurationLike.[[Weeks]] is not undefined, then // 8. If temporalDurationLike.[[Weeks]] is not undefined, then
// a. Let weeks be temporalDurationLike.[[Weeks]]. // a. Let weeks be temporalDurationLike.[[Weeks]].
// 9. Else, // 9. Else,
// a. Let weeks be duration.[[Weeks]]. // a. Let weeks be duration.[[Weeks]].
let weeks = if temporal_duration_like.weeks().is_nan() { let weeks = if temporal_duration_like.date().weeks().is_nan() {
duration.inner.weeks() duration.inner.date().weeks()
} else { } else {
temporal_duration_like.weeks() temporal_duration_like.date().weeks()
}; };
// 10. If temporalDurationLike.[[Days]] is not undefined, then // 10. If temporalDurationLike.[[Days]] is not undefined, then
// a. Let days be temporalDurationLike.[[Days]]. // a. Let days be temporalDurationLike.[[Days]].
// 11. Else, // 11. Else,
// a. Let days be duration.[[Days]]. // a. Let days be duration.[[Days]].
let days = if temporal_duration_like.days().is_nan() { let days = if temporal_duration_like.date().days().is_nan() {
duration.inner.days() duration.inner.date().days()
} else { } else {
temporal_duration_like.days() temporal_duration_like.date().days()
}; };
// 12. If temporalDurationLike.[[Hours]] is not undefined, then // 12. If temporalDurationLike.[[Hours]] is not undefined, then
// a. Let hours be temporalDurationLike.[[Hours]]. // a. Let hours be temporalDurationLike.[[Hours]].
// 13. Else, // 13. Else,
// a. Let hours be duration.[[Hours]]. // a. Let hours be duration.[[Hours]].
let hours = if temporal_duration_like.hours().is_nan() { let hours = if temporal_duration_like.time().hours().is_nan() {
duration.inner.hours() duration.inner.time().hours()
} else { } else {
temporal_duration_like.hours() temporal_duration_like.time().hours()
}; };
// 14. If temporalDurationLike.[[Minutes]] is not undefined, then // 14. If temporalDurationLike.[[Minutes]] is not undefined, then
// a. Let minutes be temporalDurationLike.[[Minutes]]. // a. Let minutes be temporalDurationLike.[[Minutes]].
// 15. Else, // 15. Else,
// a. Let minutes be duration.[[Minutes]]. // a. Let minutes be duration.[[Minutes]].
let minutes = if temporal_duration_like.minutes().is_nan() { let minutes = if temporal_duration_like.time().minutes().is_nan() {
duration.inner.minutes() duration.inner.time().minutes()
} else { } else {
temporal_duration_like.minutes() temporal_duration_like.time().minutes()
}; };
// 16. If temporalDurationLike.[[Seconds]] is not undefined, then // 16. If temporalDurationLike.[[Seconds]] is not undefined, then
// a. Let seconds be temporalDurationLike.[[Seconds]]. // a. Let seconds be temporalDurationLike.[[Seconds]].
// 17. Else, // 17. Else,
// a. Let seconds be duration.[[Seconds]]. // a. Let seconds be duration.[[Seconds]].
let seconds = if temporal_duration_like.seconds().is_nan() { let seconds = if temporal_duration_like.time().seconds().is_nan() {
duration.inner.seconds() duration.inner.time().seconds()
} else { } else {
temporal_duration_like.seconds() temporal_duration_like.time().seconds()
}; };
// 18. If temporalDurationLike.[[Milliseconds]] is not undefined, then // 18. If temporalDurationLike.[[Milliseconds]] is not undefined, then
// a. Let milliseconds be temporalDurationLike.[[Milliseconds]]. // a. Let milliseconds be temporalDurationLike.[[Milliseconds]].
// 19. Else, // 19. Else,
// a. Let milliseconds be duration.[[Milliseconds]]. // a. Let milliseconds be duration.[[Milliseconds]].
let milliseconds = if temporal_duration_like.milliseconds().is_nan() { let milliseconds = if temporal_duration_like.time().milliseconds().is_nan() {
duration.inner.milliseconds() duration.inner.time().milliseconds()
} else { } else {
temporal_duration_like.milliseconds() temporal_duration_like.time().milliseconds()
}; };
// 20. If temporalDurationLike.[[Microseconds]] is not undefined, then // 20. If temporalDurationLike.[[Microseconds]] is not undefined, then
// a. Let microseconds be temporalDurationLike.[[Microseconds]]. // a. Let microseconds be temporalDurationLike.[[Microseconds]].
// 21. Else, // 21. Else,
// a. Let microseconds be duration.[[Microseconds]]. // a. Let microseconds be duration.[[Microseconds]].
let microseconds = if temporal_duration_like.microseconds().is_nan() { let microseconds = if temporal_duration_like.time().microseconds().is_nan() {
duration.inner.microseconds() duration.inner.time().microseconds()
} else { } else {
temporal_duration_like.microseconds() temporal_duration_like.time().microseconds()
}; };
// 22. If temporalDurationLike.[[Nanoseconds]] is not undefined, then // 22. If temporalDurationLike.[[Nanoseconds]] is not undefined, then
// a. Let nanoseconds be temporalDurationLike.[[Nanoseconds]]. // a. Let nanoseconds be temporalDurationLike.[[Nanoseconds]].
// 23. Else, // 23. Else,
// a. Let nanoseconds be duration.[[Nanoseconds]]. // a. Let nanoseconds be duration.[[Nanoseconds]].
let nanoseconds = if temporal_duration_like.nanoseconds().is_nan() { let nanoseconds = if temporal_duration_like.time().nanoseconds().is_nan() {
duration.inner.nanoseconds() duration.inner.time().nanoseconds()
} else { } else {
temporal_duration_like.nanoseconds() temporal_duration_like.time().nanoseconds()
}; };
// 24. Return ? CreateTemporalDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). // 24. Return ? CreateTemporalDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
let new_duration = DurationRecord::new( let new_duration = InnerDuration::new(
DateDuration::new(years, months, weeks, days), years,
TimeDuration::new( months,
hours, weeks,
minutes, days,
seconds, hours,
milliseconds, minutes,
microseconds, seconds,
nanoseconds, milliseconds,
), microseconds,
); nanoseconds,
)?;
new_duration.as_object(context).map(Into::into) create_temporal_duration(new_duration, None, context).map(Into::into)
} }
/// 7.3.16 `Temporal.Duration.prototype.negated ( )` /// 7.3.16 `Temporal.Duration.prototype.negated ( )`
@ -544,7 +549,7 @@ impl Duration {
let abs = duration.inner.abs(); let abs = duration.inner.abs();
abs.as_object(context).map(Into::into) create_temporal_duration(abs, None, context).map(Into::into)
} }
/// 7.3.18 `Temporal.Duration.prototype.add ( other [ , options ] )` /// 7.3.18 `Temporal.Duration.prototype.add ( other [ , options ] )`
@ -630,7 +635,7 @@ impl Duration {
let rounding_increment = get_temporal_rounding_increment(&round_to, context)?; let rounding_increment = get_temporal_rounding_increment(&round_to, context)?;
// 14. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). // 14. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand").
let rounding_mode = get_option(&round_to, utf16!("roundingMode"), context)? let _rounding_mode = get_option(&round_to, utf16!("roundingMode"), context)?
.unwrap_or(RoundingMode::HalfExpand); .unwrap_or(RoundingMode::HalfExpand);
// 15. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit", datetime, undefined). // 15. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit", datetime, undefined).
@ -697,19 +702,20 @@ impl Duration {
// 25. Let hoursToDaysConversionMayOccur be false. // 25. Let hoursToDaysConversionMayOccur be false.
// 26. If duration.[[Days]] ≠ 0 and zonedRelativeTo is not undefined, set hoursToDaysConversionMayOccur to true. // 26. If duration.[[Days]] ≠ 0 and zonedRelativeTo is not undefined, set hoursToDaysConversionMayOccur to true.
// 27. Else if abs(duration.[[Hours]]) ≥ 24, set hoursToDaysConversionMayOccur to true. // 27. Else if abs(duration.[[Hours]]) ≥ 24, set hoursToDaysConversionMayOccur to true.
let conversion_may_occur = if duration.inner.days() != 0.0 && zoned_relative_to.is_some() { let conversion_may_occur =
true if duration.inner.date().days() != 0.0 && zoned_relative_to.is_some() {
} else { true
24f64 <= duration.inner.hours().abs() } else {
}; 24f64 <= duration.inner.time().hours().abs()
};
// 28. If smallestUnit is "nanosecond" and roundingIncrement = 1, let roundingGranularityIsNoop be true; else let roundingGranularityIsNoop be false. // 28. If smallestUnit is "nanosecond" and roundingIncrement = 1, let roundingGranularityIsNoop be true; else let roundingGranularityIsNoop be false.
let is_noop = smallest_unit == TemporalUnit::Nanosecond && rounding_increment == 1; let is_noop = smallest_unit == TemporalUnit::Nanosecond && rounding_increment == 1;
// 29. If duration.[[Years]] = 0 and duration.[[Months]] = 0 and duration.[[Weeks]] = 0, let calendarUnitsPresent be false; else let calendarUnitsPresent be true. // 29. If duration.[[Years]] = 0 and duration.[[Months]] = 0 and duration.[[Weeks]] = 0, let calendarUnitsPresent be false; else let calendarUnitsPresent be true.
let calendar_units_present = !(duration.inner.years() == 0f64 let calendar_units_present = !(duration.inner.date().years() == 0f64
|| duration.inner.months() == 0f64 || duration.inner.date().months() == 0f64
|| duration.inner.weeks() == 0f64); || duration.inner.date().weeks() == 0f64);
// 30. If roundingGranularityIsNoop is true, and largestUnit is existingLargestUnit, // 30. If roundingGranularityIsNoop is true, and largestUnit is existingLargestUnit,
// and calendarUnitsPresent is false, and hoursToDaysConversionMayOccur is false, // and calendarUnitsPresent is false, and hoursToDaysConversionMayOccur is false,
@ -740,10 +746,10 @@ impl Duration {
|| largest_unit == TemporalUnit::Week || largest_unit == TemporalUnit::Week
|| largest_unit == TemporalUnit::Day || largest_unit == TemporalUnit::Day
|| calendar_units_present || calendar_units_present
|| duration.inner.days() != 0f64; || duration.inner.date().days() != 0f64;
// 33. If zonedRelativeTo is not undefined and plainDateTimeOrRelativeToWillBeUsed is true, then // 33. If zonedRelativeTo is not undefined and plainDateTimeOrRelativeToWillBeUsed is true, then
let (plain_relative_to, precalc_pdt) = if zoned_relative_to.is_some() let (_plain_relative_to, _precalc_pdt) = if zoned_relative_to.is_some()
&& pdt_or_rel_will_be_used && pdt_or_rel_will_be_used
{ {
// TODO(TimeZone): Implement GetPlainDateTimeFor // TODO(TimeZone): Implement GetPlainDateTimeFor
@ -763,29 +769,10 @@ impl Duration {
}; };
// 34. Let unbalanceResult be ? UnbalanceDateDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], largestUnit, plainRelativeTo). // 34. Let unbalanceResult be ? UnbalanceDateDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], largestUnit, plainRelativeTo).
let unbalance_result = duration.inner.unbalance_duration_relative(
largest_unit,
plain_relative_to.as_ref(),
context,
)?;
// 35. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]], // 35. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]],
// unbalanceResult.[[Weeks]], unbalanceResult.[[Days]], duration.[[Hours]], duration.[[Minutes]], // unbalanceResult.[[Weeks]], unbalanceResult.[[Days]], duration.[[Hours]], duration.[[Minutes]],
// duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], // duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]],
// roundingIncrement, smallestUnit, roundingMode, plainRelativeTo, zonedRelativeTo, precalculatedPlainDateTime). // roundingIncrement, smallestUnit, roundingMode, plainRelativeTo, zonedRelativeTo, precalculatedPlainDateTime).
let (_round_result, _) = duration.inner.round_duration(
unbalance_result,
rounding_increment.into(),
smallest_unit,
rounding_mode,
(
plain_relative_to.as_ref(),
zoned_relative_to.as_ref(),
precalc_pdt.as_ref(),
),
context,
)?;
// 36. Let roundResult be roundRecord.[[DurationRecord]]. // 36. Let roundResult be roundRecord.[[DurationRecord]].
// 37. If zonedRelativeTo is not undefined, then // 37. If zonedRelativeTo is not undefined, then
// a. Set roundResult to ? AdjustRoundedDurationDays(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode, zonedRelativeTo, precalculatedPlainDateTime). // a. Set roundResult to ? AdjustRoundedDurationDays(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode, zonedRelativeTo, precalculatedPlainDateTime).
@ -887,7 +874,7 @@ impl Duration {
// -- Duration Abstract Operations -- // -- Duration Abstract Operations --
/// 7.5.8 `ToTemporalDuration ( item )` /// 7.5.8 `ToTemporalDuration ( item )`
pub(crate) fn to_temporal_duration(item: &JsValue) -> JsResult<DurationRecord> { pub(crate) fn to_temporal_duration(item: &JsValue) -> JsResult<InnerDuration> {
// 1a. If Type(item) is Object // 1a. If Type(item) is Object
if item.is_object() { if item.is_object() {
// 1b. and item has an [[InitializedTemporalDuration]] internal slot, then // 1b. and item has an [[InitializedTemporalDuration]] internal slot, then
@ -911,7 +898,7 @@ pub(crate) fn to_temporal_duration(item: &JsValue) -> JsResult<DurationRecord> {
/// 7.5.9 `ToTemporalDurationRecord ( temporalDurationLike )` /// 7.5.9 `ToTemporalDurationRecord ( temporalDurationLike )`
pub(crate) fn to_temporal_duration_record( pub(crate) fn to_temporal_duration_record(
_temporal_duration_like: &JsValue, _temporal_duration_like: &JsValue,
) -> JsResult<DurationRecord> { ) -> JsResult<InnerDuration> {
Err(JsNativeError::range() Err(JsNativeError::range()
.with_message("Duration Parsing is not yet complete.") .with_message("Duration Parsing is not yet complete.")
.into()) .into())
@ -919,16 +906,11 @@ pub(crate) fn to_temporal_duration_record(
/// 7.5.14 `CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] )` /// 7.5.14 `CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] )`
pub(crate) fn create_temporal_duration( pub(crate) fn create_temporal_duration(
record: DurationRecord, inner: InnerDuration,
new_target: Option<&JsValue>, new_target: Option<&JsValue>,
context: &mut Context, context: &mut Context,
) -> JsResult<JsObject> { ) -> JsResult<JsObject> {
// 1. If ! IsValidDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) is false, throw a RangeError exception. // 1. If ! IsValidDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds) is false, throw a RangeError exception.
if !record.is_valid_duration() {
return Err(JsNativeError::range()
.with_message("Duration values are not valid.")
.into());
}
// 2. If newTarget is not present, set newTarget to %Temporal.Duration%. // 2. If newTarget is not present, set newTarget to %Temporal.Duration%.
let new_target = if let Some(target) = new_target { let new_target = if let Some(target) = new_target {
@ -958,38 +940,105 @@ pub(crate) fn create_temporal_duration(
// 12. Set object.[[Microseconds]] to ℝ(𝔽(microseconds)). // 12. Set object.[[Microseconds]] to ℝ(𝔽(microseconds)).
// 13. Set object.[[Nanoseconds]] to ℝ(𝔽(nanoseconds)). // 13. Set object.[[Nanoseconds]] to ℝ(𝔽(nanoseconds)).
let obj = let obj = JsObject::from_proto_and_data(prototype, ObjectData::duration(Duration::new(inner)));
JsObject::from_proto_and_data(prototype, ObjectData::duration(Duration { inner: record }));
// 14. Return object. // 14. Return object.
Ok(obj) Ok(obj)
} }
/// 7.5.23 `DaysUntil ( earlier, later )` /// Equivalent to 7.5.13 `ToTemporalPartialDurationRecord ( temporalDurationLike )`
pub(crate) fn days_until(earlier: &PlainDate, later: &PlainDate) -> i32 { pub(crate) fn to_temporal_partial_duration(
// 1. Let epochDays1 be ISODateToEpochDays(earlier.[[ISOYear]], earlier.[[ISOMonth]] - 1, earlier.[[ISODay]]). duration_like: &JsValue,
let epoch_days_one = earlier.inner.as_epoch_days(); context: &mut Context,
) -> JsResult<InnerDuration> {
// 1. If Type(temporalDurationLike) is not Object, then
let JsValue::Object(unknown_object) = duration_like else {
// a. Throw a TypeError exception.
return Err(JsNativeError::typ()
.with_message("temporalDurationLike must be an object.")
.into());
};
// 2. Let epochDays2 be ISODateToEpochDays(later.[[ISOYear]], later.[[ISOMonth]] - 1, later.[[ISODay]]). // 2. Let result be a new partial Duration Record with each field set to undefined.
let epoch_days_two = later.inner.as_epoch_days(); let mut result = InnerDuration::partial();
// 3. Return epochDays2 - epochDays1. // 3. NOTE: The following steps read properties and perform independent validation in alphabetical order.
epoch_days_two - epoch_days_one // 4. Let days be ? Get(temporalDurationLike, "days").
} let days = unknown_object.get(utf16!("days"), context)?;
if !days.is_undefined() {
// 5. If days is not undefined, set result.[[Days]] to ? ToIntegerIfIntegral(days).
result.set_days(f64::from(to_integer_if_integral(&days, context)?));
}
/// Abstract Operation 7.5.24 `MoveRelativeDate ( calendar, relativeTo, duration, dateAdd )` // 6. Let hours be ? Get(temporalDurationLike, "hours").
fn move_relative_date( let hours = unknown_object.get(utf16!("hours"), context)?;
calendar: &JsValue, // 7. If hours is not undefined, set result.[[Hours]] to ? ToIntegerIfIntegral(hours).
relative_to: &PlainDate, if !hours.is_undefined() {
duration: &DurationRecord, result.set_days(f64::from(to_integer_if_integral(&hours, context)?));
context: &mut Context, }
) -> JsResult<(PlainDate, f64)> {
let new_date = plain_date::add_date( // 8. Let microseconds be ? Get(temporalDurationLike, "microseconds").
calendar, let microseconds = unknown_object.get(utf16!("microseconds"), context)?;
relative_to, // 9. If microseconds is not undefined, set result.[[Microseconds]] to ? ToIntegerIfIntegral(microseconds).
duration, if !microseconds.is_undefined() {
&JsValue::undefined(), result.set_days(f64::from(to_integer_if_integral(&microseconds, context)?));
context, }
)?;
let days = days_until(relative_to, &new_date); // 10. Let milliseconds be ? Get(temporalDurationLike, "milliseconds").
Ok((new_date, f64::from(days))) let milliseconds = unknown_object.get(utf16!("milliseconds"), context)?;
// 11. If milliseconds is not undefined, set result.[[Milliseconds]] to ? ToIntegerIfIntegral(milliseconds).
if !milliseconds.is_undefined() {
result.set_days(f64::from(to_integer_if_integral(&milliseconds, context)?));
}
// 12. Let minutes be ? Get(temporalDurationLike, "minutes").
let minutes = unknown_object.get(utf16!("minutes"), context)?;
// 13. If minutes is not undefined, set result.[[Minutes]] to ? ToIntegerIfIntegral(minutes).
if !minutes.is_undefined() {
result.set_days(f64::from(to_integer_if_integral(&minutes, context)?));
}
// 14. Let months be ? Get(temporalDurationLike, "months").
let months = unknown_object.get(utf16!("months"), context)?;
// 15. If months is not undefined, set result.[[Months]] to ? ToIntegerIfIntegral(months).
if !months.is_undefined() {
result.set_days(f64::from(to_integer_if_integral(&months, context)?));
}
// 16. Let nanoseconds be ? Get(temporalDurationLike, "nanoseconds").
let nanoseconds = unknown_object.get(utf16!("nanoseconds"), context)?;
// 17. If nanoseconds is not undefined, set result.[[Nanoseconds]] to ? ToIntegerIfIntegral(nanoseconds).
if !nanoseconds.is_undefined() {
result.set_days(f64::from(to_integer_if_integral(&nanoseconds, context)?));
}
// 18. Let seconds be ? Get(temporalDurationLike, "seconds").
let seconds = unknown_object.get(utf16!("seconds"), context)?;
// 19. If seconds is not undefined, set result.[[Seconds]] to ? ToIntegerIfIntegral(seconds).
if !seconds.is_undefined() {
result.set_days(f64::from(to_integer_if_integral(&seconds, context)?));
}
// 20. Let weeks be ? Get(temporalDurationLike, "weeks").
let weeks = unknown_object.get(utf16!("weeks"), context)?;
// 21. If weeks is not undefined, set result.[[Weeks]] to ? ToIntegerIfIntegral(weeks).
if !weeks.is_undefined() {
result.set_days(f64::from(to_integer_if_integral(&weeks, context)?));
}
// 22. Let years be ? Get(temporalDurationLike, "years").
let years = unknown_object.get(utf16!("years"), context)?;
// 23. If years is not undefined, set result.[[Years]] to ? ToIntegerIfIntegral(years).
if !years.is_undefined() {
result.set_days(f64::from(to_integer_if_integral(&years, context)?));
}
// 24. If years is undefined, and months is undefined, and weeks is undefined, and days is undefined, and hours is undefined, and minutes is undefined, and seconds is undefined, and milliseconds is undefined, and microseconds is undefined, and nanoseconds is undefined, throw a TypeError exception.
if result.into_iter().all(f64::is_nan) {
return Err(JsNativeError::typ()
.with_message("no valid Duration fields on temporalDurationLike.")
.into());
}
// 25. Return result.
Ok(result)
} }

20
boa_engine/src/builtins/temporal/error.rs

@ -0,0 +1,20 @@
use boa_temporal::error::{ErrorKind, TemporalError};
use crate::{JsError, JsNativeError};
impl From<TemporalError> for JsNativeError {
fn from(value: TemporalError) -> Self {
match value.kind() {
ErrorKind::Range => JsNativeError::range().with_message(value.message()),
ErrorKind::Type => JsNativeError::typ().with_message(value.message()),
ErrorKind::Generic => JsNativeError::error().with_message(value.message()),
}
}
}
impl From<TemporalError> for JsError {
fn from(value: TemporalError) -> Self {
let native: JsNativeError = value.into();
native.into()
}
}

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

@ -1,587 +1,174 @@
//! A Rust native implementation of the `fields` object used in `Temporal`. //! A Rust native implementation of the `fields` object used in `Temporal`.
use std::str::FromStr;
use crate::{ use crate::{
js_string, property::PropertyKey, value::PreferredType, Context, JsNativeError, JsObject, js_string, property::PropertyKey, value::PreferredType, Context, JsNativeError, JsObject,
JsResult, JsString, JsValue, JsResult, JsString, JsValue,
}; };
use super::options::ArithmeticOverflow;
use bitflags::bitflags;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
bitflags! { use boa_temporal::fields::{FieldConversion, FieldValue, TemporalFields};
#[derive(Debug, PartialEq, Eq)]
pub struct FieldMap: u16 { use super::{to_integer_with_truncation, to_positive_integer_with_trunc};
const YEAR = 0b0000_0000_0000_0001;
const MONTH = 0b0000_0000_0000_0010; // TODO: Move extended and required fields into the temporal library?
const MONTH_CODE = 0b0000_0000_0000_0100; /// `PrepareTemporalFeilds`
const DAY = 0b0000_0000_0000_1000; pub(crate) fn prepare_temporal_fields(
const HOUR = 0b0000_0000_0001_0000; fields: &JsObject,
const MINUTE = 0b0000_0000_0010_0000; field_names: &mut Vec<JsString>,
const SECOND = 0b0000_0000_0100_0000; required_fields: &mut Vec<JsString>,
const MILLISECOND = 0b0000_0000_1000_0000; extended_fields: Option<Vec<(String, bool)>>,
const MICROSECOND = 0b0000_0001_0000_0000; partial: bool,
const NANOSECOND = 0b0000_0010_0000_0000; dup_behaviour: Option<JsString>,
const OFFSET = 0b0000_0100_0000_0000; context: &mut Context,
const ERA = 0b0000_1000_0000_0000; ) -> JsResult<TemporalFields> {
const ERA_YEAR = 0b0001_0000_0000_0000; // 1. If duplicateBehaviour is not present, set duplicateBehaviour to throw.
const TIME_ZONE = 0b0010_0000_0000_0000; let dup_option = dup_behaviour.unwrap_or_else(|| js_string!("throw"));
}
} // 2. Let result be OrdinaryObjectCreate(null).
let mut result = TemporalFields::default();
/// 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). // 3. Let any be false.
/// let mut any = false;
/// `TemporalFields` is meant to act as a native Rust implementation // 4. If extraFieldDescriptors is present, then
/// of the fields. 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
/// ## Table 17: Temporal field requirements // i. Assert: fieldNames does not contain desc.[[Property]].
/// // ii. Append desc.[[Property]] to fieldNames.
/// | Property | Conversion | Default | field_names.push(JsString::from(field_name.clone()));
/// | -------------|-----------------------------------|------------|
/// | "year" | `ToIntegerWithTruncation` | undefined | // iii. If desc.[[Required]] is true and requiredFields is a List, then
/// | "month" | `ToPositiveIntegerWithTruncation` | undefined | if required && !partial {
/// | "monthCode" | `ToPrimitiveAndRequireString` | undefined | // 1. Append desc.[[Property]] to requiredFields.
/// | "day" | `ToPositiveIntegerWithTruncation` | undefined | required_fields.push(JsString::from(field_name));
/// | "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" | `None` | 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: &JsString,
value: &JsValue,
context: &mut Context,
) -> JsResult<()> {
match field.to_std_string_escaped().as_str() {
"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] // 5. Let sortedFieldNames be SortStringListByCodeUnit(fieldNames).
fn set_day(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { // 6. Let previousProperty be undefined.
let d = super::to_positive_integer_with_trunc(value, context)?; let mut dups_map = FxHashSet::default();
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(()) // 7. For each property name property of sortedFieldNames, do
} for field in &*field_names {
// a. If property is one of "constructor" or "__proto__", then
#[inline] if field.to_std_string_escaped().as_str() == "constructor"
fn set_era(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { || field.to_std_string_escaped().as_str() == "__proto__"
let mc = value.to_primitive(context, PreferredType::String)?; {
if let Some(string) = mc.as_string() { // i. Throw a RangeError exception.
self.era = Some(string.clone()); return Err(JsNativeError::range()
} else { .with_message("constructor or proto is out of field range.")
return Err(JsNativeError::typ()
.with_message("ToPrimativeAndRequireString must be of type String.")
.into()); .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<JsString>,
required_fields: &mut Vec<JsString>, // None when Partial
extended_fields: Option<Vec<(JsString, 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.to_std_string_escaped().as_str() == "constructor"
|| field.to_std_string_escaped().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 let new_value = dups_map.insert(field);
if new_value {
// i. Let value be ? Get(fields, property). // b. If property is not equal to previousProperty, then
let value = fields.get(PropertyKey::from(field.clone()), context)?; if new_value {
// ii. If value is not undefined, then // i. Let value be ? Get(fields, property).
if !value.is_undefined() { let value = fields.get(PropertyKey::from(field.clone()), context)?;
// 1. Set any to true. // ii. If value is not undefined, then
any = true; if !value.is_undefined() {
// 1. Set any to true.
// 2. If property is in the Property column of Table 17 and there is a Conversion value in the same row, then any = true;
// a. Let Conversion be the Conversion value of the same row.
// b. If Conversion is ToIntegerWithTruncation, then // 2. If property is in the Property column of Table 17 and there is a Conversion value in the same row, then
// i. Set value to ? ToIntegerWithTruncation(value). // a. Let Conversion be the Conversion value of the same row.
// ii. Set value to 𝔽(value).
// TODO: Conversion from TemporalError -> JsError
let conversion = FieldConversion::from_str(field.to_std_string_escaped().as_str())
.map_err(|_| JsNativeError::range().with_message("wrong field value"))?;
// b. If Conversion is ToIntegerWithTruncation, then
let converted_value = match conversion {
FieldConversion::ToIntegerWithTruncation => {
// i. Set value to ? ToIntegerWithTruncation(value).
let v = to_integer_with_truncation(&value, context)?;
// ii. Set value to 𝔽(value).
FieldValue::Integer(v)
}
// c. Else if Conversion is ToPositiveIntegerWithTruncation, then // c. Else if Conversion is ToPositiveIntegerWithTruncation, then
// i. Set value to ? ToPositiveIntegerWithTruncation(value). FieldConversion::ToPositiveIntegerWithTruncation => {
// ii. Set value to 𝔽(value). // i. Set value to ? ToPositiveIntegerWithTruncation(value).
let v = to_positive_integer_with_trunc(&value, context)?;
// ii. Set value to 𝔽(value).
FieldValue::Integer(v)
}
// d. Else, // d. Else,
// i. Assert: Conversion is ToPrimitiveAndRequireString. // 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. FieldConversion::ToPrimativeAndRequireString => {
// iii. Set value to ? ToPrimitive(value, string). // ii. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings.
// iv. If value is not a String, throw a TypeError exception. // iii. Set value to ? ToPrimitive(value, string).
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value). let primitive = value.to_primitive(context, PreferredType::String)?;
result.set_field_value(field, &value, context)?; // iv. If value is not a String, throw a TypeError exception.
// iii. Else if requiredFields is a List, then FieldValue::String(primitive.to_string(context)?.to_std_string_escaped())
} 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());
} }
FieldConversion::None => {
// NOTE: Values set to a default on init. unreachable!("todo need to implement conversion handling for tz.")
// 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).
} // 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
// c. Else if duplicateBehaviour is throw, then result
} else if dup_option.to_std_string_escaped() == "throw" { .set_field_value(&field.to_std_string_escaped(), &converted_value)
// i. Throw a RangeError exception. .expect("FieldConversion enforces the appropriate type");
return Err(JsNativeError::range() // iii. Else if requiredFields is a List, then
.with_message("Cannot have a duplicate field") } else if !partial {
.into()); // 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: flag that the value is active and the default should be used.
// 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).
result.require_field(&field.to_std_string_escaped());
} }
// d. Set previousProperty to property. // c. Else if duplicateBehaviour is throw, then
} } else if dup_option.to_std_string_escaped() == "throw" {
// i. Throw a RangeError exception.
// 8. If requiredFields is partial and any is false, then
if partial && !any {
// a. Throw a TypeError exception.
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("requiredFields cannot be partial when any is false") .with_message("Cannot have a duplicate field")
.into()); .into());
} }
// d. Set previousProperty to property.
// 9. Return result.
Ok(result)
} }
/// Convert a `TemporalFields` struct into a `JsObject`. // 8. If requiredFields is partial and any is false, then
pub(crate) fn as_object(&self, context: &mut Context) -> JsResult<JsObject> { if partial && !any {
let obj = JsObject::with_null_proto(); // a. Throw a TypeError exception.
return Err(JsNativeError::range()
for bit in self.bit_map.iter() { .with_message("requiredFields cannot be partial when any is false")
match bit { .into());
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>. // 9. Return result.
/// A function to regulate the current `TemporalFields` according to the overflow value Ok(result)
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) { impl JsObject {
match self.month { pub(crate) fn from_temporal_fields(
Some(month) if overflow == ArithmeticOverflow::Constrain => { fields: &TemporalFields,
let m = month.clamp(1, 12); context: &mut Context,
self.month = Some(m); ) -> JsResult<Self> {
} let obj = JsObject::with_null_proto();
_ => {}
}
}
/// Resolve the month and monthCode on this `TemporalFields`. for (key, value) in fields.active_kvs() {
pub(crate) fn iso_resolve_month(&mut self) -> JsResult<()> { let js_value = match value {
if self.month_code.is_none() { FieldValue::Undefined => JsValue::undefined(),
if self.month.is_some() { FieldValue::Integer(x) => JsValue::Integer(x),
return Ok(()); FieldValue::String(s) => JsValue::String(s.into()),
} };
return Err(JsNativeError::range() obj.create_data_property_or_throw(JsString::from(key), js_value, context)?;
.with_message("month and MonthCode values cannot both be undefined.")
.into());
} }
let unresolved_month_code = self Ok(obj)
.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()),
} }
} }

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

@ -4,11 +4,8 @@
use crate::{ use crate::{
builtins::{ builtins::{
options::{get_option, get_options_object, RoundingMode}, options::{get_option, get_options_object, RoundingMode},
temporal::{ temporal::options::{
duration::{DateDuration, DurationRecord, TimeDuration}, get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup,
options::{
get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup,
},
}, },
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
}, },
@ -21,8 +18,9 @@ use crate::{
Context, JsArgs, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, Context, JsArgs, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{duration::Duration, options::TemporalUnit};
use super::{duration, ns_max_instant, ns_min_instant, MIS_PER_DAY, MS_PER_DAY, NS_PER_DAY}; use super::{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_SECOND: i64 = 10_000_000_000;
const NANOSECONDS_PER_MINUTE: i64 = 600_000_000_000; const NANOSECONDS_PER_MINUTE: i64 = 600_000_000_000;
@ -567,70 +565,41 @@ fn add_instant(
fn diff_instant( fn diff_instant(
ns1: &JsBigInt, ns1: &JsBigInt,
ns2: &JsBigInt, ns2: &JsBigInt,
rounding_increment: f64, _rounding_increment: f64,
smallest_unit: TemporalUnit, _smallest_unit: TemporalUnit,
largest_unit: TemporalUnit, _largest_unit: TemporalUnit,
rounding_mode: RoundingMode, _rounding_mode: RoundingMode,
context: &mut Context, _context: &mut Context,
) -> JsResult<DurationRecord> { ) -> JsResult<Duration> {
// 1. Let difference be ℝ(ns2) - ℝ(ns1). // 1. Let difference be ℝ(ns2) - ℝ(ns1).
let difference = JsBigInt::sub(ns1, ns2); let difference = JsBigInt::sub(ns1, ns2);
// 2. Let nanoseconds be remainder(difference, 1000). // 2. Let nanoseconds be remainder(difference, 1000).
let nanoseconds = JsBigInt::rem(&difference, &JsBigInt::from(1000)); let _nanoseconds = JsBigInt::rem(&difference, &JsBigInt::from(1000));
// 3. Let microseconds be remainder(truncate(difference / 1000), 1000). // 3. Let microseconds be remainder(truncate(difference / 1000), 1000).
let truncated_micro = JsBigInt::try_from((&difference.to_f64() / 1000_f64).trunc()) let truncated_micro = JsBigInt::try_from((&difference.to_f64() / 1000_f64).trunc())
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
let microseconds = JsBigInt::rem(&truncated_micro, &JsBigInt::from(1000)); let _microseconds = JsBigInt::rem(&truncated_micro, &JsBigInt::from(1000));
// 4. Let milliseconds be remainder(truncate(difference / 106), 1000). // 4. Let milliseconds be remainder(truncate(difference / 106), 1000).
let truncated_milli = JsBigInt::try_from((&difference.to_f64() / 1_000_000_f64).trunc()) let truncated_milli = JsBigInt::try_from((&difference.to_f64() / 1_000_000_f64).trunc())
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
let milliseconds = JsBigInt::rem(&truncated_milli, &JsBigInt::from(1000)); let _milliseconds = JsBigInt::rem(&truncated_milli, &JsBigInt::from(1000));
// 5. Let seconds be truncate(difference / 10^9). // 5. Let seconds be truncate(difference / 10^9).
let seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc(); let _seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc();
// Create TimeDuration
let mut time_duration = DurationRecord::from_day_and_time(
0f64,
TimeDuration::new(
0f64,
0f64,
seconds,
milliseconds.to_f64(),
microseconds.to_f64(),
nanoseconds.to_f64(),
),
);
// TODO: Update to new Temporal library
// 6. If smallestUnit is "nanosecond" and roundingIncrement is 1, then // 6. If smallestUnit is "nanosecond" and roundingIncrement is 1, then
if smallest_unit == TemporalUnit::Nanosecond && (rounding_increment - 1f64).abs() < f64::EPSILON // a. Return ! BalanceTimeDuration(0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, largestUnit).
{
// a. Return ! BalanceTimeDuration(0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, largestUnit).
time_duration.balance_time_duration(largest_unit, None)?;
return Ok(time_duration);
}
// 7. Let roundResult be ! RoundDuration(0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, roundingIncrement, smallestUnit, largestUnit, roundingMode). // 7. Let roundResult be ! RoundDuration(0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, roundingIncrement, smallestUnit, largestUnit, roundingMode).
let (mut round_result, _total) = time_duration.round_duration(
DateDuration::default(),
rounding_increment,
smallest_unit,
rounding_mode,
(None, None, None),
context,
)?;
// 8. Assert: roundResult.[[Days]] is 0. // 8. Assert: roundResult.[[Days]] is 0.
assert_eq!(round_result.days() as i32, 0);
// 9. Return ! BalanceTimeDuration(0, roundResult.[[Hours]], roundResult.[[Minutes]], // 9. Return ! BalanceTimeDuration(0, roundResult.[[Hours]], roundResult.[[Minutes]],
// roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], // roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]],
// roundResult.[[Nanoseconds]], largestUnit). // roundResult.[[Nanoseconds]], largestUnit).
round_result.balance_time_duration(largest_unit, None)?;
Ok(round_result) Err(JsNativeError::error()
.with_message("not yet implemented.")
.into())
} }
/// 8.5.8 `RoundTemporalInstant ( ns, increment, unit, roundingMode )` /// 8.5.8 `RoundTemporalInstant ( ns, increment, unit, roundingMode )`
@ -691,7 +660,7 @@ fn diff_temporal_instant(
context: &mut Context, context: &mut Context,
) -> JsResult<JsValue> { ) -> JsResult<JsValue> {
// 1. If operation is since, let sign be -1. Otherwise, let sign be 1. // 1. If operation is since, let sign be -1. Otherwise, let sign be 1.
let sign = if op { 1_f64 } else { -1_f64 }; let _sign = if op { 1_f64 } else { -1_f64 };
// 2. Set other to ? ToTemporalInstant(other). // 2. Set other to ? ToTemporalInstant(other).
let other = to_temporal_instant(other)?; let other = to_temporal_instant(other)?;
// 3. Let resolvedOptions be ? CopyOptions(options). // 3. Let resolvedOptions be ? CopyOptions(options).
@ -710,7 +679,7 @@ fn diff_temporal_instant(
)?; )?;
// 5. Let result be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[LargestUnit]], settings.[[RoundingMode]]). // 5. Let result be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[LargestUnit]], settings.[[RoundingMode]]).
let result = diff_instant( let _result = diff_instant(
&instant.nanoseconds, &instant.nanoseconds,
&other.nanoseconds, &other.nanoseconds,
settings.3, settings.3,
@ -720,23 +689,9 @@ fn diff_temporal_instant(
context, context,
)?; )?;
// TODO: diff_instant will error so this shouldn't run.
unimplemented!();
// 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]]). // 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 )` /// 8.5.11 `AddDurationToOrSubtractDurationFromInstant ( operation, instant, temporalDurationLike )`
@ -752,25 +707,25 @@ fn add_or_subtract_duration_from_instant(
// 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike). // 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike).
let duration = super::to_temporal_duration_record(temporal_duration_like)?; let duration = super::to_temporal_duration_record(temporal_duration_like)?;
// 3. If duration.[[Days]] is not 0, throw a RangeError exception. // 3. If duration.[[Days]] is not 0, throw a RangeError exception.
if duration.days() != 0_f64 { if duration.date().days() != 0_f64 {
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("DurationDays cannot be 0") .with_message("DurationDays cannot be 0")
.into()); .into());
} }
// 4. If duration.[[Months]] is not 0, throw a RangeError exception. // 4. If duration.[[Months]] is not 0, throw a RangeError exception.
if duration.months() != 0_f64 { if duration.date().months() != 0_f64 {
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("DurationMonths cannot be 0") .with_message("DurationMonths cannot be 0")
.into()); .into());
} }
// 5. If duration.[[Weeks]] is not 0, throw a RangeError exception. // 5. If duration.[[Weeks]] is not 0, throw a RangeError exception.
if duration.weeks() != 0_f64 { if duration.date().weeks() != 0_f64 {
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("DurationWeeks cannot be 0") .with_message("DurationWeeks cannot be 0")
.into()); .into());
} }
// 6. If duration.[[Years]] is not 0, throw a RangeError exception. // 6. If duration.[[Years]] is not 0, throw a RangeError exception.
if duration.years() != 0_f64 { if duration.date().years() != 0_f64 {
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("DurationYears cannot be 0") .with_message("DurationYears cannot be 0")
.into()); .into());
@ -780,12 +735,12 @@ fn add_or_subtract_duration_from_instant(
// sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]). // sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]).
let new = add_instant( let new = add_instant(
&instant.nanoseconds, &instant.nanoseconds,
sign * duration.hours() as i32, sign * duration.time().hours() as i32,
sign * duration.minutes() as i32, sign * duration.time().minutes() as i32,
sign * duration.seconds() as i32, sign * duration.time().seconds() as i32,
sign * duration.milliseconds() as i32, sign * duration.time().milliseconds() as i32,
sign * duration.microseconds() as i32, sign * duration.time().microseconds() as i32,
sign * duration.nanoseconds() as i32, sign * duration.time().nanoseconds() as i32,
)?; )?;
// 8. Return ! CreateTemporalInstant(ns). // 8. Return ! CreateTemporalInstant(ns).
create_temporal_instant(new, None, context) create_temporal_instant(new, None, context)

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

@ -5,8 +5,8 @@
//! [spec]: https://tc39.es/proposal-temporal/ //! [spec]: https://tc39.es/proposal-temporal/
mod calendar; mod calendar;
mod date_equations;
mod duration; mod duration;
mod error;
mod fields; mod fields;
mod instant; mod instant;
mod now; mod now;
@ -22,11 +22,7 @@ mod zoned_date_time;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub(crate) use fields::TemporalFields; use self::options::{get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup};
use self::options::{
get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup,
};
pub use self::{ pub use self::{
calendar::*, duration::*, instant::*, now::*, plain_date::*, plain_date_time::*, calendar::*, duration::*, instant::*, now::*, plain_date::*, plain_date_time::*,
plain_month_day::*, plain_time::*, plain_year_month::*, time_zone::*, zoned_date_time::*, plain_month_day::*, plain_time::*, plain_year_month::*, time_zone::*, zoned_date_time::*,
@ -47,6 +43,7 @@ use crate::{
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::options::TemporalUnit;
// Relavant numeric constants // Relavant numeric constants
/// Nanoseconds per day constant: 8.64e+13 /// Nanoseconds per day constant: 8.64e+13
@ -192,7 +189,7 @@ fn to_zero_padded_decimal_string(n: u64, min_length: usize) -> String {
/// Abstract Operation 13.1 [`IteratorToListOfType`][proposal] /// Abstract Operation 13.1 [`IteratorToListOfType`][proposal]
/// ///
/// [proposal]: https://tc39.es/proposal-temporal/#sec-iteratortolistoftype /// [proposal]: https://tc39.es/proposal-temporal/#sec-iteratortolistoftype
pub(crate) fn iterator_to_list_of_types( pub(crate) fn _iterator_to_list_of_types(
iterator: &mut IteratorRecord, iterator: &mut IteratorRecord,
element_types: &[Type], element_types: &[Type],
context: &mut Context, context: &mut Context,
@ -229,7 +226,7 @@ pub(crate) fn iterator_to_list_of_types(
// Note: implemented on IsoDateRecord. // Note: implemented on IsoDateRecord.
// Abstract Operation 13.3 `EpochDaysToEpochMs` // Abstract Operation 13.3 `EpochDaysToEpochMs`
pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: i32) -> f64 { 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)) f64::from(day).mul_add(f64::from(MS_PER_DAY), f64::from(time))
} }
@ -370,7 +367,7 @@ fn apply_unsigned_rounding_mode(
} }
/// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )` /// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )`
pub(crate) fn round_number_to_increment( pub(crate) fn _round_number_to_increment(
x: f64, x: f64,
increment: f64, increment: f64,
rounding_mode: RoundingMode, rounding_mode: RoundingMode,

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

@ -12,7 +12,9 @@ use crate::{
builtins::options::{get_option, ParsableOptionType}, builtins::options::{get_option, ParsableOptionType},
js_string, Context, JsNativeError, JsObject, JsResult, js_string, Context, JsNativeError, JsObject, JsResult,
}; };
use std::{fmt, str::FromStr}; use boa_temporal::options::{
ArithmeticOverflow, DurationOverflow, InstantDisambiguation, OffsetDisambiguation, TemporalUnit,
};
// TODO: Expand docs on the below options. // TODO: Expand docs on the below options.
@ -123,262 +125,9 @@ fn date_units() -> impl Iterator<Item = TemporalUnit> {
fn datetime_units() -> impl Iterator<Item = TemporalUnit> { fn datetime_units() -> impl Iterator<Item = TemporalUnit> {
date_units().chain(time_units()) 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 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 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 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 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 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

@ -1,236 +0,0 @@
//! An `IsoDateRecord` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
use crate::{
builtins::temporal::{self, options::ArithmeticOverflow, DateDuration, 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: ArithmeticOverflow,
) -> JsResult<Self> {
match overflow {
ArithmeticOverflow::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))
}
ArithmeticOverflow::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)
}
}
}
/// 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: ArithmeticOverflow,
) -> 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: ArithmeticOverflow,
) -> 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,
date_duration: DateDuration,
overflow: ArithmeticOverflow,
) -> 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 + date_duration.years() as i32,
self.month + date_duration.months() as i32,
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 = date_duration.days() as i32 + (date_duration.weeks() as i32 * 7);
new_date.day += additional_days;
// 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
new_date.balance();
Ok(new_date)
}
}

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

@ -1,10 +1,11 @@
//! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object. //! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object.
#![allow(dead_code, unused_variables)] #![allow(dead_code, unused_variables)]
use std::str::FromStr;
use crate::{ use crate::{
builtins::{ builtins::{
options::{get_option, get_options_object}, options::{get_option, get_options_object},
temporal::options::TemporalUnit,
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
}, },
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
@ -17,27 +18,24 @@ use crate::{
}; };
use boa_parser::temporal::{IsoCursor, TemporalDateTimeString}; use boa_parser::temporal::{IsoCursor, TemporalDateTimeString};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{
use super::{ calendar::{AvailableCalendars, CalendarSlot},
calendar, duration::DurationRecord, options::ArithmeticOverflow, date::Date as InnerDate,
plain_date::iso::IsoDateRecord, plain_date_time, DateDuration, TimeDuration, datetime::DateTime,
options::ArithmeticOverflow,
}; };
pub(crate) mod iso; use super::calendar;
/// The `Temporal.PlainDate` object. /// The `Temporal.PlainDate` object.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PlainDate { pub struct PlainDate {
pub(crate) inner: IsoDateRecord, pub(crate) inner: InnerDate,
pub(crate) calendar: JsValue, // Calendar can probably be stored as a JsObject.
} }
impl PlainDate { impl PlainDate {
pub(crate) fn new(record: IsoDateRecord, calendar: JsValue) -> Self { pub(crate) fn new(inner: InnerDate) -> Self {
Self { Self { inner }
inner: record,
calendar,
}
} }
} }
@ -219,12 +217,18 @@ impl BuiltInConstructor for PlainDate {
let iso_year = super::to_integer_with_truncation(args.get_or_undefined(0), context)?; 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_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 iso_day = super::to_integer_with_truncation(args.get_or_undefined(2), context)?;
let default_calendar = JsValue::from(js_string!("iso8601")); let calendar_slot =
let calendar_like = args.get(3).unwrap_or(&default_calendar); calendar::to_temporal_calendar_slot_value(args.get_or_undefined(3), context)?;
let iso = IsoDateRecord::new(iso_year, iso_month, iso_day); let date = InnerDate::new(
iso_year,
iso_month,
iso_day,
calendar_slot,
ArithmeticOverflow::Reject,
)?;
Ok(create_temporal_date(iso, calendar_like.clone(), Some(new_target), context)?.into()) Ok(create_temporal_date(date, Some(new_target), context)?.into())
} }
} }
@ -390,7 +394,7 @@ impl PlainDate {
impl PlainDate { impl PlainDate {
/// Utitily function for translating a `Temporal.PlainDate` into a `JsObject`. /// Utitily function for translating a `Temporal.PlainDate` into a `JsObject`.
pub(crate) fn as_object(&self, context: &mut Context) -> JsResult<JsObject> { pub(crate) fn as_object(&self, context: &mut Context) -> JsResult<JsObject> {
create_temporal_date(self.inner, self.calendar.clone(), None, context) create_temporal_date(self.inner.clone(), None, context)
} }
} }
@ -399,24 +403,19 @@ impl PlainDate {
/// 3.5.3 `CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] )` /// 3.5.3 `CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] )`
pub(crate) fn create_temporal_date( pub(crate) fn create_temporal_date(
iso_date: IsoDateRecord, inner: InnerDate,
calendar: JsValue,
new_target: Option<&JsValue>, new_target: Option<&JsValue>,
context: &mut Context, context: &mut Context,
) -> JsResult<JsObject> { ) -> JsResult<JsObject> {
// 1. If IsValidISODate(isoYear, isoMonth, isoDay) is false, throw a RangeError exception. // 1. If IsValidISODate(isoYear, isoMonth, isoDay) is false, throw a RangeError exception.
if !iso_date.is_valid() { if !inner.is_valid() {
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("Date is not a valid ISO date.") .with_message("Date is not a valid ISO date.")
.into()); .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. // 2. If ISODateTimeWithinLimits(isoYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception.
if iso_date_time.is_valid() { if !DateTime::validate(&inner) {
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("Date is not within ISO date time limits.") .with_message("Date is not within ISO date time limits.")
.into()); .into());
@ -443,10 +442,8 @@ pub(crate) fn create_temporal_date(
// 6. Set object.[[ISOMonth]] to isoMonth. // 6. Set object.[[ISOMonth]] to isoMonth.
// 7. Set object.[[ISODay]] to isoDay. // 7. Set object.[[ISODay]] to isoDay.
// 8. Set object.[[Calendar]] to calendar. // 8. Set object.[[Calendar]] to calendar.
let obj = JsObject::from_proto_and_data( let obj =
prototype, JsObject::from_proto_and_data(prototype, ObjectData::plain_date(PlainDate::new(inner)));
ObjectData::plain_date(PlainDate::new(iso_date, calendar)),
);
// 9. Return object. // 9. Return object.
Ok(obj) Ok(obj)
@ -474,10 +471,7 @@ pub(crate) fn to_temporal_date(
// i. Return item. // i. Return item.
let obj = object.borrow(); let obj = object.borrow();
let date = obj.as_plain_date().expect("obj must be a PlainDate."); let date = obj.as_plain_date().expect("obj must be a PlainDate.");
return Ok(PlainDate { return Ok(PlainDate::new(date.inner.clone()));
inner: date.inner,
calendar: date.calendar.clone(),
});
// b. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then // b. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then
} else if object.is_zoned_date_time() { } else if object.is_zoned_date_time() {
return Err(JsNativeError::range() return Err(JsNativeError::range()
@ -499,16 +493,11 @@ pub(crate) fn to_temporal_date(
.as_plain_date_time() .as_plain_date_time()
.expect("obj must be a PlainDateTime"); .expect("obj must be a PlainDateTime");
let iso = date_time.inner.iso_date(); let date = InnerDate::from_datetime(date_time.inner());
let calendar = date_time.calendar.clone();
drop(obj); drop(obj);
// ii. Return ! CreateTemporalDate(item.[[ISOYear]], item.[[ISOMonth]], item.[[ISODay]], item.[[Calendar]]). // ii. Return ! CreateTemporalDate(item.[[ISOYear]], item.[[ISOMonth]], item.[[ISODay]], item.[[Calendar]]).
return Ok(PlainDate { return Ok(PlainDate::new(date));
inner: iso,
calendar,
});
} }
// d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item). // d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item).
@ -521,140 +510,39 @@ pub(crate) fn to_temporal_date(
} }
// 5. If item is not a String, throw a TypeError exception. // 5. If item is not a String, throw a TypeError exception.
match item { let JsValue::String(date_like_string) = item else {
JsValue::String(s) => { return Err(JsNativeError::typ()
// 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.") .with_message("ToTemporalDate item must be an object or string.")
.into()), .into());
} };
}
// 3.5.5. DifferenceIsoDate
// Implemented on IsoDateRecord.
/// 3.5.6 `DifferenceDate ( calendar, one, two, options )`
pub(crate) fn difference_date(
calendar: &JsValue,
one: &PlainDate,
two: &PlainDate,
largest_unit: TemporalUnit,
context: &mut Context,
) -> JsResult<DurationRecord> {
// 1. Assert: one.[[Calendar]] and two.[[Calendar]] have been determined to be equivalent as with CalendarEquals.
// 2. Assert: options is an ordinary Object.
// 3. Assert: options.[[Prototype]] is null.
// 4. Assert: options has a "largestUnit" data property.
// 5. If one.[[ISOYear]] = two.[[ISOYear]] and one.[[ISOMonth]] = two.[[ISOMonth]] and one.[[ISODay]] = two.[[ISODay]], then
if one.inner.year() == two.inner.year()
&& one.inner.month() == two.inner.month()
&& one.inner.day() == two.inner.day()
{
// a. Return ! CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0).
return Ok(DurationRecord::default());
}
// 6. If ! Get(options, "largestUnit") is "day", then
if largest_unit == TemporalUnit::Day {
// a. Let days be DaysUntil(one, two).
let days = super::duration::days_until(one, two);
// b. Return ! CreateTemporalDuration(0, 0, 0, days, 0, 0, 0, 0, 0, 0).
return Ok(DurationRecord::new(
DateDuration::new(0.0, 0.0, 0.0, f64::from(days)),
TimeDuration::default(),
));
}
// Create the options object prior to sending it to the calendars.
let options_obj = JsObject::with_null_proto();
options_obj.create_data_property_or_throw(
utf16!("largestUnit"),
JsString::from(largest_unit.to_string()),
context,
)?;
// 7. Return ? CalendarDateUntil(calendar, one, two, options).
calendar::calendar_date_until(calendar, one, two, &options_obj.into(), context)
}
// 3.5.7 RegulateIsoDate
// Implemented on IsoDateRecord.
// 3.5.8 IsValidIsoDate
// Implemented on IsoDateRecord.
// 3.5.9 BalanceIsoDate
// Implemented on IsoDateRecord.
// 3.5.12 AddISODate ( year, month, day, years, months, weeks, days, overflow )
// Implemented on IsoDateRecord
/// 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ]] )` // 6. Let result be ? ParseTemporalDateString(item).
pub(crate) fn add_date( let result = TemporalDateTimeString::parse(
calendar: &JsValue, false,
plain_date: &PlainDate, &mut IsoCursor::new(&date_like_string.to_std_string_escaped()),
duration: &DurationRecord, )
options: &JsValue, .map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
context: &mut Context,
) -> JsResult<PlainDate> { // 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true.
// 1. If options is not present, set options to undefined. // 8. Let calendar be result.[[Calendar]].
// 2. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then // 9. If calendar is undefined, set calendar to "iso8601".
if duration.years() != 0.0 || duration.months() != 0.0 || duration.weeks() != 0.0 { let identifier = result.date.calendar.unwrap_or("iso8601".to_string());
// a. If dateAdd is not present, then
// i. Set dateAdd to unused. // 10. If IsBuiltinCalendar(calendar) is false, throw a RangeError exception.
// ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd"). let _ = AvailableCalendars::from_str(identifier.to_ascii_lowercase().as_str())?;
// b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd).
return calendar::calendar_date_add(calendar, plain_date, duration, options, context); // 11. Set calendar to the ASCII-lowercase of calendar.
} let calendar = CalendarSlot::Identifier(identifier.to_ascii_lowercase());
// 3. Let overflow be ? ToTemporalOverflow(options). // 12. Perform ? ToTemporalOverflow(options).
let options_obj = get_options_object(options)?; let _ = get_option::<ArithmeticOverflow>(&options_obj, utf16!("overflow"), context)?;
let overflow = get_option(&options_obj, utf16!("overflow"), context)?
.unwrap_or(ArithmeticOverflow::Constrain); // 13. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar).
Ok(PlainDate::new(InnerDate::new(
let mut intermediate = *duration; result.date.year,
// 4. Let days be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").[[Days]]. result.date.month,
intermediate.balance_time_duration(TemporalUnit::Day, None)?; result.date.day,
calendar,
// 5. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow). ArithmeticOverflow::Reject,
let result = plain_date )?))
.inner
.add_iso_date(intermediate.date(), overflow)?;
// 6. Return ! CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar).
Ok(PlainDate::new(result, plain_date.calendar.clone()))
} }

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

@ -1,100 +0,0 @@
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)
}
}

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

@ -11,15 +11,22 @@ use crate::{
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use self::iso::IsoDateTimeRecord; use boa_temporal::datetime::DateTime as InnerDateTime;
pub(crate) mod iso;
/// The `Temporal.PlainDateTime` object. /// The `Temporal.PlainDateTime` object.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PlainDateTime { pub struct PlainDateTime {
pub(crate) inner: IsoDateTimeRecord, pub(crate) inner: InnerDateTime,
pub(crate) calendar: JsValue, }
impl PlainDateTime {
fn new(inner: InnerDateTime) -> Self {
Self { inner }
}
pub(crate) fn inner(&self) -> &InnerDateTime {
&self.inner
}
} }
impl BuiltInObject for PlainDateTime { impl BuiltInObject for PlainDateTime {

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

@ -11,13 +11,18 @@ use crate::{
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use super::{plain_date::iso::IsoDateRecord, plain_date_time::iso::IsoDateTimeRecord}; use boa_temporal::{datetime::DateTime, month_day::MonthDay as InnerMonthDay};
/// The `Temporal.PlainMonthDay` object. /// The `Temporal.PlainMonthDay` object.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PlainMonthDay { pub struct PlainMonthDay {
pub(crate) inner: IsoDateRecord, pub(crate) inner: InnerMonthDay,
pub(crate) calendar: JsValue, }
impl PlainMonthDay {
fn new(inner: InnerMonthDay) -> Self {
Self { inner }
}
} }
impl BuiltInObject for PlainMonthDay { impl BuiltInObject for PlainMonthDay {
@ -62,24 +67,13 @@ impl BuiltInConstructor for PlainMonthDay {
// ==== `PlainMonthDay` Abstract Operations ==== // ==== `PlainMonthDay` Abstract Operations ====
pub(crate) fn create_temporal_month_day( pub(crate) fn create_temporal_month_day(
iso: IsoDateRecord, inner: InnerMonthDay,
calendar: JsValue,
new_target: Option<&JsValue>, new_target: Option<&JsValue>,
context: &mut Context, context: &mut Context,
) -> JsResult<JsValue> { ) -> JsResult<JsValue> {
// 1. If IsValidISODate(referenceISOYear, isoMonth, isoDay) is false, throw a RangeError exception. // 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. // 2. If ISODateTimeWithinLimits(referenceISOYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception.
let iso_date_time = IsoDateTimeRecord::default() if DateTime::validate(&inner) {
.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() return Err(JsNativeError::range()
.with_message("PlainMonthDay is not a valid ISO date time.") .with_message("PlainMonthDay is not a valid ISO date time.")
.into()); .into());
@ -111,10 +105,7 @@ pub(crate) fn create_temporal_month_day(
// 8. Set object.[[ISOYear]] to referenceISOYear. // 8. Set object.[[ISOYear]] to referenceISOYear.
let obj = JsObject::from_proto_and_data( let obj = JsObject::from_proto_and_data(
proto, proto,
ObjectData::plain_month_day(PlainMonthDay { ObjectData::plain_month_day(PlainMonthDay::new(inner)),
inner: iso,
calendar,
}),
); );
// 9. Return object. // 9. Return object.

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

@ -12,13 +12,19 @@ use crate::{
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use super::plain_date::iso::IsoDateRecord; use super::calendar::to_temporal_calendar_slot_value;
use boa_temporal::{options::ArithmeticOverflow, year_month::YearMonth as InnerYearMonth};
/// The `Temporal.PlainYearMonth` object. /// The `Temporal.PlainYearMonth` object.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PlainYearMonth { pub struct PlainYearMonth {
pub(crate) inner: IsoDateRecord, pub(crate) inner: InnerYearMonth,
pub(crate) calendar: JsValue, }
impl PlainYearMonth {
pub(crate) fn new(inner: InnerYearMonth) -> Self {
Self { inner }
}
} }
impl BuiltInObject for PlainYearMonth { impl BuiltInObject for PlainYearMonth {
@ -142,28 +148,23 @@ impl BuiltInConstructor for PlainYearMonth {
// 2. If referenceISODay is undefined, then // 2. If referenceISODay is undefined, then
let ref_day = if day.is_undefined() { let ref_day = if day.is_undefined() {
// a. Set referenceISODay to 1𝔽. // a. Set referenceISODay to 1𝔽.
1 None
} else { } else {
// 6. Let ref be ? ToIntegerWithTruncation(referenceISODay). // 6. Let ref be ? ToIntegerWithTruncation(referenceISODay).
super::to_integer_with_truncation(day, context)? Some(super::to_integer_with_truncation(day, context)?)
}; };
// 3. Let y be ? ToIntegerWithTruncation(isoYear). // 3. Let y be ? ToIntegerWithTruncation(isoYear).
let y = super::to_integer_with_truncation(args.get_or_undefined(0), context)?; let y = super::to_integer_with_truncation(args.get_or_undefined(0), context)?;
// 4. Let m be ? ToIntegerWithTruncation(isoMonth). // 4. Let m be ? ToIntegerWithTruncation(isoMonth).
let m = super::to_integer_with_truncation(args.get_or_undefined(1), context)?; let m = super::to_integer_with_truncation(args.get_or_undefined(1), context)?;
// TODO: calendar handling.
// 5. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601"). // 5. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601").
let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(2), context)?;
// 7. Return ? CreateTemporalYearMonth(y, m, calendar, ref, NewTarget). // 7. Return ? CreateTemporalYearMonth(y, m, calendar, ref, NewTarget).
let record = IsoDateRecord::new(y, m, ref_day); let inner = InnerYearMonth::new(y, m, ref_day, calendar, ArithmeticOverflow::Reject)?;
create_temporal_year_month(
record, create_temporal_year_month(inner, Some(new_target), context)
JsValue::from(js_string!("iso8601")),
Some(new_target),
context,
)
} }
} }
@ -266,24 +267,12 @@ impl PlainYearMonth {
// 9.5.5 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )` // 9.5.5 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )`
pub(crate) fn create_temporal_year_month( pub(crate) fn create_temporal_year_month(
year_month_record: IsoDateRecord, ym: InnerYearMonth,
calendar: JsValue,
new_target: Option<&JsValue>, new_target: Option<&JsValue>,
context: &mut Context, context: &mut Context,
) -> JsResult<JsValue> { ) -> JsResult<JsValue> {
// 1. If IsValidISODate(isoYear, isoMonth, referenceISODay) is false, throw a RangeError exception. // 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. // 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%. // 3. If newTarget is not present, set newTarget to %Temporal.PlainYearMonth%.
let new_target = if let Some(target) = new_target { let new_target = if let Some(target) = new_target {
@ -310,13 +299,8 @@ pub(crate) fn create_temporal_year_month(
// 7. Set object.[[Calendar]] to calendar. // 7. Set object.[[Calendar]] to calendar.
// 8. Set object.[[ISODay]] to referenceISODay. // 8. Set object.[[ISODay]] to referenceISODay.
let obj = JsObject::from_proto_and_data( let obj =
proto, JsObject::from_proto_and_data(proto, ObjectData::plain_year_month(PlainYearMonth::new(ym)));
ObjectData::plain_year_month(PlainYearMonth {
inner: year_month_record,
calendar,
}),
);
// 9. Return object. // 9. Return object.
Ok(obj.into()) Ok(obj.into())

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

@ -1,4 +1,3 @@
use super::date_equations::{epoch_time_to_month_in_year, mathematical_in_leap_year};
use crate::{js_string, run_test_actions, JsValue, TestAction}; use crate::{js_string, run_test_actions, JsValue, TestAction};
// Temporal Object tests. // Temporal Object tests.
@ -34,19 +33,3 @@ fn now_object() {
} }
// Date Equations // 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);
}

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

@ -8,6 +8,7 @@ use crate::{
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::duration::Duration as TemporalDuration;
/// The `Temporal.ZonedDateTime` object. /// The `Temporal.ZonedDateTime` object.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -64,7 +65,7 @@ pub(crate) fn add_zoned_date_time(
epoch_nanos: &JsBigInt, epoch_nanos: &JsBigInt,
time_zone: &JsObject, time_zone: &JsObject,
calendar: &JsObject, calendar: &JsObject,
duration: super::duration::DurationRecord, duration: TemporalDuration,
options: Option<&JsObject>, options: Option<&JsObject>,
) -> JsResult<JsBigInt> { ) -> JsResult<JsBigInt> {
// 1. If options is not present, set options to undefined. // 1. If options is not present, set options to undefined.

23
boa_temporal/Cargo.toml

@ -0,0 +1,23 @@
[package]
name = "boa_temporal"
keywords = ["javascript", "js", "compiler", "temporal", "calendar", "date", "time"]
categories = ["date", "time", "calendars"]
readme = "./README.md"
description.workspace = true
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
tinystr = "0.7.4"
icu_calendar = { workspace = true, default-features = false }
rustc-hash = { workspace = true, features = ["std"] }
num-bigint = { workspace = true, features = ["serde"] }
bitflags.workspace = true
num-traits.workspace = true
[lints]
workspace = true

11
boa_temporal/README.md

@ -0,0 +1,11 @@
# Temporal in Rust
Provides a standard API for working with dates and time.
IMPORTANT NOTE: The Temporal Proposal is still in Stage 3. As such, this crate should be viewed
as highly experimental until the proposal has been completely standardized and released.
## Goal
The intended goal of this crate is to provide an engine agnostic
implementation of `ECMAScript`'s Temporal algorithms.

579
boa_temporal/src/calendar.rs

@ -0,0 +1,579 @@
//! Temporal calendar traits and implementations.
//!
//! The goal of the calendar module of `boa_temporal` is to provide
//! Temporal compatible calendar implementations.
//!
//! The implementation will only be of calendar's prexisting calendars. This library
//! does not come with a pre-existing `CustomCalendar` (i.e., an object that implements
//! the calendar protocol), but it does aim to provide the necessary tools and API for
//! implementing one.
use std::{any::Any, str::FromStr};
use crate::{
date::Date,
datetime::DateTime,
duration::Duration,
fields::TemporalFields,
iso::{IsoDate, IsoDateSlots},
month_day::MonthDay,
options::{ArithmeticOverflow, TemporalUnit},
year_month::YearMonth,
TemporalError, TemporalResult,
};
use tinystr::TinyAsciiStr;
use self::iso::IsoCalendar;
pub mod iso;
/// The ECMAScript defined protocol methods
pub const CALENDAR_PROTOCOL_METHODS: [&str; 21] = [
"dateAdd",
"dateFromFields",
"dateUntil",
"day",
"dayOfWeek",
"dayOfYear",
"daysInMonth",
"daysInWeek",
"daysInYear",
"fields",
"id",
"inLeapYear",
"mergeFields",
"month",
"monthCode",
"monthDayFromFields",
"monthsInYear",
"weekOfYear",
"year",
"yearMonthFromFields",
"yearOfWeek",
];
/// Designate the type of `CalendarFields` needed
#[derive(Debug, Clone, Copy)]
pub enum CalendarFieldsType {
/// Whether the Fields should return for a Date.
Date,
/// Whether the Fields should return for a YearMonth.
YearMonth,
/// Whether the Fields should return for a MonthDay.
MonthDay,
}
// TODO: Optimize to TinyStr or &str.
impl From<&[String]> for CalendarFieldsType {
fn from(value: &[String]) -> Self {
let year_present = value.contains(&"year".to_owned());
let day_present = value.contains(&"day".to_owned());
if year_present && day_present {
CalendarFieldsType::Date
} else if year_present {
CalendarFieldsType::YearMonth
} else {
CalendarFieldsType::MonthDay
}
}
}
/// `AvailableCalendars` lists the currently implemented `CalendarProtocols`
#[derive(Debug, Clone, Copy)]
pub enum AvailableCalendars {
/// The ISO8601 calendar.
Iso,
}
// NOTE: Should `s` be forced to lowercase or should the user be expected to provide the lowercase.
impl FromStr for AvailableCalendars {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"iso8601" => Ok(Self::Iso),
_ => {
Err(TemporalError::range().with_message("CalendarId is not an available Calendar"))
}
}
}
}
impl AvailableCalendars {
/// Returns the `CalendarProtocol` for the `AvailableCalendar`
#[must_use]
pub fn to_protocol(&self) -> Box<dyn CalendarProtocol> {
match self {
Self::Iso => Box::new(IsoCalendar),
}
}
}
/// The `DateLike` objects that can be provided to the `CalendarProtocol`.
#[derive(Debug)]
pub enum CalendarDateLike {
/// Represents a `Date` datelike
Date(Date),
/// Represents a `DateTime` datelike
DateTime(DateTime),
/// Represents a `YearMonth` datelike
YearMonth(YearMonth),
/// Represents a `MonthDay` datelike
MonthDay(MonthDay),
}
impl CalendarDateLike {
/// Retrieves the internal `IsoDate` field.
#[inline]
#[must_use]
pub fn as_iso_date(&self) -> IsoDate {
match self {
CalendarDateLike::Date(d) => d.iso_date(),
CalendarDateLike::DateTime(dt) => dt.iso_date(),
CalendarDateLike::MonthDay(md) => md.iso_date(),
CalendarDateLike::YearMonth(ym) => ym.iso_date(),
}
}
}
// ==== CalendarProtocol trait ====
/// The `CalendarProtocol`'s Clone supertrait.
pub trait CalendarProtocolClone {
/// Clone's the current `CalendarProtocol`
fn clone_box(&self) -> Box<dyn CalendarProtocol>;
}
impl<P> CalendarProtocolClone for P
where
P: 'static + CalendarProtocol + Clone,
{
fn clone_box(&self) -> Box<dyn CalendarProtocol> {
Box::new(self.clone())
}
}
// TODO: Split further into `CalendarProtocol` and `BuiltinCalendar` to better handle
// fields and mergeFields.
/// A trait for implementing a Builtin Calendar's Calendar Protocol in Rust.
pub trait CalendarProtocol: CalendarProtocolClone {
/// Creates a `Temporal.PlainDate` object from provided fields.
fn date_from_fields(
&self,
fields: &mut TemporalFields,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<Date>;
/// Creates a `Temporal.PlainYearMonth` object from the provided fields.
fn year_month_from_fields(
&self,
fields: &mut TemporalFields,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<YearMonth>;
/// Creates a `Temporal.PlainMonthDay` object from the provided fields.
fn month_day_from_fields(
&self,
fields: &mut TemporalFields,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<MonthDay>;
/// Returns a `Temporal.PlainDate` based off an added date.
fn date_add(
&self,
date: &Date,
duration: &Duration,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<Date>;
/// Returns a `Temporal.Duration` representing the duration between two dates.
fn date_until(
&self,
one: &Date,
two: &Date,
largest_unit: TemporalUnit,
context: &mut dyn Any,
) -> TemporalResult<Duration>;
/// Returns the era for a given `temporaldatelike`.
fn era(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<Option<TinyAsciiStr<8>>>;
/// Returns the era year for a given `temporaldatelike`
fn era_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<Option<i32>>;
/// Returns the `year` for a given `temporaldatelike`
fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<i32>;
/// Returns the `month` for a given `temporaldatelike`
fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8>;
// Note: Best practice would probably be to switch to a MonthCode enum after extraction.
/// Returns the `monthCode` for a given `temporaldatelike`
fn month_code(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<TinyAsciiStr<4>>;
/// Returns the `day` for a given `temporaldatelike`
fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8>;
/// Returns a value representing the day of the week for a date.
fn day_of_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16>;
/// Returns a value representing the day of the year for a given calendar.
fn day_of_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16>;
/// Returns a value representing the week of the year for a given calendar.
fn week_of_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16>;
/// Returns the year of a given week.
fn year_of_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<i32>;
/// Returns the days in a week for a given calendar.
fn days_in_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16>;
/// Returns the days in a month for a given calendar.
fn days_in_month(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16>;
/// Returns the days in a year for a given calendar.
fn days_in_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16>;
/// Returns the months in a year for a given calendar.
fn months_in_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16>;
/// Returns whether a value is within a leap year according to the designated calendar.
fn in_leap_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<bool>;
/// Resolve the `TemporalFields` for the implemented Calendar
fn resolve_fields(
&self,
fields: &mut TemporalFields,
r#type: CalendarFieldsType,
) -> TemporalResult<()>;
/// Return this calendar's a fieldName and whether it is required depending on type (date, day-month).
fn field_descriptors(&self, r#type: CalendarFieldsType) -> Vec<(String, bool)>;
/// Return the fields to ignore for this Calendar based on provided keys.
fn field_keys_to_ignore(&self, additional_keys: Vec<String>) -> Vec<String>;
/// Debug name
fn identifier(&self, context: &mut dyn Any) -> TemporalResult<String>;
}
impl core::fmt::Debug for dyn CalendarProtocol {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
self.identifier(&mut ()).unwrap_or_default().as_str()
)
}
}
/// The `[[Calendar]]` field slot of a Temporal Object.
#[derive(Debug)]
pub enum CalendarSlot {
/// The calendar identifier string.
Identifier(String),
/// A `CalendarProtocol` implementation.
Protocol(Box<dyn CalendarProtocol>),
}
impl Clone for CalendarSlot {
fn clone(&self) -> Self {
match self {
Self::Identifier(s) => Self::Identifier(s.clone()),
Self::Protocol(b) => Self::Protocol(b.clone_box()),
}
}
}
impl Clone for Box<dyn CalendarProtocol + 'static> {
fn clone(&self) -> Self {
self.clone_box()
}
}
impl Default for CalendarSlot {
fn default() -> Self {
Self::Identifier("iso8601".to_owned())
}
}
// TODO: Handle `CalendarFields` and `CalendarMergeFields`
impl CalendarSlot {
/// `CalendarDateAdd`
///
/// TODO: More Docs
pub fn date_add(
&self,
date: &Date,
duration: &Duration,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<Date> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.date_add(date, duration, overflow, context)
}
Self::Protocol(protocol) => protocol.date_add(date, duration, overflow, context),
}
}
/// `CalendarDateUntil`
///
/// TODO: More Docs
pub fn date_until(
&self,
one: &Date,
two: &Date,
largest_unit: TemporalUnit,
context: &mut dyn Any,
) -> TemporalResult<Duration> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.date_until(one, two, largest_unit, context)
}
Self::Protocol(protocol) => protocol.date_until(one, two, largest_unit, context),
}
}
/// `CalendarYear`
///
/// TODO: More docs.
pub fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<i32> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.year(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.year(date_like, context),
}
}
/// `CalendarMonth`
///
/// TODO: More docs.
pub fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.month(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.month(date_like, context),
}
}
/// `CalendarMonthCode`
///
/// TODO: More docs.
pub fn month_code(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<TinyAsciiStr<4>> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.month_code(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.month_code(date_like, context),
}
}
/// `CalendarDay`
///
/// TODO: More docs.
pub fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.day(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.day(date_like, context),
}
}
/// `CalendarDayOfWeek`
///
/// TODO: More docs.
pub fn day_of_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.day_of_week(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.day_of_week(date_like, context),
}
}
/// `CalendarDayOfYear`
///
/// TODO: More docs.
pub fn day_of_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.day_of_year(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.day_of_year(date_like, context),
}
}
/// `CalendarWeekOfYear`
///
/// TODO: More docs.
pub fn week_of_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.week_of_year(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.week_of_year(date_like, context),
}
}
/// `CalendarYearOfWeek`
///
/// TODO: More docs.
pub fn year_of_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<i32> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.year_of_week(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.year_of_week(date_like, context),
}
}
/// `CalendarDaysInWeek`
///
/// TODO: More docs.
pub fn days_in_week(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.days_in_week(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.days_in_week(date_like, context),
}
}
/// `CalendarDaysInMonth`
///
/// TODO: More docs.
pub fn days_in_month(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.days_in_month(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.days_in_month(date_like, context),
}
}
/// `CalendarDaysInYear`
///
/// TODO: More docs.
pub fn days_in_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.days_in_year(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.days_in_year(date_like, context),
}
}
/// `CalendarMonthsInYear`
///
/// TODO: More docs.
pub fn months_in_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<u16> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.months_in_year(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.months_in_year(date_like, context),
}
}
/// `CalendarInLeapYear`
///
/// TODO: More docs.
pub fn in_leap_year(
&self,
date_like: &CalendarDateLike,
context: &mut dyn Any,
) -> TemporalResult<bool> {
match self {
Self::Identifier(id) => {
let protocol = AvailableCalendars::from_str(id)?.to_protocol();
protocol.in_leap_year(date_like, &mut ())
}
Self::Protocol(protocol) => protocol.in_leap_year(date_like, context),
}
}
}

285
boa_temporal/src/calendar/iso.rs

@ -0,0 +1,285 @@
//! Implementation of the "iso8601" calendar.
use crate::{
date::Date,
duration::Duration,
error::TemporalError,
fields::TemporalFields,
month_day::MonthDay,
options::{ArithmeticOverflow, TemporalUnit},
utils,
year_month::YearMonth,
TemporalResult,
};
use std::any::Any;
use tinystr::TinyAsciiStr;
use super::{CalendarDateLike, CalendarFieldsType, CalendarProtocol, CalendarSlot};
use icu_calendar::week::{RelativeUnit, WeekCalculator};
/// This represents the implementation of the `ISO8601`
/// calendar for Temporal.
#[derive(Debug, Clone, Copy)]
pub struct IsoCalendar;
impl CalendarProtocol 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 TemporalFields,
overflow: ArithmeticOverflow,
_: &mut dyn Any,
) -> TemporalResult<Date> {
// NOTE: we are in ISO by default here.
// a. Perform ? ISOResolveMonth(fields).
// b. Let result be ? ISODateFromFields(fields, overflow).
fields.iso_resolve_month()?;
// 9. Return ? CreateDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
Date::new(
fields.year().unwrap_or(0),
fields.month().unwrap_or(0),
fields.day().unwrap_or(0),
CalendarSlot::Identifier("iso8601".to_string()),
overflow,
)
}
/// 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 TemporalFields,
overflow: ArithmeticOverflow,
_: &mut dyn Any,
) -> TemporalResult<YearMonth> {
// 9. If calendar.[[Identifier]] is "iso8601", then
// a. Perform ? ISOResolveMonth(fields).
fields.iso_resolve_month()?;
// TODO: Do we even need ISOYearMonthFromFields? YearMonth would should pass as a valid date
// b. Let result be ? ISOYearMonthFromFields(fields, overflow).
// 10. Return ? CreateYearMonth(result.[[Year]], result.[[Month]], "iso8601", result.[[ReferenceISODay]]).
YearMonth::new(
fields.year().unwrap_or(0),
fields.month().unwrap_or(0),
fields.day(),
CalendarSlot::Identifier("iso8601".to_string()),
overflow,
)
}
/// 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 TemporalFields,
overflow: ArithmeticOverflow,
_: &mut dyn Any,
) -> TemporalResult<MonthDay> {
// 8. Perform ? ISOResolveMonth(fields).
fields.iso_resolve_month()?;
// TODO: double check error mapping is correct for specifcation/test262.
// 9. Let result be ? ISOMonthDayFromFields(fields, overflow).
// 10. Return ? CreateMonthDay(result.[[Month]], result.[[Day]], "iso8601", result.[[ReferenceISOYear]]).
MonthDay::new(
fields.month().unwrap_or(0),
fields.month().unwrap_or(0),
CalendarSlot::Identifier("iso8601".to_string()),
overflow,
)
}
/// 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: &Date,
_duration: &Duration,
_overflow: ArithmeticOverflow,
_: &mut dyn Any,
) -> TemporalResult<Date> {
// TODO: Not stable on `ICU4X`. Implement once completed.
Err(TemporalError::range().with_message("feature not implemented."))
// 9. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], balanceResult.[[Days]], overflow).
// 10. Return ? CreateDate(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: &Date,
_two: &Date,
_largest_unit: TemporalUnit,
_: &mut dyn Any,
) -> TemporalResult<Duration> {
// TODO: Not stable on `ICU4X`. Implement once completed.
Err(TemporalError::range().with_message("Feature not yet implemented."))
// 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,
_: &CalendarDateLike,
_: &mut dyn Any,
) -> TemporalResult<Option<TinyAsciiStr<8>>> {
// Returns undefined on iso8601.
Ok(None)
}
/// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar.
fn era_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<Option<i32>> {
// Returns undefined on iso8601.
Ok(None)
}
/// Returns the `year` for the `Iso` calendar.
fn year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<i32> {
Ok(date_like.as_iso_date().year())
}
/// Returns the `month` for the `Iso` calendar.
fn month(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u8> {
Ok(date_like.as_iso_date().month())
}
/// Returns the `monthCode` for the `Iso` calendar.
fn month_code(
&self,
date_like: &CalendarDateLike,
_: &mut dyn Any,
) -> TemporalResult<TinyAsciiStr<4>> {
let date = date_like.as_iso_date().as_icu4x()?;
Ok(date.month().code.0)
}
/// Returns the `day` for the `Iso` calendar.
fn day(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u8> {
Ok(date_like.as_iso_date().day())
}
/// Returns the `dayOfWeek` for the `Iso` calendar.
fn day_of_week(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> {
let date = date_like.as_iso_date().as_icu4x()?;
Ok(date.day_of_week() as u16)
}
/// Returns the `dayOfYear` for the `Iso` calendar.
fn day_of_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> {
let date = date_like.as_iso_date().as_icu4x()?;
Ok(date.day_of_year_info().day_of_year)
}
/// Returns the `weekOfYear` for the `Iso` calendar.
fn week_of_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> {
let date = date_like.as_iso_date().as_icu4x()?;
let week_calculator = WeekCalculator::default();
let week_of = date
.week_of_year(&week_calculator)
.map_err(|err| TemporalError::range().with_message(err.to_string()))?;
Ok(week_of.week)
}
/// Returns the `yearOfWeek` for the `Iso` calendar.
fn year_of_week(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<i32> {
let date = date_like.as_iso_date().as_icu4x()?;
let week_calculator = WeekCalculator::default();
let week_of = date
.week_of_year(&week_calculator)
.map_err(|err| TemporalError::range().with_message(err.to_string()))?;
// TODO: Reach out and see about RelativeUnit starting at -1
// Ok(date.year().number - week_of.unit)
match week_of.unit {
RelativeUnit::Previous => Ok(date.year().number - 1),
RelativeUnit::Current => Ok(date.year().number),
RelativeUnit::Next => Ok(date.year().number + 1),
}
}
/// Returns the `daysInWeek` value for the `Iso` calendar.
fn days_in_week(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> {
Ok(7)
}
/// Returns the `daysInMonth` value for the `Iso` calendar.
fn days_in_month(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> {
let date = date_like.as_iso_date().as_icu4x()?;
Ok(u16::from(date.days_in_month()))
}
/// Returns the `daysInYear` value for the `Iso` calendar.
fn days_in_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> {
let date = date_like.as_iso_date().as_icu4x()?;
Ok(date.days_in_year())
}
/// Return the amount of months in an ISO Calendar.
fn months_in_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> {
Ok(12)
}
/// Returns whether provided date is in a leap year according to this calendar.
fn in_leap_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<bool> {
// `ICU4X`'s `CalendarArithmetic` is currently private.
Ok(utils::mathematical_days_in_year(date_like.as_iso_date().year()) == 366)
}
// Resolve the fields for the iso calendar.
fn resolve_fields(
&self,
fields: &mut TemporalFields,
_: CalendarFieldsType,
) -> TemporalResult<()> {
fields.iso_resolve_month()?;
Ok(())
}
/// Returns the ISO field descriptors, which is not called for the iso8601 calendar.
fn field_descriptors(&self, _: CalendarFieldsType) -> 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<String>) -> Vec<String> {
let mut result = Vec::new();
for key in &additional_keys {
result.push(key.clone());
if key.as_str() == "month" {
result.push("monthCode".to_string());
} else if key.as_str() == "monthCode" {
result.push("month".to_string());
}
}
result
}
// NOTE: This is currently not a name that is compliant with
// the Temporal proposal. For debugging purposes only.
/// Returns the debug name.
fn identifier(&self, _: &mut dyn Any) -> TemporalResult<String> {
Ok("iso8601".to_string())
}
}

220
boa_temporal/src/date.rs

@ -0,0 +1,220 @@
//! The `PlainDate` representation.
use crate::{
calendar::CalendarSlot,
datetime::DateTime,
duration::{DateDuration, Duration},
iso::{IsoDate, IsoDateSlots},
options::{ArithmeticOverflow, TemporalUnit},
TemporalResult,
};
use std::any::Any;
/// The `Temporal.PlainDate` equivalent
#[derive(Debug, Default, Clone)]
pub struct Date {
iso: IsoDate,
calendar: CalendarSlot,
}
// ==== Private API ====
impl Date {
/// Create a new `Date` with the date values and calendar slot.
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self {
Self { iso, calendar }
}
#[inline]
/// Returns a new moved date and the days associated with that adjustment
pub(crate) fn move_relative_date(
&self,
duration: &Duration,
context: &mut dyn Any,
) -> TemporalResult<(Self, f64)> {
let new_date =
self.contextual_add_date(duration, ArithmeticOverflow::Constrain, context)?;
let days = f64::from(self.days_until(&new_date));
Ok((new_date, days))
}
}
// ==== Public API ====
impl Date {
/// Creates a new `Date` while checking for validity.
pub fn new(
year: i32,
month: i32,
day: i32,
calendar: CalendarSlot,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
let iso = IsoDate::new(year, month, day, overflow)?;
Ok(Self::new_unchecked(iso, calendar))
}
#[must_use]
/// Creates a `Date` from a `DateTime`.
pub fn from_datetime(dt: &DateTime) -> Self {
Self {
iso: dt.iso_date(),
calendar: dt.calendar().clone(),
}
}
#[inline]
#[must_use]
/// Returns this `Date`'s year value.
pub const fn year(&self) -> i32 {
self.iso.year()
}
#[inline]
#[must_use]
/// Returns this `Date`'s month value.
pub const fn month(&self) -> u8 {
self.iso.month()
}
#[inline]
#[must_use]
/// Returns this `Date`'s day value.
pub const fn day(&self) -> u8 {
self.iso.day()
}
#[inline]
#[must_use]
/// Returns the `Date`'s inner `IsoDate` record.
pub const fn iso_date(&self) -> IsoDate {
self.iso
}
#[inline]
#[must_use]
/// Returns a reference to this `Date`'s calendar slot.
pub fn calendar(&self) -> &CalendarSlot {
&self.calendar
}
/// 3.5.7 `IsValidISODate`
///
/// Checks if the current date is a valid `ISODate`.
#[must_use]
pub fn is_valid(&self) -> bool {
self.iso.is_valid()
}
/// `DaysUntil`
///
/// Calculates the epoch days between two `Date`s
#[inline]
#[must_use]
pub fn days_until(&self, other: &Self) -> i32 {
other.iso.to_epoch_days() - self.iso.to_epoch_days()
}
}
impl IsoDateSlots for Date {
/// Returns the structs `IsoDate`
fn iso_date(&self) -> IsoDate {
self.iso
}
}
// ==== Context based API ====
impl Date {
/// Returns the date after adding the given duration to date with a provided context.
///
/// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )`
#[inline]
pub fn contextual_add_date(
&self,
duration: &Duration,
overflow: ArithmeticOverflow,
context: &mut dyn Any,
) -> TemporalResult<Self> {
// 1. If options is not present, set options to undefined.
// 2. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then
if duration.date().years() != 0.0
|| duration.date().months() != 0.0
|| duration.date().weeks() != 0.0
{
// a. If dateAdd is not present, then
// i. Set dateAdd to unused.
// ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd").
// b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd).
return self.calendar().date_add(self, duration, overflow, context);
}
// 3. Let overflow be ? ToTemporalOverflow(options).
// 4. Let days be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").[[Days]].
let (days, _) = duration.balance_time_duration(TemporalUnit::Day)?;
// 5. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow).
let result = self
.iso
.add_iso_date(&DateDuration::new(0f64, 0f64, 0f64, days), overflow)?;
Ok(Self::new_unchecked(result, self.calendar().clone()))
}
/// Returns the date after adding the given duration to date.
///
/// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )`
#[inline]
pub fn add_date(
&self,
duration: &Duration,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
self.contextual_add_date(duration, overflow, &mut ())
}
/// Returns a duration representing the difference between the dates one and two with a provided context.
///
/// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )`
#[inline]
pub fn contextual_difference_date(
&self,
other: &Self,
largest_unit: TemporalUnit,
context: &mut dyn Any,
) -> TemporalResult<Duration> {
if self.iso.year() == other.iso.year()
&& self.iso.month() == other.iso.month()
&& self.iso.day() == other.iso.day()
{
return Ok(Duration::default());
}
if largest_unit == TemporalUnit::Day {
let days = self.days_until(other);
return Ok(Duration::from_date_duration(DateDuration::new(
0f64,
0f64,
0f64,
f64::from(days),
)));
}
self.calendar()
.date_until(self, other, largest_unit, context)
}
/// Returns a duration representing the difference between the dates one and two.
///
/// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )`
#[inline]
pub fn difference_date(
&self,
other: &Self,
largest_unit: TemporalUnit,
) -> TemporalResult<Duration> {
self.contextual_difference_date(other, largest_unit, &mut ())
}
}

95
boa_temporal/src/datetime.rs

@ -0,0 +1,95 @@
//! Temporal implementation of `DateTime`
use crate::{
calendar::CalendarSlot,
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime},
options::ArithmeticOverflow,
TemporalResult,
};
/// The `DateTime` struct.
#[derive(Debug, Default, Clone)]
pub struct DateTime {
iso: IsoDateTime,
calendar: CalendarSlot,
}
// ==== Private DateTime API ====
impl DateTime {
/// Creates a new unchecked `DateTime`.
#[inline]
#[must_use]
pub(crate) fn new_unchecked(date: IsoDate, time: IsoTime, calendar: CalendarSlot) -> Self {
Self {
iso: IsoDateTime::new_unchecked(date, time),
calendar,
}
}
#[inline]
#[must_use]
/// Utility function for validating `IsoDate`s
fn validate_iso(iso: IsoDate) -> bool {
IsoDateTime::new_unchecked(iso, IsoTime::noon()).is_within_limits()
}
}
// ==== Public DateTime API ====
impl DateTime {
/// Creates a new validated `DateTime`.
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn new(
year: i32,
month: i32,
day: i32,
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
calendar: CalendarSlot,
) -> TemporalResult<Self> {
let iso_date = IsoDate::new(year, month, day, ArithmeticOverflow::Reject)?;
let iso_time = IsoTime::new(
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
ArithmeticOverflow::Reject,
)?;
Ok(Self::new_unchecked(iso_date, iso_time, calendar))
}
/// Validates whether ISO date slots are within iso limits at noon.
#[inline]
pub fn validate<T: IsoDateSlots>(target: &T) -> bool {
Self::validate_iso(target.iso_date())
}
/// Returns the inner `IsoDate` value.
#[inline]
#[must_use]
pub fn iso_date(&self) -> IsoDate {
self.iso.iso_date()
}
/// Returns the inner `IsoTime` value.
#[inline]
#[must_use]
pub fn iso_time(&self) -> IsoTime {
self.iso.iso_time()
}
/// Returns the Calendar value.
#[inline]
#[must_use]
pub fn calendar(&self) -> &CalendarSlot {
&self.calendar
}
}

1500
boa_engine/src/builtins/temporal/duration/record.rs → boa_temporal/src/duration.rs

File diff suppressed because it is too large Load Diff

98
boa_temporal/src/error.rs

@ -0,0 +1,98 @@
//! An error type for Temporal Errors.
use core::fmt;
/// `TemporalError`'s error type.
#[derive(Debug, Default, Clone, Copy)]
pub enum ErrorKind {
/// Error.
#[default]
Generic,
/// TypeError
Type,
/// RangeError
Range,
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Generic => "Error",
Self::Type => "TypeError",
Self::Range => "RangeError",
}
.fmt(f)
}
}
/// The error type for `boa_temporal`.
#[derive(Debug, Clone)]
pub struct TemporalError {
kind: ErrorKind,
msg: Box<str>,
}
impl TemporalError {
fn new(kind: ErrorKind) -> Self {
Self {
kind,
msg: Box::default(),
}
}
/// Create a generic error
#[must_use]
pub fn general<S>(msg: S) -> Self
where
S: Into<Box<str>>,
{
Self::new(ErrorKind::Generic).with_message(msg)
}
/// Create a range error.
#[must_use]
pub fn range() -> Self {
Self::new(ErrorKind::Range)
}
/// Create a type error.
#[must_use]
pub fn r#type() -> Self {
Self::new(ErrorKind::Type)
}
/// Add a message to the error.
#[must_use]
pub fn with_message<S>(mut self, msg: S) -> Self
where
S: Into<Box<str>>,
{
self.msg = msg.into();
self
}
/// Returns this error's kind.
#[must_use]
pub fn kind(&self) -> ErrorKind {
self.kind
}
/// Returns the error message.
#[must_use]
pub fn message(&self) -> &str {
&self.msg
}
}
impl fmt::Display for TemporalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
let msg = self.msg.trim();
if !msg.is_empty() {
write!(f, ": {msg}")?;
}
Ok(())
}
}

487
boa_temporal/src/fields.rs

@ -0,0 +1,487 @@
//! `TemporalFields` native Rust representation.
use std::str::FromStr;
use crate::{error::TemporalError, TemporalResult};
use bitflags::bitflags;
// use rustc_hash::FxHashSet;
use tinystr::{TinyAsciiStr, TinyStr16, TinyStr4};
bitflags! {
/// FieldMap maps the currently active fields on the `TemporalField`
#[derive(Debug, PartialEq, Eq)]
pub struct FieldMap: u16 {
/// Represents an active `year` field
const YEAR = 0b0000_0000_0000_0001;
/// Represents an active `month` field
const MONTH = 0b0000_0000_0000_0010;
/// Represents an active `monthCode` field
const MONTH_CODE = 0b0000_0000_0000_0100;
/// Represents an active `day` field
const DAY = 0b0000_0000_0000_1000;
/// Represents an active `hour` field
const HOUR = 0b0000_0000_0001_0000;
/// Represents an active `minute` field
const MINUTE = 0b0000_0000_0010_0000;
/// Represents an active `second` field
const SECOND = 0b0000_0000_0100_0000;
/// Represents an active `millisecond` field
const MILLISECOND = 0b0000_0000_1000_0000;
/// Represents an active `microsecond` field
const MICROSECOND = 0b0000_0001_0000_0000;
/// Represents an active `nanosecond` field
const NANOSECOND = 0b0000_0010_0000_0000;
/// Represents an active `offset` field
const OFFSET = 0b0000_0100_0000_0000;
/// Represents an active `era` field
const ERA = 0b0000_1000_0000_0000;
/// Represents an active `eraYear` field
const ERA_YEAR = 0b0001_0000_0000_0000;
/// Represents an active `timeZone` field
const TIME_ZONE = 0b0010_0000_0000_0000;
// NOTE(nekevss): Two bits preserved if needed.
}
}
/// The post conversion field value.
#[derive(Debug)]
#[allow(variant_size_differences)]
pub enum FieldValue {
/// Designates the values as an integer.
Integer(i32),
/// Designates that the value is undefined.
Undefined,
/// Designates the value as a string.
String(String),
}
/// The Conversion type of a field.
#[derive(Debug, Clone, Copy)]
pub enum FieldConversion {
/// Designates the Conversion type is `ToIntegerWithTruncation`
ToIntegerWithTruncation,
/// Designates the Conversion type is `ToPositiveIntegerWithTruncation`
ToPositiveIntegerWithTruncation,
/// Designates the Conversion type is `ToPrimitiveRequireString`
ToPrimativeAndRequireString,
/// Designates the Conversion type is nothing
None,
}
impl FromStr for FieldConversion {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"year" | "hour" | "minute" | "second" | "millisecond" | "microsecond"
| "nanosecond" => Ok(Self::ToIntegerWithTruncation),
"month" | "day" => Ok(Self::ToPositiveIntegerWithTruncation),
"monthCode" | "offset" | "eraYear" => Ok(Self::ToPrimativeAndRequireString),
_ => Err(TemporalError::range()
.with_message(format!("{s} is not a valid TemporalField Property"))),
}
}
}
/// 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" | `None` | undefined |
#[derive(Debug)]
pub struct TemporalFields {
bit_map: FieldMap,
year: Option<i32>,
month: Option<i32>,
month_code: Option<TinyStr4>, // TODO: Switch to icu compatible value.
day: Option<i32>,
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
offset: Option<String>, // TODO: Switch to tinystr?
era: Option<TinyStr16>, // TODO: switch to icu compatible value.
era_year: Option<i32>, // TODO: switch to icu compatible value.
time_zone: Option<String>, // 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
}
}
// TODO: Update the below.
impl TemporalFields {
/// Flags a field as being required.
#[inline]
pub fn require_field(&mut self, field: &str) {
match field {
"year" => self.bit_map.set(FieldMap::YEAR, true),
"month" => self.bit_map.set(FieldMap::MONTH, true),
"monthCode" => self.bit_map.set(FieldMap::MONTH_CODE, true),
"day" => self.bit_map.set(FieldMap::DAY, true),
"hour" => self.bit_map.set(FieldMap::HOUR, true),
"minute" => self.bit_map.set(FieldMap::MINUTE, true),
"second" => self.bit_map.set(FieldMap::SECOND, true),
"millisecond" => self.bit_map.set(FieldMap::MILLISECOND, true),
"microsecond" => self.bit_map.set(FieldMap::MICROSECOND, true),
"nanosecond" => self.bit_map.set(FieldMap::NANOSECOND, true),
"offset" => self.bit_map.set(FieldMap::OFFSET, true),
"era" => self.bit_map.set(FieldMap::ERA, true),
"eraYear" => self.bit_map.set(FieldMap::ERA_YEAR, true),
"timeZone" => self.bit_map.set(FieldMap::TIME_ZONE, true),
_ => {}
}
}
#[inline]
/// A generic field setter for `TemporalFields`
///
/// This method will not run any `JsValue` conversion. `FieldValue` is
/// expected to contain a preconverted value.
pub fn set_field_value(&mut self, field: &str, value: &FieldValue) -> TemporalResult<()> {
match field {
"year" => self.set_year(value)?,
"month" => self.set_month(value)?,
"monthCode" => self.set_month_code(value)?,
"day" => self.set_day(value)?,
"hour" => self.set_hour(value)?,
"minute" => self.set_minute(value)?,
"second" => self.set_second(value)?,
"millisecond" => self.set_milli(value)?,
"microsecond" => self.set_micro(value)?,
"nanosecond" => self.set_nano(value)?,
"offset" => self.set_offset(value)?,
"era" => self.set_era(value)?,
"eraYear" => self.set_era_year(value)?,
"timeZone" => self.set_time_zone(value)?,
_ => unreachable!(),
}
Ok(())
}
#[inline]
fn set_year(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(y) = value else {
return Err(TemporalError::r#type().with_message("Year must be an integer."));
};
self.year = Some(*y);
self.bit_map.set(FieldMap::YEAR, true);
Ok(())
}
#[inline]
fn set_month(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(mo) = value else {
return Err(TemporalError::r#type().with_message("Month must be an integer."));
};
self.year = Some(*mo);
self.bit_map.set(FieldMap::MONTH, true);
Ok(())
}
#[inline]
fn set_month_code(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::String(mc) = value else {
return Err(TemporalError::r#type().with_message("monthCode must be string."));
};
self.month_code =
Some(TinyStr4::from_bytes(mc.as_bytes()).expect("monthCode must be less than 4 chars"));
self.bit_map.set(FieldMap::MONTH_CODE, true);
Ok(())
}
#[inline]
fn set_day(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(d) = value else {
return Err(TemporalError::r#type().with_message("day must be an integer."));
};
self.day = Some(*d);
self.bit_map.set(FieldMap::DAY, true);
Ok(())
}
#[inline]
fn set_hour(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(h) = value else {
return Err(TemporalError::r#type().with_message("hour must be an integer."));
};
self.hour = *h;
self.bit_map.set(FieldMap::HOUR, true);
Ok(())
}
#[inline]
fn set_minute(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(min) = value else {
return Err(TemporalError::r#type().with_message("minute must be an integer."));
};
self.minute = *min;
self.bit_map.set(FieldMap::MINUTE, true);
Ok(())
}
#[inline]
fn set_second(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(sec) = value else {
return Err(TemporalError::r#type().with_message("Second must be an integer."));
};
self.second = *sec;
self.bit_map.set(FieldMap::SECOND, true);
Ok(())
}
#[inline]
fn set_milli(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(milli) = value else {
return Err(TemporalError::r#type().with_message("Second must be an integer."));
};
self.millisecond = *milli;
self.bit_map.set(FieldMap::MILLISECOND, true);
Ok(())
}
#[inline]
fn set_micro(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(micro) = value else {
return Err(TemporalError::r#type().with_message("microsecond must be an integer."));
};
self.microsecond = *micro;
self.bit_map.set(FieldMap::MICROSECOND, true);
Ok(())
}
#[inline]
fn set_nano(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(nano) = value else {
return Err(TemporalError::r#type().with_message("nanosecond must be an integer."));
};
self.nanosecond = *nano;
self.bit_map.set(FieldMap::NANOSECOND, true);
Ok(())
}
#[inline]
fn set_offset(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::String(offset) = value else {
return Err(TemporalError::r#type().with_message("offset must be string."));
};
self.offset = Some(offset.to_string());
self.bit_map.set(FieldMap::OFFSET, true);
Ok(())
}
#[inline]
fn set_era(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::String(era) = value else {
return Err(TemporalError::r#type().with_message("era must be string."));
};
self.era =
Some(TinyStr16::from_bytes(era.as_bytes()).expect("era should not exceed 16 bytes."));
self.bit_map.set(FieldMap::ERA, true);
Ok(())
}
#[inline]
fn set_era_year(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::Integer(era_year) = value else {
return Err(TemporalError::r#type().with_message("eraYear must be an integer."));
};
self.era_year = Some(*era_year);
self.bit_map.set(FieldMap::ERA_YEAR, true);
Ok(())
}
#[inline]
fn set_time_zone(&mut self, value: &FieldValue) -> TemporalResult<()> {
let FieldValue::String(tz) = value else {
return Err(TemporalError::r#type().with_message("tz must be string."));
};
self.time_zone = Some(tz.to_string());
self.bit_map.set(FieldMap::TIME_ZONE, true);
Ok(())
}
}
// TODO: optimize into iter.
impl TemporalFields {
/// Returns a vector filled with the key-value pairs marked as active.
pub fn active_kvs(&self) -> Vec<(String, FieldValue)> {
let mut result = Vec::default();
for field in self.bit_map.iter() {
match field {
FieldMap::YEAR => result.push((
"year".to_owned(),
self.year.map_or(FieldValue::Undefined, FieldValue::Integer),
)),
FieldMap::MONTH => result.push((
"month".to_owned(),
self.month
.map_or(FieldValue::Undefined, FieldValue::Integer),
)),
FieldMap::MONTH_CODE => result.push((
"monthCode".to_owned(),
self.month_code
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())),
)),
FieldMap::DAY => result.push((
"day".to_owned(),
self.day.map_or(FieldValue::Undefined, FieldValue::Integer),
)),
FieldMap::HOUR => result.push(("hour".to_owned(), FieldValue::Integer(self.hour))),
FieldMap::MINUTE => {
result.push(("minute".to_owned(), FieldValue::Integer(self.minute)));
}
FieldMap::SECOND => {
result.push(("second".to_owned(), FieldValue::Integer(self.second)));
}
FieldMap::MILLISECOND => result.push((
"millisecond".to_owned(),
FieldValue::Integer(self.millisecond),
)),
FieldMap::MICROSECOND => result.push((
"microsecond".to_owned(),
FieldValue::Integer(self.microsecond),
)),
FieldMap::NANOSECOND => result.push((
"nanosecond".to_owned(),
FieldValue::Integer(self.nanosecond),
)),
FieldMap::OFFSET => result.push((
"offset".to_owned(),
self.offset
.clone()
.map_or(FieldValue::Undefined, FieldValue::String),
)),
FieldMap::ERA => result.push((
"era".to_owned(),
self.era
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())),
)),
FieldMap::ERA_YEAR => result.push((
"eraYear".to_owned(),
self.era_year
.map_or(FieldValue::Undefined, FieldValue::Integer),
)),
FieldMap::TIME_ZONE => result.push((
"timeZone".to_owned(),
self.time_zone
.clone()
.map_or(FieldValue::Undefined, FieldValue::String),
)),
_ => {}
}
}
result
}
/// Resolve `TemporalFields` month and monthCode fields.
pub(crate) fn iso_resolve_month(&mut self) -> TemporalResult<()> {
if self.month_code.is_none() {
if self.month.is_some() {
return Ok(());
}
return Err(TemporalError::range()
.with_message("month and MonthCode values cannot both be undefined."));
}
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(
TemporalError::range().with_message("month and monthCode cannot be resolved.")
)
}
_ => month_code_integer,
};
self.month = Some(new_month);
Ok(())
}
}
fn month_code_to_integer(mc: TinyAsciiStr<4>) -> TemporalResult<i32> {
match mc.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(TemporalError::range().with_message("monthCode is not within the valid values.")),
}
}

341
boa_temporal/src/iso.rs

@ -0,0 +1,341 @@
//! The `ISO` module implements the internal ISO field slots.
//!
//! The three main types of slots are:
//! - `IsoDateTime`
//! - `IsoDate`
//! - `IsoTime`
//!
//! An `IsoDate` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
//! An `IsoTime` that represents the `[[ISOHour]]`, `[[ISOMinute]]`, `[[ISOsecond]]`, `[[ISOmillisecond]]`,
//! `[[ISOmicrosecond]]`, and `[[ISOnanosecond]]` internal slots.
//! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`.
use crate::{
duration::DateDuration, error::TemporalError, options::ArithmeticOverflow, utils,
TemporalResult,
};
use icu_calendar::{Date as IcuDate, Iso};
use num_bigint::BigInt;
use num_traits::cast::FromPrimitive;
/// `IsoDateTime` is the Temporal internal representation of
/// a `DateTime` record
#[derive(Debug, Default, Clone, Copy)]
pub struct IsoDateTime {
date: IsoDate,
time: IsoTime,
}
impl IsoDateTime {
/// Creates a new `IsoDateTime` without any validaiton.
pub(crate) fn new_unchecked(date: IsoDate, time: IsoTime) -> Self {
Self { date, time }
}
/// Returns whether the `IsoDateTime` is within valid limits.
pub(crate) fn is_within_limits(&self) -> bool {
let Some(ns) = self.to_utc_epoch_nanoseconds(0f64) else {
return false;
};
let max = BigInt::from(crate::NS_MAX_INSTANT + i128::from(crate::NS_PER_DAY));
let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(crate::NS_PER_DAY));
min < ns && max > ns
}
/// Returns the UTC epoch nanoseconds for this `IsoDateTime`.
pub(crate) fn to_utc_epoch_nanoseconds(self, offset: f64) -> Option<BigInt> {
let day = self.date.to_epoch_days();
let time = self.time.to_epoch_ms();
let epoch_ms = utils::epoch_days_to_epoch_ms(day, time);
let epoch_nanos = epoch_ms.mul_add(
1_000_000f64,
f64::from(self.time.microsecond).mul_add(1_000f64, f64::from(self.time.nanosecond)),
);
BigInt::from_f64(epoch_nanos - offset)
}
pub(crate) fn iso_date(&self) -> IsoDate {
self.date
}
pub(crate) fn iso_time(&self) -> IsoTime {
self.time
}
}
// ==== `IsoDate` section ====
// TODO: Figure out `ICU4X` interop / replacement?
/// A trait for accessing the `IsoDate` across the various Temporal objects
pub trait IsoDateSlots {
/// Returns the target's internal `IsoDate`.
fn iso_date(&self) -> IsoDate;
}
/// `IsoDate` serves as a 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 struct IsoDate {
year: i32,
month: u8,
day: u8,
}
impl IsoDate {
/// Creates a new `IsoDate` without determining the validity.
pub(crate) const fn new_unchecked(year: i32, month: u8, day: u8) -> Self {
Self { year, month, day }
}
pub(crate) fn new(
year: i32,
month: i32,
day: i32,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
match overflow {
ArithmeticOverflow::Constrain => {
let m = month.clamp(1, 12);
let days_in_month = utils::iso_days_in_month(year, month);
let d = day.clamp(1, days_in_month);
Ok(Self::new_unchecked(year, m as u8, d as u8))
}
ArithmeticOverflow::Reject => {
if !is_valid_date(year, month, day) {
return Err(TemporalError::range().with_message("not a valid ISO date."));
}
// NOTE: Values have been verified to be in a u8 range.
Ok(Self::new_unchecked(year, month as u8, day as u8))
}
}
}
/// Create a balanced `IsoDate`
///
/// Equivalent to `BalanceISODate`.
fn balance(year: i32, month: i32, day: i32) -> Self {
let epoch_days = iso_date_to_epoch_days(year, month - 1, day);
let ms = utils::epoch_days_to_epoch_ms(epoch_days, 0f64);
Self::new_unchecked(
utils::epoch_time_to_epoch_year(ms),
utils::epoch_time_to_month_in_year(ms) + 1,
utils::epoch_time_to_date(ms),
)
}
/// Returns the year field
pub(crate) const fn year(self) -> i32 {
self.year
}
/// Returns the month field
pub(crate) const fn month(self) -> u8 {
self.month
}
/// Returns the day field
pub(crate) const fn day(self) -> u8 {
self.day
}
/// Functionally the same as Date's abstract operation `MakeDay`
///
/// Equivalent to `IsoDateToEpochDays`
pub(crate) fn to_epoch_days(self) -> i32 {
iso_date_to_epoch_days(self.year, self.month.into(), self.day.into())
}
/// Returns if the current `IsoDate` is valid.
pub(crate) fn is_valid(self) -> bool {
is_valid_date(self.year, self.month.into(), self.day.into())
}
/// Returns the resulting `IsoDate` from adding a provided `Duration` to this `IsoDate`
pub(crate) fn add_iso_date(
self,
duration: &DateDuration,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
// 1. Assert: year, month, day, years, months, weeks, and days are integers.
// 2. Assert: overflow is either "constrain" or "reject".
// 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months).
let mut intermediate_year = self.year + duration.years() as i32;
let mut intermediate_month = i32::from(self.month) + duration.months() as i32;
intermediate_year += (intermediate_month - 1) / 12;
intermediate_month = (intermediate_month - 1) % 12 + 1;
// 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow).
let intermediate = Self::new(
intermediate_year,
intermediate_month,
i32::from(self.day),
overflow,
)?;
// 5. Set days to days + 7 × weeks.
// 6. Let d be intermediate.[[Day]] + days.
let additional_days = duration.days() as i32 + (duration.weeks() as i32 * 7);
let d = i32::from(intermediate.day) + additional_days;
// 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
Ok(Self::balance(
intermediate.year,
intermediate.month.into(),
d,
))
}
}
impl IsoDate {
/// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date<Iso>` struct.
pub(crate) fn as_icu4x(self) -> TemporalResult<IcuDate<Iso>> {
IcuDate::try_new_iso_date(self.year, self.month, self.day)
.map_err(|e| TemporalError::range().with_message(e.to_string()))
}
}
// ==== `IsoTime` section ====
/// An `IsoTime` record that contains `Temporal`'s
/// time slots.
#[derive(Debug, Default, Clone, Copy)]
pub struct IsoTime {
hour: i32, // 0..=23
minute: i32, // 0..=59
second: i32, // 0..=59
millisecond: i32, // 0..=999
microsecond: i32, // 0..=999
nanosecond: i32, // 0..=999
}
impl IsoTime {
/// Creates a new `IsoTime` without any validation.
pub(crate) fn new_unchecked(
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
) -> Self {
Self {
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
}
}
/// Creates a new regulated `IsoTime`.
pub fn new(
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
overflow: ArithmeticOverflow,
) -> TemporalResult<IsoTime> {
match overflow {
ArithmeticOverflow::Constrain => {
let h = hour.clamp(0, 23);
let min = minute.clamp(0, 59);
let sec = second.clamp(0, 59);
let milli = millisecond.clamp(0, 999);
let micro = microsecond.clamp(0, 999);
let nano = nanosecond.clamp(0, 999);
Ok(Self::new_unchecked(h, min, sec, milli, micro, nano))
}
ArithmeticOverflow::Reject => {
// TODO: Invert structure validation and update fields to u16.
let time =
Self::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond);
if !time.is_valid() {
return Err(TemporalError::range().with_message("IsoTime is not valid"));
}
Ok(time)
}
}
}
/// Returns an `IsoTime` set to 12:00:00
pub(crate) const fn noon() -> Self {
Self {
hour: 12,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
nanosecond: 0,
}
}
/// Checks if the time is a valid `IsoTime`
pub(crate) fn is_valid(&self) -> bool {
if !(0..=23).contains(&self.hour) {
return false;
}
let min_sec = 0..=59;
if !min_sec.contains(&self.minute) || !min_sec.contains(&self.second) {
return false;
}
let sub_second = 0..=999;
sub_second.contains(&self.millisecond)
&& sub_second.contains(&self.microsecond)
&& sub_second.contains(&self.nanosecond)
}
/// `IsoTimeToEpochMs`
///
/// Note: This method is library specific and not in spec
///
/// Functionally the same as Date's `MakeTime`
pub(crate) fn to_epoch_ms(self) -> f64 {
f64::from(self.hour).mul_add(
utils::MS_PER_HOUR,
f64::from(self.minute) * utils::MS_PER_MINUTE,
) + f64::from(self.second).mul_add(1000f64, f64::from(self.millisecond))
}
}
// ==== `IsoDate` specific utiltiy functions ====
/// Returns the Epoch days based off the given year, month, and day.
#[inline]
fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 {
// 1. Let resolvedYear be year + floor(month / 12).
let resolved_year = year + (f64::from(month) / 12_f64).floor() as i32;
// 2. Let resolvedMonth be month modulo 12.
let resolved_month = 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 = utils::epoch_time_for_year(resolved_year);
let month_t = utils::epoch_time_for_month_given_year(resolved_month, resolved_year);
// 4. Return EpochTimeToDayNumber(t) + date - 1.
utils::epoch_time_to_day_number(year_t + month_t) + day - 1
}
#[inline]
// Determines if the month and day are valid for the given year.
fn is_valid_date(year: i32, month: i32, day: i32) -> bool {
if !(1..=12).contains(&month) {
return false;
}
let days_in_month = utils::iso_days_in_month(year, month);
(1..=days_in_month).contains(&day)
}

61
boa_temporal/src/lib.rs

@ -0,0 +1,61 @@
//! Boa's `boa_temporal` crate is intended to serve as an engine agnostic
//! implementation the ECMAScript's Temporal builtin and algorithm.
#![doc = include_str!("../../ABOUT.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg",
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg"
)]
#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
#![allow(
// Currently throws a false positive regarding dependencies that are only used in benchmarks.
unused_crate_dependencies,
clippy::module_name_repetitions,
clippy::redundant_pub_crate,
clippy::too_many_lines,
clippy::cognitive_complexity,
clippy::missing_errors_doc,
clippy::let_unit_value,
clippy::option_if_let_else,
// It may be worth to look if we can fix the issues highlighted by these lints.
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
// Add temporarily - Needs addressing
clippy::missing_panics_doc,
)]
pub mod calendar;
pub mod date;
pub mod datetime;
pub mod duration;
pub mod error;
pub mod fields;
pub mod iso;
pub mod month_day;
pub mod options;
pub mod time;
pub(crate) mod utils;
pub mod year_month;
pub mod zoneddatetime;
// TODO: evaluate positives and negatives of using tinystr.
// Re-exporting tinystr as a convenience, as it is currently tied into the API.
pub use tinystr::TinyAsciiStr;
pub use error::TemporalError;
/// The `Temporal` result type
pub type TemporalResult<T> = Result<T, TemporalError>;
// Relevant numeric constants
/// Nanoseconds per day constant: 8.64e+13
pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000;
/// Milliseconds per day constant: 8.64e+7
pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000;
/// Max Instant nanosecond constant
pub(crate) const NS_MAX_INSTANT: i128 = NS_PER_DAY as i128 * 100_000_000i128;
/// Min Instant nanosecond constant
pub(crate) const NS_MIN_INSTANT: i128 = -NS_MAX_INSTANT;

51
boa_temporal/src/month_day.rs

@ -0,0 +1,51 @@
//! `MonthDay`
use crate::{
calendar::CalendarSlot,
iso::{IsoDate, IsoDateSlots},
options::ArithmeticOverflow,
TemporalResult,
};
/// The `MonthDay` struct
#[derive(Debug, Default, Clone)]
pub struct MonthDay {
iso: IsoDate,
calendar: CalendarSlot,
}
impl MonthDay {
/// Creates a new unchecked `MonthDay`
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self {
Self { iso, calendar }
}
#[inline]
/// Creates a new valid `MonthDay`.
pub fn new(
month: i32,
day: i32,
calendar: CalendarSlot,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
let iso = IsoDate::new(1972, month, day, overflow)?;
Ok(Self::new_unchecked(iso, calendar))
}
#[inline]
#[must_use]
/// Returns a reference to `MonthDay`'s `CalendarSlot`
pub fn calendar(&self) -> &CalendarSlot {
&self.calendar
}
}
impl IsoDateSlots for MonthDay {
#[inline]
/// Returns this structs `IsoDate`.
fn iso_date(&self) -> IsoDate {
self.iso
}
}

413
boa_temporal/src/options.rs

@ -0,0 +1,413 @@
//! Temporal Options
use core::{fmt, str::FromStr};
use crate::TemporalError;
/// The relevant unit that should be used for the operation that
/// this option is provided as a value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TemporalUnit {
/// The `Auto` unit
Auto = 0,
/// The `Nanosecond` unit
Nanosecond,
/// The `Microsecond` unit
Microsecond,
/// The `Millisecond` unit
Millisecond,
/// The `Second` unit
Second,
/// The `Minute` unit
Minute,
/// The `Hour` unit
Hour,
/// The `Day` unit
Day,
/// The `Week` unit
Week,
/// The `Month` unit
Month,
/// The `Year` unit
Year,
}
impl TemporalUnit {
#[inline]
#[must_use]
/// Returns the `MaximumRoundingIncrement` for the current `TemporalUnit`.
pub 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!(),
}
}
}
/// A parsing error for `TemporalUnit`
#[derive(Debug, Clone, Copy)]
pub 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 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 enum ArithmeticOverflow {
/// Constrain option
Constrain,
/// Constrain option
Reject,
}
/// A parsing error for `ArithemeticOverflow`
#[derive(Debug, Clone, Copy)]
pub 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 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.
#[derive(Debug, Clone, Copy)]
pub enum DurationOverflow {
/// Constrain option
Constrain,
/// Balance option
Balance,
}
/// A parsing error for `DurationOverflow`.
#[derive(Debug, Clone, Copy)]
pub 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 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.
#[derive(Debug, Clone, Copy)]
pub enum InstantDisambiguation {
/// Compatible option
Compatible,
/// Earlier option
Earlier,
/// Later option
Later,
/// Reject option
Reject,
}
/// A parsing error on `InstantDisambiguation` options.
#[derive(Debug, Clone, Copy)]
pub 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 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.
#[derive(Debug, Clone, Copy)]
pub enum OffsetDisambiguation {
/// Use option
Use,
/// Prefer option
Prefer,
/// Ignore option
Ignore,
/// Reject option
Reject,
}
/// A parsing error for `OffsetDisambiguation` parsing.
#[derive(Debug, Clone, Copy)]
pub 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 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)
}
}
// TODO: Figure out what to do with intl's RoundingMode
/// Declares the specified `RoundingMode` for the operation.
#[derive(Debug, Copy, Clone, Default)]
pub enum TemporalRoundingMode {
/// Ceil RoundingMode
Ceil,
/// Floor RoundingMode
Floor,
/// Expand RoundingMode
Expand,
/// Truncate RoundingMode
Trunc,
/// HalfCeil RoundingMode
HalfCeil,
/// HalfFloor RoundingMode
HalfFloor,
/// HalfExpand RoundingMode - Default
#[default]
HalfExpand,
/// HalfTruncate RoundingMode
HalfTrunc,
/// HalfEven RoundingMode
HalfEven,
}
/// The `UnsignedRoundingMode`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemporalUnsignedRoundingMode {
/// `Infinity` `RoundingMode`
Infinity,
/// `Zero` `RoundingMode`
Zero,
/// `HalfInfinity` `RoundingMode`
HalfInfinity,
/// `HalfZero` `RoundingMode`
HalfZero,
/// `HalfEven` `RoundingMode`
HalfEven,
}
impl TemporalRoundingMode {
#[inline]
#[must_use]
/// Negates the current `RoundingMode`.
pub const fn negate(self) -> Self {
use TemporalRoundingMode::{
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,
}
}
#[inline]
#[must_use]
/// Returns the `UnsignedRoundingMode`
pub const fn get_unsigned_round_mode(self, is_negative: bool) -> TemporalUnsignedRoundingMode {
use TemporalRoundingMode::{
Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc,
};
match self {
Ceil if !is_negative => TemporalUnsignedRoundingMode::Infinity,
Ceil => TemporalUnsignedRoundingMode::Zero,
Floor if !is_negative => TemporalUnsignedRoundingMode::Zero,
Floor | Trunc | Expand => TemporalUnsignedRoundingMode::Infinity,
HalfCeil if !is_negative => TemporalUnsignedRoundingMode::HalfInfinity,
HalfCeil | HalfTrunc => TemporalUnsignedRoundingMode::HalfZero,
HalfFloor if !is_negative => TemporalUnsignedRoundingMode::HalfZero,
HalfFloor | HalfExpand => TemporalUnsignedRoundingMode::HalfInfinity,
HalfEven => TemporalUnsignedRoundingMode::HalfEven,
}
}
}
impl FromStr for TemporalRoundingMode {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ceil" => Ok(Self::Ceil),
"floor" => Ok(Self::Floor),
"expand" => Ok(Self::Expand),
"trunc" => Ok(Self::Trunc),
"halfCeil" => Ok(Self::HalfCeil),
"halfFloor" => Ok(Self::HalfFloor),
"halfExpand" => Ok(Self::HalfExpand),
"halfTrunc" => Ok(Self::HalfTrunc),
"halfEven" => Ok(Self::HalfEven),
_ => Err(TemporalError::range().with_message("RoundingMode not an accepted value.")),
}
}
}
impl fmt::Display for TemporalRoundingMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ceil => "ceil",
Self::Floor => "floor",
Self::Expand => "expand",
Self::Trunc => "trunc",
Self::HalfCeil => "halfCeil",
Self::HalfFloor => "halfFloor",
Self::HalfExpand => "halfExpand",
Self::HalfTrunc => "halfTrunc",
Self::HalfEven => "halfEven",
}
.fmt(f)
}
}

34
boa_temporal/src/time.rs

@ -0,0 +1,34 @@
//! Temporal Time Representation.
use crate::iso::IsoTime;
/// The Temporal `PlainTime` object.
#[derive(Debug, Default, Clone, Copy)]
#[allow(dead_code)]
pub struct Time {
iso: IsoTime,
}
// ==== Private API ====
impl Time {
#[allow(dead_code)]
pub(crate) fn new_unchecked(
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
) -> Self {
Self {
iso: IsoTime::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond),
}
}
/// Returns true if a valid `Time`.
#[allow(dead_code)]
pub(crate) fn is_valid(&self) -> bool {
self.iso.is_valid()
}
}

284
boa_temporal/src/utils.rs

@ -0,0 +1,284 @@
//! Utility equations for Temporal
use crate::{
options::{TemporalRoundingMode, TemporalUnsignedRoundingMode},
MS_PER_DAY,
};
use std::ops::Mul;
// NOTE: Review the below for optimizations and add ALOT of tests.
fn apply_unsigned_rounding_mode(
x: f64,
r1: f64,
r2: f64,
unsigned_rounding_mode: TemporalUnsignedRoundingMode,
) -> 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 == TemporalUnsignedRoundingMode::Zero {
return r1;
};
// 5. If unsignedRoundingMode is infinity, return r2.
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::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 == TemporalUnsignedRoundingMode::HalfZero {
return r1;
};
// 12. If unsignedRoundingMode is half-infinity, return r2.
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfInfinity {
return r2;
};
// 13. Assert: unsignedRoundingMode is half-even.
assert!(unsigned_rounding_mode == TemporalUnsignedRoundingMode::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: TemporalRoundingMode,
) -> 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
}
// ==== Begin Date Equations ====
pub(crate) const MS_PER_HOUR: f64 = 3_600_000f64;
pub(crate) const MS_PER_MINUTE: f64 = 60_000f64;
/// `EpochDaysToEpochMS`
///
/// Functionally the same as Date's abstract operation `MakeDate`
pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: f64) -> f64 {
f64::from(day).mul_add(f64::from(MS_PER_DAY), time).floor()
}
/// `EpochTimeToDayNumber`
///
/// This equation is the equivalent to `ECMAScript`'s `Date(t)`
pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 {
(t / f64::from(MS_PER_DAY)).floor() as i32
}
/// Mathematically determine the days in a year.
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
}
}
/// Returns the epoch day number for a given year.
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(MS_PER_DAY) * epoch_day_number_for_year(f64::from(y))
}
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) + 1970;
loop {
if epoch_time_for_year(year) <= t {
break;
}
year -= 1;
}
year
}
/// 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) -> u8 {
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 u8,
}
}
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!(),
};
f64::from(MS_PER_DAY).mul(f64::from(days))
}
pub(crate) fn epoch_time_to_date(t: f64) -> u8 {
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;
}
// This return of date should be < 31.
date as u8
}
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)
}
// EpochTimeTOWeekDay -> REMOVED
// ==== End Date Equations ====
// ==== Begin Calendar Equations ====
// NOTE: below was the iso methods in temporal::calendar -> Need to be reassessed.
/// 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 + mathematical_in_leap_year(epoch_time_for_year(year)),
_ => unreachable!("an invalid month value is an implementation error."),
}
}
// The below calendar abstract equations/utilities were removed for being unused.
// 12.2.32 `ToISOWeekOfYear ( year, month, day )`
// 12.2.33 `ISOMonthCode ( month )`
// 12.2.39 `ToISODayOfYear ( year, month, day )`
// 12.2.40 `ToISODayOfWeek ( year, month, day )`
// ==== End Calendar Equations ====
// ==== Tests =====
// TODO(nekevss): Add way more to the below.
#[cfg(test)]
mod tests {
use super::*;
#[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);
}
}

53
boa_temporal/src/year_month.rs

@ -0,0 +1,53 @@
//! `YearMonth`
use crate::{
calendar::CalendarSlot,
iso::{IsoDate, IsoDateSlots},
options::ArithmeticOverflow,
TemporalResult,
};
/// The `YearMonth` struct
#[derive(Debug, Default, Clone)]
pub struct YearMonth {
iso: IsoDate,
calendar: CalendarSlot,
}
impl YearMonth {
/// Creates an unvalidated `YearMonth`.
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self {
Self { iso, calendar }
}
/// Creates a new valid `YearMonth`.
#[inline]
pub fn new(
year: i32,
month: i32,
reference_day: Option<i32>,
calendar: CalendarSlot,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
let day = reference_day.unwrap_or(1);
let iso = IsoDate::new(year, month, day, overflow)?;
Ok(Self::new_unchecked(iso, calendar))
}
#[inline]
#[must_use]
/// Returns a reference to `YearMonth`'s `CalendarSlot`
pub fn calendar(&self) -> &CalendarSlot {
&self.calendar
}
}
impl IsoDateSlots for YearMonth {
#[inline]
/// Returns this `YearMonth`'s `IsoDate`
fn iso_date(&self) -> IsoDate {
self.iso
}
}

7
boa_temporal/src/zoneddatetime.rs

@ -0,0 +1,7 @@
//! The `ZonedDateTime` module.
// NOTE: Mostly serves as a placeholder currently
// until the rest can be implemented.
/// `TemporalZoneDateTime`
#[derive(Debug, Clone, Copy)]
pub struct ZonedDateTime;
Loading…
Cancel
Save