mirror of https://github.com/boa-dev/boa.git
Browse Source
* Started with the Temporal implementation * Implemented some useful functions * Updaating some spec references * Initial work on TimeZone and Instant * More work completed on Temporal.Duration and Temporal.Instant * General scaffolding and heavy work on Instant and Duration complete * ZonedDateTime and Calendar started with further work on duration abstract ops * Further work on temporal work and clippy fixes * Post rebase fixes/reverts * Add BuiltinCalendar and begin IsoCalendar impl * More work completed on calendar/date/yearmonth/monthday * Calendar and iso impl close to completion - no datelike parsing * Initial work on temporal ISO8601 parsing and grammar * Post rebase fixes and updates * More on parser/Duration and work through clippy lints * Fix bug on peek_n and add temporal cfg * Fix clippy lints on parser tests * Build out calendar with icu_calendar, add some tests, and misc. * Fix spec hyperlinks * Parser clean up and invalid annotations * Add Duration and Temporal Parsing * Remove IsoYearMonthRecord * Post rebase update * Fix and add to ISO Parser docs * Parser/ast cleanup and duration refactor/additions * Review feedback, options update, and duration changes * Review changes, general cleanup, and post rebase fixes * Fix time zone parsing issue/test logic * Clean up parse output nodes * Apply review feedback and various fixes * Review feedback and get_option changes * Review feedback --------- Co-authored-by: Iban Eguia Moraza <razican@protonmail.ch> Co-authored-by: José Julián Espina <jedel0124@gmail.com>pull/3376/head
Kevin
1 year ago
committed by
GitHub
49 changed files with 13531 additions and 7 deletions
@ -0,0 +1,113 @@
|
||||
//! AST nodes for Temporal's implementation of ISO8601 grammar.
|
||||
|
||||
/// An ISO Date Node consisting of non-validated date fields and calendar value.
|
||||
#[derive(Default, Debug)] |
||||
pub struct IsoDate { |
||||
/// Date Year
|
||||
pub year: i32, |
||||
/// Date Month
|
||||
pub month: i32, |
||||
/// Date Day
|
||||
pub day: i32, |
||||
/// The calendar value.
|
||||
pub calendar: Option<String>, |
||||
} |
||||
|
||||
/// The `IsoTime` node consists of non-validated time fields.
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
pub struct IsoTime { |
||||
/// An hour value between 0-23
|
||||
pub hour: u8, |
||||
/// A minute value between 0-59
|
||||
pub minute: u8, |
||||
/// A second value between 0-60
|
||||
pub second: u8, |
||||
/// A millisecond value between 0-999
|
||||
pub millisecond: u16, |
||||
/// A microsecond value between 0-999
|
||||
pub microsecond: u16, |
||||
/// A nanosecond value between 0-999
|
||||
pub nanosecond: u16, |
||||
} |
||||
|
||||
impl IsoTime { |
||||
#[must_use] |
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] |
||||
/// A utility initialization function to create `ISOTime` from the `TimeSpec` components.
|
||||
pub fn from_components(hour: u8, minute: u8, second: u8, fraction: f64) -> Self { |
||||
// Note: Precision on nanoseconds drifts, so opting for round over floor or ceil for now.
|
||||
// e.g. 0.329402834 becomes 329.402833.999
|
||||
let millisecond = fraction * 1000.0; |
||||
let micros = millisecond.rem_euclid(1.0) * 1000.0; |
||||
let nanos = micros.rem_euclid(1.0) * 1000.0; |
||||
|
||||
Self { |
||||
hour, |
||||
minute, |
||||
second, |
||||
millisecond: millisecond.floor() as u16, |
||||
microsecond: micros.floor() as u16, |
||||
nanosecond: nanos.round() as u16, |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// The `IsoDateTime` node output by the ISO parser
|
||||
#[derive(Default, Debug)] |
||||
pub struct IsoDateTime { |
||||
/// The `ISODate` record
|
||||
pub date: IsoDate, |
||||
/// The `ISOTime` record
|
||||
pub time: IsoTime, |
||||
/// The `TimeZone` value for this `ISODateTime`
|
||||
pub tz: Option<TimeZone>, |
||||
} |
||||
|
||||
/// `TimeZone` data
|
||||
#[derive(Default, Debug, Clone)] |
||||
pub struct TimeZone { |
||||
/// TimeZoneIANAName
|
||||
pub name: Option<String>, |
||||
/// TimeZoneOffset
|
||||
pub offset: Option<UTCOffset>, |
||||
} |
||||
|
||||
/// A full precision `UtcOffset`
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct UTCOffset { |
||||
/// The `+`/`-` sign of this `UtcOffset`
|
||||
pub sign: i8, |
||||
/// The hour value of the `UtcOffset`
|
||||
pub hour: u8, |
||||
/// The minute value of the `UtcOffset`.
|
||||
pub minute: u8, |
||||
/// The second value of the `UtcOffset`.
|
||||
pub second: u8, |
||||
/// Any sub second components of the `UTCOffset`
|
||||
pub fraction: f64, |
||||
} |
||||
|
||||
/// An `IsoDuration` Node output by the ISO parser.
|
||||
#[derive(Debug, Default, Clone, Copy)] |
||||
pub struct IsoDuration { |
||||
/// Years value.
|
||||
pub years: i32, |
||||
/// Months value.
|
||||
pub months: i32, |
||||
/// Weeks value.
|
||||
pub weeks: i32, |
||||
/// Days value.
|
||||
pub days: i32, |
||||
/// Hours value.
|
||||
pub hours: i32, |
||||
/// Minutes value.
|
||||
pub minutes: f64, |
||||
/// Seconds value.
|
||||
pub seconds: f64, |
||||
/// Milliseconds value.
|
||||
pub milliseconds: f64, |
||||
/// Microseconds value.
|
||||
pub microseconds: f64, |
||||
/// Nanoseconds value.
|
||||
pub nanoseconds: f64, |
||||
} |
@ -0,0 +1,367 @@
|
||||
//! Implementation of the "iso8601" `BuiltinCalendar`.
|
||||
|
||||
use crate::{ |
||||
builtins::temporal::{ |
||||
self, create_temporal_date, |
||||
date_equations::mathematical_days_in_year, |
||||
options::{ArithmeticOverflow, TemporalUnit}, |
||||
plain_date::iso::IsoDateRecord, |
||||
}, |
||||
js_string, |
||||
property::PropertyKey, |
||||
string::utf16, |
||||
Context, JsNativeError, JsResult, JsString, JsValue, |
||||
}; |
||||
|
||||
use super::BuiltinCalendar; |
||||
|
||||
use icu_calendar::{ |
||||
iso::Iso, |
||||
week::{RelativeUnit, WeekCalculator}, |
||||
Calendar, Date, |
||||
}; |
||||
|
||||
pub(crate) struct IsoCalendar; |
||||
|
||||
impl BuiltinCalendar for IsoCalendar { |
||||
/// Temporal 15.8.2.1 `Temporal.prototype.dateFromFields( fields [, options])` - Supercedes 12.5.4
|
||||
///
|
||||
/// This is a basic implementation for an iso8601 calendar's `dateFromFields` method.
|
||||
fn date_from_fields( |
||||
&self, |
||||
fields: &mut temporal::TemporalFields, |
||||
overflow: ArithmeticOverflow, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// NOTE: we are in ISO by default here.
|
||||
// a. Perform ? ISOResolveMonth(fields).
|
||||
// b. Let result be ? ISODateFromFields(fields, overflow).
|
||||
fields.iso_resolve_month()?; |
||||
|
||||
// Extra: handle reulating/overflow until implemented on `icu_calendar`
|
||||
fields.regulate(overflow)?; |
||||
|
||||
let date = Date::try_new_iso_date( |
||||
fields.year().unwrap_or(0), |
||||
fields.month().unwrap_or(250) as u8, |
||||
fields.day().unwrap_or(250) as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
// 9. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
|
||||
Ok(create_temporal_date( |
||||
IsoDateRecord::from_date_iso(date), |
||||
js_string!("iso8601").into(), |
||||
None, |
||||
context, |
||||
)? |
||||
.into()) |
||||
} |
||||
|
||||
/// 12.5.5 `Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] )`
|
||||
///
|
||||
/// This is a basic implementation for an iso8601 calendar's `yearMonthFromFields` method.
|
||||
fn year_month_from_fields( |
||||
&self, |
||||
fields: &mut temporal::TemporalFields, |
||||
overflow: ArithmeticOverflow, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 9. If calendar.[[Identifier]] is "iso8601", then
|
||||
// a. Perform ? ISOResolveMonth(fields).
|
||||
fields.iso_resolve_month()?; |
||||
|
||||
// b. Let result be ? ISOYearMonthFromFields(fields, overflow).
|
||||
fields.regulate_year_month(overflow); |
||||
|
||||
let result = Date::try_new_iso_date( |
||||
fields.year().unwrap_or(0), |
||||
fields.month().unwrap_or(250) as u8, |
||||
fields.day().unwrap_or(20) as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
// 10. Return ? CreateTemporalYearMonth(result.[[Year]], result.[[Month]], "iso8601", result.[[ReferenceISODay]]).
|
||||
temporal::create_temporal_year_month( |
||||
IsoDateRecord::from_date_iso(result), |
||||
js_string!("iso8601").into(), |
||||
None, |
||||
context, |
||||
) |
||||
} |
||||
|
||||
/// 12.5.6 `Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] )`
|
||||
///
|
||||
/// This is a basic implementation for an iso8601 calendar's `monthDayFromFields` method.
|
||||
fn month_day_from_fields( |
||||
&self, |
||||
fields: &mut temporal::TemporalFields, |
||||
overflow: ArithmeticOverflow, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 8. Perform ? ISOResolveMonth(fields).
|
||||
fields.iso_resolve_month()?; |
||||
|
||||
fields.regulate(overflow)?; |
||||
|
||||
// TODO: double check error mapping is correct for specifcation/test262.
|
||||
// 9. Let result be ? ISOMonthDayFromFields(fields, overflow).
|
||||
let result = Date::try_new_iso_date( |
||||
fields.year().unwrap_or(1972), |
||||
fields.month().unwrap_or(250) as u8, |
||||
fields.day().unwrap_or(250) as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
// 10. Return ? CreateTemporalMonthDay(result.[[Month]], result.[[Day]], "iso8601", result.[[ReferenceISOYear]]).
|
||||
temporal::create_temporal_month_day( |
||||
IsoDateRecord::from_date_iso(result), |
||||
js_string!("iso8601").into(), |
||||
None, |
||||
context, |
||||
) |
||||
} |
||||
|
||||
/// 12.5.7 `Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] )`
|
||||
///
|
||||
/// Below implements the basic implementation for an iso8601 calendar's `dateAdd` method.
|
||||
fn date_add( |
||||
&self, |
||||
_date: &temporal::PlainDate, |
||||
_duration: &temporal::duration::DurationRecord, |
||||
_overflow: ArithmeticOverflow, |
||||
_context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// TODO: Not stable on `ICU4X`. Implement once completed.
|
||||
Err(JsNativeError::range() |
||||
.with_message("feature not implemented.") |
||||
.into()) |
||||
|
||||
// 9. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], balanceResult.[[Days]], overflow).
|
||||
// 10. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
|
||||
} |
||||
|
||||
/// 12.5.8 `Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] )`
|
||||
///
|
||||
/// Below implements the basic implementation for an iso8601 calendar's `dateUntil` method.
|
||||
fn date_until( |
||||
&self, |
||||
_one: &temporal::PlainDate, |
||||
_two: &temporal::PlainDate, |
||||
_largest_unit: TemporalUnit, |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// TODO: Not stable on `ICU4X`. Implement once completed.
|
||||
Err(JsNativeError::range() |
||||
.with_message("Feature not yet implemented.") |
||||
.into()) |
||||
|
||||
// 9. Let result be DifferenceISODate(one.[[ISOYear]], one.[[ISOMonth]], one.[[ISODay]], two.[[ISOYear]], two.[[ISOMonth]], two.[[ISODay]], largestUnit).
|
||||
// 10. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], 0, 0, 0, 0, 0, 0).
|
||||
} |
||||
|
||||
/// `Temporal.Calendar.prototype.era( dateLike )` for iso8601 calendar.
|
||||
fn era(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
// Returns undefined on iso8601.
|
||||
Ok(JsValue::undefined()) |
||||
} |
||||
|
||||
/// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar.
|
||||
fn era_year(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
// Returns undefined on iso8601.
|
||||
Ok(JsValue::undefined()) |
||||
} |
||||
|
||||
/// Returns the `year` for the `Iso` calendar.
|
||||
fn year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(date.year().number.into()) |
||||
} |
||||
|
||||
/// Returns the `month` for the `Iso` calendar.
|
||||
fn month(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(date.month().ordinal.into()) |
||||
} |
||||
|
||||
/// Returns the `monthCode` for the `Iso` calendar.
|
||||
fn month_code(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(JsString::from(date.month().code.to_string()).into()) |
||||
} |
||||
|
||||
/// Returns the `day` for the `Iso` calendar.
|
||||
fn day(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(date.day_of_month().0.into()) |
||||
} |
||||
|
||||
/// Returns the `dayOfWeek` for the `Iso` calendar.
|
||||
fn day_of_week(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok((date.day_of_week() as u8).into()) |
||||
} |
||||
|
||||
/// Returns the `dayOfYear` for the `Iso` calendar.
|
||||
fn day_of_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(i32::from(date.day_of_year_info().day_of_year).into()) |
||||
} |
||||
|
||||
/// Returns the `weekOfYear` for the `Iso` calendar.
|
||||
fn week_of_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
// TODO: Determine `ICU4X` equivalent.
|
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
let week_calculator = WeekCalculator::default(); |
||||
|
||||
let week_of = date |
||||
.week_of_year(&week_calculator) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(week_of.week.into()) |
||||
} |
||||
|
||||
/// Returns the `yearOfWeek` for the `Iso` calendar.
|
||||
fn year_of_week(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
// TODO: Determine `ICU4X` equivalent.
|
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
let week_calculator = WeekCalculator::default(); |
||||
|
||||
let week_of = date |
||||
.week_of_year(&week_calculator) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
match week_of.unit { |
||||
RelativeUnit::Previous => Ok((date.year().number - 1).into()), |
||||
RelativeUnit::Current => Ok(date.year().number.into()), |
||||
RelativeUnit::Next => Ok((date.year().number + 1).into()), |
||||
} |
||||
} |
||||
|
||||
/// Returns the `daysInWeek` value for the `Iso` calendar.
|
||||
fn days_in_week(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Ok(7.into()) |
||||
} |
||||
|
||||
/// Returns the `daysInMonth` value for the `Iso` calendar.
|
||||
fn days_in_month(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(date.days_in_month().into()) |
||||
} |
||||
|
||||
/// Returns the `daysInYear` value for the `Iso` calendar.
|
||||
fn days_in_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let date = Date::try_new_iso_date( |
||||
date_like.year(), |
||||
date_like.month() as u8, |
||||
date_like.day() as u8, |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
Ok(date.days_in_year().into()) |
||||
} |
||||
|
||||
/// Return the amount of months in an ISO Calendar.
|
||||
fn months_in_year(&self, _: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Ok(12.into()) |
||||
} |
||||
|
||||
/// Returns whether provided date is in a leap year according to this calendar.
|
||||
fn in_leap_year(&self, date_like: &IsoDateRecord, _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
// `ICU4X`'s `CalendarArithmetic` is currently private.
|
||||
if mathematical_days_in_year(date_like.year()) == 366 { |
||||
return Ok(true.into()); |
||||
} |
||||
Ok(false.into()) |
||||
} |
||||
|
||||
// Resolve the fields for the iso calendar.
|
||||
fn resolve_fields(&self, fields: &mut temporal::TemporalFields, _: &str) -> JsResult<()> { |
||||
fields.iso_resolve_month()?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Returns the ISO field descriptors, which is not called for the iso8601 calendar.
|
||||
fn field_descriptors(&self, _: &[String]) -> Vec<(String, bool)> { |
||||
// NOTE(potential improvement): look into implementing field descriptors and call
|
||||
// ISO like any other calendar?
|
||||
// Field descriptors is unused on ISO8601.
|
||||
unreachable!() |
||||
} |
||||
|
||||
/// Returns the `CalendarFieldKeysToIgnore` implementation for ISO.
|
||||
fn field_keys_to_ignore(&self, additional_keys: Vec<PropertyKey>) -> Vec<PropertyKey> { |
||||
let mut result = Vec::new(); |
||||
for key in additional_keys { |
||||
let key_string = key.to_string(); |
||||
result.push(key); |
||||
if key_string.as_str() == "month" { |
||||
result.push(utf16!("monthCode").into()); |
||||
} else if key_string.as_str() == "monthCode" { |
||||
result.push(utf16!("month").into()); |
||||
} |
||||
} |
||||
result |
||||
} |
||||
|
||||
// NOTE: This is currently not a name that is compliant with
|
||||
// the Temporal proposal. For debugging purposes only.
|
||||
/// Returns the debug name.
|
||||
fn debug_name(&self) -> &str { |
||||
Iso.debug_name() |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
use crate::{js_string, run_test_actions, TestAction}; |
||||
|
||||
#[test] |
||||
fn calendar_constructor() { |
||||
// TODO: Add other BuiltinCalendars
|
||||
run_test_actions([TestAction::assert_eq( |
||||
"new Temporal.Calendar('iso8601').id", |
||||
js_string!("iso8601"), |
||||
)]); |
||||
} |
||||
|
||||
#[test] |
||||
fn calendar_methods() { |
||||
run_test_actions([ |
||||
TestAction::run("let iso = new Temporal.Calendar('iso8601');"), |
||||
TestAction::assert_eq("iso.inLeapYear('2020-11-20')", true), |
||||
TestAction::assert_eq("iso.daysInYear('2020-11-20')", 366), |
||||
TestAction::assert_eq("iso.daysInYear('2021-11-20')", 365), |
||||
TestAction::assert_eq("iso.monthsInYear('2021-11-20')", 12), |
||||
TestAction::assert_eq("iso.daysInWeek('2021-11-20')", 7), |
||||
]); |
||||
} |
@ -0,0 +1,107 @@
|
||||
//! Calendar utility calculations
|
||||
|
||||
// TODO: determine if any of the below are needed.
|
||||
|
||||
use crate::builtins::temporal::{self, date_equations, plain_date::iso::IsoDateRecord}; |
||||
use crate::JsString; |
||||
|
||||
/// 12.2.31 `ISODaysInMonth ( year, month )`
|
||||
pub(crate) fn iso_days_in_month(year: i32, month: i32) -> i32 { |
||||
match month { |
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, |
||||
4 | 6 | 9 | 11 => 30, |
||||
2 => { |
||||
28 + temporal::date_equations::mathematical_in_leap_year( |
||||
temporal::date_equations::epoch_time_for_year(year), |
||||
) |
||||
} |
||||
_ => unreachable!("an invalid month value is an implementation error."), |
||||
} |
||||
} |
||||
|
||||
/// 12.2.32 `ToISOWeekOfYear ( year, month, day )`
|
||||
///
|
||||
/// Takes an `[[IsoYear]]`, `[[IsoMonth]]`, and `[[IsoDay]]` and returns a (week, year) record.
|
||||
#[allow(unused)] |
||||
pub(crate) fn to_iso_week_of_year(year: i32, month: i32, day: i32) -> (i32, i32) { |
||||
// Function constants
|
||||
// 2. Let wednesday be 3.
|
||||
// 3. Let thursday be 4.
|
||||
// 4. Let friday be 5.
|
||||
// 5. Let saturday be 6.
|
||||
// 6. Let daysInWeek be 7.
|
||||
// 7. Let maxWeekNumber be 53.
|
||||
let day_of_year = to_iso_day_of_year(year, month, day); |
||||
let day_of_week = to_iso_day_of_week(year, month, day); |
||||
let week = (day_of_week + 7 - day_of_week + 3) / 7; |
||||
|
||||
if week < 1 { |
||||
let first_day_of_year = to_iso_day_of_week(year, 1, 1); |
||||
if first_day_of_year == 5 { |
||||
return (53, year - 1); |
||||
} else if first_day_of_year == 6 |
||||
&& date_equations::mathematical_in_leap_year(date_equations::epoch_time_for_year( |
||||
year - 1, |
||||
)) == 1 |
||||
{ |
||||
return (52, year - 1); |
||||
} |
||||
return (52, year - 1); |
||||
} else if week == 53 { |
||||
let days_in_year = date_equations::mathematical_days_in_year(year); |
||||
let days_later_in_year = days_in_year - day_of_year; |
||||
let days_after_thursday = 4 - day_of_week; |
||||
if days_later_in_year < days_after_thursday { |
||||
return (1, year - 1); |
||||
} |
||||
} |
||||
(week, year) |
||||
} |
||||
|
||||
/// 12.2.33 `ISOMonthCode ( month )`
|
||||
#[allow(unused)] |
||||
fn iso_month_code(month: i32) -> JsString { |
||||
// TODO: optimize
|
||||
if month < 10 { |
||||
JsString::from(format!("M0{month}")) |
||||
} else { |
||||
JsString::from(format!("M{month}")) |
||||
} |
||||
} |
||||
|
||||
// 12.2.34 `ISOResolveMonth ( fields )`
|
||||
// Note: currently implemented on TemporalFields -> implement in this mod?
|
||||
|
||||
// 12.2.35 ISODateFromFields ( fields, overflow )
|
||||
// Note: implemented on IsoDateRecord.
|
||||
|
||||
// 12.2.36 ISOYearMonthFromFields ( fields, overflow )
|
||||
// TODO: implement on a IsoYearMonthRecord
|
||||
|
||||
// 12.2.37 ISOMonthDayFromFields ( fields, overflow )
|
||||
// TODO: implement as method on IsoDateRecord.
|
||||
|
||||
// 12.2.38 IsoFieldKeysToIgnore
|
||||
// TODO: determine usefulness.
|
||||
|
||||
/// 12.2.39 `ToISODayOfYear ( year, month, day )`
|
||||
#[allow(unused)] |
||||
fn to_iso_day_of_year(year: i32, month: i32, day: i32) -> i32 { |
||||
// TODO: update fn parameter to take IsoDateRecord.
|
||||
let iso = IsoDateRecord::new(year, month - 1, day); |
||||
let epoch_days = iso.as_epoch_days(); |
||||
date_equations::epoch_time_to_day_in_year(temporal::epoch_days_to_epoch_ms(epoch_days, 0)) + 1 |
||||
} |
||||
|
||||
/// 12.2.40 `ToISODayOfWeek ( year, month, day )`
|
||||
#[allow(unused)] |
||||
pub(crate) fn to_iso_day_of_week(year: i32, month: i32, day: i32) -> i32 { |
||||
let iso = IsoDateRecord::new(year, month - 1, day); |
||||
let epoch_days = iso.as_epoch_days(); |
||||
let day_of_week = |
||||
date_equations::epoch_time_to_week_day(temporal::epoch_days_to_epoch_ms(epoch_days, 0)); |
||||
if day_of_week == 0 { |
||||
return 7; |
||||
} |
||||
day_of_week |
||||
} |
@ -0,0 +1,121 @@
|
||||
//! This file represents all equations listed under section 13.4 of the [Temporal Specification][spec]
|
||||
//!
|
||||
//! [spec]: https://tc39.es/proposal-temporal/#sec-date-equations
|
||||
|
||||
use std::ops::Mul; |
||||
|
||||
pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 { |
||||
(t / f64::from(super::MS_PER_DAY)).floor() as i32 |
||||
} |
||||
|
||||
pub(crate) fn mathematical_days_in_year(y: i32) -> i32 { |
||||
if y % 4 != 0 { |
||||
365 |
||||
} else if y % 4 == 0 && y % 100 != 0 { |
||||
366 |
||||
} else if y % 100 == 0 && y % 400 != 0 { |
||||
365 |
||||
} else { |
||||
// Assert that y is divisble by 400 to ensure we are returning the correct result.
|
||||
assert_eq!(y % 400, 0); |
||||
366 |
||||
} |
||||
} |
||||
|
||||
pub(crate) fn epoch_day_number_for_year(y: f64) -> f64 { |
||||
365.0f64.mul_add(y - 1970.0, ((y - 1969.0) / 4.0).floor()) - ((y - 1901.0) / 100.0).floor() |
||||
+ ((y - 1601.0) / 400.0).floor() |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_for_year(y: i32) -> f64 { |
||||
f64::from(super::MS_PER_DAY) * epoch_day_number_for_year(f64::from(y)) |
||||
} |
||||
|
||||
// NOTE: The below returns the epoch years (years since 1970). The spec
|
||||
// appears to assume the below returns with the epoch applied.
|
||||
pub(crate) fn epoch_time_to_epoch_year(t: f64) -> i32 { |
||||
// roughly calculate the largest possible year given the time t,
|
||||
// then check and refine the year.
|
||||
let day_count = epoch_time_to_day_number(t); |
||||
let mut year = day_count / 365; |
||||
loop { |
||||
if epoch_time_for_year(year) <= t { |
||||
break; |
||||
} |
||||
year -= 1; |
||||
} |
||||
|
||||
year + 1970 |
||||
} |
||||
|
||||
/// Returns either 1 (true) or 0 (false)
|
||||
pub(crate) fn mathematical_in_leap_year(t: f64) -> i32 { |
||||
mathematical_days_in_year(epoch_time_to_epoch_year(t)) - 365 |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_month_in_year(t: f64) -> i32 { |
||||
const DAYS: [i32; 11] = [30, 58, 89, 120, 150, 181, 212, 242, 272, 303, 333]; |
||||
const LEAP_DAYS: [i32; 11] = [30, 59, 90, 121, 151, 182, 213, 242, 272, 303, 334]; |
||||
|
||||
let in_leap_year = mathematical_in_leap_year(t) == 1; |
||||
let day = epoch_time_to_day_in_year(t); |
||||
|
||||
let result = if in_leap_year { |
||||
LEAP_DAYS.binary_search(&day) |
||||
} else { |
||||
DAYS.binary_search(&day) |
||||
}; |
||||
|
||||
match result { |
||||
Ok(i) | Err(i) => i as i32, |
||||
} |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_for_month_given_year(m: i32, y: i32) -> f64 { |
||||
let leap_day = mathematical_days_in_year(y) - 365; |
||||
|
||||
let days = match m { |
||||
0 => 1, |
||||
1 => 31, |
||||
2 => 59 + leap_day, |
||||
3 => 90 + leap_day, |
||||
4 => 121 + leap_day, |
||||
5 => 151 + leap_day, |
||||
6 => 182 + leap_day, |
||||
7 => 213 + leap_day, |
||||
8 => 243 + leap_day, |
||||
9 => 273 + leap_day, |
||||
10 => 304 + leap_day, |
||||
11 => 334 + leap_day, |
||||
_ => unreachable!(), |
||||
}; |
||||
|
||||
(super::NS_PER_DAY as f64).mul(f64::from(days)) |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_date(t: f64) -> i32 { |
||||
const OFFSETS: [i16; 12] = [ |
||||
1, -30, -58, -89, -119, -150, -180, -211, -242, -272, -303, -333, |
||||
]; |
||||
let day_in_year = epoch_time_to_day_in_year(t); |
||||
let in_leap_year = mathematical_in_leap_year(t); |
||||
let month = epoch_time_to_month_in_year(t); |
||||
|
||||
// Cast from i32 to usize should be safe as the return must be 0-11
|
||||
let mut date = day_in_year + i32::from(OFFSETS[month as usize]); |
||||
|
||||
if month >= 2 { |
||||
date -= in_leap_year; |
||||
} |
||||
|
||||
date |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_day_in_year(t: f64) -> i32 { |
||||
epoch_time_to_day_number(t) |
||||
- (epoch_day_number_for_year(f64::from(epoch_time_to_epoch_year(t))) as i32) |
||||
} |
||||
|
||||
pub(crate) fn epoch_time_to_week_day(t: f64) -> i32 { |
||||
(epoch_time_to_day_number(t) + 4) % 7 |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
||||
use crate::{run_test_actions, TestAction}; |
||||
|
||||
#[test] |
||||
fn duration_constructor() { |
||||
run_test_actions([ |
||||
TestAction::run("let dur = new Temporal.Duration(1, 1, 0, 1)"), |
||||
TestAction::assert_eq("dur.years", 1), |
||||
TestAction::assert_eq("dur.months", 1), |
||||
TestAction::assert_eq("dur.weeks", 0), |
||||
TestAction::assert_eq("dur.days", 1), |
||||
TestAction::assert_eq("dur.milliseconds", 0), |
||||
]); |
||||
} |
||||
|
||||
#[test] |
||||
fn duration_abs() { |
||||
run_test_actions([ |
||||
TestAction::run("let dur = new Temporal.Duration(-1, -1, 0, -1)"), |
||||
TestAction::assert_eq("dur.sign", -1), |
||||
TestAction::run("let abs = dur.abs()"), |
||||
TestAction::assert_eq("abs.years", 1), |
||||
TestAction::assert_eq("abs.months", 1), |
||||
TestAction::assert_eq("abs.weeks", 0), |
||||
TestAction::assert_eq("abs.days", 1), |
||||
TestAction::assert_eq("abs.milliseconds", 0), |
||||
]); |
||||
} |
@ -0,0 +1,587 @@
|
||||
//! A Rust native implementation of the `fields` object used in `Temporal`.
|
||||
|
||||
use crate::{ |
||||
js_string, property::PropertyKey, value::PreferredType, Context, JsNativeError, JsObject, |
||||
JsResult, JsString, JsValue, |
||||
}; |
||||
|
||||
use super::options::ArithmeticOverflow; |
||||
|
||||
use bitflags::bitflags; |
||||
use rustc_hash::FxHashSet; |
||||
|
||||
bitflags! { |
||||
#[derive(Debug, PartialEq, Eq)] |
||||
pub struct FieldMap: u16 { |
||||
const YEAR = 0b0000_0000_0000_0001; |
||||
const MONTH = 0b0000_0000_0000_0010; |
||||
const MONTH_CODE = 0b0000_0000_0000_0100; |
||||
const DAY = 0b0000_0000_0000_1000; |
||||
const HOUR = 0b0000_0000_0001_0000; |
||||
const MINUTE = 0b0000_0000_0010_0000; |
||||
const SECOND = 0b0000_0000_0100_0000; |
||||
const MILLISECOND = 0b0000_0000_1000_0000; |
||||
const MICROSECOND = 0b0000_0001_0000_0000; |
||||
const NANOSECOND = 0b0000_0010_0000_0000; |
||||
const OFFSET = 0b0000_0100_0000_0000; |
||||
const ERA = 0b0000_1000_0000_0000; |
||||
const ERA_YEAR = 0b0001_0000_0000_0000; |
||||
const TIME_ZONE = 0b0010_0000_0000_0000; |
||||
} |
||||
} |
||||
|
||||
/// The temporal fields are laid out in the Temporal proposal under section 13.46 `PrepareTemporalFields`
|
||||
/// with conversion and defaults laid out by Table 17 (displayed below).
|
||||
///
|
||||
/// `TemporalFields` is meant to act as a native Rust implementation
|
||||
/// of the fields.
|
||||
///
|
||||
///
|
||||
/// ## Table 17: Temporal field requirements
|
||||
///
|
||||
/// | Property | Conversion | Default |
|
||||
/// | -------------|---------------------------------|------------|
|
||||
/// | "year" | `ToIntegerWithTruncation` | undefined |
|
||||
/// | "month" | `ToPositiveIntegerWithTruncation` | undefined |
|
||||
/// | "monthCode" | `ToPrimitiveAndRequireString` | undefined |
|
||||
/// | "day" | `ToPositiveIntegerWithTruncation` | undefined |
|
||||
/// | "hour" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "minute" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "second" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "millisecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "microsecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "nanosecond" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||
/// | "offset" | `ToPrimitiveAndRequireString` | undefined |
|
||||
/// | "era" | `ToPrimitiveAndRequireString` | undefined |
|
||||
/// | "eraYear" | `ToIntegerWithTruncation` | undefined |
|
||||
/// | "timeZone" | | undefined |
|
||||
///
|
||||
#[derive(Debug)] |
||||
pub(crate) struct TemporalFields { |
||||
bit_map: FieldMap, |
||||
year: Option<i32>, |
||||
month: Option<i32>, |
||||
month_code: Option<JsString>, // TODO: Switch to icu compatible value.
|
||||
day: Option<i32>, |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
millisecond: i32, |
||||
microsecond: i32, |
||||
nanosecond: i32, |
||||
offset: Option<JsString>, |
||||
era: Option<JsString>, // TODO: switch to icu compatible value.
|
||||
era_year: Option<i32>, // TODO: switch to icu compatible value.
|
||||
time_zone: Option<JsString>, // TODO: figure out the identifier for TimeZone.
|
||||
} |
||||
|
||||
impl Default for TemporalFields { |
||||
fn default() -> Self { |
||||
Self { |
||||
bit_map: FieldMap::empty(), |
||||
year: None, |
||||
month: None, |
||||
month_code: None, |
||||
day: None, |
||||
hour: 0, |
||||
minute: 0, |
||||
second: 0, |
||||
millisecond: 0, |
||||
microsecond: 0, |
||||
nanosecond: 0, |
||||
offset: None, |
||||
era: None, |
||||
era_year: None, |
||||
time_zone: None, |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl TemporalFields { |
||||
pub(crate) const fn year(&self) -> Option<i32> { |
||||
self.year |
||||
} |
||||
|
||||
pub(crate) const fn month(&self) -> Option<i32> { |
||||
self.month |
||||
} |
||||
|
||||
pub(crate) const fn day(&self) -> Option<i32> { |
||||
self.day |
||||
} |
||||
} |
||||
|
||||
impl TemporalFields { |
||||
#[inline] |
||||
fn set_field_value( |
||||
&mut self, |
||||
field: &str, |
||||
value: &JsValue, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<()> { |
||||
match field { |
||||
"year" => self.set_year(value, context)?, |
||||
"month" => self.set_month(value, context)?, |
||||
"monthCode" => self.set_month_code(value, context)?, |
||||
"day" => self.set_day(value, context)?, |
||||
"hour" => self.set_hour(value, context)?, |
||||
"minute" => self.set_minute(value, context)?, |
||||
"second" => self.set_second(value, context)?, |
||||
"millisecond" => self.set_milli(value, context)?, |
||||
"microsecond" => self.set_micro(value, context)?, |
||||
"nanosecond" => self.set_nano(value, context)?, |
||||
"offset" => self.set_offset(value, context)?, |
||||
"era" => self.set_era(value, context)?, |
||||
"eraYear" => self.set_era_year(value, context)?, |
||||
"timeZone" => self.set_time_zone(value), |
||||
_ => unreachable!(), |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_year(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let y = super::to_integer_with_truncation(value, context)?; |
||||
self.year = Some(y); |
||||
self.bit_map.set(FieldMap::YEAR, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_month(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let mo = super::to_positive_integer_with_trunc(value, context)?; |
||||
self.year = Some(mo); |
||||
self.bit_map.set(FieldMap::MONTH, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_month_code(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let mc = value.to_primitive(context, PreferredType::String)?; |
||||
if let Some(string) = mc.as_string() { |
||||
self.month_code = Some(string.clone()); |
||||
} else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("ToPrimativeAndRequireString must be of type String.") |
||||
.into()); |
||||
} |
||||
|
||||
self.bit_map.set(FieldMap::MONTH_CODE, true); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_day(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let d = super::to_positive_integer_with_trunc(value, context)?; |
||||
self.day = Some(d); |
||||
self.bit_map.set(FieldMap::DAY, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_hour(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let h = super::to_integer_with_truncation(value, context)?; |
||||
self.hour = h; |
||||
self.bit_map.set(FieldMap::HOUR, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_minute(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let m = super::to_integer_with_truncation(value, context)?; |
||||
self.minute = m; |
||||
self.bit_map.set(FieldMap::MINUTE, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_second(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let sec = super::to_integer_with_truncation(value, context)?; |
||||
self.second = sec; |
||||
self.bit_map.set(FieldMap::SECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_milli(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let milli = super::to_integer_with_truncation(value, context)?; |
||||
self.millisecond = milli; |
||||
self.bit_map.set(FieldMap::MILLISECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_micro(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let micro = super::to_integer_with_truncation(value, context)?; |
||||
self.microsecond = micro; |
||||
self.bit_map.set(FieldMap::MICROSECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_nano(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let nano = super::to_integer_with_truncation(value, context)?; |
||||
self.nanosecond = nano; |
||||
self.bit_map.set(FieldMap::NANOSECOND, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_offset(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let mc = value.to_primitive(context, PreferredType::String)?; |
||||
if let Some(string) = mc.as_string() { |
||||
self.offset = Some(string.clone()); |
||||
} else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("ToPrimativeAndRequireString must be of type String.") |
||||
.into()); |
||||
} |
||||
self.bit_map.set(FieldMap::OFFSET, true); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_era(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let mc = value.to_primitive(context, PreferredType::String)?; |
||||
if let Some(string) = mc.as_string() { |
||||
self.era = Some(string.clone()); |
||||
} else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("ToPrimativeAndRequireString must be of type String.") |
||||
.into()); |
||||
} |
||||
self.bit_map.set(FieldMap::ERA, true); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_era_year(&mut self, value: &JsValue, context: &mut Context<'_>) -> JsResult<()> { |
||||
let ey = super::to_integer_with_truncation(value, context)?; |
||||
self.era_year = Some(ey); |
||||
self.bit_map.set(FieldMap::ERA_YEAR, true); |
||||
Ok(()) |
||||
} |
||||
|
||||
#[inline] |
||||
fn set_time_zone(&mut self, value: &JsValue) { |
||||
let tz = value.as_string().cloned(); |
||||
self.time_zone = tz; |
||||
self.bit_map.set(FieldMap::TIME_ZONE, true); |
||||
} |
||||
} |
||||
|
||||
impl TemporalFields { |
||||
// TODO: Shift to JsString or utf16 over String.
|
||||
/// A method for creating a Native representation for `TemporalFields` from
|
||||
/// a `JsObject`.
|
||||
///
|
||||
/// This is the equivalant to Abstract Operation 13.46 `PrepareTemporalFields`
|
||||
pub(crate) fn from_js_object( |
||||
fields: &JsObject, |
||||
field_names: &mut Vec<String>, |
||||
required_fields: &mut Vec<String>, // None when Partial
|
||||
extended_fields: Option<Vec<(String, bool)>>, |
||||
partial: bool, |
||||
dup_behaviour: Option<JsString>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<Self> { |
||||
// 1. If duplicateBehaviour is not present, set duplicateBehaviour to throw.
|
||||
let dup_option = dup_behaviour.unwrap_or_else(|| js_string!("throw")); |
||||
|
||||
// 2. Let result be OrdinaryObjectCreate(null).
|
||||
let mut result = Self::default(); |
||||
|
||||
// 3. Let any be false.
|
||||
let mut any = false; |
||||
// 4. If extraFieldDescriptors is present, then
|
||||
if let Some(extra_fields) = extended_fields { |
||||
for (field_name, required) in extra_fields { |
||||
// a. For each Calendar Field Descriptor Record desc of extraFieldDescriptors, do
|
||||
// i. Assert: fieldNames does not contain desc.[[Property]].
|
||||
// ii. Append desc.[[Property]] to fieldNames.
|
||||
field_names.push(field_name.clone()); |
||||
|
||||
// iii. If desc.[[Required]] is true and requiredFields is a List, then
|
||||
if required && !partial { |
||||
// 1. Append desc.[[Property]] to requiredFields.
|
||||
required_fields.push(field_name); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 5. Let sortedFieldNames be SortStringListByCodeUnit(fieldNames).
|
||||
// 6. Let previousProperty be undefined.
|
||||
let mut dups_map = FxHashSet::default(); |
||||
|
||||
// 7. For each property name property of sortedFieldNames, do
|
||||
for field in &*field_names { |
||||
// a. If property is one of "constructor" or "__proto__", then
|
||||
if field.as_str() == "constructor" || field.as_str() == "__proto__" { |
||||
// i. Throw a RangeError exception.
|
||||
return Err(JsNativeError::range() |
||||
.with_message("constructor or proto is out of field range.") |
||||
.into()); |
||||
} |
||||
|
||||
let new_value = dups_map.insert(field); |
||||
|
||||
// b. If property is not equal to previousProperty, then
|
||||
if new_value { |
||||
// i. Let value be ? Get(fields, property).
|
||||
let value = |
||||
fields.get(PropertyKey::from(JsString::from(field.clone())), context)?; |
||||
// ii. If value is not undefined, then
|
||||
if !value.is_undefined() { |
||||
// 1. Set any to true.
|
||||
any = true; |
||||
|
||||
// 2. If property is in the Property column of Table 17 and there is a Conversion value in the same row, then
|
||||
// a. Let Conversion be the Conversion value of the same row.
|
||||
// b. If Conversion is ToIntegerWithTruncation, then
|
||||
// i. Set value to ? ToIntegerWithTruncation(value).
|
||||
// ii. Set value to 𝔽(value).
|
||||
// c. Else if Conversion is ToPositiveIntegerWithTruncation, then
|
||||
// i. Set value to ? ToPositiveIntegerWithTruncation(value).
|
||||
// ii. Set value to 𝔽(value).
|
||||
// d. Else,
|
||||
// i. Assert: Conversion is ToPrimitiveAndRequireString.
|
||||
// ii. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings.
|
||||
// iii. Set value to ? ToPrimitive(value, string).
|
||||
// iv. If value is not a String, throw a TypeError exception.
|
||||
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
|
||||
result.set_field_value(field, &value, context)?; |
||||
// iii. Else if requiredFields is a List, then
|
||||
} else if !partial { |
||||
// 1. If requiredFields contains property, then
|
||||
if required_fields.contains(field) { |
||||
// a. Throw a TypeError exception.
|
||||
return Err(JsNativeError::typ() |
||||
.with_message("A required TemporalField was not provided.") |
||||
.into()); |
||||
} |
||||
|
||||
// NOTE: Values set to a default on init.
|
||||
// 2. If property is in the Property column of Table 17, then
|
||||
// a. Set value to the corresponding Default value of the same row.
|
||||
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
|
||||
} |
||||
// c. Else if duplicateBehaviour is throw, then
|
||||
} else if dup_option.to_std_string_escaped() == "throw" { |
||||
// i. Throw a RangeError exception.
|
||||
return Err(JsNativeError::range() |
||||
.with_message("Cannot have a duplicate field") |
||||
.into()); |
||||
} |
||||
// d. Set previousProperty to property.
|
||||
} |
||||
|
||||
// 8. If requiredFields is partial and any is false, then
|
||||
if partial && !any { |
||||
// a. Throw a TypeError exception.
|
||||
return Err(JsNativeError::range() |
||||
.with_message("requiredFields cannot be partial when any is false") |
||||
.into()); |
||||
} |
||||
|
||||
// 9. Return result.
|
||||
Ok(result) |
||||
} |
||||
|
||||
/// Convert a `TemporalFields` struct into a `JsObject`.
|
||||
pub(crate) fn as_object(&self, context: &mut Context<'_>) -> JsResult<JsObject> { |
||||
let obj = JsObject::with_null_proto(); |
||||
|
||||
for bit in self.bit_map.iter() { |
||||
match bit { |
||||
FieldMap::YEAR => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("year"), |
||||
self.year.map_or(JsValue::undefined(), JsValue::from), |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::MONTH => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("month"), |
||||
self.month.map_or(JsValue::undefined(), JsValue::from), |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::MONTH_CODE => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("monthCode"), |
||||
self.month_code |
||||
.as_ref() |
||||
.map_or(JsValue::undefined(), |f| f.clone().into()), |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::DAY => { |
||||
obj.create_data_property( |
||||
js_string!("day"), |
||||
self.day().map_or(JsValue::undefined(), JsValue::from), |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::HOUR => { |
||||
obj.create_data_property(js_string!("hour"), self.hour, context)?; |
||||
} |
||||
FieldMap::MINUTE => { |
||||
obj.create_data_property(js_string!("minute"), self.minute, context)?; |
||||
} |
||||
FieldMap::SECOND => { |
||||
obj.create_data_property_or_throw(js_string!("second"), self.second, context)?; |
||||
} |
||||
FieldMap::MILLISECOND => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("millisecond"), |
||||
self.millisecond, |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::MICROSECOND => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("microsecond"), |
||||
self.microsecond, |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::NANOSECOND => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("nanosecond"), |
||||
self.nanosecond, |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::OFFSET => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("offset"), |
||||
self.offset |
||||
.as_ref() |
||||
.map_or(JsValue::undefined(), |s| s.clone().into()), |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::ERA => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("era"), |
||||
self.era |
||||
.as_ref() |
||||
.map_or(JsValue::undefined(), |s| s.clone().into()), |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::ERA_YEAR => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("eraYear"), |
||||
self.era_year.map_or(JsValue::undefined(), JsValue::from), |
||||
context, |
||||
)?; |
||||
} |
||||
FieldMap::TIME_ZONE => { |
||||
obj.create_data_property_or_throw( |
||||
js_string!("timeZone"), |
||||
self.time_zone |
||||
.as_ref() |
||||
.map_or(JsValue::undefined(), |s| s.clone().into()), |
||||
context, |
||||
)?; |
||||
} |
||||
_ => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
Ok(obj) |
||||
} |
||||
|
||||
// Note placeholder until overflow is implemented on `ICU4x`'s Date<Iso>.
|
||||
/// A function to regulate the current `TemporalFields` according to the overflow value
|
||||
pub(crate) fn regulate(&mut self, overflow: ArithmeticOverflow) -> JsResult<()> { |
||||
if let (Some(year), Some(month), Some(day)) = (self.year(), self.month(), self.day()) { |
||||
match overflow { |
||||
ArithmeticOverflow::Constrain => { |
||||
let m = month.clamp(1, 12); |
||||
let days_in_month = super::calendar::utils::iso_days_in_month(year, month); |
||||
let d = day.clamp(1, days_in_month); |
||||
|
||||
self.month = Some(m); |
||||
self.day = Some(d); |
||||
} |
||||
ArithmeticOverflow::Reject => { |
||||
return Err(JsNativeError::range() |
||||
.with_message("TemporalFields is out of a valid range.") |
||||
.into()) |
||||
} |
||||
} |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
pub(crate) fn regulate_year_month(&mut self, overflow: ArithmeticOverflow) { |
||||
match self.month { |
||||
Some(month) if overflow == ArithmeticOverflow::Constrain => { |
||||
let m = month.clamp(1, 12); |
||||
self.month = Some(m); |
||||
} |
||||
_ => {} |
||||
} |
||||
} |
||||
|
||||
/// Resolve the month and monthCode on this `TemporalFields`.
|
||||
pub(crate) fn iso_resolve_month(&mut self) -> JsResult<()> { |
||||
if self.month_code.is_none() { |
||||
if self.month.is_some() { |
||||
return Ok(()); |
||||
} |
||||
|
||||
return Err(JsNativeError::range() |
||||
.with_message("month and MonthCode values cannot both be undefined.") |
||||
.into()); |
||||
} |
||||
|
||||
let unresolved_month_code = self |
||||
.month_code |
||||
.as_ref() |
||||
.expect("monthCode must exist at this point."); |
||||
|
||||
let month_code_integer = month_code_to_integer(unresolved_month_code)?; |
||||
|
||||
let new_month = match self.month { |
||||
Some(month) if month != month_code_integer => { |
||||
return Err(JsNativeError::range() |
||||
.with_message("month and monthCode cannot be resolved.") |
||||
.into()) |
||||
} |
||||
_ => month_code_integer, |
||||
}; |
||||
|
||||
self.month = Some(new_month); |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
fn month_code_to_integer(mc: &JsString) -> JsResult<i32> { |
||||
match mc.to_std_string_escaped().as_str() { |
||||
"M01" => Ok(1), |
||||
"M02" => Ok(2), |
||||
"M03" => Ok(3), |
||||
"M04" => Ok(4), |
||||
"M05" => Ok(5), |
||||
"M06" => Ok(6), |
||||
"M07" => Ok(7), |
||||
"M08" => Ok(8), |
||||
"M09" => Ok(9), |
||||
"M10" => Ok(10), |
||||
"M11" => Ok(11), |
||||
"M12" => Ok(12), |
||||
"M13" => Ok(13), |
||||
_ => Err(JsNativeError::range() |
||||
.with_message("monthCode is not within the valid values.") |
||||
.into()), |
||||
} |
||||
} |
@ -0,0 +1,784 @@
|
||||
//! Boa's implementation of ECMAScript's `Temporal.Instant` builtin object.
|
||||
#![allow(dead_code)] |
||||
|
||||
use crate::{ |
||||
builtins::{ |
||||
options::{get_option, get_options_object, RoundingMode}, |
||||
temporal::{ |
||||
duration::{DateDuration, TimeDuration}, |
||||
options::{ |
||||
get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, |
||||
}, |
||||
}, |
||||
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, |
||||
}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
js_string, |
||||
object::{internal_methods::get_prototype_from_constructor, ObjectData}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::{common::StaticJsStrings, utf16}, |
||||
Context, JsArgs, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
use super::{duration, ns_max_instant, ns_min_instant, MIS_PER_DAY, MS_PER_DAY, NS_PER_DAY}; |
||||
|
||||
const NANOSECONDS_PER_SECOND: i64 = 10_000_000_000; |
||||
const NANOSECONDS_PER_MINUTE: i64 = 600_000_000_000; |
||||
const NANOSECONDS_PER_HOUR: i64 = 36_000_000_000_000; |
||||
|
||||
/// The `Temporal.Instant` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct Instant { |
||||
pub(crate) nanoseconds: JsBigInt, |
||||
} |
||||
|
||||
impl BuiltInObject for Instant { |
||||
const NAME: JsString = StaticJsStrings::INSTANT; |
||||
} |
||||
|
||||
impl IntrinsicObject for Instant { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
let get_seconds = BuiltInBuilder::callable(realm, Self::get_epoc_seconds) |
||||
.name(js_string!("get epochSeconds")) |
||||
.build(); |
||||
|
||||
let get_millis = BuiltInBuilder::callable(realm, Self::get_epoc_milliseconds) |
||||
.name(js_string!("get epochMilliseconds")) |
||||
.build(); |
||||
|
||||
let get_micros = BuiltInBuilder::callable(realm, Self::get_epoc_microseconds) |
||||
.name(js_string!("get epochMicroseconds")) |
||||
.build(); |
||||
|
||||
let get_nanos = BuiltInBuilder::callable(realm, Self::get_epoc_nanoseconds) |
||||
.name(js_string!("get epochNanoseconds")) |
||||
.build(); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::CONFIGURABLE, |
||||
) |
||||
.accessor( |
||||
utf16!("epochSeconds"), |
||||
Some(get_seconds), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("epochMilliseconds"), |
||||
Some(get_millis), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("epochMicroseconds"), |
||||
Some(get_micros), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("epochNanoseconds"), |
||||
Some(get_nanos), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.method(Self::add, js_string!("add"), 1) |
||||
.method(Self::subtract, js_string!("subtract"), 1) |
||||
.method(Self::until, js_string!("until"), 2) |
||||
.method(Self::since, js_string!("since"), 2) |
||||
.method(Self::round, js_string!("round"), 1) |
||||
.method(Self::equals, js_string!("equals"), 1) |
||||
.method(Self::to_zoned_date_time, js_string!("toZonedDateTime"), 1) |
||||
.method( |
||||
Self::to_zoned_date_time_iso, |
||||
js_string!("toZonedDateTimeISO"), |
||||
1, |
||||
) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for Instant { |
||||
const LENGTH: usize = 1; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::instant; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If NewTarget is undefined, then
|
||||
if new_target.is_undefined() { |
||||
// a. Throw a TypeError exception.
|
||||
return Err(JsNativeError::typ() |
||||
.with_message("Temporal.Instant new target cannot be undefined.") |
||||
.into()); |
||||
}; |
||||
|
||||
// 2. Let epochNanoseconds be ? ToBigInt(epochNanoseconds).
|
||||
let epoch_nanos = args.get_or_undefined(0).to_bigint(context)?; |
||||
|
||||
// 3. If ! IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception.
|
||||
if !is_valid_epoch_nanos(&epoch_nanos) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("Temporal.Instant must have a valid epochNanoseconds.") |
||||
.into()); |
||||
}; |
||||
// 4. Return ? CreateTemporalInstant(epochNanoseconds, NewTarget).
|
||||
create_temporal_instant(epoch_nanos, Some(new_target.clone()), context) |
||||
} |
||||
} |
||||
|
||||
// -- Instant method implementations --
|
||||
|
||||
impl Instant { |
||||
/// 8.3.3 get Temporal.Instant.prototype.epochSeconds
|
||||
pub(crate) fn get_epoc_seconds( |
||||
this: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
// 3. Let ns be instant.[[Nanoseconds]].
|
||||
let ns = &instant.nanoseconds; |
||||
// 4. Let s be floor(ℝ(ns) / 10e9).
|
||||
let s = (ns.to_f64() / 10e9).floor(); |
||||
// 5. Return 𝔽(s).
|
||||
Ok(s.into()) |
||||
} |
||||
|
||||
/// 8.3.4 get Temporal.Instant.prototype.epochMilliseconds
|
||||
pub(crate) fn get_epoc_milliseconds( |
||||
this: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
// 3. Let ns be instant.[[Nanoseconds]].
|
||||
let ns = &instant.nanoseconds; |
||||
// 4. Let ms be floor(ℝ(ns) / 106).
|
||||
let ms = (ns.to_f64() / 10e6).floor(); |
||||
// 5. Return 𝔽(ms).
|
||||
Ok(ms.into()) |
||||
} |
||||
|
||||
/// 8.3.5 get Temporal.Instant.prototype.epochMicroseconds
|
||||
pub(crate) fn get_epoc_microseconds( |
||||
this: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
// 3. Let ns be instant.[[Nanoseconds]].
|
||||
let ns = &instant.nanoseconds; |
||||
// 4. Let µs be floor(ℝ(ns) / 103).
|
||||
let micro_s = (ns.to_f64() / 10e3).floor(); |
||||
// 5. Return ℤ(µs).
|
||||
let big_int = JsBigInt::try_from(micro_s).map_err(|_| { |
||||
JsNativeError::typ().with_message("Could not convert microseconds to JsBigInt value") |
||||
})?; |
||||
Ok(big_int.into()) |
||||
} |
||||
|
||||
/// 8.3.6 get Temporal.Instant.prototype.epochNanoseconds
|
||||
pub(crate) fn get_epoc_nanoseconds( |
||||
this: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
// 3. Let ns be instant.[[Nanoseconds]].
|
||||
let ns = &instant.nanoseconds; |
||||
// 4. Return ns.
|
||||
Ok(ns.clone().into()) |
||||
} |
||||
|
||||
/// 8.3.7 `Temporal.Instant.prototype.add ( temporalDurationLike )`
|
||||
pub(crate) fn add( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
|
||||
// 3. Return ? AddDurationToOrSubtractDurationFromInstant(add, instant, temporalDurationLike).
|
||||
let temporal_duration_like = args.get_or_undefined(0); |
||||
add_or_subtract_duration_from_instant(true, instant, temporal_duration_like, context) |
||||
} |
||||
|
||||
/// 8.3.8 `Temporal.Instant.prototype.subtract ( temporalDurationLike )`
|
||||
pub(crate) fn subtract( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
|
||||
// 3. Return ? AddDurationToOrSubtractDurationFromInstant(subtract, instant, temporalDurationLike).
|
||||
let temporal_duration_like = args.get_or_undefined(0); |
||||
add_or_subtract_duration_from_instant(false, instant, temporal_duration_like, context) |
||||
} |
||||
|
||||
/// 8.3.9 `Temporal.Instant.prototype.until ( other [ , options ] )`
|
||||
pub(crate) fn until( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
|
||||
// 3. Return ? DifferenceTemporalInstant(until, instant, other, options).
|
||||
let other = args.get_or_undefined(0); |
||||
let option = args.get_or_undefined(1); |
||||
diff_temporal_instant(true, instant, other, option, context) |
||||
} |
||||
|
||||
/// 8.3.10 `Temporal.Instant.prototype.since ( other [ , options ] )`
|
||||
pub(crate) fn since( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
|
||||
// 3. Return ? DifferenceTemporalInstant(since, instant, other, options).
|
||||
let other = args.get_or_undefined(0); |
||||
let option = args.get_or_undefined(1); |
||||
diff_temporal_instant(false, instant, other, option, context) |
||||
} |
||||
|
||||
/// 8.3.11 `Temporal.Instant.prototype.round ( roundTo )`
|
||||
pub(crate) fn round( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
|
||||
let round_to = args.get_or_undefined(0); |
||||
// 3. If roundTo is undefined, then
|
||||
if round_to.is_undefined() { |
||||
// a. Throw a TypeError exception.
|
||||
return Err(JsNativeError::typ() |
||||
.with_message("roundTo cannot be undefined.") |
||||
.into()); |
||||
}; |
||||
// 4. If Type(roundTo) is String, then
|
||||
let round_to = if round_to.is_string() { |
||||
// a. Let paramString be roundTo.
|
||||
let param_string = round_to |
||||
.as_string() |
||||
.expect("roundTo is confirmed to be a string here."); |
||||
// b. Set roundTo to OrdinaryObjectCreate(null).
|
||||
let new_round_to = JsObject::with_null_proto(); |
||||
// c. Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit"), paramString).
|
||||
new_round_to.create_data_property_or_throw( |
||||
utf16!("smallestUnit"), |
||||
param_string.clone(), |
||||
context, |
||||
)?; |
||||
new_round_to |
||||
// 5. Else,
|
||||
} else { |
||||
// a. Set roundTo to ? GetOptionsObject(roundTo).
|
||||
get_options_object(round_to)? |
||||
}; |
||||
|
||||
// 6. NOTE: The following steps read options and perform independent validation in
|
||||
// alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode").
|
||||
// 7. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo).
|
||||
let rounding_increment = get_temporal_rounding_increment(&round_to, context)?; |
||||
|
||||
// 8. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand").
|
||||
let rounding_mode = |
||||
get_option(&round_to, utf16!("roundingMode"), context)?.unwrap_or_default(); |
||||
|
||||
// 9. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit"), time, required).
|
||||
let smallest_unit = get_temporal_unit( |
||||
&round_to, |
||||
utf16!("smallestUnit"), |
||||
TemporalUnitGroup::Time, |
||||
None, |
||||
context, |
||||
)? |
||||
.ok_or_else(|| JsNativeError::range().with_message("smallestUnit cannot be undefined."))?; |
||||
|
||||
let maximum = match smallest_unit { |
||||
// 10. If smallestUnit is "hour"), then
|
||||
// a. Let maximum be HoursPerDay.
|
||||
TemporalUnit::Hour => 24, |
||||
// 11. Else if smallestUnit is "minute"), then
|
||||
// a. Let maximum be MinutesPerHour × HoursPerDay.
|
||||
TemporalUnit::Minute => 14400, |
||||
// 12. Else if smallestUnit is "second"), then
|
||||
// a. Let maximum be SecondsPerMinute × MinutesPerHour × HoursPerDay.
|
||||
TemporalUnit::Second => 86400, |
||||
// 13. Else if smallestUnit is "millisecond"), then
|
||||
// a. Let maximum be ℝ(msPerDay).
|
||||
TemporalUnit::Millisecond => i64::from(MS_PER_DAY), |
||||
// 14. Else if smallestUnit is "microsecond"), then
|
||||
// a. Let maximum be 10^3 × ℝ(msPerDay).
|
||||
TemporalUnit::Microsecond => MIS_PER_DAY, |
||||
// 15. Else,
|
||||
// a. Assert: smallestUnit is "nanosecond".
|
||||
// b. Let maximum be nsPerDay.
|
||||
TemporalUnit::Nanosecond => NS_PER_DAY, |
||||
// unreachable here functions as 15.a.
|
||||
_ => unreachable!(), |
||||
}; |
||||
|
||||
// 16. Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, true).
|
||||
super::validate_temporal_rounding_increment(rounding_increment, maximum as f64, true)?; |
||||
|
||||
// 17. Let roundedNs be RoundTemporalInstant(instant.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode).
|
||||
let rounded_ns = round_temporal_instant( |
||||
&instant.nanoseconds, |
||||
rounding_increment, |
||||
smallest_unit, |
||||
rounding_mode, |
||||
)?; |
||||
|
||||
// 18. Return ! CreateTemporalInstant(roundedNs).
|
||||
create_temporal_instant(rounded_ns, None, context) |
||||
} |
||||
|
||||
/// 8.3.12 `Temporal.Instant.prototype.equals ( other )`
|
||||
pub(crate) fn equals( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let instant be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
|
||||
// 4. If instant.[[Nanoseconds]] ≠ other.[[Nanoseconds]], return false.
|
||||
// 5. Return true.
|
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value of Instant must be an object.") |
||||
})?; |
||||
let instant = o.as_instant().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("the this object must be an instant object.") |
||||
})?; |
||||
|
||||
// 3. Set other to ? ToTemporalInstant(other).
|
||||
let other = args.get_or_undefined(0); |
||||
let other_instant = to_temporal_instant(other)?; |
||||
|
||||
if instant.nanoseconds != other_instant.nanoseconds { |
||||
return Ok(false.into()); |
||||
} |
||||
Ok(true.into()) |
||||
} |
||||
|
||||
/// 8.3.17 `Temporal.Instant.prototype.toZonedDateTime ( item )`
|
||||
pub(crate) fn to_zoned_date_time( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// TODO: Complete
|
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// 8.3.18 `Temporal.Instant.prototype.toZonedDateTimeISO ( timeZone )`
|
||||
pub(crate) fn to_zoned_date_time_iso( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// TODO Complete
|
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// -- Instant Abstract Operations --
|
||||
|
||||
/// 8.5.1 `IsValidEpochNanoseconds ( epochNanoseconds )`
|
||||
#[inline] |
||||
fn is_valid_epoch_nanos(epoch_nanos: &JsBigInt) -> bool { |
||||
// 1. Assert: Type(epochNanoseconds) is BigInt.
|
||||
// 2. If ℝ(epochNanoseconds) < nsMinInstant or ℝ(epochNanoseconds) > nsMaxInstant, then
|
||||
if epoch_nanos.to_f64() < ns_min_instant().to_f64() |
||||
|| epoch_nanos.to_f64() > ns_max_instant().to_f64() |
||||
{ |
||||
// a. Return false.
|
||||
return false; |
||||
} |
||||
// 3. Return true.
|
||||
true |
||||
} |
||||
|
||||
/// 8.5.2 `CreateTemporalInstant ( epochNanoseconds [ , newTarget ] )`
|
||||
#[inline] |
||||
fn create_temporal_instant( |
||||
epoch_nanos: JsBigInt, |
||||
new_target: Option<JsValue>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Assert: ! IsValidEpochNanoseconds(epochNanoseconds) is true.
|
||||
assert!(is_valid_epoch_nanos(&epoch_nanos)); |
||||
// 2. If newTarget is not present, set newTarget to %Temporal.Instant%.
|
||||
let new_target = new_target.unwrap_or_else(|| { |
||||
context |
||||
.realm() |
||||
.intrinsics() |
||||
.constructors() |
||||
.instant() |
||||
.constructor() |
||||
.into() |
||||
}); |
||||
// 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.Instant.prototype%"), « [[InitializedTemporalInstant]], [[Nanoseconds]] »).
|
||||
let proto = |
||||
get_prototype_from_constructor(&new_target, StandardConstructors::instant, context)?; |
||||
|
||||
// 4. Set object.[[Nanoseconds]] to epochNanoseconds.
|
||||
let obj = JsObject::from_proto_and_data( |
||||
proto, |
||||
ObjectData::instant(Instant { |
||||
nanoseconds: epoch_nanos, |
||||
}), |
||||
); |
||||
|
||||
// 5. Return object.
|
||||
Ok(obj.into()) |
||||
} |
||||
|
||||
/// 8.5.3 `ToTemporalInstant ( item )`
|
||||
#[inline] |
||||
fn to_temporal_instant(_: &JsValue) -> JsResult<Instant> { |
||||
// TODO: Need to implement parsing.
|
||||
Err(JsNativeError::error() |
||||
.with_message("Instant parsing is not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// 8.5.6 `AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds )`
|
||||
#[inline] |
||||
fn add_instant( |
||||
epoch_nanos: &JsBigInt, |
||||
hours: i32, |
||||
minutes: i32, |
||||
seconds: i32, |
||||
millis: i32, |
||||
micros: i32, |
||||
nanos: i32, |
||||
) -> JsResult<JsBigInt> { |
||||
let result = JsBigInt::add_n(&[ |
||||
JsBigInt::mul( |
||||
&JsBigInt::from(hours), |
||||
&JsBigInt::from(NANOSECONDS_PER_HOUR), |
||||
), |
||||
JsBigInt::mul( |
||||
&JsBigInt::from(minutes), |
||||
&JsBigInt::from(NANOSECONDS_PER_MINUTE), |
||||
), |
||||
JsBigInt::mul( |
||||
&JsBigInt::from(seconds), |
||||
&JsBigInt::from(NANOSECONDS_PER_SECOND), |
||||
), |
||||
JsBigInt::mul(&JsBigInt::from(millis), &JsBigInt::from(10_000_000_i32)), |
||||
JsBigInt::mul(&JsBigInt::from(micros), &JsBigInt::from(1000_i32)), |
||||
JsBigInt::add(&JsBigInt::from(nanos), epoch_nanos), |
||||
]); |
||||
if !is_valid_epoch_nanos(&result) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("result is not a valid epoch nanosecond value.") |
||||
.into()); |
||||
} |
||||
Ok(result) |
||||
} |
||||
|
||||
/// 8.5.7 `DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, roundingMode )`
|
||||
#[inline] |
||||
fn diff_instant( |
||||
ns1: &JsBigInt, |
||||
ns2: &JsBigInt, |
||||
rounding_increment: f64, |
||||
smallest_unit: TemporalUnit, |
||||
largest_unit: TemporalUnit, |
||||
rounding_mode: RoundingMode, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<duration::DurationRecord> { |
||||
// 1. Let difference be ℝ(ns2) - ℝ(ns1).
|
||||
let difference = JsBigInt::sub(ns1, ns2); |
||||
// 2. Let nanoseconds be remainder(difference, 1000).
|
||||
let nanoseconds = JsBigInt::rem(&difference, &JsBigInt::from(1000)); |
||||
// 3. Let microseconds be remainder(truncate(difference / 1000), 1000).
|
||||
let truncated_micro = JsBigInt::try_from((&difference.to_f64() / 1000_f64).trunc()) |
||||
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; |
||||
let microseconds = JsBigInt::rem(&truncated_micro, &JsBigInt::from(1000)); |
||||
|
||||
// 4. Let milliseconds be remainder(truncate(difference / 106), 1000).
|
||||
let truncated_milli = JsBigInt::try_from((&difference.to_f64() / 1_000_000_f64).trunc()) |
||||
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; |
||||
let milliseconds = JsBigInt::rem(&truncated_milli, &JsBigInt::from(1000)); |
||||
|
||||
// 5. Let seconds be truncate(difference / 109).
|
||||
let seconds = (&difference.to_f64() / 1_000_000_000_f64).trunc(); |
||||
|
||||
// 6. Let roundResult be ! RoundDuration(0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, roundingIncrement, smallestUnit, largestUnit, roundingMode).
|
||||
let mut roundable_duration = duration::DurationRecord::new( |
||||
DateDuration::default(), |
||||
TimeDuration::new( |
||||
0.0, |
||||
0.0, |
||||
seconds, |
||||
milliseconds.to_f64(), |
||||
microseconds.to_f64(), |
||||
nanoseconds.to_f64(), |
||||
), |
||||
); |
||||
let _rem = roundable_duration.round_duration( |
||||
rounding_increment, |
||||
smallest_unit, |
||||
rounding_mode, |
||||
None, |
||||
context, |
||||
)?; |
||||
|
||||
// 7. Assert: roundResult.[[Days]] is 0.
|
||||
assert_eq!(roundable_duration.days() as i32, 0); |
||||
|
||||
// 8. Return ! BalanceTimeDuration(0, roundResult.[[Hours]], roundResult.[[Minutes]],
|
||||
// roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]],
|
||||
// roundResult.[[Nanoseconds]], largestUnit).
|
||||
roundable_duration.balance_time_duration(largest_unit, None)?; |
||||
|
||||
Ok(roundable_duration) |
||||
} |
||||
|
||||
/// 8.5.8 `RoundTemporalInstant ( ns, increment, unit, roundingMode )`
|
||||
#[inline] |
||||
fn round_temporal_instant( |
||||
ns: &JsBigInt, |
||||
increment: f64, |
||||
unit: TemporalUnit, |
||||
rounding_mode: RoundingMode, |
||||
) -> JsResult<JsBigInt> { |
||||
let increment_ns = match unit { |
||||
// 1. If unit is "hour"), then
|
||||
TemporalUnit::Hour => { |
||||
// a. Let incrementNs be increment × 3.6 × 10^12.
|
||||
increment as i64 * NANOSECONDS_PER_HOUR |
||||
} |
||||
// 2. Else if unit is "minute"), then
|
||||
TemporalUnit::Minute => { |
||||
// a. Let incrementNs be increment × 6 × 10^10.
|
||||
increment as i64 * NANOSECONDS_PER_MINUTE |
||||
} |
||||
// 3. Else if unit is "second"), then
|
||||
TemporalUnit::Second => { |
||||
// a. Let incrementNs be increment × 10^9.
|
||||
increment as i64 * NANOSECONDS_PER_SECOND |
||||
} |
||||
// 4. Else if unit is "millisecond"), then
|
||||
TemporalUnit::Millisecond => { |
||||
// a. Let incrementNs be increment × 10^6.
|
||||
increment as i64 * 1_000_000 |
||||
} |
||||
// 5. Else if unit is "microsecond"), then
|
||||
TemporalUnit::Microsecond => { |
||||
// a. Let incrementNs be increment × 10^3.
|
||||
increment as i64 * 1000 |
||||
} |
||||
// 6. Else,
|
||||
TemporalUnit::Nanosecond => { |
||||
// NOTE: We shouldn't have to assert here as `unreachable` asserts instead.
|
||||
// a. Assert: unit is "nanosecond".
|
||||
// b. Let incrementNs be increment.
|
||||
increment as i64 |
||||
} |
||||
_ => unreachable!(), |
||||
}; |
||||
|
||||
// 7. Return ℤ(RoundNumberToIncrementAsIfPositive(ℝ(ns), incrementNs, roundingMode)).
|
||||
super::round_to_increment_as_if_positive(ns, increment_ns, rounding_mode) |
||||
} |
||||
|
||||
/// 8.5.10 `DifferenceTemporalInstant ( operation, instant, other, options )`
|
||||
#[inline] |
||||
fn diff_temporal_instant( |
||||
op: bool, |
||||
instant: &Instant, |
||||
other: &JsValue, |
||||
options: &JsValue, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If operation is since, let sign be -1. Otherwise, let sign be 1.
|
||||
let sign = if op { 1_f64 } else { -1_f64 }; |
||||
// 2. Set other to ? ToTemporalInstant(other).
|
||||
let other = to_temporal_instant(other)?; |
||||
// 3. Let resolvedOptions be ? CopyOptions(options).
|
||||
let resolved_options = |
||||
super::snapshot_own_properties(&get_options_object(options)?, None, None, context)?; |
||||
|
||||
// 4. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, time, « », "nanosecond"), "second").
|
||||
let settings = super::get_diff_settings( |
||||
op, |
||||
&resolved_options, |
||||
TemporalUnitGroup::Time, |
||||
&[], |
||||
TemporalUnit::Nanosecond, |
||||
TemporalUnit::Second, |
||||
context, |
||||
)?; |
||||
|
||||
// 5. Let result be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[LargestUnit]], settings.[[RoundingMode]]).
|
||||
let result = diff_instant( |
||||
&instant.nanoseconds, |
||||
&other.nanoseconds, |
||||
settings.3, |
||||
settings.0, |
||||
settings.1, |
||||
settings.2, |
||||
context, |
||||
)?; |
||||
|
||||
// 6. Return ! CreateTemporalDuration(0, 0, 0, 0, sign × result.[[Hours]], sign × result.[[Minutes]], sign × result.[[Seconds]], sign × result.[[Milliseconds]], sign × result.[[Microseconds]], sign × result.[[Nanoseconds]]).
|
||||
Ok(duration::create_temporal_duration( |
||||
duration::DurationRecord::new( |
||||
DateDuration::default(), |
||||
TimeDuration::new( |
||||
sign * result.hours(), |
||||
sign * result.minutes(), |
||||
sign * result.seconds(), |
||||
sign * result.milliseconds(), |
||||
sign * result.microseconds(), |
||||
sign * result.nanoseconds(), |
||||
), |
||||
), |
||||
None, |
||||
context, |
||||
)? |
||||
.into()) |
||||
} |
||||
|
||||
/// 8.5.11 `AddDurationToOrSubtractDurationFromInstant ( operation, instant, temporalDurationLike )`
|
||||
#[inline] |
||||
fn add_or_subtract_duration_from_instant( |
||||
op: bool, |
||||
instant: &Instant, |
||||
temporal_duration_like: &JsValue, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If operation is subtract, let sign be -1. Otherwise, let sign be 1.
|
||||
let sign = if op { 1 } else { -1 }; |
||||
// 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike).
|
||||
let duration = super::to_temporal_duration_record(temporal_duration_like)?; |
||||
// 3. If duration.[[Days]] is not 0, throw a RangeError exception.
|
||||
if duration.days() != 0_f64 { |
||||
return Err(JsNativeError::range() |
||||
.with_message("DurationDays cannot be 0") |
||||
.into()); |
||||
} |
||||
// 4. If duration.[[Months]] is not 0, throw a RangeError exception.
|
||||
if duration.months() != 0_f64 { |
||||
return Err(JsNativeError::range() |
||||
.with_message("DurationMonths cannot be 0") |
||||
.into()); |
||||
} |
||||
// 5. If duration.[[Weeks]] is not 0, throw a RangeError exception.
|
||||
if duration.weeks() != 0_f64 { |
||||
return Err(JsNativeError::range() |
||||
.with_message("DurationWeeks cannot be 0") |
||||
.into()); |
||||
} |
||||
// 6. If duration.[[Years]] is not 0, throw a RangeError exception.
|
||||
if duration.years() != 0_f64 { |
||||
return Err(JsNativeError::range() |
||||
.with_message("DurationYears cannot be 0") |
||||
.into()); |
||||
} |
||||
// 7. Let ns be ? AddInstant(instant.[[Nanoseconds]], sign × duration.[[Hours]],
|
||||
// sign × duration.[[Minutes]], sign × duration.[[Seconds]], sign × duration.[[Milliseconds]],
|
||||
// sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]).
|
||||
let new = add_instant( |
||||
&instant.nanoseconds, |
||||
sign * duration.hours() as i32, |
||||
sign * duration.minutes() as i32, |
||||
sign * duration.seconds() as i32, |
||||
sign * duration.milliseconds() as i32, |
||||
sign * duration.microseconds() as i32, |
||||
sign * duration.nanoseconds() as i32, |
||||
)?; |
||||
// 8. Return ! CreateTemporalInstant(ns).
|
||||
create_temporal_instant(new, None, context) |
||||
} |
@ -0,0 +1,660 @@
|
||||
//! The ECMAScript `Temporal` stage 3 built-in implementation.
|
||||
//!
|
||||
//! More information:
|
||||
//!
|
||||
//! [spec]: https://tc39.es/proposal-temporal/
|
||||
|
||||
mod calendar; |
||||
mod date_equations; |
||||
mod duration; |
||||
mod fields; |
||||
mod instant; |
||||
mod now; |
||||
mod options; |
||||
mod plain_date; |
||||
mod plain_date_time; |
||||
mod plain_month_day; |
||||
mod plain_time; |
||||
mod plain_year_month; |
||||
mod time_zone; |
||||
mod zoned_date_time; |
||||
|
||||
#[cfg(feature = "experimental")] |
||||
#[cfg(test)] |
||||
mod tests; |
||||
|
||||
pub(crate) use fields::TemporalFields; |
||||
|
||||
use self::options::{ |
||||
get_temporal_rounding_increment, get_temporal_unit, TemporalUnit, TemporalUnitGroup, |
||||
}; |
||||
pub use self::{ |
||||
calendar::*, duration::*, instant::*, now::*, plain_date::*, plain_date_time::*, |
||||
plain_month_day::*, plain_time::*, plain_year_month::*, time_zone::*, zoned_date_time::*, |
||||
}; |
||||
|
||||
use crate::{ |
||||
builtins::{ |
||||
iterable::IteratorRecord, |
||||
options::{get_option, RoundingMode, UnsignedRoundingMode}, |
||||
BuiltInBuilder, BuiltInObject, IntrinsicObject, |
||||
}, |
||||
context::intrinsics::Intrinsics, |
||||
js_string, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::{common::StaticJsStrings, utf16}, |
||||
value::Type, |
||||
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
// Relavant numeric constants
|
||||
/// Nanoseconds per day constant: 8.64e+13
|
||||
pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000; |
||||
/// Microseconds per day constant: 8.64e+10
|
||||
pub(crate) const MIS_PER_DAY: i64 = 8_640_000_000; |
||||
/// Milliseconds per day constant: 8.64e+7
|
||||
pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; |
||||
|
||||
pub(crate) fn ns_max_instant() -> JsBigInt { |
||||
JsBigInt::from(i128::from(NS_PER_DAY) * 100_000_000_i128) |
||||
} |
||||
|
||||
pub(crate) fn ns_min_instant() -> JsBigInt { |
||||
JsBigInt::from(i128::from(NS_PER_DAY) * -100_000_000_i128) |
||||
} |
||||
|
||||
// An enum representing common fields across `Temporal` objects.
|
||||
#[allow(unused)] |
||||
pub(crate) enum DateTimeValues { |
||||
Year, |
||||
Month, |
||||
MonthCode, |
||||
Week, |
||||
Day, |
||||
Hour, |
||||
Minute, |
||||
Second, |
||||
Millisecond, |
||||
Microsecond, |
||||
Nanosecond, |
||||
} |
||||
|
||||
/// The [`Temporal`][spec] builtin object.
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-objects
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
||||
pub(crate) struct Temporal; |
||||
|
||||
impl BuiltInObject for Temporal { |
||||
const NAME: JsString = StaticJsStrings::TEMPORAL; |
||||
} |
||||
|
||||
impl IntrinsicObject for Temporal { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
BuiltInBuilder::with_intrinsic::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("Now"), |
||||
realm.intrinsics().objects().now(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("Calendar"), |
||||
realm.intrinsics().constructors().calendar().constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("Duration"), |
||||
realm.intrinsics().constructors().duration().constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("Instant"), |
||||
realm.intrinsics().constructors().instant().constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("PlainDate"), |
||||
realm.intrinsics().constructors().plain_date().constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("PlainDateTime"), |
||||
realm |
||||
.intrinsics() |
||||
.constructors() |
||||
.plain_date_time() |
||||
.constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("PlainMonthDay"), |
||||
realm |
||||
.intrinsics() |
||||
.constructors() |
||||
.plain_month_day() |
||||
.constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("PlainTime"), |
||||
realm.intrinsics().constructors().plain_time().constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("PlainYearMonth"), |
||||
realm |
||||
.intrinsics() |
||||
.constructors() |
||||
.plain_year_month() |
||||
.constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("TimeZone"), |
||||
realm.intrinsics().constructors().time_zone().constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
js_string!("ZonedDateTime"), |
||||
realm |
||||
.intrinsics() |
||||
.constructors() |
||||
.zoned_date_time() |
||||
.constructor(), |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
intrinsics.objects().temporal() |
||||
} |
||||
} |
||||
|
||||
// -- Temporal Abstract Operations --
|
||||
|
||||
/// Abstract operation `ToZeroPaddedDecimalString ( n, minLength )`
|
||||
///
|
||||
/// The abstract operation `ToZeroPaddedDecimalString` takes arguments `n` (a non-negative integer)
|
||||
/// and `minLength` (a non-negative integer) and returns a String.
|
||||
fn to_zero_padded_decimal_string(n: u64, min_length: usize) -> String { |
||||
format!("{n:0min_length$}") |
||||
} |
||||
|
||||
/// Abstract Operation 13.1 [`IteratorToListOfType`][proposal]
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#sec-iteratortolistoftype
|
||||
pub(crate) fn iterator_to_list_of_types( |
||||
iterator: &mut IteratorRecord, |
||||
element_types: &[Type], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<Vec<JsValue>> { |
||||
// 1. Let values be a new empty List.
|
||||
let mut values = Vec::new(); |
||||
|
||||
// 2. Let next be true.
|
||||
// 3. Repeat, while next is not false,
|
||||
// a. Set next to ? IteratorStep(iteratorRecord).
|
||||
// b. If next is not false, then
|
||||
while iterator.step(context)? { |
||||
// i. Let nextValue be ? IteratorValue(next).
|
||||
let next_value = iterator.value(context)?; |
||||
// ii. If Type(nextValue) is not an element of elementTypes, then
|
||||
if element_types.contains(&next_value.get_type()) { |
||||
// 1. Let completion be ThrowCompletion(a newly created TypeError object).
|
||||
let completion = JsNativeError::typ() |
||||
.with_message("IteratorNext is not within allowed type values."); |
||||
|
||||
// NOTE: The below should return as we are forcing a ThrowCompletion.
|
||||
// 2. Return ? IteratorClose(iteratorRecord, completion).
|
||||
let _never = iterator.close(Err(completion.into()), context)?; |
||||
} |
||||
// iii. Append nextValue to the end of the List values.
|
||||
values.push(next_value); |
||||
} |
||||
|
||||
// 4. Return values.
|
||||
Ok(values) |
||||
} |
||||
|
||||
/// 13.2 `ISODateToEpochDays ( year, month, date )`
|
||||
// Note: implemented on IsoDateRecord.
|
||||
|
||||
// Abstract Operation 13.3 `EpochDaysToEpochMs`
|
||||
pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: i32) -> f64 { |
||||
f64::from(day).mul_add(f64::from(MS_PER_DAY), f64::from(time)) |
||||
} |
||||
|
||||
// 13.4 Date Equations
|
||||
// implemented in temporal/date_equations.rs
|
||||
|
||||
// Abstract Operation 13.5 `GetOptionsObject ( options )`
|
||||
// Implemented in builtin/options.rs
|
||||
|
||||
// 13.6 `GetOption ( options, property, type, values, default )`
|
||||
// Implemented in builtin/options.rs
|
||||
|
||||
/// 13.7 `ToTemporalOverflow (options)`
|
||||
// Now implemented in temporal/options.rs
|
||||
|
||||
/// 13.10 `ToTemporalRoundingMode ( normalizedOptions, fallback )`
|
||||
// Now implemented in builtin/options.rs
|
||||
|
||||
// 13.11 `NegateTemporalRoundingMode ( roundingMode )`
|
||||
// Now implemented in builtin/options.rs
|
||||
|
||||
// 13.16 `ToTemporalRoundingIncrement ( normalizedOptions )`
|
||||
// Now implemented in temporal/options.rs
|
||||
|
||||
/// 13.17 `ValidateTemporalRoundingIncrement ( increment, dividend, inclusive )`
|
||||
#[inline] |
||||
pub(crate) fn validate_temporal_rounding_increment( |
||||
increment: f64, |
||||
dividend: f64, |
||||
inclusive: bool, |
||||
) -> JsResult<()> { |
||||
// 1. If inclusive is true, then
|
||||
let maximum = if inclusive { |
||||
// a. Let maximum be dividend.
|
||||
dividend |
||||
// 2. Else,
|
||||
} else { |
||||
// a. Assert: dividend > 1.
|
||||
assert!(dividend > 1.0); |
||||
// b. Let maximum be dividend - 1.
|
||||
dividend - 1.0 |
||||
}; |
||||
|
||||
// 3. If increment > maximum, throw a RangeError exception.
|
||||
if increment > maximum { |
||||
return Err(JsNativeError::range() |
||||
.with_message("increment is exceeds the range of the allowed maximum.") |
||||
.into()); |
||||
} |
||||
// 4. If dividend modulo increment ≠ 0, then
|
||||
if dividend % increment != 0.0 { |
||||
// a. Throw a RangeError exception.
|
||||
return Err(JsNativeError::range() |
||||
.with_message("Temporal rounding increment is not valid.") |
||||
.into()); |
||||
} |
||||
// 5. Return unused.
|
||||
Ok(()) |
||||
} |
||||
|
||||
/// 13.21 `ToRelativeTemporalObject ( options )`
|
||||
pub(crate) fn to_relative_temporal_object( |
||||
_options: &JsObject, |
||||
_context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::range() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
// 13.22 `LargerOfTwoTemporalUnits ( u1, u2 )`
|
||||
// use core::cmp::max
|
||||
|
||||
// 13.23 `MaximumTemporalDurationRoundingIncrement ( unit )`
|
||||
// Implemented on TemporalUnit in temporal/options.rs
|
||||
|
||||
// 13.26 `GetUnsignedRoundingMode ( roundingMode, isNegative )`
|
||||
// Implemented on RoundingMode in builtins/options.rs
|
||||
|
||||
/// 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )`
|
||||
#[inline] |
||||
fn apply_unsigned_rounding_mode( |
||||
x: f64, |
||||
r1: f64, |
||||
r2: f64, |
||||
unsigned_rounding_mode: UnsignedRoundingMode, |
||||
) -> f64 { |
||||
// 1. If x is equal to r1, return r1.
|
||||
if (x - r1).abs() == 0.0 { |
||||
return r1; |
||||
}; |
||||
// 2. Assert: r1 < x < r2.
|
||||
assert!(r1 < x && x < r2); |
||||
// 3. Assert: unsignedRoundingMode is not undefined.
|
||||
|
||||
// 4. If unsignedRoundingMode is zero, return r1.
|
||||
if unsigned_rounding_mode == UnsignedRoundingMode::Zero { |
||||
return r1; |
||||
}; |
||||
// 5. If unsignedRoundingMode is infinity, return r2.
|
||||
if unsigned_rounding_mode == UnsignedRoundingMode::Infinity { |
||||
return r2; |
||||
}; |
||||
|
||||
// 6. Let d1 be x – r1.
|
||||
let d1 = x - r1; |
||||
// 7. Let d2 be r2 – x.
|
||||
let d2 = r2 - x; |
||||
// 8. If d1 < d2, return r1.
|
||||
if d1 < d2 { |
||||
return r1; |
||||
} |
||||
// 9. If d2 < d1, return r2.
|
||||
if d2 < d1 { |
||||
return r2; |
||||
} |
||||
// 10. Assert: d1 is equal to d2.
|
||||
assert!((d1 - d2).abs() == 0.0); |
||||
|
||||
// 11. If unsignedRoundingMode is half-zero, return r1.
|
||||
if unsigned_rounding_mode == UnsignedRoundingMode::HalfZero { |
||||
return r1; |
||||
}; |
||||
// 12. If unsignedRoundingMode is half-infinity, return r2.
|
||||
if unsigned_rounding_mode == UnsignedRoundingMode::HalfInfinity { |
||||
return r2; |
||||
}; |
||||
// 13. Assert: unsignedRoundingMode is half-even.
|
||||
assert!(unsigned_rounding_mode == UnsignedRoundingMode::HalfEven); |
||||
// 14. Let cardinality be (r1 / (r2 – r1)) modulo 2.
|
||||
let cardinality = (r1 / (r2 - r1)) % 2.0; |
||||
// 15. If cardinality is 0, return r1.
|
||||
if cardinality == 0.0 { |
||||
return r1; |
||||
} |
||||
// 16. Return r2.
|
||||
r2 |
||||
} |
||||
|
||||
/// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )`
|
||||
pub(crate) fn round_number_to_increment( |
||||
x: f64, |
||||
increment: f64, |
||||
rounding_mode: RoundingMode, |
||||
) -> f64 { |
||||
// 1. Let quotient be x / increment.
|
||||
let mut quotient = x / increment; |
||||
|
||||
// 2. If quotient < 0, then
|
||||
let is_negative = if quotient < 0_f64 { |
||||
// a. Let isNegative be true.
|
||||
// b. Set quotient to -quotient.
|
||||
quotient = -quotient; |
||||
true |
||||
// 3. Else,
|
||||
} else { |
||||
// a. Let isNegative be false.
|
||||
false |
||||
}; |
||||
|
||||
// 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative).
|
||||
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative); |
||||
// 5. Let r1 be the largest integer such that r1 ≤ quotient.
|
||||
let r1 = quotient.ceil(); |
||||
// 6. Let r2 be the smallest integer such that r2 > quotient.
|
||||
let r2 = quotient.floor(); |
||||
// 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
|
||||
let mut rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); |
||||
// 8. If isNegative is true, set rounded to -rounded.
|
||||
if is_negative { |
||||
rounded = -rounded; |
||||
}; |
||||
// 9. Return rounded × increment.
|
||||
rounded * increment |
||||
} |
||||
|
||||
/// 13.29 `RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode )`
|
||||
#[inline] |
||||
pub(crate) fn round_to_increment_as_if_positive( |
||||
ns: &JsBigInt, |
||||
increment: i64, |
||||
rounding_mode: RoundingMode, |
||||
) -> JsResult<JsBigInt> { |
||||
// 1. Let quotient be x / increment.
|
||||
let q = ns.to_f64() / increment as f64; |
||||
// 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false).
|
||||
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false); |
||||
// 3. Let r1 be the largest integer such that r1 ≤ quotient.
|
||||
let r1 = q.trunc(); |
||||
// 4. Let r2 be the smallest integer such that r2 > quotient.
|
||||
let r2 = q.trunc() + 1.0; |
||||
// 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
|
||||
let rounded = apply_unsigned_rounding_mode(q, r1, r2, unsigned_rounding_mode); |
||||
|
||||
// 6. Return rounded × increment.
|
||||
let rounded = JsBigInt::try_from(rounded) |
||||
.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; |
||||
|
||||
Ok(JsBigInt::mul(&rounded, &JsBigInt::from(increment))) |
||||
} |
||||
|
||||
/// 13.43 `ToPositiveIntegerWithTruncation ( argument )`
|
||||
#[inline] |
||||
pub(crate) fn to_positive_integer_with_trunc( |
||||
value: &JsValue, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<i32> { |
||||
// 1. Let integer be ? ToIntegerWithTruncation(argument).
|
||||
let int = to_integer_with_truncation(value, context)?; |
||||
// 2. If integer ≤ 0, throw a RangeError exception.
|
||||
if int <= 0 { |
||||
return Err(JsNativeError::range() |
||||
.with_message("value is not a positive integer") |
||||
.into()); |
||||
} |
||||
// 3. Return integer.
|
||||
Ok(int) |
||||
} |
||||
|
||||
/// 13.44 `ToIntegerWithTruncation ( argument )`
|
||||
#[inline] |
||||
pub(crate) fn to_integer_with_truncation( |
||||
value: &JsValue, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<i32> { |
||||
// 1. Let number be ? ToNumber(argument).
|
||||
let number = value.to_number(context)?; |
||||
// 2. If number is NaN, +∞𝔽 or -∞𝔽, throw a RangeError exception.
|
||||
if number.is_nan() || number.is_infinite() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("truncation target must be an integer.") |
||||
.into()); |
||||
} |
||||
// 3. Return truncate(ℝ(number)).
|
||||
Ok(number.trunc() as i32) |
||||
} |
||||
|
||||
/// Abstract operation 13.45 `ToIntegerIfIntegral( argument )`
|
||||
#[inline] |
||||
pub(crate) fn to_integer_if_integral(arg: &JsValue, context: &mut Context<'_>) -> JsResult<i32> { |
||||
// 1. Let number be ? ToNumber(argument).
|
||||
// 2. If IsIntegralNumber(number) is false, throw a RangeError exception.
|
||||
// 3. Return ℝ(number).
|
||||
if !arg.is_integer() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("value to convert is not an integral number.") |
||||
.into()); |
||||
} |
||||
|
||||
arg.to_i32(context) |
||||
} |
||||
|
||||
// 13.46 `PrepareTemporalFields ( fields, fieldNames, requiredFields [ , duplicateBehaviour ] )`
|
||||
// See fields.rs
|
||||
|
||||
// NOTE: op -> true == until | false == since
|
||||
/// 13.47 `GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )`
|
||||
#[inline] |
||||
pub(crate) fn get_diff_settings( |
||||
op: bool, |
||||
options: &JsObject, |
||||
unit_group: TemporalUnitGroup, |
||||
disallowed_units: &[TemporalUnit], |
||||
fallback_smallest_unit: TemporalUnit, |
||||
smallest_largest_default_unit: TemporalUnit, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<(TemporalUnit, TemporalUnit, RoundingMode, f64)> { |
||||
// 1. NOTE: The following steps read options and perform independent validation in alphabetical order (ToTemporalRoundingIncrement reads "roundingIncrement" and ToTemporalRoundingMode reads "roundingMode").
|
||||
// 2. Let largestUnit be ? GetTemporalUnit(options, "largestUnit", unitGroup, "auto").
|
||||
let mut largest_unit = |
||||
get_temporal_unit(options, utf16!("largestUnit"), unit_group, None, context)? |
||||
.unwrap_or(TemporalUnit::Auto); |
||||
|
||||
// 3. If disallowedUnits contains largestUnit, throw a RangeError exception.
|
||||
if disallowed_units.contains(&largest_unit) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("largestUnit is not an allowed unit.") |
||||
.into()); |
||||
} |
||||
|
||||
// 4. Let roundingIncrement be ? ToTemporalRoundingIncrement(options).
|
||||
let rounding_increment = get_temporal_rounding_increment(options, context)?; |
||||
|
||||
// 5. Let roundingMode be ? ToTemporalRoundingMode(options, "trunc").
|
||||
let mut rounding_mode = |
||||
get_option(options, utf16!("roundingMode"), context)?.unwrap_or(RoundingMode::Trunc); |
||||
|
||||
// 6. If operation is since, then
|
||||
if !op { |
||||
// a. Set roundingMode to ! NegateTemporalRoundingMode(roundingMode).
|
||||
rounding_mode = rounding_mode.negate(); |
||||
} |
||||
|
||||
// 7. Let smallestUnit be ? GetTemporalUnit(options, "smallestUnit", unitGroup, fallbackSmallestUnit).
|
||||
let smallest_unit = |
||||
get_temporal_unit(options, utf16!("smallestUnit"), unit_group, None, context)? |
||||
.unwrap_or(fallback_smallest_unit); |
||||
|
||||
// 8. If disallowedUnits contains smallestUnit, throw a RangeError exception.
|
||||
if disallowed_units.contains(&smallest_unit) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("smallestUnit is not an allowed unit.") |
||||
.into()); |
||||
} |
||||
|
||||
// 9. Let defaultLargestUnit be ! LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit).
|
||||
let default_largest_unit = core::cmp::max(smallest_largest_default_unit, smallest_unit); |
||||
|
||||
// 10. If largestUnit is "auto", set largestUnit to defaultLargestUnit.
|
||||
if largest_unit == TemporalUnit::Auto { |
||||
largest_unit = default_largest_unit; |
||||
} |
||||
|
||||
// 11. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception.
|
||||
if largest_unit != core::cmp::max(largest_unit, smallest_unit) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("largestUnit must be larger than smallestUnit") |
||||
.into()); |
||||
} |
||||
|
||||
// 12. Let maximum be ! MaximumTemporalDurationRoundingIncrement(smallestUnit).
|
||||
let maximum = smallest_unit.to_maximum_rounding_increment(); |
||||
|
||||
// 13. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
|
||||
if let Some(max) = maximum { |
||||
validate_temporal_rounding_increment(rounding_increment, f64::from(max), false)?; |
||||
} |
||||
|
||||
// 14. Return the Record { [[SmallestUnit]]: smallestUnit, [[LargestUnit]]: largestUnit, [[RoundingMode]]: roundingMode, [[RoundingIncrement]]: roundingIncrement, }.
|
||||
Ok(( |
||||
smallest_unit, |
||||
largest_unit, |
||||
rounding_mode, |
||||
rounding_increment, |
||||
)) |
||||
} |
||||
|
||||
// NOTE: used for MergeFields methods. Potentially can be omitted in favor of `TemporalFields`.
|
||||
/// 14.6 `CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] )`
|
||||
pub(crate) fn copy_data_properties( |
||||
target: &JsObject, |
||||
source: &JsValue, |
||||
excluded_keys: &Vec<JsString>, |
||||
excluded_values: Option<&Vec<JsValue>>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<()> { |
||||
// 1. If source is undefined or null, return unused.
|
||||
if source.is_null_or_undefined() { |
||||
return Ok(()); |
||||
} |
||||
|
||||
// 2. Let from be ! ToObject(source).
|
||||
let from = source.to_object(context)?; |
||||
|
||||
// 3. Let keys be ? from.[[OwnPropertyKeys]]().
|
||||
let keys = from.__own_property_keys__(context)?; |
||||
|
||||
// 4. For each element nextKey of keys, do
|
||||
for next_key in keys { |
||||
// a. Let excluded be false.
|
||||
let mut excluded = false; |
||||
// b. For each element e of excludedItemsexcludedKeys, do
|
||||
for e in excluded_keys { |
||||
// i. If SameValue(e, nextKey) is true, then
|
||||
if next_key.to_string() == e.to_std_string_escaped() { |
||||
// 1. Set excluded to true.
|
||||
excluded = true; |
||||
} |
||||
} |
||||
|
||||
// c. If excluded is false, then
|
||||
if !excluded { |
||||
// i. Let desc be ? from.[[GetOwnProperty]](nextKey).
|
||||
let desc = from.__get_own_property__(&next_key, context)?; |
||||
// ii. If desc is not undefined and desc.[[Enumerable]] is true, then
|
||||
match desc { |
||||
Some(d) |
||||
if d.enumerable() |
||||
.expect("enumerable field must be set per spec.") => |
||||
{ |
||||
// 1. Let propValue be ? Get(from, nextKey).
|
||||
let prop_value = from.get(next_key.clone(), context)?; |
||||
// 2. If excludedValues is present, then
|
||||
if let Some(values) = excluded_values { |
||||
// a. For each element e of excludedValues, do
|
||||
for e in values { |
||||
// i. If SameValue(e, propValue) is true, then
|
||||
if JsValue::same_value(e, &prop_value) { |
||||
// i. Set excluded to true.
|
||||
excluded = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 3. PerformIf excluded is false, perform ! CreateDataPropertyOrThrow(target, nextKey, propValue).
|
||||
if !excluded { |
||||
target.create_data_property_or_throw(next_key, prop_value, context)?; |
||||
} |
||||
} |
||||
_ => {} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 5. Return unused.
|
||||
Ok(()) |
||||
} |
||||
|
||||
// Note: Deviates from Proposal spec -> proto appears to be always null across the specification.
|
||||
/// 14.7 `SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] )`
|
||||
fn snapshot_own_properties( |
||||
source: &JsObject, |
||||
excluded_keys: Option<Vec<JsString>>, |
||||
excluded_values: Option<Vec<JsValue>>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsObject> { |
||||
// 1. Let copy be OrdinaryObjectCreate(proto).
|
||||
let copy = JsObject::with_null_proto(); |
||||
// 2. If excludedKeys is not present, set excludedKeys to « ».
|
||||
let keys = excluded_keys.unwrap_or_default(); |
||||
// 3. If excludedValues is not present, set excludedValues to « ».
|
||||
let values = excluded_values.unwrap_or_default(); |
||||
// 4. Perform ? CopyDataProperties(copy, source, excludedKeys, excludedValues).
|
||||
copy_data_properties(©, &source.clone().into(), &keys, Some(&values), context)?; |
||||
// 5. Return copy.
|
||||
Ok(copy) |
||||
} |
@ -0,0 +1,188 @@
|
||||
//! Boa's implementation of `Temporal.Now` ECMAScript Builtin object.
|
||||
|
||||
use crate::{ |
||||
builtins::{ |
||||
temporal::{create_temporal_time_zone, default_time_zone}, |
||||
BuiltInBuilder, BuiltInObject, IntrinsicObject, |
||||
}, |
||||
context::intrinsics::Intrinsics, |
||||
js_string, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::common::StaticJsStrings, |
||||
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
use super::{ns_max_instant, ns_min_instant}; |
||||
use std::time::SystemTime; |
||||
|
||||
/// JavaScript `Temporal.Now` object.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
||||
pub struct Now; |
||||
|
||||
impl IntrinsicObject for Now { |
||||
/// Initializes the `Temporal.Now` object.
|
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
// is an ordinary object.
|
||||
// has a [[Prototype]] internal slot whose value is %Object.prototype%.
|
||||
// is not a function object.
|
||||
// does not have a [[Construct]] internal method; it cannot be used as a constructor with the new operator.
|
||||
// does not have a [[Call]] internal method; it cannot be invoked as a function.
|
||||
BuiltInBuilder::with_intrinsic::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_method(Self::time_zone_id, js_string!("timeZoneId"), 0) |
||||
.static_method(Self::instant, js_string!("instant"), 0) |
||||
.static_method(Self::plain_date_time, js_string!("plainDateTime"), 2) |
||||
.static_method(Self::plain_date_time_iso, js_string!("plainDateTimeISO"), 1) |
||||
.static_method(Self::zoned_date_time, js_string!("zonedDateTime"), 2) |
||||
.static_method(Self::zoned_date_time_iso, js_string!("zonedDateTimeISO"), 1) |
||||
.static_method(Self::plain_date, js_string!("plainDate"), 2) |
||||
.static_method(Self::plain_date_iso, js_string!("plainDateISO"), 1) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
intrinsics.objects().now() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInObject for Now { |
||||
const NAME: JsString = StaticJsStrings::NOW; |
||||
} |
||||
|
||||
impl Now { |
||||
/// `Temporal.Now.timeZoneId ( )`
|
||||
///
|
||||
/// More information:
|
||||
/// - [ECMAScript specififcation][spec]
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal.now.timezone
|
||||
#[allow(clippy::unnecessary_wraps)] |
||||
fn time_zone_id( |
||||
_: &JsValue, |
||||
_args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Return ! SystemTimeZone().
|
||||
Ok(system_time_zone(context).expect("retrieving the system timezone must not fail")) |
||||
} |
||||
|
||||
/// `Temporal.Now.instant()`
|
||||
fn instant(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// `Temporal.Now.plainDateTime()`
|
||||
fn plain_date_time(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// `Temporal.Now.plainDateTimeISO`
|
||||
fn plain_date_time_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// `Temporal.Now.zonedDateTime`
|
||||
fn zoned_date_time(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// `Temporal.Now.zonedDateTimeISO`
|
||||
fn zoned_date_time_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// `Temporal.Now.plainDate()`
|
||||
fn plain_date(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// `Temporal.Now.plainDateISO`
|
||||
fn plain_date_iso(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// -- Temporal.Now abstract operations --
|
||||
|
||||
/// 2.3.1 `HostSystemUTCEpochNanoseconds ( global )`
|
||||
fn host_system_utc_epoch_nanoseconds() -> JsResult<JsBigInt> { |
||||
// TODO: Implement `SystemTime::now()` calls for `no_std`
|
||||
let epoch_nanos = SystemTime::now() |
||||
.duration_since(SystemTime::UNIX_EPOCH) |
||||
.map_err(|e| JsNativeError::range().with_message(e.to_string()))? |
||||
.as_nanos(); |
||||
Ok(clamp_epoc_nanos(JsBigInt::from(epoch_nanos))) |
||||
} |
||||
|
||||
fn clamp_epoc_nanos(ns: JsBigInt) -> JsBigInt { |
||||
let max = ns_max_instant(); |
||||
let min = ns_min_instant(); |
||||
ns.clamp(min, max) |
||||
} |
||||
|
||||
/// 2.3.2 `SystemUTCEpochMilliseconds`
|
||||
#[allow(unused)] |
||||
fn system_utc_epoch_millis() -> JsResult<f64> { |
||||
let now = host_system_utc_epoch_nanoseconds()?; |
||||
Ok(now.to_f64().div_euclid(1_000_000_f64).floor()) |
||||
} |
||||
|
||||
/// 2.3.3 `SystemUTCEpochNanoseconds`
|
||||
#[allow(unused)] |
||||
fn system_utc_epoch_nanos() -> JsResult<JsBigInt> { |
||||
host_system_utc_epoch_nanoseconds() |
||||
} |
||||
|
||||
/// `SystemInstant`
|
||||
#[allow(unused)] |
||||
fn system_instant() { |
||||
todo!() |
||||
} |
||||
|
||||
/// `SystemDateTime`
|
||||
#[allow(unused)] |
||||
fn system_date_time() { |
||||
todo!() |
||||
} |
||||
|
||||
/// `SystemZonedDateTime`
|
||||
#[allow(unused)] |
||||
fn system_zoned_date_time() { |
||||
todo!() |
||||
} |
||||
|
||||
/// Abstract operation `SystemTimeZone ( )`
|
||||
///
|
||||
/// More information:
|
||||
/// - [ECMAScript specififcation][spec]
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-systemtimezone
|
||||
#[allow(unused)] |
||||
fn system_time_zone(context: &mut Context<'_>) -> JsResult<JsValue> { |
||||
// 1. Let identifier be ! DefaultTimeZone().
|
||||
let identifier = default_time_zone(context); |
||||
// 2. Return ! CreateTemporalTimeZone(identifier).
|
||||
create_temporal_time_zone(identifier, None, context) |
||||
} |
@ -0,0 +1,384 @@
|
||||
//! Temporal Option types.
|
||||
|
||||
// Implementation Note:
|
||||
//
|
||||
// The below Option types are adapted from the types laid out by
|
||||
// the Temporal proposal's polyfill types that can be found at the
|
||||
// below link.
|
||||
//
|
||||
// https://github.com/tc39/proposal-temporal/blob/main/polyfill/index.d.ts
|
||||
|
||||
use crate::{ |
||||
builtins::options::{get_option, ParsableOptionType}, |
||||
js_string, Context, JsNativeError, JsObject, JsResult, |
||||
}; |
||||
use std::{fmt, str::FromStr}; |
||||
|
||||
// TODO: Expand docs on the below options.
|
||||
|
||||
#[inline] |
||||
pub(crate) fn get_temporal_rounding_increment( |
||||
options: &JsObject, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<f64> { |
||||
// 1. Let increment be ? GetOption(normalizedOptions, "roundingIncrement", "number", undefined, 1𝔽).
|
||||
let value = options.get(js_string!("roundingIncrement"), context)?; |
||||
|
||||
let increment = if value.is_undefined() { |
||||
1.0 |
||||
} else { |
||||
value.to_number(context)? |
||||
}; |
||||
|
||||
// 2. If increment is not finite, throw a RangeError exception.
|
||||
if !increment.is_finite() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("rounding increment was out of range.") |
||||
.into()); |
||||
} |
||||
|
||||
// 3. Let integerIncrement be truncate(ℝ(increment)).
|
||||
let integer_increment = increment.trunc(); |
||||
|
||||
// 4. If integerIncrement < 1 or integerIncrement > 10^9, throw a RangeError exception.
|
||||
if (1.0..=1_000_000_000.0).contains(&integer_increment) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("rounding increment was out of range.") |
||||
.into()); |
||||
} |
||||
|
||||
// 5. Return integerIncrement.
|
||||
Ok(integer_increment) |
||||
} |
||||
|
||||
/// Gets the `TemporalUnit` from an options object.
|
||||
#[inline] |
||||
pub(crate) fn get_temporal_unit( |
||||
options: &JsObject, |
||||
key: &[u16], |
||||
unit_group: TemporalUnitGroup, |
||||
extra_values: Option<Vec<TemporalUnit>>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<Option<TemporalUnit>> { |
||||
let extra = extra_values.unwrap_or_default(); |
||||
let mut unit_values = unit_group.group(); |
||||
unit_values.extend(extra); |
||||
|
||||
let unit = get_option(options, key, context)?; |
||||
|
||||
if let Some(u) = &unit { |
||||
if !unit_values.contains(u) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("TemporalUnit was not part of the valid UnitGroup.") |
||||
.into()); |
||||
} |
||||
} |
||||
|
||||
Ok(unit) |
||||
} |
||||
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub(crate) enum TemporalUnitGroup { |
||||
Date, |
||||
Time, |
||||
DateTime, |
||||
} |
||||
|
||||
impl TemporalUnitGroup { |
||||
fn group(self) -> Vec<TemporalUnit> { |
||||
use TemporalUnitGroup::{Date, DateTime, Time}; |
||||
|
||||
match self { |
||||
Date => date_units().collect(), |
||||
Time => time_units().collect(), |
||||
DateTime => datetime_units().collect(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn time_units() -> impl Iterator<Item = TemporalUnit> { |
||||
[ |
||||
TemporalUnit::Hour, |
||||
TemporalUnit::Minute, |
||||
TemporalUnit::Second, |
||||
TemporalUnit::Millisecond, |
||||
TemporalUnit::Microsecond, |
||||
TemporalUnit::Nanosecond, |
||||
] |
||||
.iter() |
||||
.copied() |
||||
} |
||||
|
||||
fn date_units() -> impl Iterator<Item = TemporalUnit> { |
||||
[ |
||||
TemporalUnit::Year, |
||||
TemporalUnit::Month, |
||||
TemporalUnit::Week, |
||||
TemporalUnit::Day, |
||||
] |
||||
.iter() |
||||
.copied() |
||||
} |
||||
|
||||
fn datetime_units() -> impl Iterator<Item = TemporalUnit> { |
||||
date_units().chain(time_units()) |
||||
} |
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
||||
pub(crate) enum TemporalUnit { |
||||
Auto = 0, |
||||
Nanosecond, |
||||
Microsecond, |
||||
Millisecond, |
||||
Second, |
||||
Minute, |
||||
Hour, |
||||
Day, |
||||
Week, |
||||
Month, |
||||
Year, |
||||
} |
||||
|
||||
impl TemporalUnit { |
||||
pub(crate) fn to_maximum_rounding_increment(self) -> Option<u16> { |
||||
use TemporalUnit::{ |
||||
Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week, |
||||
Year, |
||||
}; |
||||
// 1. If unit is "year", "month", "week", or "day", then
|
||||
// a. Return undefined.
|
||||
// 2. If unit is "hour", then
|
||||
// a. Return 24.
|
||||
// 3. If unit is "minute" or "second", then
|
||||
// a. Return 60.
|
||||
// 4. Assert: unit is one of "millisecond", "microsecond", or "nanosecond".
|
||||
// 5. Return 1000.
|
||||
match self { |
||||
Year | Month | Week | Day => None, |
||||
Hour => Some(24), |
||||
Minute | Second => Some(60), |
||||
Millisecond | Microsecond | Nanosecond => Some(1000), |
||||
Auto => unreachable!(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub(crate) struct ParseTemporalUnitError; |
||||
|
||||
impl fmt::Display for ParseTemporalUnitError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid TemporalUnit") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for TemporalUnit { |
||||
type Err = ParseTemporalUnitError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"auto" => Ok(Self::Auto), |
||||
"year" | "years" => Ok(Self::Year), |
||||
"month" | "months" => Ok(Self::Month), |
||||
"week" | "weeks" => Ok(Self::Week), |
||||
"day" | "days" => Ok(Self::Day), |
||||
"hour" | "hours" => Ok(Self::Hour), |
||||
"minute" | "minutes" => Ok(Self::Minute), |
||||
"second" | "seconds" => Ok(Self::Second), |
||||
"millisecond" | "milliseconds" => Ok(Self::Millisecond), |
||||
"microsecond" | "microseconds" => Ok(Self::Microsecond), |
||||
"nanosecond" | "nanoseconds" => Ok(Self::Nanosecond), |
||||
_ => Err(ParseTemporalUnitError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl ParsableOptionType for TemporalUnit {} |
||||
|
||||
impl fmt::Display for TemporalUnit { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Auto => "auto", |
||||
Self::Year => "constrain", |
||||
Self::Month => "month", |
||||
Self::Week => "week", |
||||
Self::Day => "day", |
||||
Self::Hour => "hour", |
||||
Self::Minute => "minute", |
||||
Self::Second => "second", |
||||
Self::Millisecond => "millsecond", |
||||
Self::Microsecond => "microsecond", |
||||
Self::Nanosecond => "nanosecond", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// `ArithmeticOverflow` can also be used as an
|
||||
/// assignment overflow and consists of the "constrain"
|
||||
/// and "reject" options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||
pub(crate) enum ArithmeticOverflow { |
||||
Constrain, |
||||
Reject, |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub(crate) struct ParseArithmeticOverflowError; |
||||
|
||||
impl fmt::Display for ParseArithmeticOverflowError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid overflow value") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for ArithmeticOverflow { |
||||
type Err = ParseArithmeticOverflowError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"constrain" => Ok(Self::Constrain), |
||||
"reject" => Ok(Self::Reject), |
||||
_ => Err(ParseArithmeticOverflowError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl ParsableOptionType for ArithmeticOverflow {} |
||||
|
||||
impl fmt::Display for ArithmeticOverflow { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Constrain => "constrain", |
||||
Self::Reject => "reject", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// `Duration` overflow options.
|
||||
pub(crate) enum DurationOverflow { |
||||
Constrain, |
||||
Balance, |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub(crate) struct ParseDurationOverflowError; |
||||
|
||||
impl fmt::Display for ParseDurationOverflowError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid duration overflow value") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for DurationOverflow { |
||||
type Err = ParseDurationOverflowError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"constrain" => Ok(Self::Constrain), |
||||
"balance" => Ok(Self::Balance), |
||||
_ => Err(ParseDurationOverflowError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl ParsableOptionType for DurationOverflow {} |
||||
|
||||
impl fmt::Display for DurationOverflow { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Constrain => "constrain", |
||||
Self::Balance => "balance", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// The disambiguation options for an instant.
|
||||
pub(crate) enum InstantDisambiguation { |
||||
Compatible, |
||||
Earlier, |
||||
Later, |
||||
Reject, |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub(crate) struct ParseInstantDisambiguationError; |
||||
|
||||
impl fmt::Display for ParseInstantDisambiguationError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid instant disambiguation value") |
||||
} |
||||
} |
||||
impl FromStr for InstantDisambiguation { |
||||
type Err = ParseInstantDisambiguationError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"compatible" => Ok(Self::Compatible), |
||||
"earlier" => Ok(Self::Earlier), |
||||
"later" => Ok(Self::Later), |
||||
"reject" => Ok(Self::Reject), |
||||
_ => Err(ParseInstantDisambiguationError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl ParsableOptionType for InstantDisambiguation {} |
||||
|
||||
impl fmt::Display for InstantDisambiguation { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Compatible => "compatible", |
||||
Self::Earlier => "earlier", |
||||
Self::Later => "later", |
||||
Self::Reject => "reject", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
||||
|
||||
/// Offset disambiguation options.
|
||||
pub(crate) enum OffsetDisambiguation { |
||||
Use, |
||||
Prefer, |
||||
Ignore, |
||||
Reject, |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub(crate) struct ParseOffsetDisambiguationError; |
||||
|
||||
impl fmt::Display for ParseOffsetDisambiguationError { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
f.write_str("provided string was not a valid offset disambiguation value") |
||||
} |
||||
} |
||||
|
||||
impl FromStr for OffsetDisambiguation { |
||||
type Err = ParseOffsetDisambiguationError; |
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||
match s { |
||||
"use" => Ok(Self::Use), |
||||
"prefer" => Ok(Self::Prefer), |
||||
"ignore" => Ok(Self::Ignore), |
||||
"reject" => Ok(Self::Reject), |
||||
_ => Err(ParseOffsetDisambiguationError), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl ParsableOptionType for OffsetDisambiguation {} |
||||
|
||||
impl fmt::Display for OffsetDisambiguation { |
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||
match self { |
||||
Self::Use => "use", |
||||
Self::Prefer => "prefer", |
||||
Self::Ignore => "ignore", |
||||
Self::Reject => "reject", |
||||
} |
||||
.fmt(f) |
||||
} |
||||
} |
@ -0,0 +1,236 @@
|
||||
//! An `IsoDateRecord` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
|
||||
|
||||
use crate::{ |
||||
builtins::temporal::{self, TemporalFields}, |
||||
JsNativeError, JsResult, JsString, |
||||
}; |
||||
|
||||
use icu_calendar::{Date, Iso}; |
||||
|
||||
// TODO: Move ISODateRecord to a more generalized location.
|
||||
|
||||
// TODO: Determine whether month/day should be u8 or i32.
|
||||
|
||||
/// `IsoDateRecord` serves as an record for the `[[ISOYear]]`, `[[ISOMonth]]`,
|
||||
/// and `[[ISODay]]` internal fields.
|
||||
///
|
||||
/// These fields are used for the `Temporal.PlainDate` object, the
|
||||
/// `Temporal.YearMonth` object, and the `Temporal.MonthDay` object.
|
||||
#[derive(Debug, Clone, Copy, Default)] |
||||
pub(crate) struct IsoDateRecord { |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
} |
||||
|
||||
// TODO: determine whether the below is neccessary.
|
||||
impl IsoDateRecord { |
||||
pub(crate) const fn year(&self) -> i32 { |
||||
self.year |
||||
} |
||||
pub(crate) const fn month(&self) -> i32 { |
||||
self.month |
||||
} |
||||
pub(crate) const fn day(&self) -> i32 { |
||||
self.day |
||||
} |
||||
} |
||||
|
||||
impl IsoDateRecord { |
||||
// TODO: look into using Date<Iso> across the board...TBD.
|
||||
/// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date<Iso>` struct.
|
||||
pub(crate) fn from_date_iso(date: Date<Iso>) -> Self { |
||||
Self { |
||||
year: date.year().number, |
||||
month: date.month().ordinal as i32, |
||||
day: i32::from(date.days_in_month()), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl IsoDateRecord { |
||||
/// 3.5.2 `CreateISODateRecord`
|
||||
pub(crate) const fn new(year: i32, month: i32, day: i32) -> Self { |
||||
Self { year, month, day } |
||||
} |
||||
|
||||
/// 3.5.6 `RegulateISODate`
|
||||
pub(crate) fn from_unregulated( |
||||
year: i32, |
||||
month: i32, |
||||
day: i32, |
||||
overflow: &JsString, |
||||
) -> JsResult<Self> { |
||||
match overflow.to_std_string_escaped().as_str() { |
||||
"constrain" => { |
||||
let m = month.clamp(1, 12); |
||||
let days_in_month = temporal::calendar::utils::iso_days_in_month(year, month); |
||||
let d = day.clamp(1, days_in_month); |
||||
Ok(Self::new(year, m, d)) |
||||
} |
||||
"reject" => { |
||||
let date = Self::new(year, month, day); |
||||
if !date.is_valid() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("not a valid ISO date.") |
||||
.into()); |
||||
} |
||||
Ok(date) |
||||
} |
||||
_ => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
/// 12.2.35 `ISODateFromFields ( fields, overflow )`
|
||||
///
|
||||
/// Note: fields.month must be resolved prior to using `from_temporal_fields`
|
||||
pub(crate) fn from_temporal_fields( |
||||
fields: &TemporalFields, |
||||
overflow: &JsString, |
||||
) -> JsResult<Self> { |
||||
Self::from_unregulated( |
||||
fields.year().expect("Cannot fail per spec"), |
||||
fields.month().expect("cannot fail after resolution"), |
||||
fields.day().expect("cannot fail per spec"), |
||||
overflow, |
||||
) |
||||
} |
||||
|
||||
/// Create a Month-Day record from a `TemporalFields` object.
|
||||
pub(crate) fn month_day_from_temporal_fields( |
||||
fields: &TemporalFields, |
||||
overflow: &JsString, |
||||
) -> JsResult<Self> { |
||||
match fields.year() { |
||||
Some(year) => Self::from_unregulated( |
||||
year, |
||||
fields.month().expect("month must exist."), |
||||
fields.day().expect("cannot fail per spec"), |
||||
overflow, |
||||
), |
||||
None => Self::from_unregulated( |
||||
1972, |
||||
fields.month().expect("cannot fail per spec"), |
||||
fields.day().expect("cannot fail per spec."), |
||||
overflow, |
||||
), |
||||
} |
||||
} |
||||
|
||||
/// Within `YearMonth` valid limits
|
||||
pub(crate) const fn within_year_month_limits(&self) -> bool { |
||||
if self.year < -271_821 || self.year > 275_760 { |
||||
return false; |
||||
} |
||||
|
||||
if self.year == -271_821 && self.month < 4 { |
||||
return false; |
||||
} |
||||
|
||||
if self.year == 275_760 && self.month > 9 { |
||||
return true; |
||||
} |
||||
|
||||
true |
||||
} |
||||
|
||||
/// 3.5.5 `DifferenceISODate`
|
||||
pub(crate) fn diff_iso_date( |
||||
&self, |
||||
o: &Self, |
||||
largest_unit: &JsString, |
||||
) -> JsResult<temporal::duration::DurationRecord> { |
||||
debug_assert!(self.is_valid()); |
||||
// TODO: Implement on `ICU4X`.
|
||||
|
||||
Err(JsNativeError::range() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// 3.5.7 `IsValidISODate`
|
||||
pub(crate) fn is_valid(&self) -> bool { |
||||
if self.month < 1 || self.month > 12 { |
||||
return false; |
||||
} |
||||
|
||||
let days_in_month = temporal::calendar::utils::iso_days_in_month(self.year, self.month); |
||||
|
||||
if self.day < 1 || self.day > days_in_month { |
||||
return false; |
||||
} |
||||
true |
||||
} |
||||
|
||||
/// 13.2 `IsoDateToEpochDays`
|
||||
pub(crate) fn as_epoch_days(&self) -> i32 { |
||||
// 1. Let resolvedYear be year + floor(month / 12).
|
||||
let resolved_year = self.year + (f64::from(self.month) / 12_f64).floor() as i32; |
||||
// 2. Let resolvedMonth be month modulo 12.
|
||||
let resolved_month = self.month % 12; |
||||
|
||||
// 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1.
|
||||
let year_t = temporal::date_equations::epoch_time_for_year(resolved_year); |
||||
let month_t = temporal::date_equations::epoch_time_for_month_given_year( |
||||
resolved_month, |
||||
resolved_year, |
||||
); |
||||
|
||||
// 4. Return EpochTimeToDayNumber(t) + date - 1.
|
||||
temporal::date_equations::epoch_time_to_day_number(year_t + month_t) + self.day - 1 |
||||
} |
||||
|
||||
// NOTE: Implementing as mut self so balance is applied to self, but TBD.
|
||||
/// 3.5.8 `BalanceIsoDate`
|
||||
pub(crate) fn balance(&mut self) { |
||||
let epoch_days = self.as_epoch_days(); |
||||
let ms = temporal::epoch_days_to_epoch_ms(epoch_days, 0); |
||||
|
||||
// Balance current values
|
||||
self.year = temporal::date_equations::epoch_time_to_epoch_year(ms); |
||||
self.month = temporal::date_equations::epoch_time_to_month_in_year(ms); |
||||
self.day = temporal::date_equations::epoch_time_to_date(ms); |
||||
} |
||||
|
||||
// NOTE: Used in AddISODate only, so could possibly be deleted in the future.
|
||||
/// 9.5.4 `BalanceISOYearMonth ( year, month )`
|
||||
pub(crate) fn balance_year_month(&mut self) { |
||||
self.year += (self.month - 1) / 12; |
||||
self.month = ((self.month - 1) % 12) + 1; |
||||
} |
||||
|
||||
/// 3.5.11 `AddISODate ( year, month, day, years, months, weeks, days, overflow )`
|
||||
pub(crate) fn add_iso_date( |
||||
&self, |
||||
years: i32, |
||||
months: i32, |
||||
weeks: i32, |
||||
days: i32, |
||||
overflow: &JsString, |
||||
) -> JsResult<Self> { |
||||
// 1. Assert: year, month, day, years, months, weeks, and days are integers.
|
||||
// 2. Assert: overflow is either "constrain" or "reject".
|
||||
let mut intermediate = Self::new(self.year + years, self.month + months, 0); |
||||
|
||||
// 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months).
|
||||
intermediate.balance_year_month(); |
||||
|
||||
// 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow).
|
||||
let mut new_date = Self::from_unregulated( |
||||
intermediate.year(), |
||||
intermediate.month(), |
||||
self.day, |
||||
overflow, |
||||
)?; |
||||
|
||||
// 5. Set days to days + 7 × weeks.
|
||||
// 6. Let d be intermediate.[[Day]] + days.
|
||||
let additional_days = days + (weeks * 7); |
||||
new_date.day += additional_days; |
||||
|
||||
// 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
|
||||
new_date.balance(); |
||||
|
||||
Ok(new_date) |
||||
} |
||||
} |
@ -0,0 +1,567 @@
|
||||
//! Boa's implementation of the ECMAScript `Temporal.PlainDate` builtin object.
|
||||
#![allow(dead_code, unused_variables)] |
||||
|
||||
use crate::{ |
||||
builtins::{ |
||||
options::{get_option, get_options_object}, |
||||
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, |
||||
}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
js_string, |
||||
object::{internal_methods::get_prototype_from_constructor, ObjectData}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::{common::StaticJsStrings, utf16}, |
||||
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_parser::temporal::{IsoCursor, TemporalDateTimeString}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
use super::{options::ArithmeticOverflow, plain_date::iso::IsoDateRecord, plain_date_time}; |
||||
|
||||
pub(crate) mod iso; |
||||
|
||||
/// The `Temporal.PlainDate` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct PlainDate { |
||||
pub(crate) inner: IsoDateRecord, |
||||
pub(crate) calendar: JsValue, // Calendar can probably be stored as a JsObject.
|
||||
} |
||||
|
||||
impl BuiltInObject for PlainDate { |
||||
const NAME: JsString = StaticJsStrings::PLAIN_DATE; |
||||
} |
||||
|
||||
impl IntrinsicObject for PlainDate { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
let get_calendar_id = BuiltInBuilder::callable(realm, Self::get_calendar_id) |
||||
.name(js_string!("get calendarId")) |
||||
.build(); |
||||
|
||||
let get_year = BuiltInBuilder::callable(realm, Self::get_year) |
||||
.name(js_string!("get year")) |
||||
.build(); |
||||
|
||||
let get_month = BuiltInBuilder::callable(realm, Self::get_month) |
||||
.name(js_string!("get month")) |
||||
.build(); |
||||
|
||||
let get_month_code = BuiltInBuilder::callable(realm, Self::get_month_code) |
||||
.name(js_string!("get monthCode")) |
||||
.build(); |
||||
|
||||
let get_day = BuiltInBuilder::callable(realm, Self::get_day) |
||||
.name(js_string!("get day")) |
||||
.build(); |
||||
|
||||
let get_day_of_week = BuiltInBuilder::callable(realm, Self::get_day_of_week) |
||||
.name(js_string!("get dayOfWeek")) |
||||
.build(); |
||||
|
||||
let get_day_of_year = BuiltInBuilder::callable(realm, Self::get_day_of_year) |
||||
.name(js_string!("get dayOfYear")) |
||||
.build(); |
||||
|
||||
let get_week_of_year = BuiltInBuilder::callable(realm, Self::get_week_of_year) |
||||
.name(js_string!("get weekOfYear")) |
||||
.build(); |
||||
|
||||
let get_year_of_week = BuiltInBuilder::callable(realm, Self::get_year_of_week) |
||||
.name(js_string!("get yearOfWeek")) |
||||
.build(); |
||||
|
||||
let get_days_in_week = BuiltInBuilder::callable(realm, Self::get_days_in_week) |
||||
.name(js_string!("get daysInWeek")) |
||||
.build(); |
||||
|
||||
let get_days_in_month = BuiltInBuilder::callable(realm, Self::get_days_in_month) |
||||
.name(js_string!("get daysInMonth")) |
||||
.build(); |
||||
|
||||
let get_days_in_year = BuiltInBuilder::callable(realm, Self::get_days_in_year) |
||||
.name(js_string!("get daysInYear")) |
||||
.build(); |
||||
|
||||
let get_months_in_year = BuiltInBuilder::callable(realm, Self::get_months_in_year) |
||||
.name(js_string!("get monthsInYear")) |
||||
.build(); |
||||
|
||||
let get_in_leap_year = BuiltInBuilder::callable(realm, Self::get_in_leap_year) |
||||
.name(js_string!("get inLeapYear")) |
||||
.build(); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::CONFIGURABLE, |
||||
) |
||||
.accessor( |
||||
utf16!("calendarId"), |
||||
Some(get_calendar_id), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor(utf16!("year"), Some(get_year), None, Attribute::default()) |
||||
.accessor(utf16!("month"), Some(get_month), None, Attribute::default()) |
||||
.accessor( |
||||
utf16!("monthCode"), |
||||
Some(get_month_code), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor(utf16!("day"), Some(get_day), None, Attribute::default()) |
||||
.accessor( |
||||
utf16!("dayOfWeek"), |
||||
Some(get_day_of_week), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("dayOfYear"), |
||||
Some(get_day_of_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("weekOfYear"), |
||||
Some(get_week_of_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("yearOfWeek"), |
||||
Some(get_year_of_week), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("daysInWeek"), |
||||
Some(get_days_in_week), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("daysInMonth"), |
||||
Some(get_days_in_month), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("daysInYear"), |
||||
Some(get_days_in_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("monthsInYear"), |
||||
Some(get_months_in_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("inLeapYear"), |
||||
Some(get_in_leap_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.method(Self::to_plain_year_month, js_string!("toPlainYearMonth"), 0) |
||||
.method(Self::to_plain_month_day, js_string!("toPlainMonthDay"), 0) |
||||
.method(Self::get_iso_fields, js_string!("getISOFields"), 0) |
||||
.method(Self::get_calendar, js_string!("getCalendar"), 0) |
||||
.method(Self::add, js_string!("add"), 2) |
||||
.method(Self::subtract, js_string!("subtract"), 2) |
||||
.method(Self::with, js_string!("with"), 2) |
||||
.method(Self::with_calendar, js_string!("withCalendar"), 1) |
||||
.method(Self::until, js_string!("until"), 2) |
||||
.method(Self::since, js_string!("since"), 2) |
||||
.method(Self::equals, js_string!("equals"), 1) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for PlainDate { |
||||
const LENGTH: usize = 0; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::plain_date; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
if new_target.is_undefined() { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("NewTarget cannot be undefined.") |
||||
.into()); |
||||
}; |
||||
|
||||
let iso_year = super::to_integer_with_truncation(args.get_or_undefined(0), context)?; |
||||
let iso_month = super::to_integer_with_truncation(args.get_or_undefined(1), context)?; |
||||
let iso_day = super::to_integer_with_truncation(args.get_or_undefined(2), context)?; |
||||
let default_calendar = JsValue::from(js_string!("iso8601")); |
||||
let calendar_like = args.get(3).unwrap_or(&default_calendar); |
||||
|
||||
let iso = IsoDateRecord::new(iso_year, iso_month, iso_day); |
||||
|
||||
Ok(create_temporal_date(iso, calendar_like.clone(), Some(new_target), context)?.into()) |
||||
} |
||||
} |
||||
|
||||
// -- `PlainDate` getter methods --
|
||||
impl PlainDate { |
||||
fn get_calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_month(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_month_code(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_day_of_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_day_of_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_week_of_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_year_of_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_days_in_week(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_days_in_month(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_days_in_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_months_in_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_in_leap_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// ==== `PlainDate.prototype` method implementation ====
|
||||
|
||||
impl PlainDate { |
||||
fn to_plain_year_month( |
||||
this: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn to_plain_month_day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_iso_fields(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_calendar(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn add(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn subtract(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn with(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn with_calendar(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn until(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn since(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn equals(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// -- `PlainDate` Abstract Operations --
|
||||
|
||||
// 3.5.2 `CreateIsoDateRecord`
|
||||
// Implemented on `IsoDateRecord`
|
||||
|
||||
/// 3.5.3 `CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] )`
|
||||
pub(crate) fn create_temporal_date( |
||||
iso_date: IsoDateRecord, |
||||
calendar: JsValue, |
||||
new_target: Option<&JsValue>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsObject> { |
||||
// 1. If IsValidISODate(isoYear, isoMonth, isoDay) is false, throw a RangeError exception.
|
||||
if !iso_date.is_valid() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("Date is not a valid ISO date.") |
||||
.into()); |
||||
}; |
||||
|
||||
let iso_date_time = plain_date_time::iso::IsoDateTimeRecord::default() |
||||
.with_date(iso_date.year(), iso_date.month(), iso_date.day()) |
||||
.with_time(12, 0, 0, 0, 0, 0); |
||||
|
||||
// 2. If ISODateTimeWithinLimits(isoYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception.
|
||||
if iso_date_time.is_valid() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("Date is not within ISO date time limits.") |
||||
.into()); |
||||
} |
||||
|
||||
// 3. If newTarget is not present, set newTarget to %Temporal.PlainDate%.
|
||||
let new_target = if let Some(new_target) = new_target { |
||||
new_target.clone() |
||||
} else { |
||||
context |
||||
.realm() |
||||
.intrinsics() |
||||
.constructors() |
||||
.plain_date() |
||||
.constructor() |
||||
.into() |
||||
}; |
||||
|
||||
// 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainDate.prototype%", « [[InitializedTemporalDate]], [[ISOYear]], [[ISOMonth]], [[ISODay]], [[Calendar]] »).
|
||||
let prototype = |
||||
get_prototype_from_constructor(&new_target, StandardConstructors::plain_date, context)?; |
||||
|
||||
// 5. Set object.[[ISOYear]] to isoYear.
|
||||
// 6. Set object.[[ISOMonth]] to isoMonth.
|
||||
// 7. Set object.[[ISODay]] to isoDay.
|
||||
// 8. Set object.[[Calendar]] to calendar.
|
||||
let obj = JsObject::from_proto_and_data( |
||||
prototype, |
||||
ObjectData::plain_date(PlainDate { |
||||
inner: iso_date, |
||||
calendar, |
||||
}), |
||||
); |
||||
|
||||
// 9. Return object.
|
||||
Ok(obj) |
||||
} |
||||
|
||||
/// 3.5.4 `ToTemporalDate ( item [ , options ] )`
|
||||
///
|
||||
/// Converts an ambiguous `JsValue` into a `PlainDate`
|
||||
pub(crate) fn to_temporal_date( |
||||
item: &JsValue, |
||||
options: Option<JsValue>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<PlainDate> { |
||||
// 1. If options is not present, set options to undefined.
|
||||
let options = options.unwrap_or(JsValue::undefined()); |
||||
|
||||
// 2. Assert: Type(options) is Object or Undefined.
|
||||
// 3. If options is not undefined, set options to ? SnapshotOwnProperties(? GetOptionsObject(options), null).
|
||||
let options_obj = get_options_object(&options)?; |
||||
|
||||
// 4. If Type(item) is Object, then
|
||||
if let Some(object) = item.as_object() { |
||||
// a. If item has an [[InitializedTemporalDate]] internal slot, then
|
||||
if object.is_plain_date() { |
||||
// i. Return item.
|
||||
let obj = object.borrow(); |
||||
let date = obj.as_plain_date().expect("obj must be a PlainDate."); |
||||
return Ok(PlainDate { |
||||
inner: date.inner, |
||||
calendar: date.calendar.clone(), |
||||
}); |
||||
// b. If item has an [[InitializedTemporalZonedDateTime]] internal slot, then
|
||||
} else if object.is_zoned_date_time() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("ZonedDateTime not yet implemented.") |
||||
.into()); |
||||
// i. Perform ? ToTemporalOverflow(options).
|
||||
// ii. Let instant be ! CreateTemporalInstant(item.[[Nanoseconds]]).
|
||||
// iii. Let plainDateTime be ? GetPlainDateTimeFor(item.[[TimeZone]], instant, item.[[Calendar]]).
|
||||
// iv. Return ! CreateTemporalDate(plainDateTime.[[ISOYear]], plainDateTime.[[ISOMonth]], plainDateTime.[[ISODay]], plainDateTime.[[Calendar]]).
|
||||
|
||||
// c. If item has an [[InitializedTemporalDateTime]] internal slot, then
|
||||
} else if object.is_plain_date_time() { |
||||
// i. Perform ? ToTemporalOverflow(options).
|
||||
let _o = get_option(&options_obj, utf16!("overflow"), context)? |
||||
.unwrap_or(ArithmeticOverflow::Constrain); |
||||
|
||||
let obj = object.borrow(); |
||||
let date_time = obj |
||||
.as_plain_date_time() |
||||
.expect("obj must be a PlainDateTime"); |
||||
|
||||
let iso = date_time.inner.iso_date(); |
||||
let calendar = date_time.calendar.clone(); |
||||
|
||||
drop(obj); |
||||
|
||||
// ii. Return ! CreateTemporalDate(item.[[ISOYear]], item.[[ISOMonth]], item.[[ISODay]], item.[[Calendar]]).
|
||||
return Ok(PlainDate { |
||||
inner: iso, |
||||
calendar, |
||||
}); |
||||
} |
||||
|
||||
// d. Let calendar be ? GetTemporalCalendarSlotValueWithISODefault(item).
|
||||
// e. Let fieldNames be ? CalendarFields(calendar, « "day", "month", "monthCode", "year" »).
|
||||
// f. Let fields be ? PrepareTemporalFields(item, fieldNames, «»).
|
||||
// g. Return ? CalendarDateFromFields(calendar, fields, options).
|
||||
return Err(JsNativeError::error() |
||||
.with_message("CalendarDateFields not yet implemented.") |
||||
.into()); |
||||
} |
||||
|
||||
// 5. If item is not a String, throw a TypeError exception.
|
||||
match item { |
||||
JsValue::String(s) => { |
||||
// 6. Let result be ? ParseTemporalDateString(item).
|
||||
let result = TemporalDateTimeString::parse( |
||||
false, |
||||
&mut IsoCursor::new(&s.to_std_string_escaped()), |
||||
) |
||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
||||
|
||||
// 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true.
|
||||
// 8. Let calendar be result.[[Calendar]].
|
||||
// 9. If calendar is undefined, set calendar to "iso8601".
|
||||
let identifier = result |
||||
.date |
||||
.calendar |
||||
.map_or_else(|| js_string!("iso8601"), JsString::from); |
||||
|
||||
// 10. If IsBuiltinCalendar(calendar) is false, throw a RangeError exception.
|
||||
if !super::calendar::is_builtin_calendar(&identifier) { |
||||
return Err(JsNativeError::range() |
||||
.with_message("not a valid calendar identifier.") |
||||
.into()); |
||||
} |
||||
|
||||
// TODO: impl to ASCII-lowercase on JsStirng
|
||||
// 11. Set calendar to the ASCII-lowercase of calendar.
|
||||
|
||||
// 12. Perform ? ToTemporalOverflow(options).
|
||||
let _result = get_option(&options_obj, utf16!("overflow"), context)? |
||||
.unwrap_or(ArithmeticOverflow::Constrain); |
||||
|
||||
// 13. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar).
|
||||
Ok(PlainDate { |
||||
inner: IsoDateRecord::new(result.date.year, result.date.month, result.date.day), |
||||
calendar: identifier.into(), |
||||
}) |
||||
} |
||||
_ => Err(JsNativeError::typ() |
||||
.with_message("ToTemporalDate item must be an object or string.") |
||||
.into()), |
||||
} |
||||
} |
||||
|
||||
// 3.5.5. DifferenceIsoDate
|
||||
// Implemented on IsoDateRecord.
|
||||
|
||||
// 3.5.6 RegulateIsoDate
|
||||
// Implemented on IsoDateRecord.
|
||||
|
||||
// 3.5.7 IsValidIsoDate
|
||||
// Implemented on IsoDateRecord.
|
||||
|
||||
// 3.5.8 BalanceIsoDate
|
||||
// Implemented on IsoDateRecord.
|
||||
|
||||
// 3.5.11 AddISODate ( year, month, day, years, months, weeks, days, overflow )
|
||||
// Implemented on IsoDateRecord
|
@ -0,0 +1,100 @@
|
||||
use crate::{ |
||||
builtins::{ |
||||
date::utils, |
||||
temporal::{self, plain_date::iso::IsoDateRecord}, |
||||
}, |
||||
JsBigInt, |
||||
}; |
||||
|
||||
#[derive(Default, Debug, Clone)] |
||||
pub(crate) struct IsoDateTimeRecord { |
||||
iso_date: IsoDateRecord, |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
millisecond: i32, |
||||
microsecond: i32, |
||||
nanosecond: i32, |
||||
} |
||||
|
||||
impl IsoDateTimeRecord { |
||||
pub(crate) const fn iso_date(&self) -> IsoDateRecord { |
||||
self.iso_date |
||||
} |
||||
} |
||||
|
||||
// ==== `IsoDateTimeRecord` methods ====
|
||||
|
||||
impl IsoDateTimeRecord { |
||||
pub(crate) const fn with_date(mut self, year: i32, month: i32, day: i32) -> Self { |
||||
let iso_date = IsoDateRecord::new(year, month, day); |
||||
self.iso_date = iso_date; |
||||
self |
||||
} |
||||
|
||||
pub(crate) const fn with_time( |
||||
mut self, |
||||
hour: i32, |
||||
minute: i32, |
||||
second: i32, |
||||
ms: i32, |
||||
mis: i32, |
||||
ns: i32, |
||||
) -> Self { |
||||
self.hour = hour; |
||||
self.minute = minute; |
||||
self.second = second; |
||||
self.millisecond = ms; |
||||
self.microsecond = mis; |
||||
self.nanosecond = ns; |
||||
self |
||||
} |
||||
|
||||
/// 5.5.1 `ISODateTimeWithinLimits ( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond )`
|
||||
pub(crate) fn is_valid(&self) -> bool { |
||||
self.iso_date.is_valid(); |
||||
let ns = self.get_utc_epoch_ns(None).to_f64(); |
||||
|
||||
if ns <= temporal::ns_min_instant().to_f64() - (temporal::NS_PER_DAY as f64) |
||||
|| ns >= temporal::ns_max_instant().to_f64() + (temporal::NS_PER_DAY as f64) |
||||
{ |
||||
return false; |
||||
} |
||||
true |
||||
} |
||||
|
||||
/// 14.8.1 `GetUTCEpochNanoseconds`
|
||||
pub(crate) fn get_utc_epoch_ns(&self, offset_ns: Option<i64>) -> JsBigInt { |
||||
let day = utils::make_day( |
||||
i64::from(self.iso_date.year()), |
||||
i64::from(self.iso_date.month()), |
||||
i64::from(self.iso_date.day()), |
||||
) |
||||
.unwrap_or_default(); |
||||
let time = utils::make_time( |
||||
i64::from(self.hour), |
||||
i64::from(self.minute), |
||||
i64::from(self.second), |
||||
i64::from(self.millisecond), |
||||
) |
||||
.unwrap_or_default(); |
||||
|
||||
let ms = utils::make_date(day, time).unwrap_or_default(); |
||||
|
||||
let epoch_ns = match offset_ns { |
||||
Some(offset) if offset != 0 => { |
||||
let ns = (ms * 1_000_000_i64) |
||||
+ (i64::from(self.microsecond) * 1_000_i64) |
||||
+ i64::from(self.nanosecond); |
||||
ns - offset |
||||
} |
||||
_ => { |
||||
(ms * 1_000_000_i64) |
||||
+ (i64::from(self.microsecond) * 1_000_i64) |
||||
+ i64::from(self.nanosecond) |
||||
} |
||||
}; |
||||
|
||||
JsBigInt::from(epoch_ns) |
||||
} |
||||
} |
@ -0,0 +1,148 @@
|
||||
//! Boa's implementation of the ECMAScript `Temporal.PlainDateTime` builtin object.
|
||||
#![allow(dead_code, unused_variables)] |
||||
|
||||
use crate::{ |
||||
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::common::StaticJsStrings, |
||||
Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
use self::iso::IsoDateTimeRecord; |
||||
|
||||
pub(crate) mod iso; |
||||
|
||||
/// The `Temporal.PlainDateTime` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct PlainDateTime { |
||||
pub(crate) inner: IsoDateTimeRecord, |
||||
pub(crate) calendar: JsValue, |
||||
} |
||||
|
||||
impl BuiltInObject for PlainDateTime { |
||||
const NAME: JsString = StaticJsStrings::PLAIN_DATETIME; |
||||
} |
||||
|
||||
impl IntrinsicObject for PlainDateTime { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::CONFIGURABLE, |
||||
) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for PlainDateTime { |
||||
const LENGTH: usize = 0; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::plain_date_time; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::range() |
||||
.with_message("Not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// ==== `PlainDateTime` Accessor Properties ====
|
||||
|
||||
impl PlainDateTime { |
||||
fn calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn month(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn month_code(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn day(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn hour(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn minute(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn second(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn millisecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn microsecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn nanosecond(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn era(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn era_year(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("calendars not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// ==== `PlainDateTime` Abstract Operations` ====
|
||||
|
||||
// See `IsoDateTimeRecord`
|
@ -0,0 +1,122 @@
|
||||
//! Boa's implementation of the ECMAScript `Temporal.PlainMonthDay` builtin object.
|
||||
#![allow(dead_code, unused_variables)] |
||||
use crate::{ |
||||
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
object::{internal_methods::get_prototype_from_constructor, ObjectData}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::common::StaticJsStrings, |
||||
Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
use super::{plain_date::iso::IsoDateRecord, plain_date_time::iso::IsoDateTimeRecord}; |
||||
|
||||
/// The `Temporal.PlainMonthDay` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct PlainMonthDay { |
||||
pub(crate) inner: IsoDateRecord, |
||||
pub(crate) calendar: JsValue, |
||||
} |
||||
|
||||
impl BuiltInObject for PlainMonthDay { |
||||
const NAME: JsString = StaticJsStrings::PLAIN_MD; |
||||
} |
||||
|
||||
impl IntrinsicObject for PlainMonthDay { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::CONFIGURABLE, |
||||
) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for PlainMonthDay { |
||||
const LENGTH: usize = 0; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::plain_month_day; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::range() |
||||
.with_message("Not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// ==== `PlainMonthDay` Abstract Operations ====
|
||||
|
||||
pub(crate) fn create_temporal_month_day( |
||||
iso: IsoDateRecord, |
||||
calendar: JsValue, |
||||
new_target: Option<&JsValue>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If IsValidISODate(referenceISOYear, isoMonth, isoDay) is false, throw a RangeError exception.
|
||||
if iso.is_valid() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("PlainMonthDay is not a valid ISO date.") |
||||
.into()); |
||||
} |
||||
|
||||
// 2. If ISODateTimeWithinLimits(referenceISOYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0) is false, throw a RangeError exception.
|
||||
let iso_date_time = IsoDateTimeRecord::default() |
||||
.with_date(iso.year(), iso.month(), iso.day()) |
||||
.with_time(12, 0, 0, 0, 0, 0); |
||||
|
||||
if !iso_date_time.is_valid() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("PlainMonthDay is not a valid ISO date time.") |
||||
.into()); |
||||
} |
||||
|
||||
// 3. If newTarget is not present, set newTarget to %Temporal.PlainMonthDay%.
|
||||
let new_target = if let Some(target) = new_target { |
||||
target.clone() |
||||
} else { |
||||
context |
||||
.realm() |
||||
.intrinsics() |
||||
.constructors() |
||||
.plain_month_day() |
||||
.constructor() |
||||
.into() |
||||
}; |
||||
|
||||
// 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainMonthDay.prototype%", « [[InitializedTemporalMonthDay]], [[ISOMonth]], [[ISODay]], [[ISOYear]], [[Calendar]] »).
|
||||
let proto = get_prototype_from_constructor( |
||||
&new_target, |
||||
StandardConstructors::plain_month_day, |
||||
context, |
||||
)?; |
||||
|
||||
// 5. Set object.[[ISOMonth]] to isoMonth.
|
||||
// 6. Set object.[[ISODay]] to isoDay.
|
||||
// 7. Set object.[[Calendar]] to calendar.
|
||||
// 8. Set object.[[ISOYear]] to referenceISOYear.
|
||||
let obj = JsObject::from_proto_and_data( |
||||
proto, |
||||
ObjectData::plain_month_day(PlainMonthDay { |
||||
inner: iso, |
||||
calendar, |
||||
}), |
||||
); |
||||
|
||||
// 9. Return object.
|
||||
Ok(obj.into()) |
||||
} |
@ -0,0 +1,62 @@
|
||||
//! Boa's implementation of the ECMAScript `Temporal.PlainTime` builtin object.
|
||||
#![allow(dead_code, unused_variables)] |
||||
|
||||
use crate::{ |
||||
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::common::StaticJsStrings, |
||||
Context, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
/// The `Temporal.PlainTime` object.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct PlainTime { |
||||
iso_hour: i32, // integer between 0-23
|
||||
iso_minute: i32, // integer between 0-59
|
||||
iso_second: i32, // integer between 0-59
|
||||
iso_millisecond: i32, // integer between 0-999
|
||||
iso_microsecond: i32, // integer between 0-999
|
||||
iso_nanosecond: i32, // integer between 0-999
|
||||
} |
||||
|
||||
impl BuiltInObject for PlainTime { |
||||
const NAME: JsString = StaticJsStrings::PLAIN_TIME; |
||||
} |
||||
|
||||
impl IntrinsicObject for PlainTime { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::CONFIGURABLE, |
||||
) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for PlainTime { |
||||
const LENGTH: usize = 0; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::plain_time; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::range() |
||||
.with_message("Not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
@ -0,0 +1,327 @@
|
||||
//! Boa's implementation of the `Temporal.PlainYearMonth` builtin object.
|
||||
|
||||
use crate::{ |
||||
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
js_string, |
||||
object::{internal_methods::get_prototype_from_constructor, ObjectData}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::{common::StaticJsStrings, utf16}, |
||||
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
use super::plain_date::iso::IsoDateRecord; |
||||
|
||||
/// The `Temporal.PlainYearMonth` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct PlainYearMonth { |
||||
pub(crate) inner: IsoDateRecord, |
||||
pub(crate) calendar: JsValue, |
||||
} |
||||
|
||||
impl BuiltInObject for PlainYearMonth { |
||||
const NAME: JsString = StaticJsStrings::PLAIN_YM; |
||||
} |
||||
|
||||
impl IntrinsicObject for PlainYearMonth { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
let get_calendar_id = BuiltInBuilder::callable(realm, Self::get_calendar_id) |
||||
.name(js_string!("get calendarId")) |
||||
.build(); |
||||
|
||||
let get_year = BuiltInBuilder::callable(realm, Self::get_year) |
||||
.name(js_string!("get year")) |
||||
.build(); |
||||
|
||||
let get_month = BuiltInBuilder::callable(realm, Self::get_month) |
||||
.name(js_string!("get month")) |
||||
.build(); |
||||
|
||||
let get_month_code = BuiltInBuilder::callable(realm, Self::get_month_code) |
||||
.name(js_string!("get monthCode")) |
||||
.build(); |
||||
|
||||
let get_days_in_month = BuiltInBuilder::callable(realm, Self::get_days_in_month) |
||||
.name(js_string!("get daysInMonth")) |
||||
.build(); |
||||
|
||||
let get_days_in_year = BuiltInBuilder::callable(realm, Self::get_days_in_year) |
||||
.name(js_string!("get daysInYear")) |
||||
.build(); |
||||
|
||||
let get_months_in_year = BuiltInBuilder::callable(realm, Self::get_months_in_year) |
||||
.name(js_string!("get monthsInYear")) |
||||
.build(); |
||||
|
||||
let get_in_leap_year = BuiltInBuilder::callable(realm, Self::get_in_leap_year) |
||||
.name(js_string!("get inLeapYear")) |
||||
.build(); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::CONFIGURABLE, |
||||
) |
||||
.accessor( |
||||
utf16!("calendarId"), |
||||
Some(get_calendar_id), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor(utf16!("year"), Some(get_year), None, Attribute::default()) |
||||
.accessor(utf16!("month"), Some(get_month), None, Attribute::default()) |
||||
.accessor( |
||||
utf16!("monthCode"), |
||||
Some(get_month_code), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("daysInMonth"), |
||||
Some(get_days_in_month), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("daysInYear"), |
||||
Some(get_days_in_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("monthsInYear"), |
||||
Some(get_months_in_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.accessor( |
||||
utf16!("inLeapYear"), |
||||
Some(get_in_leap_year), |
||||
None, |
||||
Attribute::default(), |
||||
) |
||||
.method(Self::with, js_string!("with"), 2) |
||||
.method(Self::add, js_string!("add"), 2) |
||||
.method(Self::subtract, js_string!("subtract"), 2) |
||||
.method(Self::until, js_string!("until"), 2) |
||||
.method(Self::since, js_string!("since"), 2) |
||||
.method(Self::equals, js_string!("equals"), 1) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for PlainYearMonth { |
||||
const LENGTH: usize = 0; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::plain_year_month; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If NewTarget is undefined, then
|
||||
if new_target.is_undefined() { |
||||
// a. Throw a TypeError exception.
|
||||
return Err(JsNativeError::typ() |
||||
.with_message("NewTarget cannot be undefined when constructing a PlainYearMonth.") |
||||
.into()); |
||||
} |
||||
|
||||
let day = args.get_or_undefined(3); |
||||
// 2. If referenceISODay is undefined, then
|
||||
let ref_day = if day.is_undefined() { |
||||
// a. Set referenceISODay to 1𝔽.
|
||||
1 |
||||
} else { |
||||
// 6. Let ref be ? ToIntegerWithTruncation(referenceISODay).
|
||||
super::to_integer_with_truncation(day, context)? |
||||
}; |
||||
|
||||
// 3. Let y be ? ToIntegerWithTruncation(isoYear).
|
||||
let y = super::to_integer_with_truncation(args.get_or_undefined(0), context)?; |
||||
// 4. Let m be ? ToIntegerWithTruncation(isoMonth).
|
||||
let m = super::to_integer_with_truncation(args.get_or_undefined(1), context)?; |
||||
|
||||
// TODO: calendar handling.
|
||||
// 5. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601").
|
||||
|
||||
// 7. Return ? CreateTemporalYearMonth(y, m, calendar, ref, NewTarget).
|
||||
let record = IsoDateRecord::new(y, m, ref_day); |
||||
create_temporal_year_month( |
||||
record, |
||||
JsValue::from(js_string!("iso8601")), |
||||
Some(new_target), |
||||
context, |
||||
) |
||||
} |
||||
} |
||||
|
||||
// ==== `PlainYearMonth` Accessor Implementations ====
|
||||
|
||||
impl PlainYearMonth { |
||||
fn get_calendar_id(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_year(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_month(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_month_code(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_days_in_year(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_days_in_month(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_months_in_year( |
||||
_this: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn get_in_leap_year(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// ==== `PlainYearMonth` Method Implementations ====
|
||||
|
||||
impl PlainYearMonth { |
||||
fn with(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::typ() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn add(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::typ() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn subtract(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::typ() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn until(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::typ() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn since(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::typ() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
fn equals(_this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Err(JsNativeError::typ() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// ==== Abstract Operations ====
|
||||
|
||||
// 9.5.2 `RegulateISOYearMonth ( year, month, overflow )`
|
||||
// Implemented on `TemporalFields`.
|
||||
|
||||
// 9.5.5 `CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , newTarget ] )`
|
||||
pub(crate) fn create_temporal_year_month( |
||||
year_month_record: IsoDateRecord, |
||||
calendar: JsValue, |
||||
new_target: Option<&JsValue>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If IsValidISODate(isoYear, isoMonth, referenceISODay) is false, throw a RangeError exception.
|
||||
if !year_month_record.is_valid() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("PlainYearMonth values are not a valid ISO date.") |
||||
.into()); |
||||
} |
||||
|
||||
// 2. If ! ISOYearMonthWithinLimits(isoYear, isoMonth) is false, throw a RangeError exception.
|
||||
if year_month_record.within_year_month_limits() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("PlainYearMonth values are not a valid ISO date.") |
||||
.into()); |
||||
} |
||||
|
||||
// 3. If newTarget is not present, set newTarget to %Temporal.PlainYearMonth%.
|
||||
let new_target = if let Some(target) = new_target { |
||||
target.clone() |
||||
} else { |
||||
context |
||||
.realm() |
||||
.intrinsics() |
||||
.constructors() |
||||
.plain_year_month() |
||||
.constructor() |
||||
.into() |
||||
}; |
||||
|
||||
// 4. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainYearMonth.prototype%", « [[InitializedTemporalYearMonth]], [[ISOYear]], [[ISOMonth]], [[ISODay]], [[Calendar]] »).
|
||||
let proto = get_prototype_from_constructor( |
||||
&new_target, |
||||
StandardConstructors::plain_year_month, |
||||
context, |
||||
)?; |
||||
|
||||
// 5. Set object.[[ISOYear]] to isoYear.
|
||||
// 6. Set object.[[ISOMonth]] to isoMonth.
|
||||
// 7. Set object.[[Calendar]] to calendar.
|
||||
// 8. Set object.[[ISODay]] to referenceISODay.
|
||||
|
||||
let obj = JsObject::from_proto_and_data( |
||||
proto, |
||||
ObjectData::plain_year_month(PlainYearMonth { |
||||
inner: year_month_record, |
||||
calendar, |
||||
}), |
||||
); |
||||
|
||||
// 9. Return object.
|
||||
Ok(obj.into()) |
||||
} |
@ -0,0 +1,52 @@
|
||||
use super::date_equations::{epoch_time_to_month_in_year, mathematical_in_leap_year}; |
||||
use crate::{js_string, run_test_actions, JsValue, TestAction}; |
||||
|
||||
// Temporal Object tests.
|
||||
|
||||
#[test] |
||||
fn temporal_object() { |
||||
// Temporal Builtin tests.
|
||||
run_test_actions([ |
||||
TestAction::assert_eq( |
||||
"Object.prototype.toString.call(Temporal)", |
||||
js_string!("[object Temporal]"), |
||||
), |
||||
TestAction::assert_eq("String(Temporal)", js_string!("[object Temporal]")), |
||||
TestAction::assert_eq("Object.keys(Temporal).length === 0", true), |
||||
]); |
||||
} |
||||
|
||||
#[test] |
||||
fn now_object() { |
||||
// Now Builtin tests.
|
||||
run_test_actions([ |
||||
TestAction::assert_eq("Object.isExtensible(Temporal.Now)", true), |
||||
TestAction::assert_eq( |
||||
"Object.prototype.toString.call(Temporal.Now)", |
||||
js_string!("[object Temporal.Now]"), |
||||
), |
||||
TestAction::assert_eq( |
||||
"Object.getPrototypeOf(Temporal.Now) === Object.prototype", |
||||
true, |
||||
), |
||||
TestAction::assert_eq("Temporal.Now.prototype", JsValue::undefined()), |
||||
]); |
||||
} |
||||
|
||||
// Date Equations
|
||||
|
||||
#[test] |
||||
fn time_to_month() { |
||||
let oct_2023 = 1_696_459_917_000_f64; |
||||
let mar_1_2020 = 1_583_020_800_000_f64; |
||||
let feb_29_2020 = 1_582_934_400_000_f64; |
||||
let mar_1_2021 = 1_614_556_800_000_f64; |
||||
|
||||
assert_eq!(epoch_time_to_month_in_year(oct_2023), 9); |
||||
assert_eq!(epoch_time_to_month_in_year(mar_1_2020), 2); |
||||
assert_eq!(mathematical_in_leap_year(mar_1_2020), 1); |
||||
assert_eq!(epoch_time_to_month_in_year(feb_29_2020), 1); |
||||
assert_eq!(mathematical_in_leap_year(feb_29_2020), 1); |
||||
assert_eq!(epoch_time_to_month_in_year(mar_1_2021), 2); |
||||
assert_eq!(mathematical_in_leap_year(mar_1_2021), 0); |
||||
} |
@ -0,0 +1,491 @@
|
||||
#![allow(dead_code)] |
||||
|
||||
use crate::{ |
||||
builtins::{ |
||||
temporal::to_zero_padded_decimal_string, BuiltInBuilder, BuiltInConstructor, BuiltInObject, |
||||
IntrinsicObject, |
||||
}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
js_string, |
||||
object::{internal_methods::get_prototype_from_constructor, ObjectData, CONSTRUCTOR}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::{common::StaticJsStrings, utf16}, |
||||
Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
/// The `Temporal.TimeZone` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct TimeZone { |
||||
pub(crate) initialized_temporal_time_zone: bool, |
||||
pub(crate) identifier: String, |
||||
pub(crate) offset_nanoseconds: Option<i64>, |
||||
} |
||||
|
||||
impl BuiltInObject for TimeZone { |
||||
const NAME: JsString = StaticJsStrings::TIMEZONE; |
||||
} |
||||
|
||||
impl IntrinsicObject for TimeZone { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
let get_id = BuiltInBuilder::callable(realm, Self::get_id) |
||||
.name(js_string!("get Id")) |
||||
.build(); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.method( |
||||
Self::get_offset_nanoseconds_for, |
||||
js_string!("getOffsetNanosecondsFor"), |
||||
1, |
||||
) |
||||
.method( |
||||
Self::get_offset_string_for, |
||||
js_string!("getOffsetStringFor"), |
||||
1, |
||||
) |
||||
.method( |
||||
Self::get_plain_date_time_for, |
||||
js_string!("getPlainDateTimeFor"), |
||||
2, |
||||
) |
||||
.method(Self::get_instant_for, js_string!("getInstantFor"), 2) |
||||
.method( |
||||
Self::get_possible_instants_for, |
||||
js_string!("getPossibleInstantFor"), |
||||
1, |
||||
) |
||||
.method( |
||||
Self::get_next_transition, |
||||
js_string!("getNextTransition"), |
||||
1, |
||||
) |
||||
.method( |
||||
Self::get_previous_transition, |
||||
js_string!("getPreviousTransition"), |
||||
1, |
||||
) |
||||
.method(Self::to_string, js_string!("toString"), 0) |
||||
.method(Self::to_string, js_string!("toJSON"), 0) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.static_property( |
||||
CONSTRUCTOR, |
||||
realm.intrinsics().constructors().time_zone().prototype(), |
||||
Attribute::default(), |
||||
) |
||||
.accessor(utf16!("id"), Some(get_id), None, Attribute::default()) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for TimeZone { |
||||
const LENGTH: usize = 1; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::time_zone; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If NewTarget is undefined, then
|
||||
// 1a. Throw a TypeError exception.
|
||||
if new_target.is_undefined() { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("newTarget cannot be undefined for Temporal.TimeZone constructor") |
||||
.into()); |
||||
}; |
||||
|
||||
// 2. Set identifier to ? ToString(identifier).
|
||||
let identifier = args.get_or_undefined(0); |
||||
if identifier.is_undefined() { |
||||
return Err(JsNativeError::range() |
||||
.with_message("Temporal.TimeZone must be called with a valid initializer") |
||||
.into()); |
||||
} |
||||
|
||||
// 3. If IsTimeZoneOffsetString(identifier) is false, then
|
||||
// a. If IsAvailableTimeZoneName(identifier) is false, then
|
||||
// i. Throw a RangeError exception.
|
||||
// b. Set identifier to ! CanonicalizeTimeZoneName(identifier).
|
||||
// 4. Return ? CreateTemporalTimeZone(identifier, NewTarget).
|
||||
create_temporal_time_zone( |
||||
identifier.to_string(context)?.to_std_string_escaped(), |
||||
Some(new_target.clone()), |
||||
context, |
||||
) |
||||
} |
||||
} |
||||
|
||||
impl TimeZone { |
||||
// NOTE: id, toJSON, toString currently share the exact same implementation -> Consolidate into one function and define multiple accesors?
|
||||
pub(crate) fn get_id(this: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let o = this.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})?; |
||||
let tz = o.as_time_zone().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})?; |
||||
Ok(JsString::from(tz.identifier.clone()).into()) |
||||
} |
||||
|
||||
pub(crate) fn get_offset_nanoseconds_for( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let timeZone be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
|
||||
let _tz = this |
||||
.as_object() |
||||
.ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})? |
||||
.borrow() |
||||
.as_time_zone() |
||||
.ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})?; |
||||
// 3. Set instant to ? ToTemporalInstant(instant).
|
||||
let _i = args.get_or_undefined(0); |
||||
// TODO: to_temporal_instant is abstract operation for Temporal.Instant objects.
|
||||
// let instant = to_temporal_instant(i)?;
|
||||
|
||||
// 4. If timeZone.[[OffsetNanoseconds]] is not undefined, return 𝔽(timeZone.[[OffsetNanoseconds]]).
|
||||
// 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])).
|
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
pub(crate) fn get_offset_string_for( |
||||
this: &JsValue, |
||||
args: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let timeZone be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
|
||||
let _tz = this |
||||
.as_object() |
||||
.ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})? |
||||
.borrow() |
||||
.as_time_zone() |
||||
.ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})?; |
||||
// 3. Set instant to ? ToTemporalInstant(instant).
|
||||
let _i = args.get_or_undefined(0); |
||||
// TODO: to_temporal_instant is abstract operation for Temporal.Instant objects.
|
||||
// let instant = to_temporal_instant(i)?;
|
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
|
||||
// 4. Return ? GetOffsetStringFor(timeZone, instant).
|
||||
} |
||||
|
||||
pub(crate) fn get_plain_date_time_for( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
pub(crate) fn get_instant_for( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
pub(crate) fn get_possible_instants_for( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
pub(crate) fn get_next_transition( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
pub(crate) fn get_previous_transition( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
pub(crate) fn to_string( |
||||
this: &JsValue, |
||||
_: &[JsValue], |
||||
_: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. Let timeZone be the this value.
|
||||
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
|
||||
let o = this.as_object().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})?; |
||||
let o = o.borrow(); |
||||
let tz = o.as_time_zone().ok_or_else(|| { |
||||
JsNativeError::typ().with_message("this value must be a Temporal.TimeZone") |
||||
})?; |
||||
// 3. Return timeZone.[[Identifier]].
|
||||
Ok(JsString::from(tz.identifier.clone()).into()) |
||||
} |
||||
} |
||||
|
||||
// -- TimeZone Abstract Operations --
|
||||
|
||||
/// Abstract operation `DefaultTimeZone ( )`
|
||||
///
|
||||
/// The abstract operation `DefaultTimeZone` takes no arguments. It returns a String value
|
||||
/// representing the host environment's current time zone, which is either a valid (11.1.1) and
|
||||
/// canonicalized (11.1.2) time zone name, or an offset conforming to the syntax of a
|
||||
/// `TimeZoneNumericUTCOffset`.
|
||||
///
|
||||
/// An ECMAScript implementation that includes the ECMA-402 Internationalization API must implement
|
||||
/// the `DefaultTimeZone` abstract operation as specified in the ECMA-402 specification.
|
||||
///
|
||||
/// More information:
|
||||
/// - [ECMAScript specififcation][spec]
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-temporal/#sec-defaulttimezone
|
||||
#[allow(unused)] |
||||
pub(super) fn default_time_zone(context: &mut Context<'_>) -> String { |
||||
// The minimum implementation of DefaultTimeZone for ECMAScript implementations that do not
|
||||
// include the ECMA-402 API, supporting only the "UTC" time zone, performs the following steps
|
||||
// when called:
|
||||
|
||||
// 1. Return "UTC".
|
||||
"UTC".to_owned() |
||||
|
||||
// TO-DO: full, system-aware implementation (and intl feature)
|
||||
} |
||||
|
||||
/// Abstract operation `CreateTemporalTimeZone ( identifier [ , newTarget ] )`
|
||||
///
|
||||
/// More information:
|
||||
/// - [ECMAScript specififcation][spec]
|
||||
///
|
||||
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-createtemporaltimezone
|
||||
#[allow(clippy::needless_pass_by_value, unused)] |
||||
pub(super) fn create_temporal_time_zone( |
||||
identifier: String, |
||||
new_target: Option<JsValue>, |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// 1. If newTarget is not present, set newTarget to %Temporal.TimeZone%.
|
||||
let new_target = new_target.unwrap_or_else(|| { |
||||
context |
||||
.realm() |
||||
.intrinsics() |
||||
.constructors() |
||||
.time_zone() |
||||
.prototype() |
||||
.into() |
||||
}); |
||||
|
||||
// 2. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.TimeZone.prototype%", « [[InitializedTemporalTimeZone]], [[Identifier]], [[OffsetNanoseconds]] »).
|
||||
let prototype = |
||||
get_prototype_from_constructor(&new_target, StandardConstructors::time_zone, context)?; |
||||
|
||||
// 3. Let offsetNanosecondsResult be Completion(ParseTimeZoneOffsetString(identifier)).
|
||||
let offset_nanoseconds_result = parse_timezone_offset_string(&identifier, context); |
||||
|
||||
// 4. If offsetNanosecondsResult is an abrupt completion, then
|
||||
let (identifier, offset_nanoseconds) = if let Ok(offset_nanoseconds) = offset_nanoseconds_result |
||||
{ |
||||
// Switched conditions for more idiomatic rust code structuring
|
||||
// 5. Else,
|
||||
// a. Set object.[[Identifier]] to ! FormatTimeZoneOffsetString(offsetNanosecondsResult.[[Value]]).
|
||||
// b. Set object.[[OffsetNanoseconds]] to offsetNanosecondsResult.[[Value]].
|
||||
( |
||||
format_time_zone_offset_string(offset_nanoseconds), |
||||
Some(offset_nanoseconds), |
||||
) |
||||
} else { |
||||
// a. Assert: ! CanonicalizeTimeZoneName(identifier) is identifier.
|
||||
assert_eq!(canonicalize_time_zone_name(&identifier), identifier); |
||||
|
||||
// b. Set object.[[Identifier]] to identifier.
|
||||
// c. Set object.[[OffsetNanoseconds]] to undefined.
|
||||
(identifier, None) |
||||
}; |
||||
|
||||
// 6. Return object.
|
||||
let object = JsObject::from_proto_and_data( |
||||
prototype, |
||||
ObjectData::time_zone(TimeZone { |
||||
initialized_temporal_time_zone: false, |
||||
identifier, |
||||
offset_nanoseconds, |
||||
}), |
||||
); |
||||
Ok(object.into()) |
||||
} |
||||
|
||||
/// Abstract operation `ParseTimeZoneOffsetString ( offsetString )`
|
||||
///
|
||||
/// The abstract operation `ParseTimeZoneOffsetString` takes argument `offsetString` (a String). It
|
||||
/// parses the argument as a numeric UTC offset string and returns a signed integer representing
|
||||
/// that offset as a number of nanoseconds.
|
||||
///
|
||||
/// More information:
|
||||
/// - [ECMAScript specififcation][spec]
|
||||
///
|
||||
/// [spec]: https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring
|
||||
#[allow(clippy::unnecessary_wraps, unused)] |
||||
fn parse_timezone_offset_string(offset_string: &str, context: &mut Context<'_>) -> JsResult<i64> { |
||||
use boa_parser::temporal::{IsoCursor, TemporalTimeZoneString}; |
||||
|
||||
// 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset).
|
||||
let parse_result = TemporalTimeZoneString::parse(&mut IsoCursor::new(offset_string))?; |
||||
|
||||
// 2. Assert: parseResult is not a List of errors.
|
||||
// 3. Assert: parseResult contains a TemporalSign Parse Node.
|
||||
let Some(utc_offset) = parse_result.offset else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("Offset string was not a valid offset") |
||||
.into()); |
||||
}; |
||||
|
||||
// 4. Let parsedSign be the source text matched by the TemporalSign Parse Node contained within
|
||||
// parseResult.
|
||||
// 5. If parsedSign is the single code point U+002D (HYPHEN-MINUS) or U+2212 (MINUS SIGN), then
|
||||
let sign = utc_offset.sign; |
||||
// a. Let sign be -1.
|
||||
// 6. Else,
|
||||
// a. Let sign be 1.
|
||||
|
||||
// 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed
|
||||
// values is guaranteed to be a sufficiently short string of decimal digits.
|
||||
// 8. Assert: parseResult contains an Hour Parse Node.
|
||||
// 9. Let parsedHours be the source text matched by the Hour Parse Node contained within parseResult.
|
||||
let parsed_hours = utc_offset.hour; |
||||
|
||||
// 10. Let hours be ℝ(StringToNumber(CodePointsToString(parsedHours))).
|
||||
// 11. If parseResult does not contain a MinuteSecond Parse Node, then
|
||||
// a. Let minutes be 0.
|
||||
// 12. Else,
|
||||
// a. Let parsedMinutes be the source text matched by the first MinuteSecond Parse Node contained within parseResult.
|
||||
// b. Let minutes be ℝ(StringToNumber(CodePointsToString(parsedMinutes))).
|
||||
// 13. If parseResult does not contain two MinuteSecond Parse Nodes, then
|
||||
// a. Let seconds be 0.
|
||||
// 14. Else,
|
||||
// a. Let parsedSeconds be the source text matched by the second MinuteSecond Parse Node contained within parseResult.
|
||||
// b. Let seconds be ℝ(StringToNumber(CodePointsToString(parsedSeconds))).
|
||||
// 15. If parseResult does not contain a TemporalDecimalFraction Parse Node, then
|
||||
// a. Let nanoseconds be 0.
|
||||
// 16. Else,
|
||||
// a. Let parsedFraction be the source text matched by the TemporalDecimalFraction Parse Node contained within parseResult.
|
||||
// b. Let fraction be the string-concatenation of CodePointsToString(parsedFraction) and "000000000".
|
||||
// c. Let nanosecondsString be the substring of fraction from 1 to 10.
|
||||
// d. Let nanoseconds be ℝ(StringToNumber(nanosecondsString)).
|
||||
// 17. Return sign × (((hours × 60 + minutes) × 60 + seconds) × 10^9 + nanoseconds).
|
||||
|
||||
Err(JsNativeError::error() |
||||
.with_message("not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// Abstract operation `FormatTimeZoneOffsetString ( offsetNanoseconds )`
|
||||
fn format_time_zone_offset_string(offset_nanoseconds: i64) -> String { |
||||
// 1. Assert: offsetNanoseconds is an integer.
|
||||
|
||||
// 2. If offsetNanoseconds ≥ 0, let sign be "+"; otherwise, let sign be "-".
|
||||
let sign = if offset_nanoseconds >= 0 { "+" } else { "-" }; |
||||
|
||||
// 3. Let offsetNanoseconds be abs(offsetNanoseconds).
|
||||
let offset_nanoseconds = offset_nanoseconds.unsigned_abs(); |
||||
|
||||
// 4. Let nanoseconds be offsetNanoseconds modulo 10^9.
|
||||
let nanoseconds = offset_nanoseconds % 1_000_000_000; |
||||
|
||||
// 5. Let seconds be floor(offsetNanoseconds / 10^9) modulo 60.
|
||||
let seconds = (offset_nanoseconds / 1_000_000_000) % 60; |
||||
|
||||
// 6. Let minutes be floor(offsetNanoseconds / (6 × 10^10)) modulo 60.
|
||||
let minutes = (offset_nanoseconds / 60_000_000_000) % 60; |
||||
|
||||
// 7. Let hours be floor(offsetNanoseconds / (3.6 × 1012)).
|
||||
let hours = (offset_nanoseconds / 3_600_000_000_000) % 60; |
||||
|
||||
// 8. Let h be ToZeroPaddedDecimalString(hours, 2).
|
||||
let h = to_zero_padded_decimal_string(hours, 2); |
||||
|
||||
// 9. Let m be ToZeroPaddedDecimalString(minutes, 2).
|
||||
let m = to_zero_padded_decimal_string(minutes, 2); |
||||
|
||||
// 10. Let s be ToZeroPaddedDecimalString(seconds, 2).
|
||||
let s = to_zero_padded_decimal_string(seconds, 2); |
||||
|
||||
// 11. If nanoseconds ≠ 0, then
|
||||
let post = if nanoseconds != 0 { |
||||
// a. Let fraction be ToZeroPaddedDecimalString(nanoseconds, 9).
|
||||
let fraction = to_zero_padded_decimal_string(nanoseconds, 9); |
||||
|
||||
// b. Set fraction to the longest possible substring of fraction starting at position 0 and not ending with the code unit 0x0030 (DIGIT ZERO).
|
||||
let fraction = fraction.trim_end_matches('0'); |
||||
|
||||
// c. Let post be the string-concatenation of the code unit 0x003A (COLON), s, the code unit 0x002E (FULL STOP), and fraction.
|
||||
format!(":{s}.{fraction}") |
||||
} else if seconds != 0 { |
||||
// 12. Else if seconds ≠ 0, then
|
||||
// a. Let post be the string-concatenation of the code unit 0x003A (COLON) and s.
|
||||
format!(":{s}") |
||||
} else { |
||||
// 13. Else,
|
||||
// a. Let post be the empty String.
|
||||
String::new() |
||||
}; |
||||
|
||||
// 14. Return the string-concatenation of sign, h, the code unit 0x003A (COLON), m, and post.
|
||||
format!("{sign}{h}:{m}{post}") |
||||
} |
||||
|
||||
/// Abstract operation `CanonicalizeTimeZoneName ( timeZone )`
|
||||
///
|
||||
/// The abstract operation `CanonicalizeTimeZoneName` takes argument `timeZone` (a String that is a
|
||||
/// valid time zone name as verified by `IsAvailableTimeZoneName`). It returns the canonical and
|
||||
/// case-regularized form of `timeZone`.
|
||||
fn canonicalize_time_zone_name(time_zone: &str) -> String { |
||||
// The minimum implementation of CanonicalizeTimeZoneName for ECMAScript implementations that
|
||||
// do not include local political rules for any time zones performs the following steps when
|
||||
// called:
|
||||
// 1. Assert: timeZone is an ASCII-case-insensitive match for "UTC".
|
||||
assert!(time_zone.to_ascii_uppercase() == "UTC"); |
||||
// 2. Return "UTC".
|
||||
"UTC".to_owned() |
||||
} |
@ -0,0 +1,133 @@
|
||||
#![allow(dead_code, unused_variables)] |
||||
use crate::{ |
||||
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, |
||||
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||
property::Attribute, |
||||
realm::Realm, |
||||
string::common::StaticJsStrings, |
||||
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||
}; |
||||
use boa_profiler::Profiler; |
||||
|
||||
/// The `Temporal.ZonedDateTime` object.
|
||||
#[derive(Debug, Clone)] |
||||
pub struct ZonedDateTime { |
||||
nanoseconds: JsBigInt, |
||||
time_zone: JsObject, |
||||
calendar: JsObject, |
||||
} |
||||
|
||||
impl BuiltInObject for ZonedDateTime { |
||||
const NAME: JsString = StaticJsStrings::ZONED_DT; |
||||
} |
||||
|
||||
impl IntrinsicObject for ZonedDateTime { |
||||
fn init(realm: &Realm) { |
||||
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||
|
||||
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||
.static_property( |
||||
JsSymbol::to_string_tag(), |
||||
Self::NAME, |
||||
Attribute::CONFIGURABLE, |
||||
) |
||||
.build(); |
||||
} |
||||
|
||||
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||
} |
||||
} |
||||
|
||||
impl BuiltInConstructor for ZonedDateTime { |
||||
const LENGTH: usize = 0; |
||||
|
||||
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||
StandardConstructors::zoned_date_time; |
||||
|
||||
fn constructor( |
||||
new_target: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
// TODO: Implement ZonedDateTime.
|
||||
Err(JsNativeError::error() |
||||
.with_message("%ZonedDateTime% not yet implemented.") |
||||
.into()) |
||||
} |
||||
} |
||||
|
||||
// -- ZonedDateTime Abstract Operations --
|
||||
|
||||
///6.5.5 `AddZonedDateTime ( epochNanoseconds, timeZone, calendar, years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , options ] )`
|
||||
pub(crate) fn add_zoned_date_time( |
||||
epoch_nanos: &JsBigInt, |
||||
time_zone: &JsObject, |
||||
calendar: &JsObject, |
||||
duration: super::duration::DurationRecord, |
||||
options: Option<&JsObject>, |
||||
) -> JsResult<JsBigInt> { |
||||
// 1. If options is not present, set options to undefined.
|
||||
// 2. Assert: Type(options) is Object or Undefined.
|
||||
// 3. If years = 0, months = 0, weeks = 0, and days = 0, then
|
||||
// a. Return ? AddInstant(epochNanoseconds, hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
|
||||
// 4. Let instant be ! CreateTemporalInstant(epochNanoseconds).
|
||||
// 5. Let temporalDateTime be ? GetPlainDateTimeFor(timeZone, instant, calendar).
|
||||
// 6. Let datePart be ! CreateTemporalDate(temporalDateTime.[[ISOYear]], temporalDateTime.[[ISOMonth]], temporalDateTime.[[ISODay]], calendar).
|
||||
// 7. Let dateDuration be ! CreateTemporalDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0).
|
||||
// 8. Let addedDate be ? CalendarDateAdd(calendar, datePart, dateDuration, options).
|
||||
// 9. Let intermediateDateTime be ? CreateTemporalDateTime(addedDate.[[ISOYear]], addedDate.[[ISOMonth]], addedDate.[[ISODay]], temporalDateTime.[[ISOHour]], temporalDateTime.[[ISOMinute]], temporalDateTime.[[ISOSecond]], temporalDateTime.[[ISOMillisecond]], temporalDateTime.[[ISOMicrosecond]], temporalDateTime.[[ISONanosecond]], calendar).
|
||||
// 10. Let intermediateInstant be ? GetInstantFor(timeZone, intermediateDateTime, "compatible").
|
||||
// 11. Return ? AddInstant(intermediateInstant.[[Nanoseconds]], hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
|
||||
Err(JsNativeError::error() |
||||
.with_message("%ZonedDateTime% not yet implemented.") |
||||
.into()) |
||||
} |
||||
|
||||
/// 6.5.7 `NanosecondsToDays ( nanoseconds, relativeTo )`
|
||||
pub(crate) fn nanoseconds_to_days( |
||||
nanoseconds: f64, |
||||
relative_to: &JsValue, |
||||
) -> JsResult<(i32, i32, i32)> { |
||||
// 1. Let dayLengthNs be nsPerDay.
|
||||
// 2. If nanoseconds = 0, then
|
||||
// a. Return the Record { [[Days]]: 0, [[Nanoseconds]]: 0, [[DayLength]]: dayLengthNs }.
|
||||
// 3. If nanoseconds < 0, let sign be -1; else, let sign be 1.
|
||||
// 4. If Type(relativeTo) is not Object or relativeTo does not have an [[InitializedTemporalZonedDateTime]] internal slot, then
|
||||
// a. Return the Record { [[Days]]: truncate(nanoseconds / dayLengthNs), [[Nanoseconds]]: (abs(nanoseconds) modulo dayLengthNs) × sign, [[DayLength]]: dayLengthNs }.
|
||||
// 5. Let startNs be ℝ(relativeTo.[[Nanoseconds]]).
|
||||
// 6. Let startInstant be ! CreateTemporalInstant(ℤ(startNs)).
|
||||
// 7. Let startDateTime be ? GetPlainDateTimeFor(relativeTo.[[TimeZone]], startInstant, relativeTo.[[Calendar]]).
|
||||
// 8. Let endNs be startNs + nanoseconds.
|
||||
// 9. If ! IsValidEpochNanoseconds(ℤ(endNs)) is false, throw a RangeError exception.
|
||||
// 10. Let endInstant be ! CreateTemporalInstant(ℤ(endNs)).
|
||||
// 11. Let endDateTime be ? GetPlainDateTimeFor(relativeTo.[[TimeZone]], endInstant, relativeTo.[[Calendar]]).
|
||||
// 12. Let dateDifference be ? DifferenceISODateTime(startDateTime.[[ISOYear]], startDateTime.[[ISOMonth]], startDateTime.[[ISODay]], startDateTime.[[ISOHour]], startDateTime.[[ISOMinute]], startDateTime.[[ISOSecond]], startDateTime.[[ISOMillisecond]], startDateTime.[[ISOMicrosecond]], startDateTime.[[ISONanosecond]], endDateTime.[[ISOYear]], endDateTime.[[ISOMonth]], endDateTime.[[ISODay]], endDateTime.[[ISOHour]], endDateTime.[[ISOMinute]], endDateTime.[[ISOSecond]], endDateTime.[[ISOMillisecond]], endDateTime.[[ISOMicrosecond]], endDateTime.[[ISONanosecond]], relativeTo.[[Calendar]], "day", OrdinaryObjectCreate(null)).
|
||||
// 13. Let days be dateDifference.[[Days]].
|
||||
// 14. Let intermediateNs be ℝ(? AddZonedDateTime(ℤ(startNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
|
||||
// 15. If sign is 1, then
|
||||
// a. Repeat, while days > 0 and intermediateNs > endNs,
|
||||
// i. Set days to days - 1.
|
||||
// ii. Set intermediateNs to ℝ(? AddZonedDateTime(ℤ(startNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
|
||||
// 16. Set nanoseconds to endNs - intermediateNs.
|
||||
// 17. Let done be false.
|
||||
// 18. Repeat, while done is false,
|
||||
// a. Let oneDayFartherNs be ℝ(? AddZonedDateTime(ℤ(intermediateNs), relativeTo.[[TimeZone]], relativeTo.[[Calendar]], 0, 0, 0, sign, 0, 0, 0, 0, 0, 0)).
|
||||
// b. Set dayLengthNs to oneDayFartherNs - intermediateNs.
|
||||
// c. If (nanoseconds - dayLengthNs) × sign ≥ 0, then
|
||||
// i. Set nanoseconds to nanoseconds - dayLengthNs.
|
||||
// ii. Set intermediateNs to oneDayFartherNs.
|
||||
// iii. Set days to days + sign.
|
||||
// d. Else,
|
||||
// i. Set done to true.
|
||||
// 19. If days < 0 and sign = 1, throw a RangeError exception.
|
||||
// 20. If days > 0 and sign = -1, throw a RangeError exception.
|
||||
// 21. If nanoseconds < 0, then
|
||||
// a. Assert: sign is -1.
|
||||
// 22. If nanoseconds > 0 and sign = -1, throw a RangeError exception.
|
||||
// 23. Assert: The inequality abs(nanoseconds) < abs(dayLengthNs) holds.
|
||||
// 24. Return the Record { [[Days]]: days, [[Nanoseconds]]: nanoseconds, [[DayLength]]: abs(dayLengthNs) }.
|
||||
Err(JsNativeError::error() |
||||
.with_message("%ZonedDateTime% not yet implemented.") |
||||
.into()) |
||||
} |
@ -0,0 +1,205 @@
|
||||
/// Parsing for Temporal's `Annotations`.
|
||||
use crate::{ |
||||
error::{Error, ParseResult}, |
||||
lexer::Error as LexError, |
||||
temporal::{ |
||||
grammar::{ |
||||
is_a_key_char, is_a_key_leading_char, is_annotation_close, |
||||
is_annotation_key_value_separator, is_annotation_value_component, is_critical_flag, |
||||
}, |
||||
time_zone, |
||||
time_zone::TimeZoneAnnotation, |
||||
IsoCursor, |
||||
}, |
||||
}; |
||||
|
||||
use boa_ast::{Position, Span}; |
||||
|
||||
use super::grammar::{is_annotation_open, is_hyphen}; |
||||
|
||||
/// A `KeyValueAnnotation` Parse Node.
|
||||
#[derive(Debug, Clone)] |
||||
pub(crate) struct KeyValueAnnotation { |
||||
/// An `Annotation`'s Key.
|
||||
pub(crate) key: String, |
||||
/// An `Annotation`'s value.
|
||||
pub(crate) value: String, |
||||
/// Whether the annotation was flagged as critical.
|
||||
pub(crate) critical: bool, |
||||
} |
||||
|
||||
/// Strictly a Parsing Intermediary for the checking the common annotation backing.
|
||||
pub(crate) struct AnnotationSet { |
||||
pub(crate) tz: Option<TimeZoneAnnotation>, |
||||
pub(crate) calendar: Option<String>, |
||||
} |
||||
|
||||
/// Parse a `TimeZoneAnnotation` `Annotations` set
|
||||
pub(crate) fn parse_annotation_set( |
||||
zoned: bool, |
||||
cursor: &mut IsoCursor, |
||||
) -> ParseResult<AnnotationSet> { |
||||
// Parse the first annotation.
|
||||
let tz_annotation = time_zone::parse_ambiguous_tz_annotation(cursor)?; |
||||
|
||||
if tz_annotation.is_none() && zoned { |
||||
return Err(Error::unexpected( |
||||
"Annotation", |
||||
Span::new( |
||||
Position::new(1, cursor.pos() + 1), |
||||
Position::new(1, cursor.pos() + 2), |
||||
), |
||||
"iso8601 ZonedDateTime requires a TimeZoneAnnotation.", |
||||
)); |
||||
} |
||||
|
||||
// Parse any `Annotations`
|
||||
let annotations = cursor.check_or(false, is_annotation_open); |
||||
|
||||
if annotations { |
||||
let annotations = parse_annotations(cursor)?; |
||||
return Ok(AnnotationSet { |
||||
tz: tz_annotation, |
||||
calendar: annotations.calendar, |
||||
}); |
||||
} |
||||
|
||||
Ok(AnnotationSet { |
||||
tz: tz_annotation, |
||||
calendar: None, |
||||
}) |
||||
} |
||||
|
||||
/// An internal crate type to house any recognized annotations that are found.
|
||||
#[derive(Default)] |
||||
pub(crate) struct RecognizedAnnotations { |
||||
pub(crate) calendar: Option<String>, |
||||
} |
||||
|
||||
/// Parse any number of `KeyValueAnnotation`s
|
||||
pub(crate) fn parse_annotations(cursor: &mut IsoCursor) -> ParseResult<RecognizedAnnotations> { |
||||
let mut annotations = RecognizedAnnotations::default(); |
||||
|
||||
let mut calendar_crit = false; |
||||
while cursor.check_or(false, is_annotation_open) { |
||||
let start = Position::new(1, cursor.pos() + 1); |
||||
let kv = parse_kv_annotation(cursor)?; |
||||
|
||||
if &kv.key == "u-ca" { |
||||
if annotations.calendar.is_none() { |
||||
annotations.calendar = Some(kv.value); |
||||
calendar_crit = kv.critical; |
||||
continue; |
||||
} |
||||
|
||||
if calendar_crit || kv.critical { |
||||
return Err(Error::general( |
||||
"Cannot have critical flag with duplicate calendar annotations", |
||||
start, |
||||
)); |
||||
} |
||||
} else if kv.critical { |
||||
return Err(Error::general("Unrecognized critical annotation.", start)); |
||||
} |
||||
} |
||||
|
||||
Ok(annotations) |
||||
} |
||||
|
||||
/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair.
|
||||
fn parse_kv_annotation(cursor: &mut IsoCursor) -> ParseResult<KeyValueAnnotation> { |
||||
debug_assert!(cursor.check_or(false, is_annotation_open)); |
||||
|
||||
let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; |
||||
let (leading_char, critical) = if is_critical_flag(potential_critical) { |
||||
(cursor.next().ok_or_else(|| Error::AbruptEnd)?, true) |
||||
} else { |
||||
(potential_critical, false) |
||||
}; |
||||
|
||||
if !is_a_key_leading_char(leading_char) { |
||||
return Err(LexError::syntax( |
||||
"Invalid AnnotationKey leading character", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
|
||||
// Parse AnnotationKey.
|
||||
let annotation_key = parse_annotation_key(cursor)?; |
||||
|
||||
debug_assert!(cursor.check_or(false, is_annotation_key_value_separator)); |
||||
// Advance past the '=' character.
|
||||
cursor.advance(); |
||||
|
||||
// Parse AnnotationValue.
|
||||
let annotation_value = parse_annotation_value(cursor)?; |
||||
|
||||
// Assert that we are at the annotation close and advance cursor past annotation to close.
|
||||
debug_assert!(cursor.check_or(false, is_annotation_close)); |
||||
cursor.advance(); |
||||
|
||||
Ok(KeyValueAnnotation { |
||||
key: annotation_key, |
||||
value: annotation_value, |
||||
critical, |
||||
}) |
||||
} |
||||
|
||||
/// Parse an `AnnotationKey`.
|
||||
fn parse_annotation_key(cursor: &mut IsoCursor) -> ParseResult<String> { |
||||
let key_start = cursor.pos(); |
||||
while let Some(potential_key_char) = cursor.next() { |
||||
// End of key.
|
||||
if is_annotation_key_value_separator(potential_key_char) { |
||||
// Return found key
|
||||
return Ok(cursor.slice(key_start, cursor.pos())); |
||||
} |
||||
|
||||
if !is_a_key_char(potential_key_char) { |
||||
return Err(LexError::syntax( |
||||
"Invalid AnnotationKey Character", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
} |
||||
|
||||
Err(Error::AbruptEnd) |
||||
} |
||||
|
||||
/// Parse an `AnnotationValue`.
|
||||
fn parse_annotation_value(cursor: &mut IsoCursor) -> ParseResult<String> { |
||||
let value_start = cursor.pos(); |
||||
while let Some(potential_value_char) = cursor.next() { |
||||
if is_annotation_close(potential_value_char) { |
||||
// Return the determined AnnotationValue.
|
||||
return Ok(cursor.slice(value_start, cursor.pos())); |
||||
} |
||||
|
||||
if is_hyphen(potential_value_char) { |
||||
if !cursor |
||||
.peek_n(1) |
||||
.map_or(false, is_annotation_value_component) |
||||
{ |
||||
return Err(LexError::syntax( |
||||
"Missing AttributeValueComponent after '-'", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
cursor.advance(); |
||||
continue; |
||||
} |
||||
|
||||
if !is_annotation_value_component(potential_value_char) { |
||||
return Err(LexError::syntax( |
||||
"Invalid character in AnnotationValue", |
||||
Position::new(1, value_start + cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
} |
||||
|
||||
Err(Error::AbruptEnd) |
||||
} |
@ -0,0 +1,373 @@
|
||||
//! Parsing for Temporal's ISO8601 `Date` and `DateTime`.
|
||||
|
||||
use crate::{ |
||||
error::{Error, ParseResult}, |
||||
lexer::Error as LexError, |
||||
temporal::{ |
||||
annotations, |
||||
grammar::{is_date_time_separator, is_sign, is_utc_designator}, |
||||
time, |
||||
time::TimeSpec, |
||||
time_zone, IsoCursor, IsoParseRecord, |
||||
}, |
||||
}; |
||||
|
||||
use boa_ast::{temporal::TimeZone, Position, Span}; |
||||
|
||||
use super::grammar::{is_annotation_open, is_hyphen}; |
||||
|
||||
#[derive(Debug, Default, Clone)] |
||||
/// A `DateTime` Parse Node that contains the date, time, and offset info.
|
||||
pub(crate) struct DateTimeRecord { |
||||
/// Date
|
||||
pub(crate) date: DateRecord, |
||||
/// Time
|
||||
pub(crate) time: Option<TimeSpec>, |
||||
/// Tz Offset
|
||||
pub(crate) time_zone: Option<TimeZone>, |
||||
} |
||||
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
/// The record of a parsed date.
|
||||
pub(crate) struct DateRecord { |
||||
/// Date Year
|
||||
pub(crate) year: i32, |
||||
/// Date Month
|
||||
pub(crate) month: i32, |
||||
/// Date Day
|
||||
pub(crate) day: i32, |
||||
} |
||||
|
||||
/// This function handles parsing for [`AnnotatedDateTime`][datetime],
|
||||
/// [`AnnotatedDateTimeTimeRequred`][time], and
|
||||
/// [`TemporalInstantString.`][instant] according to the requirements
|
||||
/// provided via Spec.
|
||||
///
|
||||
/// [datetime]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime
|
||||
/// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired
|
||||
/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
|
||||
pub(crate) fn parse_annotated_date_time( |
||||
zoned: bool, |
||||
time_required: bool, |
||||
utc_required: bool, |
||||
cursor: &mut IsoCursor, |
||||
) -> ParseResult<IsoParseRecord> { |
||||
let date_time = parse_date_time(time_required, utc_required, cursor)?; |
||||
|
||||
// Peek Annotation presence
|
||||
// Throw error if annotation does not exist and zoned is true, else return.
|
||||
let annotation_check = cursor.check_or(false, is_annotation_open); |
||||
if !annotation_check { |
||||
if zoned { |
||||
return Err(Error::expected( |
||||
["TimeZoneAnnotation".into()], |
||||
"No Annotation", |
||||
Span::new( |
||||
Position::new(1, cursor.pos() + 1), |
||||
Position::new(1, cursor.pos() + 1), |
||||
), |
||||
"iso8601 grammar", |
||||
)); |
||||
} |
||||
|
||||
return Ok(IsoParseRecord { |
||||
date: date_time.date, |
||||
time: date_time.time, |
||||
tz: date_time.time_zone, |
||||
calendar: None, |
||||
}); |
||||
} |
||||
|
||||
let mut tz = TimeZone::default(); |
||||
|
||||
if let Some(tz_info) = date_time.time_zone { |
||||
tz = tz_info; |
||||
} |
||||
|
||||
let annotation_set = annotations::parse_annotation_set(zoned, cursor)?; |
||||
|
||||
if let Some(annotated_tz) = annotation_set.tz { |
||||
tz = annotated_tz.tz; |
||||
} |
||||
|
||||
let tz = if tz.name.is_some() || tz.offset.is_some() { |
||||
Some(tz) |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
Ok(IsoParseRecord { |
||||
date: date_time.date, |
||||
time: date_time.time, |
||||
tz, |
||||
calendar: annotation_set.calendar, |
||||
}) |
||||
} |
||||
|
||||
/// Parses a `DateTime` record.
|
||||
fn parse_date_time( |
||||
time_required: bool, |
||||
utc_required: bool, |
||||
cursor: &mut IsoCursor, |
||||
) -> ParseResult<DateTimeRecord> { |
||||
let date = parse_date(cursor)?; |
||||
|
||||
// If there is no `DateTimeSeparator`, return date early.
|
||||
if !cursor.check_or(false, is_date_time_separator) { |
||||
if time_required { |
||||
return Err(Error::general( |
||||
"Missing a required TimeSpec.", |
||||
Position::new(1, cursor.pos() + 1), |
||||
)); |
||||
} |
||||
|
||||
return Ok(DateTimeRecord { |
||||
date, |
||||
time: None, |
||||
time_zone: None, |
||||
}); |
||||
} |
||||
|
||||
cursor.advance(); |
||||
|
||||
let time = time::parse_time_spec(cursor)?; |
||||
|
||||
let time_zone = if cursor |
||||
.check(|ch| is_sign(ch) || is_utc_designator(ch)) |
||||
.unwrap_or(false) |
||||
{ |
||||
Some(time_zone::parse_date_time_utc(cursor)?) |
||||
} else { |
||||
if utc_required { |
||||
return Err(Error::general( |
||||
"DateTimeUTCOffset is required.", |
||||
Position::new(1, cursor.pos() + 1), |
||||
)); |
||||
} |
||||
None |
||||
}; |
||||
|
||||
Ok(DateTimeRecord { |
||||
date, |
||||
time: Some(time), |
||||
time_zone, |
||||
}) |
||||
} |
||||
|
||||
/// Parses `Date` record.
|
||||
fn parse_date(cursor: &mut IsoCursor) -> ParseResult<DateRecord> { |
||||
let year = parse_date_year(cursor)?; |
||||
let divided = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?; |
||||
|
||||
if divided { |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let month = parse_date_month(cursor)?; |
||||
|
||||
if cursor.check_or(false, is_hyphen) { |
||||
if !divided { |
||||
return Err(LexError::syntax( |
||||
"Invalid date separator", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let day = parse_date_day(cursor)?; |
||||
|
||||
Ok(DateRecord { year, month, day }) |
||||
} |
||||
|
||||
/// Determines if the string can be parsed as a `DateSpecYearMonth`.
|
||||
pub(crate) fn peek_year_month(cursor: &IsoCursor) -> ParseResult<bool> { |
||||
let mut ym_peek = if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) { |
||||
7 |
||||
} else { |
||||
4 |
||||
}; |
||||
|
||||
if cursor |
||||
.peek_n(ym_peek) |
||||
.map(is_hyphen) |
||||
.ok_or_else(|| Error::AbruptEnd)? |
||||
{ |
||||
ym_peek += 1; |
||||
} |
||||
|
||||
ym_peek += 2; |
||||
|
||||
if cursor.peek_n(ym_peek).map_or(true, is_annotation_open) { |
||||
Ok(true) |
||||
} else { |
||||
Ok(false) |
||||
} |
||||
} |
||||
|
||||
/// Parses a `DateSpecYearMonth`
|
||||
pub(crate) fn parse_year_month(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> { |
||||
let year = parse_date_year(cursor)?; |
||||
|
||||
if cursor.check_or(false, is_hyphen) { |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let month = parse_date_month(cursor)?; |
||||
|
||||
Ok((year, month)) |
||||
} |
||||
|
||||
/// Determines if the string can be parsed as a `DateSpecYearMonth`.
|
||||
pub(crate) fn peek_month_day(cursor: &IsoCursor) -> ParseResult<bool> { |
||||
let mut md_peek = if cursor |
||||
.peek_n(1) |
||||
.map(is_hyphen) |
||||
.ok_or_else(|| Error::AbruptEnd)? |
||||
{ |
||||
4 |
||||
} else { |
||||
2 |
||||
}; |
||||
|
||||
if cursor |
||||
.peek_n(md_peek) |
||||
.map(is_hyphen) |
||||
.ok_or_else(|| Error::AbruptEnd)? |
||||
{ |
||||
md_peek += 1; |
||||
} |
||||
|
||||
md_peek += 2; |
||||
|
||||
if cursor.peek_n(md_peek).map_or(true, is_annotation_open) { |
||||
Ok(true) |
||||
} else { |
||||
Ok(false) |
||||
} |
||||
} |
||||
|
||||
/// Parses a `DateSpecMonthDay`
|
||||
pub(crate) fn parse_month_day(cursor: &mut IsoCursor) -> ParseResult<(i32, i32)> { |
||||
let dash_one = cursor.check(is_hyphen).ok_or_else(|| Error::AbruptEnd)?; |
||||
let dash_two = cursor |
||||
.peek_n(1) |
||||
.map(is_hyphen) |
||||
.ok_or_else(|| Error::AbruptEnd)?; |
||||
|
||||
if dash_two && dash_one { |
||||
cursor.advance_n(2); |
||||
} else if dash_two && !dash_one { |
||||
return Err(LexError::syntax( |
||||
"MonthDay requires two dashes", |
||||
Position::new(1, cursor.pos()), |
||||
) |
||||
.into()); |
||||
} |
||||
|
||||
let month = parse_date_month(cursor)?; |
||||
if cursor.check_or(false, is_hyphen) { |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let day = parse_date_day(cursor)?; |
||||
|
||||
Ok((month, day)) |
||||
} |
||||
|
||||
// ==== Unit Parsers ====
|
||||
|
||||
fn parse_date_year(cursor: &mut IsoCursor) -> ParseResult<i32> { |
||||
if is_sign(cursor.peek().ok_or_else(|| Error::AbruptEnd)?) { |
||||
let year_start = cursor.pos(); |
||||
let sign = if cursor.check_or(false, |ch| ch == '+') { |
||||
1 |
||||
} else { |
||||
-1 |
||||
}; |
||||
|
||||
cursor.advance(); |
||||
|
||||
for _ in 0..6 { |
||||
let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; |
||||
if !year_digit.is_ascii_digit() { |
||||
return Err(Error::lex(LexError::syntax( |
||||
"DateYear must contain digit", |
||||
Position::new(1, cursor.pos() + 1), |
||||
))); |
||||
} |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let year_string = cursor.slice(year_start + 1, cursor.pos()); |
||||
let year_value = year_string |
||||
.parse::<i32>() |
||||
.map_err(|e| Error::general(e.to_string(), Position::new(1, year_start + 1)))?; |
||||
|
||||
// 13.30.1 Static Semantics: Early Errors
|
||||
//
|
||||
// It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000).
|
||||
if sign == -1 && year_value == 0 { |
||||
return Err(Error::lex(LexError::syntax( |
||||
"Cannot have negative 0 years.", |
||||
Position::new(1, year_start + 1), |
||||
))); |
||||
} |
||||
|
||||
return Ok(sign * year_value); |
||||
} |
||||
|
||||
let year_start = cursor.pos(); |
||||
|
||||
for _ in 0..4 { |
||||
let year_digit = cursor.peek().ok_or_else(|| Error::AbruptEnd)?; |
||||
if !year_digit.is_ascii_digit() { |
||||
return Err(LexError::syntax( |
||||
"DateYear must contain digit", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let year_string = cursor.slice(year_start, cursor.pos()); |
||||
let year_value = year_string |
||||
.parse::<i32>() |
||||
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?; |
||||
|
||||
Ok(year_value) |
||||
} |
||||
|
||||
fn parse_date_month(cursor: &mut IsoCursor) -> ParseResult<i32> { |
||||
let month_value = cursor |
||||
.slice(cursor.pos(), cursor.pos() + 2) |
||||
.parse::<i32>() |
||||
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() + 1)))?; |
||||
if !(1..=12).contains(&month_value) { |
||||
return Err(LexError::syntax( |
||||
"DateMonth must be in a range of 1-12", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
cursor.advance_n(2); |
||||
Ok(month_value) |
||||
} |
||||
|
||||
fn parse_date_day(cursor: &mut IsoCursor) -> ParseResult<i32> { |
||||
let day_value = cursor |
||||
.slice(cursor.pos(), cursor.pos() + 2) |
||||
.parse::<i32>() |
||||
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; |
||||
if !(1..=31).contains(&day_value) { |
||||
return Err(LexError::syntax( |
||||
"DateDay must be in a range of 1-31", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
cursor.advance_n(2); |
||||
Ok(day_value) |
||||
} |
@ -0,0 +1,275 @@
|
||||
use boa_ast::Position; |
||||
|
||||
use crate::{ |
||||
error::{Error, ParseResult}, |
||||
temporal::{ |
||||
grammar::{ |
||||
is_day_designator, is_decimal_separator, is_duration_designator, is_hour_designator, |
||||
is_minute_designator, is_month_designator, is_second_designator, is_sign, |
||||
is_time_designator, is_week_designator, is_year_designator, |
||||
}, |
||||
time::parse_fraction, |
||||
IsoCursor, |
||||
}, |
||||
}; |
||||
|
||||
/// A ISO8601 `DurationRecord` Parse Node.
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub(crate) struct DurationParseRecord { |
||||
/// Duration Sign
|
||||
pub(crate) sign: bool, |
||||
/// A `DateDuration` record.
|
||||
pub(crate) date: DateDuration, |
||||
/// A `TimeDuration` record.
|
||||
pub(crate) time: TimeDuration, |
||||
} |
||||
|
||||
/// A `DateDuration` Parse Node.
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
pub(crate) struct DateDuration { |
||||
/// Years value.
|
||||
pub(crate) years: i32, |
||||
/// Months value.
|
||||
pub(crate) months: i32, |
||||
/// Weeks value.
|
||||
pub(crate) weeks: i32, |
||||
/// Days value.
|
||||
pub(crate) days: i32, |
||||
} |
||||
|
||||
/// A `TimeDuration` Parse Node
|
||||
#[derive(Default, Debug, Clone, Copy)] |
||||
pub(crate) struct TimeDuration { |
||||
/// Hours value.
|
||||
pub(crate) hours: i32, |
||||
/// Hours fraction value.
|
||||
pub(crate) fhours: f64, |
||||
/// Minutes value with fraction.
|
||||
pub(crate) minutes: i32, |
||||
/// Minutes fraction value.
|
||||
pub(crate) fminutes: f64, |
||||
/// Seconds value with fraction.
|
||||
pub(crate) seconds: i32, |
||||
/// Seconds fraction value,
|
||||
pub(crate) fseconds: f64, |
||||
} |
||||
|
||||
pub(crate) fn parse_duration(cursor: &mut IsoCursor) -> ParseResult<DurationParseRecord> { |
||||
let sign = if cursor.check(is_sign).ok_or_else(|| Error::AbruptEnd)? { |
||||
let sign = cursor.check_or(false, |ch| ch == '+'); |
||||
cursor.advance(); |
||||
sign |
||||
} else { |
||||
true |
||||
}; |
||||
|
||||
if !cursor |
||||
.check(is_duration_designator) |
||||
.ok_or_else(|| Error::AbruptEnd)? |
||||
{ |
||||
return Err(Error::general( |
||||
"DurationString missing DurationDesignator.", |
||||
Position::new(1, cursor.pos() + 1), |
||||
)); |
||||
} |
||||
|
||||
cursor.advance(); |
||||
|
||||
let date = if cursor.check_or(false, is_time_designator) { |
||||
Some(DateDuration::default()) |
||||
} else { |
||||
Some(parse_date_duration(cursor)?) |
||||
}; |
||||
|
||||
let time = if cursor.check_or(false, is_time_designator) { |
||||
cursor.advance(); |
||||
Some(parse_time_duration(cursor)?) |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
if cursor.peek().is_some() { |
||||
return Err(Error::general( |
||||
"Unrecognized value in DurationString.", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
|
||||
Ok(DurationParseRecord { |
||||
sign, |
||||
date: date.unwrap_or_default(), |
||||
time: time.unwrap_or_default(), |
||||
}) |
||||
} |
||||
|
||||
#[derive(PartialEq, PartialOrd, Eq, Ord)] |
||||
enum DateUnit { |
||||
None = 0, |
||||
Year, |
||||
Month, |
||||
Week, |
||||
Day, |
||||
} |
||||
|
||||
pub(crate) fn parse_date_duration(cursor: &mut IsoCursor) -> ParseResult<DateDuration> { |
||||
let mut date = DateDuration::default(); |
||||
|
||||
let mut previous_unit = DateUnit::None; |
||||
while cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
let digit_start = cursor.pos(); |
||||
|
||||
while cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let value = cursor |
||||
.slice(digit_start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|err| { |
||||
Error::general(err.to_string(), Position::new(digit_start, cursor.pos())) |
||||
})?; |
||||
|
||||
match cursor.peek() { |
||||
Some(ch) if is_year_designator(ch) => { |
||||
if previous_unit > DateUnit::Year { |
||||
return Err(Error::general( |
||||
"Not a valid DateDuration order", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
date.years = value; |
||||
previous_unit = DateUnit::Year; |
||||
} |
||||
Some(ch) if is_month_designator(ch) => { |
||||
if previous_unit > DateUnit::Month { |
||||
return Err(Error::general( |
||||
"Not a valid DateDuration order", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
date.months = value; |
||||
previous_unit = DateUnit::Month; |
||||
} |
||||
Some(ch) if is_week_designator(ch) => { |
||||
if previous_unit > DateUnit::Week { |
||||
return Err(Error::general( |
||||
"Not a valid DateDuration order", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
date.weeks = value; |
||||
previous_unit = DateUnit::Week; |
||||
} |
||||
Some(ch) if is_day_designator(ch) => { |
||||
if previous_unit > DateUnit::Day { |
||||
return Err(Error::general( |
||||
"Not a valid DateDuration order", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
date.days = value; |
||||
previous_unit = DateUnit::Day; |
||||
} |
||||
Some(_) | None => return Err(Error::AbruptEnd), |
||||
} |
||||
|
||||
cursor.advance(); |
||||
} |
||||
|
||||
Ok(date) |
||||
} |
||||
|
||||
#[derive(PartialEq, PartialOrd, Eq, Ord)] |
||||
enum TimeUnit { |
||||
None = 0, |
||||
Hour, |
||||
Minute, |
||||
Second, |
||||
} |
||||
|
||||
pub(crate) fn parse_time_duration(cursor: &mut IsoCursor) -> ParseResult<TimeDuration> { |
||||
let mut time = TimeDuration::default(); |
||||
|
||||
if !cursor.check_or(false, |ch| ch.is_ascii()) { |
||||
return Err(Error::general( |
||||
"No time values provided after TimeDesignator.", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
|
||||
let mut previous_unit = TimeUnit::None; |
||||
let mut fraction_present = false; |
||||
while cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
let digit_start = cursor.pos(); |
||||
|
||||
while cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let value = cursor |
||||
.slice(digit_start, cursor.pos()) |
||||
.parse::<i32>() |
||||
.map_err(|err| { |
||||
Error::general(err.to_string(), Position::new(digit_start, cursor.pos())) |
||||
})?; |
||||
|
||||
let fraction = if cursor.check_or(false, is_decimal_separator) { |
||||
fraction_present = true; |
||||
parse_fraction(cursor)? |
||||
} else { |
||||
0.0 |
||||
}; |
||||
|
||||
match cursor.peek() { |
||||
Some(ch) if is_hour_designator(ch) => { |
||||
if previous_unit > TimeUnit::Hour { |
||||
return Err(Error::general( |
||||
"Not a valid DateDuration order", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
time.hours = value; |
||||
time.fhours = fraction; |
||||
previous_unit = TimeUnit::Hour; |
||||
} |
||||
Some(ch) if is_minute_designator(ch) => { |
||||
if previous_unit > TimeUnit::Minute { |
||||
return Err(Error::general( |
||||
"Not a valid DateDuration order", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
time.minutes = value; |
||||
time.fminutes = fraction; |
||||
previous_unit = TimeUnit::Minute; |
||||
} |
||||
Some(ch) if is_second_designator(ch) => { |
||||
if previous_unit > TimeUnit::Second { |
||||
return Err(Error::general( |
||||
"Not a valid DateDuration order", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
time.seconds = value; |
||||
time.fseconds = fraction; |
||||
previous_unit = TimeUnit::Second; |
||||
} |
||||
Some(_) | None => return Err(Error::AbruptEnd), |
||||
} |
||||
|
||||
cursor.advance(); |
||||
|
||||
if fraction_present { |
||||
if cursor.check_or(false, |ch| ch.is_ascii_digit()) { |
||||
return Err(Error::general( |
||||
"Invalid TimeDuration continuation after FractionPart.", |
||||
Position::new(1, cursor.pos()), |
||||
)); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
Ok(time) |
||||
} |
@ -0,0 +1,136 @@
|
||||
//! ISO8601 specific grammar checks.
|
||||
|
||||
/// Checks if char is a `AKeyLeadingChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_a_key_leading_char(ch: char) -> bool { |
||||
ch.is_ascii_lowercase() || ch == '_' |
||||
} |
||||
|
||||
/// Checks if char is an `AKeyChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_a_key_char(ch: char) -> bool { |
||||
is_a_key_leading_char(ch) || ch.is_ascii_digit() || ch == '-' |
||||
} |
||||
|
||||
/// Checks if char is an `AnnotationValueComponent`.
|
||||
pub(crate) const fn is_annotation_value_component(ch: char) -> bool { |
||||
ch.is_ascii_digit() || ch.is_ascii_alphabetic() |
||||
} |
||||
|
||||
/// Checks if char is a `TZLeadingChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_tz_leading_char(ch: char) -> bool { |
||||
ch.is_ascii_alphabetic() || ch == '_' || ch == '.' |
||||
} |
||||
|
||||
/// Checks if char is a `TZChar`.
|
||||
#[inline] |
||||
pub(crate) const fn is_tz_char(ch: char) -> bool { |
||||
is_tz_leading_char(ch) || ch.is_ascii_digit() || ch == '-' || ch == '+' |
||||
} |
||||
|
||||
/// Checks if char is a `TimeZoneIANAName` Separator.
|
||||
pub(crate) const fn is_tz_name_separator(ch: char) -> bool { |
||||
ch == '/' |
||||
} |
||||
|
||||
/// Checks if char is an ascii sign.
|
||||
pub(crate) const fn is_ascii_sign(ch: char) -> bool { |
||||
ch == '+' || ch == '-' |
||||
} |
||||
|
||||
/// Checks if char is an ascii sign or U+2212
|
||||
pub(crate) const fn is_sign(ch: char) -> bool { |
||||
is_ascii_sign(ch) || ch == '\u{2212}' |
||||
} |
||||
|
||||
/// Checks if char is a `TimeSeparator`.
|
||||
pub(crate) const fn is_time_separator(ch: char) -> bool { |
||||
ch == ':' |
||||
} |
||||
|
||||
/// Checks if char is a `TimeDesignator`.
|
||||
pub(crate) const fn is_time_designator(ch: char) -> bool { |
||||
ch == 'T' || ch == 't' |
||||
} |
||||
|
||||
/// Checks if char is a `DateTimeSeparator`.
|
||||
pub(crate) const fn is_date_time_separator(ch: char) -> bool { |
||||
is_time_designator(ch) || ch == '\u{0020}' |
||||
} |
||||
|
||||
/// Checks if char is a `UtcDesignator`.
|
||||
pub(crate) const fn is_utc_designator(ch: char) -> bool { |
||||
ch == 'Z' || ch == 'z' |
||||
} |
||||
|
||||
/// Checks if char is a `DurationDesignator`.
|
||||
pub(crate) const fn is_duration_designator(ch: char) -> bool { |
||||
ch == 'P' || ch == 'p' |
||||
} |
||||
|
||||
/// Checks if char is a `YearDesignator`.
|
||||
pub(crate) const fn is_year_designator(ch: char) -> bool { |
||||
ch == 'Y' || ch == 'y' |
||||
} |
||||
|
||||
/// Checks if char is a `MonthsDesignator`.
|
||||
pub(crate) const fn is_month_designator(ch: char) -> bool { |
||||
ch == 'M' || ch == 'm' |
||||
} |
||||
|
||||
/// Checks if char is a `WeekDesignator`.
|
||||
pub(crate) const fn is_week_designator(ch: char) -> bool { |
||||
ch == 'W' || ch == 'w' |
||||
} |
||||
|
||||
/// Checks if char is a `DayDesignator`.
|
||||
pub(crate) const fn is_day_designator(ch: char) -> bool { |
||||
ch == 'D' || ch == 'd' |
||||
} |
||||
|
||||
/// checks if char is a `DayDesignator`.
|
||||
pub(crate) const fn is_hour_designator(ch: char) -> bool { |
||||
ch == 'H' || ch == 'h' |
||||
} |
||||
|
||||
/// Checks if char is a `MinuteDesignator`.
|
||||
pub(crate) const fn is_minute_designator(ch: char) -> bool { |
||||
is_month_designator(ch) |
||||
} |
||||
|
||||
/// checks if char is a `SecondDesignator`.
|
||||
pub(crate) const fn is_second_designator(ch: char) -> bool { |
||||
ch == 'S' || ch == 's' |
||||
} |
||||
|
||||
/// Checks if char is a `DecimalSeparator`.
|
||||
pub(crate) const fn is_decimal_separator(ch: char) -> bool { |
||||
ch == '.' || ch == ',' |
||||
} |
||||
|
||||
/// Checks if char is an `AnnotationOpen`.
|
||||
pub(crate) const fn is_annotation_open(ch: char) -> bool { |
||||
ch == '[' |
||||
} |
||||
|
||||
/// Checks if char is an `AnnotationClose`.
|
||||
pub(crate) const fn is_annotation_close(ch: char) -> bool { |
||||
ch == ']' |
||||
} |
||||
|
||||
/// Checks if char is an `CriticalFlag`.
|
||||
pub(crate) const fn is_critical_flag(ch: char) -> bool { |
||||
ch == '!' |
||||
} |
||||
|
||||
/// Checks if char is the `AnnotationKeyValueSeparator`.
|
||||
pub(crate) const fn is_annotation_key_value_separator(ch: char) -> bool { |
||||
ch == '=' |
||||
} |
||||
|
||||
/// Checks if char is a hyphen. Hyphens are used as a Date separator
|
||||
/// and as a `AttributeValueComponent` separator.
|
||||
pub(crate) const fn is_hyphen(ch: char) -> bool { |
||||
ch == '-' |
||||
} |
@ -0,0 +1,348 @@
|
||||
//! Implementation of Iso8601 grammar lexing/parsing
|
||||
|
||||
use crate::error::ParseResult; |
||||
|
||||
mod annotations; |
||||
mod date_time; |
||||
mod duration; |
||||
mod grammar; |
||||
mod time; |
||||
mod time_zone; |
||||
|
||||
use boa_ast::temporal::{IsoDate, IsoDateTime, IsoDuration, IsoTime, TimeZone}; |
||||
|
||||
use date_time::DateRecord; |
||||
use time::TimeSpec; |
||||
|
||||
#[cfg(feature = "experimental")] |
||||
#[cfg(test)] |
||||
mod tests; |
||||
|
||||
// TODO: optimize where possible.
|
||||
|
||||
/// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions.
|
||||
///
|
||||
/// `IsoParseRecord` is converted into the ISO AST Nodes.
|
||||
#[derive(Default, Debug)] |
||||
pub(crate) struct IsoParseRecord { |
||||
/// Parsed Date Record
|
||||
pub(crate) date: DateRecord, |
||||
/// Parsed Time
|
||||
pub(crate) time: Option<TimeSpec>, |
||||
/// Parsed `TimeZone` data (UTCOffset | IANA name)
|
||||
pub(crate) tz: Option<TimeZone>, |
||||
/// The parsed calendar value.
|
||||
pub(crate) calendar: Option<String>, |
||||
} |
||||
|
||||
/// Parse a [`TemporalDateTimeString`][proposal].
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDateTimeString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalDateTimeString; |
||||
|
||||
impl TemporalDateTimeString { |
||||
/// Parses a targeted string as a `DateTime`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(zoned: bool, cursor: &mut IsoCursor) -> ParseResult<IsoDateTime> { |
||||
let parse_record = date_time::parse_annotated_date_time(zoned, false, false, cursor)?; |
||||
|
||||
let date = IsoDate { |
||||
year: parse_record.date.year, |
||||
month: parse_record.date.month, |
||||
day: parse_record.date.day, |
||||
calendar: parse_record.calendar, |
||||
}; |
||||
|
||||
let time = parse_record.time.map_or_else(IsoTime::default, |time| { |
||||
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction) |
||||
}); |
||||
|
||||
Ok(IsoDateTime { |
||||
date, |
||||
time, |
||||
tz: parse_record.tz, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/// Parse a [`TemporalTimeZoneString`][proposal].
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalTimeZoneString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalTimeZoneString; |
||||
|
||||
impl TemporalTimeZoneString { |
||||
/// Parses a targeted string as a `TimeZone`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<TimeZone> { |
||||
time_zone::parse_time_zone(cursor) |
||||
} |
||||
} |
||||
|
||||
/// Parse a [`TemporalYearMonthString`][proposal]
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalYearMonthString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalYearMonthString; |
||||
|
||||
impl TemporalYearMonthString { |
||||
/// Parses a targeted string as a `YearMonth`
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDate> { |
||||
if date_time::peek_year_month(cursor)? { |
||||
let ym = date_time::parse_year_month(cursor)?; |
||||
|
||||
let calendar = if cursor.check_or(false, |ch| ch == '[') { |
||||
let set = annotations::parse_annotation_set(false, cursor)?; |
||||
set.calendar |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
return Ok(IsoDate { |
||||
year: ym.0, |
||||
month: ym.1, |
||||
day: 0, |
||||
calendar, |
||||
}); |
||||
} |
||||
|
||||
let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?; |
||||
|
||||
Ok(IsoDate { |
||||
year: parse_record.date.year, |
||||
month: parse_record.date.month, |
||||
day: parse_record.date.day, |
||||
calendar: parse_record.calendar, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/// Parse a [`TemporalMonthDayString`][proposal]
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalMonthDayString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalMonthDayString; |
||||
|
||||
impl TemporalMonthDayString { |
||||
/// Parses a targeted string as a `MonthDay`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDate> { |
||||
if date_time::peek_month_day(cursor)? { |
||||
let md = date_time::parse_month_day(cursor)?; |
||||
|
||||
let calendar = if cursor.check_or(false, |ch| ch == '[') { |
||||
let set = annotations::parse_annotation_set(false, cursor)?; |
||||
set.calendar |
||||
} else { |
||||
None |
||||
}; |
||||
|
||||
return Ok(IsoDate { |
||||
year: 0, |
||||
month: md.0, |
||||
day: md.1, |
||||
calendar, |
||||
}); |
||||
} |
||||
|
||||
let parse_record = date_time::parse_annotated_date_time(false, false, false, cursor)?; |
||||
|
||||
Ok(IsoDate { |
||||
year: parse_record.date.year, |
||||
month: parse_record.date.month, |
||||
day: parse_record.date.day, |
||||
calendar: parse_record.calendar, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/// Parser for a [`TemporalInstantString`][proposal].
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalInstantString; |
||||
|
||||
impl TemporalInstantString { |
||||
/// Parses a targeted string as an `Instant`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDateTime> { |
||||
let parse_record = date_time::parse_annotated_date_time(false, true, true, cursor)?; |
||||
|
||||
let date = IsoDate { |
||||
year: parse_record.date.year, |
||||
month: parse_record.date.month, |
||||
day: parse_record.date.day, |
||||
calendar: parse_record.calendar, |
||||
}; |
||||
|
||||
let time = parse_record.time.map_or_else(IsoTime::default, |time| { |
||||
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction) |
||||
}); |
||||
|
||||
Ok(IsoDateTime { |
||||
date, |
||||
time, |
||||
tz: parse_record.tz, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// TODO: implement TemporalTimeString.
|
||||
|
||||
/// Parser for a [`TemporalDurationString`][proposal].
|
||||
///
|
||||
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalDurationString
|
||||
#[derive(Debug, Clone, Copy)] |
||||
pub struct TemporalDurationString; |
||||
|
||||
impl TemporalDurationString { |
||||
/// Parses a targeted string as a `Duration`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The parse will error if the provided target is not valid
|
||||
/// Iso8601 grammar.
|
||||
pub fn parse(cursor: &mut IsoCursor) -> ParseResult<IsoDuration> { |
||||
let parse_record = duration::parse_duration(cursor)?; |
||||
|
||||
let minutes = if parse_record.time.fhours > 0.0 { |
||||
parse_record.time.fhours * 60.0 |
||||
} else { |
||||
f64::from(parse_record.time.minutes) |
||||
}; |
||||
|
||||
let seconds = if parse_record.time.fminutes > 0.0 { |
||||
parse_record.time.fminutes * 60.0 |
||||
} else if parse_record.time.seconds > 0 { |
||||
f64::from(parse_record.time.seconds) |
||||
} else { |
||||
minutes.rem_euclid(1.0) * 60.0 |
||||
}; |
||||
|
||||
let milliseconds = if parse_record.time.fseconds > 0.0 { |
||||
parse_record.time.fseconds * 1000.0 |
||||
} else { |
||||
seconds.rem_euclid(1.0) * 1000.0 |
||||
}; |
||||
|
||||
let micro = milliseconds.rem_euclid(1.0) * 1000.0; |
||||
let nano = micro.rem_euclid(1.0) * 1000.0; |
||||
|
||||
let sign = if parse_record.sign { 1 } else { -1 }; |
||||
|
||||
Ok(IsoDuration { |
||||
years: parse_record.date.years * sign, |
||||
months: parse_record.date.months * sign, |
||||
weeks: parse_record.date.weeks * sign, |
||||
days: parse_record.date.days * sign, |
||||
hours: parse_record.time.hours * sign, |
||||
minutes: minutes.floor() * f64::from(sign), |
||||
seconds: seconds.floor() * f64::from(sign), |
||||
milliseconds: milliseconds.floor() * f64::from(sign), |
||||
microseconds: micro.floor() * f64::from(sign), |
||||
nanoseconds: nano.floor() * f64::from(sign), |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// ==== Mini cursor implementation for Iso8601 targets ====
|
||||
|
||||
/// `IsoCursor` is a small cursor implementation for parsing Iso8601 grammar.
|
||||
#[derive(Debug)] |
||||
pub struct IsoCursor { |
||||
pos: u32, |
||||
source: Vec<char>, |
||||
} |
||||
|
||||
impl IsoCursor { |
||||
/// Create a new cursor from a source `String` value.
|
||||
#[must_use] |
||||
pub fn new(source: &str) -> Self { |
||||
Self { |
||||
pos: 0, |
||||
source: source.chars().collect(), |
||||
} |
||||
} |
||||
|
||||
/// Returns a string value from a slice of the cursor.
|
||||
fn slice(&self, start: u32, end: u32) -> String { |
||||
self.source[start as usize..end as usize].iter().collect() |
||||
} |
||||
|
||||
/// Get current position
|
||||
const fn pos(&self) -> u32 { |
||||
self.pos |
||||
} |
||||
|
||||
/// Peek the value at the current position.
|
||||
fn peek(&self) -> Option<char> { |
||||
if (self.pos as usize) < self.source.len() { |
||||
Some(self.source[self.pos as usize]) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
|
||||
/// Peek the value at n len from current.
|
||||
fn peek_n(&self, n: u32) -> Option<char> { |
||||
let target = (self.pos + n) as usize; |
||||
if target < self.source.len() { |
||||
Some(self.source[target]) |
||||
} else { |
||||
None |
||||
} |
||||
} |
||||
|
||||
/// Returns boolean if current position passes check.
|
||||
fn check<F>(&self, f: F) -> Option<bool> |
||||
where |
||||
F: FnOnce(char) -> bool, |
||||
{ |
||||
self.peek().map(f) |
||||
} |
||||
|
||||
/// Returns boolean if current position passes check or default if None.
|
||||
fn check_or<F>(&self, default: bool, f: F) -> bool |
||||
where |
||||
F: FnOnce(char) -> bool, |
||||
{ |
||||
self.peek().map_or(default, f) |
||||
} |
||||
/// Advances the cursor's position and returns the new character.
|
||||
fn next(&mut self) -> Option<char> { |
||||
self.advance(); |
||||
self.peek() |
||||
} |
||||
|
||||
/// Advances the cursor's position by 1.
|
||||
fn advance(&mut self) { |
||||
self.pos += 1; |
||||
} |
||||
|
||||
/// Advances the cursor's position by `n`.
|
||||
fn advance_n(&mut self, n: u32) { |
||||
self.pos += n; |
||||
} |
||||
} |
@ -0,0 +1,190 @@
|
||||
use super::{ |
||||
IsoCursor, TemporalDateTimeString, TemporalDurationString, TemporalInstantString, |
||||
TemporalMonthDayString, TemporalYearMonthString, |
||||
}; |
||||
|
||||
#[test] |
||||
fn temporal_parser_basic() { |
||||
let basic = "20201108"; |
||||
let basic_separated = "2020-11-08"; |
||||
|
||||
let basic_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap(); |
||||
|
||||
let sep_result = |
||||
TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic_separated)).unwrap(); |
||||
|
||||
assert_eq!(basic_result.date.year, 2020); |
||||
assert_eq!(basic_result.date.month, 11); |
||||
assert_eq!(basic_result.date.day, 8); |
||||
assert_eq!(basic_result.date.year, sep_result.date.year); |
||||
assert_eq!(basic_result.date.month, sep_result.date.month); |
||||
assert_eq!(basic_result.date.day, sep_result.date.day); |
||||
} |
||||
|
||||
#[test] |
||||
#[allow(clippy::cast_possible_truncation)] |
||||
fn temporal_date_time_max() { |
||||
// Fractions not accurate, but for testing purposes.
|
||||
let date_time = |
||||
"+002020-11-08T12:28:32.329402834[!America/Argentina/ComodRivadavia][!u-ca=iso8601]"; |
||||
|
||||
let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(date_time)).unwrap(); |
||||
|
||||
let time_results = &result.time; |
||||
|
||||
assert_eq!(time_results.hour, 12); |
||||
assert_eq!(time_results.minute, 28); |
||||
assert_eq!(time_results.second, 32); |
||||
assert_eq!(time_results.millisecond, 329); |
||||
assert_eq!(time_results.microsecond, 402); |
||||
assert_eq!(time_results.nanosecond, 834); |
||||
|
||||
let tz = &result.tz.unwrap(); |
||||
|
||||
// OffsetSubMinute is Empty when TimeZoneIdentifier is present.
|
||||
assert!(&tz.offset.is_none()); |
||||
|
||||
let tz_name = &tz.name.clone().unwrap(); |
||||
|
||||
assert_eq!(tz_name, "America/Argentina/ComodRivadavia"); |
||||
|
||||
assert_eq!(&result.date.calendar, &Some("iso8601".to_string())); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_year_parsing() { |
||||
let long = "+002020-11-08"; |
||||
let bad_year = "-000000-11-08"; |
||||
|
||||
let result_good = TemporalDateTimeString::parse(false, &mut IsoCursor::new(long)).unwrap(); |
||||
assert_eq!(result_good.date.year, 2020); |
||||
|
||||
let err_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(bad_year)); |
||||
assert!(err_result.is_err()); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_annotated_date_time() { |
||||
let basic = "2020-11-08[America/Argentina/ComodRivadavia][u-ca=iso8601][foo=bar]"; |
||||
let omitted = "+0020201108[u-ca=iso8601][f-1a2b=a0sa-2l4s]"; |
||||
|
||||
let result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(basic)).unwrap(); |
||||
|
||||
let tz = &result.tz.unwrap().name.unwrap(); |
||||
|
||||
assert_eq!(tz, "America/Argentina/ComodRivadavia"); |
||||
|
||||
assert_eq!(&result.date.calendar, &Some("iso8601".to_string())); |
||||
|
||||
let omit_result = TemporalDateTimeString::parse(false, &mut IsoCursor::new(omitted)).unwrap(); |
||||
|
||||
assert!(&omit_result.tz.is_none()); |
||||
|
||||
assert_eq!(&omit_result.date.calendar, &Some("iso8601".to_string())); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_year_month() { |
||||
let possible_year_months = &[ |
||||
"+002020-11", |
||||
"2020-11[u-ca=iso8601]", |
||||
"+00202011", |
||||
"202011[u-ca=iso8601]", |
||||
]; |
||||
|
||||
for ym in possible_year_months { |
||||
let result = TemporalYearMonthString::parse(&mut IsoCursor::new(ym)).unwrap(); |
||||
|
||||
assert_eq!(result.year, 2020); |
||||
assert_eq!(result.month, 11); |
||||
|
||||
if let Some(calendar) = result.calendar { |
||||
assert_eq!(calendar, "iso8601"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_month_day() { |
||||
let possible_month_day = ["11-07", "1107[+04:00]", "--11-07", "--1107[+04:00]"]; |
||||
|
||||
for md in possible_month_day { |
||||
let result = TemporalMonthDayString::parse(&mut IsoCursor::new(md)).unwrap(); |
||||
|
||||
assert_eq!(result.month, 11); |
||||
assert_eq!(result.day, 7); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_invalid_annotations() { |
||||
let invalid_annotations = [ |
||||
"2020-11-11[!u-ca=iso8601][u-ca=iso8601]", |
||||
"2020-11-11[u-ca=iso8601][!u-ca=iso8601]", |
||||
"2020-11-11[u-ca=iso8601][!rip=this-invalid-annotation]", |
||||
]; |
||||
|
||||
for invalid in invalid_annotations { |
||||
let err_result = TemporalMonthDayString::parse(&mut IsoCursor::new(invalid)); |
||||
assert!(err_result.is_err()); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_valid_instant_strings() { |
||||
let instants = [ |
||||
"1970-01-01T00:00+00:00[!Africa/Abidjan]", |
||||
"1970-01-01T00:00+00:00[UTC]", |
||||
"1970-01-01T00:00Z[!Europe/Vienna]", |
||||
]; |
||||
|
||||
for test in instants { |
||||
let result = TemporalInstantString::parse(&mut IsoCursor::new(test)); |
||||
assert!(result.is_ok()); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
#[allow(clippy::cast_possible_truncation)] |
||||
fn temporal_duration_parsing() { |
||||
let durations = [ |
||||
"p1y1m1dt1h1m1s", |
||||
"P1Y1M1W1DT1H1M1.1S", |
||||
"-P1Y1M1W1DT1H1M1.123456789S", |
||||
"-P1Y3wT0,5H", |
||||
]; |
||||
|
||||
for dur in durations { |
||||
let ok_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)); |
||||
assert!(ok_result.is_ok()); |
||||
} |
||||
|
||||
let sub = durations[2]; |
||||
let sub_second = TemporalDurationString::parse(&mut IsoCursor::new(sub)).unwrap(); |
||||
|
||||
assert_eq!(sub_second.milliseconds, -123.0); |
||||
assert_eq!(sub_second.microseconds, -456.0); |
||||
assert_eq!(sub_second.nanoseconds, -789.0); |
||||
|
||||
let dur = durations[3]; |
||||
let test_result = TemporalDurationString::parse(&mut IsoCursor::new(dur)).unwrap(); |
||||
|
||||
assert_eq!(test_result.years, -1); |
||||
assert_eq!(test_result.weeks, -3); |
||||
assert_eq!(test_result.minutes, -30.0); |
||||
} |
||||
|
||||
#[test] |
||||
fn temporal_invalid_durations() { |
||||
let invalids = [ |
||||
"P1Y1M1W0,5D", |
||||
"P1Y1M1W1DT1H1M1.123456789123S", |
||||
"+PT", |
||||
"P1Y1M1W1DT1H0.5M0.5S", |
||||
]; |
||||
|
||||
for test in invalids { |
||||
let err = TemporalDurationString::parse(&mut IsoCursor::new(test)); |
||||
assert!(err.is_err()); |
||||
} |
||||
} |
@ -0,0 +1,146 @@
|
||||
//! Parsing of ISO8601 Time Values
|
||||
|
||||
use super::{ |
||||
grammar::{is_decimal_separator, is_time_separator}, |
||||
IsoCursor, |
||||
}; |
||||
use crate::{ |
||||
error::{Error, ParseResult}, |
||||
lexer::Error as LexError, |
||||
}; |
||||
|
||||
/// Parsed Time info
|
||||
#[derive(Debug, Default, Clone, Copy)] |
||||
pub(crate) struct TimeSpec { |
||||
/// An hour
|
||||
pub(crate) hour: u8, |
||||
/// A minute value
|
||||
pub(crate) minute: u8, |
||||
/// A second value.
|
||||
pub(crate) second: u8, |
||||
/// A floating point number representing the sub-second values
|
||||
pub(crate) fraction: f64, |
||||
} |
||||
|
||||
use boa_ast::Position; |
||||
|
||||
/// Parse `TimeSpec`
|
||||
pub(crate) fn parse_time_spec(cursor: &mut IsoCursor) -> ParseResult<TimeSpec> { |
||||
let hour = parse_hour(cursor)?; |
||||
let mut separator = false; |
||||
|
||||
if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) { |
||||
if cursor.check_or(false, is_time_separator) { |
||||
separator = true; |
||||
cursor.advance(); |
||||
} |
||||
} else { |
||||
return Ok(TimeSpec { |
||||
hour, |
||||
minute: 0, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}); |
||||
} |
||||
|
||||
let minute = parse_minute_second(cursor, false)?; |
||||
|
||||
if cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) { |
||||
let is_time_separator = cursor.check_or(false, is_time_separator); |
||||
if separator && is_time_separator { |
||||
cursor.advance(); |
||||
} else if is_time_separator { |
||||
return Err( |
||||
LexError::syntax("Invalid TimeSeparator", Position::new(1, cursor.pos())).into(), |
||||
); |
||||
} |
||||
} else { |
||||
return Ok(TimeSpec { |
||||
hour, |
||||
minute, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}); |
||||
} |
||||
|
||||
let second = parse_minute_second(cursor, true)?; |
||||
|
||||
let fraction = if cursor.check_or(false, is_decimal_separator) { |
||||
parse_fraction(cursor)? |
||||
} else { |
||||
0.0 |
||||
}; |
||||
|
||||
Ok(TimeSpec { |
||||
hour, |
||||
minute, |
||||
second, |
||||
fraction, |
||||
}) |
||||
} |
||||
|
||||
pub(crate) fn parse_hour(cursor: &mut IsoCursor) -> ParseResult<u8> { |
||||
let hour_value = cursor |
||||
.slice(cursor.pos(), cursor.pos() + 2) |
||||
.parse::<u8>() |
||||
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; |
||||
if !(0..=23).contains(&hour_value) { |
||||
return Err(LexError::syntax( |
||||
"Hour must be in a range of 0-23", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
cursor.advance_n(2); |
||||
Ok(hour_value) |
||||
} |
||||
|
||||
// NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`.
|
||||
/// Parse `MinuteSecond`
|
||||
pub(crate) fn parse_minute_second(cursor: &mut IsoCursor, inclusive: bool) -> ParseResult<u8> { |
||||
let min_sec_value = cursor |
||||
.slice(cursor.pos(), cursor.pos() + 2) |
||||
.parse::<u8>() |
||||
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos())))?; |
||||
|
||||
let valid_range = if inclusive { 0..=60 } else { 0..=59 }; |
||||
if !valid_range.contains(&min_sec_value) { |
||||
return Err(LexError::syntax( |
||||
"MinuteSecond must be in a range of 0-59", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
|
||||
cursor.advance_n(2); |
||||
Ok(min_sec_value) |
||||
} |
||||
|
||||
/// Parse a `Fraction` value
|
||||
///
|
||||
/// This is primarily used in ISO8601 to add percision past
|
||||
/// a second.
|
||||
pub(crate) fn parse_fraction(cursor: &mut IsoCursor) -> ParseResult<f64> { |
||||
// Decimal is skipped by next call.
|
||||
let mut fraction_components = Vec::from(['.']); |
||||
while let Some(ch) = cursor.next() { |
||||
if !ch.is_ascii_digit() { |
||||
if fraction_components.len() > 10 { |
||||
return Err(Error::general( |
||||
"Fraction exceeds 9 DecimalDigits", |
||||
Position::new(1, cursor.pos() - 1), |
||||
)); |
||||
} |
||||
|
||||
let fraction_value = fraction_components |
||||
.iter() |
||||
.collect::<String>() |
||||
.parse::<f64>() |
||||
.map_err(|e| Error::general(e.to_string(), Position::new(1, cursor.pos() - 1)))?; |
||||
return Ok(fraction_value); |
||||
} |
||||
fraction_components.push(ch); |
||||
} |
||||
|
||||
Err(Error::AbruptEnd) |
||||
} |
@ -0,0 +1,263 @@
|
||||
//! ISO8601 parsing for Time Zone and Offset data.
|
||||
|
||||
use super::{ |
||||
grammar::{ |
||||
is_a_key_char, is_a_key_leading_char, is_annotation_close, |
||||
is_annotation_key_value_separator, is_annotation_open, is_critical_flag, |
||||
is_decimal_separator, is_sign, is_time_separator, is_tz_char, is_tz_leading_char, |
||||
is_tz_name_separator, is_utc_designator, |
||||
}, |
||||
time::{parse_fraction, parse_hour, parse_minute_second}, |
||||
IsoCursor, |
||||
}; |
||||
use crate::{ |
||||
error::{Error, ParseResult}, |
||||
lexer::Error as LexError, |
||||
}; |
||||
|
||||
use boa_ast::{ |
||||
temporal::{TimeZone, UTCOffset}, |
||||
Position, |
||||
}; |
||||
|
||||
/// A `TimeZoneAnnotation`.
|
||||
#[derive(Debug, Clone)] |
||||
#[allow(unused)] |
||||
pub(crate) struct TimeZoneAnnotation { |
||||
/// Critical Flag for the annotation.
|
||||
pub(crate) critical: bool, |
||||
/// TimeZone Data
|
||||
pub(crate) tz: TimeZone, |
||||
} |
||||
|
||||
// ==== Time Zone Annotation Parsing ====
|
||||
|
||||
pub(crate) fn parse_ambiguous_tz_annotation( |
||||
cursor: &mut IsoCursor, |
||||
) -> ParseResult<Option<TimeZoneAnnotation>> { |
||||
// Peek position + 1 to check for critical flag.
|
||||
let mut current_peek = 1; |
||||
let critical = cursor |
||||
.peek_n(current_peek) |
||||
.map(is_critical_flag) |
||||
.ok_or_else(|| Error::AbruptEnd)?; |
||||
|
||||
// Advance cursor if critical flag present.
|
||||
if critical { |
||||
current_peek += 1; |
||||
} |
||||
|
||||
let leading_char = cursor |
||||
.peek_n(current_peek) |
||||
.ok_or_else(|| Error::AbruptEnd)?; |
||||
|
||||
if is_tz_leading_char(leading_char) || is_sign(leading_char) { |
||||
// Ambigious start values when lowercase alpha that is shared between `TzLeadingChar` and `KeyLeadingChar`.
|
||||
if is_a_key_leading_char(leading_char) { |
||||
let mut peek_pos = current_peek + 1; |
||||
while let Some(ch) = cursor.peek_n(peek_pos) { |
||||
if is_tz_name_separator(ch) || (is_tz_char(ch) && !is_a_key_char(ch)) { |
||||
let tz = parse_tz_annotation(cursor)?; |
||||
return Ok(Some(tz)); |
||||
} else if is_annotation_key_value_separator(ch) |
||||
|| (is_a_key_char(ch) && !is_tz_char(ch)) |
||||
{ |
||||
return Ok(None); |
||||
} else if is_annotation_close(ch) { |
||||
return Err(LexError::syntax( |
||||
"Invalid Annotation", |
||||
Position::new(1, peek_pos + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
|
||||
peek_pos += 1; |
||||
} |
||||
return Err(Error::AbruptEnd); |
||||
} |
||||
let tz = parse_tz_annotation(cursor)?; |
||||
return Ok(Some(tz)); |
||||
} |
||||
|
||||
if is_a_key_leading_char(leading_char) { |
||||
return Ok(None); |
||||
}; |
||||
|
||||
Err(Error::lex(LexError::syntax( |
||||
"Unexpected character in ambiguous annotation.", |
||||
Position::new(1, cursor.pos() + 1), |
||||
))) |
||||
} |
||||
|
||||
fn parse_tz_annotation(cursor: &mut IsoCursor) -> ParseResult<TimeZoneAnnotation> { |
||||
debug_assert!(is_annotation_open(cursor.peek().expect("annotation start"))); |
||||
|
||||
let potential_critical = cursor.next().ok_or_else(|| Error::AbruptEnd)?; |
||||
let critical = is_critical_flag(potential_critical); |
||||
|
||||
if critical { |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let tz = parse_time_zone(cursor)?; |
||||
|
||||
if !cursor.check_or(false, is_annotation_close) { |
||||
return Err(LexError::syntax( |
||||
"Invalid TimeZoneAnnotation.", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()); |
||||
} |
||||
|
||||
cursor.advance(); |
||||
|
||||
Ok(TimeZoneAnnotation { critical, tz }) |
||||
} |
||||
|
||||
/// Parses the [`TimeZoneIdentifier`][tz] node.
|
||||
///
|
||||
/// [tz]: https://tc39.es/proposal-temporal/#prod-TimeZoneIdentifier
|
||||
pub(crate) fn parse_time_zone(cursor: &mut IsoCursor) -> ParseResult<TimeZone> { |
||||
let is_iana = cursor |
||||
.check(is_tz_leading_char) |
||||
.ok_or_else(|| Error::AbruptEnd)?; |
||||
let is_offset = cursor.check_or(false, is_sign); |
||||
|
||||
if is_iana { |
||||
return parse_tz_iana_name(cursor); |
||||
} else if is_offset { |
||||
let offset = parse_utc_offset_minute_precision(cursor)?; |
||||
return Ok(TimeZone { |
||||
name: None, |
||||
offset: Some(offset), |
||||
}); |
||||
} |
||||
|
||||
Err(LexError::syntax( |
||||
"Invalid leading character for a TimeZoneIdentifier", |
||||
Position::new(1, cursor.pos() + 1), |
||||
) |
||||
.into()) |
||||
} |
||||
|
||||
/// Parse a `TimeZoneIANAName` Parse Node
|
||||
fn parse_tz_iana_name(cursor: &mut IsoCursor) -> ParseResult<TimeZone> { |
||||
let tz_name_start = cursor.pos(); |
||||
while let Some(potential_value_char) = cursor.next() { |
||||
if is_tz_name_separator(potential_value_char) { |
||||
if !cursor.peek_n(1).map_or(false, is_tz_char) { |
||||
return Err(LexError::syntax( |
||||
"Missing TimeZoneIANANameComponent after '/'", |
||||
Position::new(1, cursor.pos() + 2), |
||||
) |
||||
.into()); |
||||
} |
||||
continue; |
||||
} |
||||
|
||||
if !is_tz_char(potential_value_char) { |
||||
// Return the valid TimeZoneIANAName
|
||||
return Ok(TimeZone { |
||||
name: Some(cursor.slice(tz_name_start, cursor.pos())), |
||||
offset: None, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
Err(Error::AbruptEnd) |
||||
} |
||||
|
||||
// ==== Utc Offset Parsing ====
|
||||
|
||||
/// Parse a full precision `UtcOffset`
|
||||
pub(crate) fn parse_date_time_utc(cursor: &mut IsoCursor) -> ParseResult<TimeZone> { |
||||
if cursor.check_or(false, is_utc_designator) { |
||||
cursor.advance(); |
||||
return Ok(TimeZone { |
||||
name: Some("UTC".to_owned()), |
||||
offset: None, |
||||
}); |
||||
} |
||||
|
||||
let separated = cursor.peek_n(3).map_or(false, is_time_separator); |
||||
|
||||
let mut utc_to_minute = parse_utc_offset_minute_precision(cursor)?; |
||||
|
||||
if cursor.check_or(false, is_time_separator) { |
||||
if !separated { |
||||
return Err(LexError::syntax( |
||||
"Unexpected TimeSeparator", |
||||
Position::new(1, cursor.pos()), |
||||
) |
||||
.into()); |
||||
} |
||||
cursor.advance(); |
||||
} |
||||
|
||||
// Return early on None or next char an AnnotationOpen.
|
||||
if cursor.check_or(true, is_annotation_open) { |
||||
return Ok(TimeZone { |
||||
name: None, |
||||
offset: Some(utc_to_minute), |
||||
}); |
||||
} |
||||
|
||||
// If `UtcOffsetWithSubMinuteComponents`, continue parsing.
|
||||
utc_to_minute.second = parse_minute_second(cursor, true)?; |
||||
|
||||
let sub_second = if cursor.check_or(false, is_decimal_separator) { |
||||
parse_fraction(cursor)? |
||||
} else { |
||||
0.0 |
||||
}; |
||||
|
||||
utc_to_minute.fraction = sub_second; |
||||
|
||||
Ok(TimeZone { |
||||
name: None, |
||||
offset: Some(utc_to_minute), |
||||
}) |
||||
} |
||||
|
||||
/// Parse an `UtcOffsetMinutePrecision` node
|
||||
pub(crate) fn parse_utc_offset_minute_precision(cursor: &mut IsoCursor) -> ParseResult<UTCOffset> { |
||||
let sign = if let Some(ch) = cursor.next() { |
||||
if ch == '+' { |
||||
1_i8 |
||||
} else { |
||||
-1_i8 |
||||
} |
||||
} else { |
||||
return Err(Error::AbruptEnd); |
||||
}; |
||||
let hour = parse_hour(cursor)?; |
||||
|
||||
// If at the end of the utc, then return.
|
||||
if cursor |
||||
.check(|ch| !(ch.is_ascii_digit() || is_time_separator(ch))) |
||||
.ok_or_else(|| Error::AbruptEnd)? |
||||
{ |
||||
return Ok(UTCOffset { |
||||
sign, |
||||
hour, |
||||
minute: 0, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}); |
||||
} |
||||
|
||||
// Advance cursor beyond any TimeSeparator
|
||||
if cursor.check_or(false, is_time_separator) { |
||||
cursor.advance(); |
||||
} |
||||
|
||||
let minute = parse_minute_second(cursor, false)?; |
||||
|
||||
Ok(UTCOffset { |
||||
sign, |
||||
hour, |
||||
minute, |
||||
second: 0, |
||||
fraction: 0.0, |
||||
}) |
||||
} |
Loading…
Reference in new issue