mirror of https://github.com/boa-dev/boa.git
Browse Source
* Update round and total method * Begin Temporal crate migration * Add dyn Any context and some small changes * General completion and clean up work of migration * Finish up clean up and linting of draft * Post-rebase update and a couple changes * Rename and some cleanup * Remove migrated record file * Apply Reviewpull/3498/head
Kevin
11 months ago
committed by
GitHub
40 changed files with 5354 additions and 4445 deletions
@ -1,343 +0,0 @@ |
|||||||
//! Implementation of the "iso8601" `BuiltinCalendar`.
|
|
||||||
|
|
||||||
use crate::{ |
|
||||||
builtins::temporal::{ |
|
||||||
self, |
|
||||||
date_equations::mathematical_days_in_year, |
|
||||||
duration::DurationRecord, |
|
||||||
options::{ArithmeticOverflow, TemporalUnit}, |
|
||||||
plain_date::iso::IsoDateRecord, |
|
||||||
}, |
|
||||||
js_string, JsNativeError, JsResult, JsString, |
|
||||||
}; |
|
||||||
|
|
||||||
use super::{BuiltinCalendar, FieldsType}; |
|
||||||
|
|
||||||
use icu_calendar::{ |
|
||||||
iso::Iso, |
|
||||||
week::{RelativeUnit, WeekCalculator}, |
|
||||||
Calendar, Date, |
|
||||||
}; |
|
||||||
|
|
||||||
pub(crate) struct IsoCalendar; |
|
||||||
|
|
||||||
impl BuiltinCalendar for IsoCalendar { |
|
||||||
/// Temporal 15.8.2.1 `Temporal.prototype.dateFromFields( fields [, options])` - Supercedes 12.5.4
|
|
||||||
///
|
|
||||||
/// This is a basic implementation for an iso8601 calendar's `dateFromFields` method.
|
|
||||||
fn date_from_fields( |
|
||||||
&self, |
|
||||||
fields: &mut temporal::TemporalFields, |
|
||||||
overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<IsoDateRecord> { |
|
||||||
// NOTE: we are in ISO by default here.
|
|
||||||
// a. Perform ? ISOResolveMonth(fields).
|
|
||||||
// b. Let result be ? ISODateFromFields(fields, overflow).
|
|
||||||
fields.iso_resolve_month()?; |
|
||||||
|
|
||||||
// Extra: handle reulating/overflow until implemented on `icu_calendar`
|
|
||||||
fields.regulate(overflow)?; |
|
||||||
|
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
fields.year().unwrap_or(0), |
|
||||||
fields.month().unwrap_or(250) as u8, |
|
||||||
fields.day().unwrap_or(250) as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
// 9. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
|
|
||||||
Ok(IsoDateRecord::from_date_iso(date)) |
|
||||||
} |
|
||||||
|
|
||||||
/// 12.5.5 `Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] )`
|
|
||||||
///
|
|
||||||
/// This is a basic implementation for an iso8601 calendar's `yearMonthFromFields` method.
|
|
||||||
fn year_month_from_fields( |
|
||||||
&self, |
|
||||||
fields: &mut temporal::TemporalFields, |
|
||||||
overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<IsoDateRecord> { |
|
||||||
// 9. If calendar.[[Identifier]] is "iso8601", then
|
|
||||||
// a. Perform ? ISOResolveMonth(fields).
|
|
||||||
fields.iso_resolve_month()?; |
|
||||||
|
|
||||||
// b. Let result be ? ISOYearMonthFromFields(fields, overflow).
|
|
||||||
fields.regulate_year_month(overflow); |
|
||||||
|
|
||||||
let result = Date::try_new_iso_date( |
|
||||||
fields.year().unwrap_or(0), |
|
||||||
fields.month().unwrap_or(250) as u8, |
|
||||||
fields.day().unwrap_or(20) as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
// 10. Return ? CreateTemporalYearMonth(result.[[Year]], result.[[Month]], "iso8601", result.[[ReferenceISODay]]).
|
|
||||||
Ok(IsoDateRecord::from_date_iso(result)) |
|
||||||
} |
|
||||||
|
|
||||||
/// 12.5.6 `Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] )`
|
|
||||||
///
|
|
||||||
/// This is a basic implementation for an iso8601 calendar's `monthDayFromFields` method.
|
|
||||||
fn month_day_from_fields( |
|
||||||
&self, |
|
||||||
fields: &mut temporal::TemporalFields, |
|
||||||
overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<IsoDateRecord> { |
|
||||||
// 8. Perform ? ISOResolveMonth(fields).
|
|
||||||
fields.iso_resolve_month()?; |
|
||||||
|
|
||||||
fields.regulate(overflow)?; |
|
||||||
|
|
||||||
// TODO: double check error mapping is correct for specifcation/test262.
|
|
||||||
// 9. Let result be ? ISOMonthDayFromFields(fields, overflow).
|
|
||||||
let result = Date::try_new_iso_date( |
|
||||||
fields.year().unwrap_or(1972), |
|
||||||
fields.month().unwrap_or(250) as u8, |
|
||||||
fields.day().unwrap_or(250) as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
// 10. Return ? CreateTemporalMonthDay(result.[[Month]], result.[[Day]], "iso8601", result.[[ReferenceISOYear]]).
|
|
||||||
Ok(IsoDateRecord::from_date_iso(result)) |
|
||||||
} |
|
||||||
|
|
||||||
/// 12.5.7 `Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] )`
|
|
||||||
///
|
|
||||||
/// Below implements the basic implementation for an iso8601 calendar's `dateAdd` method.
|
|
||||||
fn date_add( |
|
||||||
&self, |
|
||||||
_date: &temporal::PlainDate, |
|
||||||
_duration: &DurationRecord, |
|
||||||
_overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<IsoDateRecord> { |
|
||||||
// TODO: Not stable on `ICU4X`. Implement once completed.
|
|
||||||
Err(JsNativeError::range() |
|
||||||
.with_message("feature not implemented.") |
|
||||||
.into()) |
|
||||||
|
|
||||||
// 9. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], balanceResult.[[Days]], overflow).
|
|
||||||
// 10. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
|
|
||||||
} |
|
||||||
|
|
||||||
/// 12.5.8 `Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] )`
|
|
||||||
///
|
|
||||||
/// Below implements the basic implementation for an iso8601 calendar's `dateUntil` method.
|
|
||||||
fn date_until( |
|
||||||
&self, |
|
||||||
_one: &temporal::PlainDate, |
|
||||||
_two: &temporal::PlainDate, |
|
||||||
_largest_unit: TemporalUnit, |
|
||||||
) -> JsResult<DurationRecord> { |
|
||||||
// TODO: Not stable on `ICU4X`. Implement once completed.
|
|
||||||
Err(JsNativeError::range() |
|
||||||
.with_message("Feature not yet implemented.") |
|
||||||
.into()) |
|
||||||
|
|
||||||
// 9. Let result be DifferenceISODate(one.[[ISOYear]], one.[[ISOMonth]], one.[[ISODay]], two.[[ISOYear]], two.[[ISOMonth]], two.[[ISODay]], largestUnit).
|
|
||||||
// 10. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], 0, 0, 0, 0, 0, 0).
|
|
||||||
} |
|
||||||
|
|
||||||
/// `Temporal.Calendar.prototype.era( dateLike )` for iso8601 calendar.
|
|
||||||
fn era(&self, _: &IsoDateRecord) -> JsResult<Option<JsString>> { |
|
||||||
// Returns undefined on iso8601.
|
|
||||||
Ok(None) |
|
||||||
} |
|
||||||
|
|
||||||
/// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar.
|
|
||||||
fn era_year(&self, _: &IsoDateRecord) -> JsResult<Option<i32>> { |
|
||||||
// Returns undefined on iso8601.
|
|
||||||
Ok(None) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `year` for the `Iso` calendar.
|
|
||||||
fn year(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(date.year().number) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `month` for the `Iso` calendar.
|
|
||||||
fn month(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(date.month().ordinal as i32) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `monthCode` for the `Iso` calendar.
|
|
||||||
fn month_code(&self, date_like: &IsoDateRecord) -> JsResult<JsString> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(JsString::from(date.month().code.to_string())) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `day` for the `Iso` calendar.
|
|
||||||
fn day(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(date.day_of_month().0 as i32) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `dayOfWeek` for the `Iso` calendar.
|
|
||||||
fn day_of_week(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(date.day_of_week() as i32) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `dayOfYear` for the `Iso` calendar.
|
|
||||||
fn day_of_year(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(i32::from(date.day_of_year_info().day_of_year)) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `weekOfYear` for the `Iso` calendar.
|
|
||||||
fn week_of_year(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
// TODO: Determine `ICU4X` equivalent.
|
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
let week_calculator = WeekCalculator::default(); |
|
||||||
|
|
||||||
let week_of = date |
|
||||||
.week_of_year(&week_calculator) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(i32::from(week_of.week)) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `yearOfWeek` for the `Iso` calendar.
|
|
||||||
fn year_of_week(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
// TODO: Determine `ICU4X` equivalent.
|
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
let week_calculator = WeekCalculator::default(); |
|
||||||
|
|
||||||
let week_of = date |
|
||||||
.week_of_year(&week_calculator) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
match week_of.unit { |
|
||||||
RelativeUnit::Previous => Ok(date.year().number - 1), |
|
||||||
RelativeUnit::Current => Ok(date.year().number), |
|
||||||
RelativeUnit::Next => Ok(date.year().number + 1), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `daysInWeek` value for the `Iso` calendar.
|
|
||||||
fn days_in_week(&self, _: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
Ok(7) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `daysInMonth` value for the `Iso` calendar.
|
|
||||||
fn days_in_month(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(i32::from(date.days_in_month())) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `daysInYear` value for the `Iso` calendar.
|
|
||||||
fn days_in_year(&self, date_like: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
let date = Date::try_new_iso_date( |
|
||||||
date_like.year(), |
|
||||||
date_like.month() as u8, |
|
||||||
date_like.day() as u8, |
|
||||||
) |
|
||||||
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?; |
|
||||||
|
|
||||||
Ok(i32::from(date.days_in_year())) |
|
||||||
} |
|
||||||
|
|
||||||
/// Return the amount of months in an ISO Calendar.
|
|
||||||
fn months_in_year(&self, _: &IsoDateRecord) -> JsResult<i32> { |
|
||||||
Ok(12) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns whether provided date is in a leap year according to this calendar.
|
|
||||||
fn in_leap_year(&self, date_like: &IsoDateRecord) -> JsResult<bool> { |
|
||||||
// `ICU4X`'s `CalendarArithmetic` is currently private.
|
|
||||||
if mathematical_days_in_year(date_like.year()) == 366 { |
|
||||||
return Ok(true); |
|
||||||
} |
|
||||||
Ok(false) |
|
||||||
} |
|
||||||
|
|
||||||
// Resolve the fields for the iso calendar.
|
|
||||||
fn resolve_fields(&self, fields: &mut temporal::TemporalFields, _: FieldsType) -> JsResult<()> { |
|
||||||
fields.iso_resolve_month()?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the ISO field descriptors, which is not called for the iso8601 calendar.
|
|
||||||
fn field_descriptors(&self, _: FieldsType) -> Vec<(JsString, bool)> { |
|
||||||
// NOTE(potential improvement): look into implementing field descriptors and call
|
|
||||||
// ISO like any other calendar?
|
|
||||||
// Field descriptors is unused on ISO8601.
|
|
||||||
unreachable!() |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the `CalendarFieldKeysToIgnore` implementation for ISO.
|
|
||||||
fn field_keys_to_ignore(&self, additional_keys: Vec<JsString>) -> Vec<JsString> { |
|
||||||
let mut result = Vec::new(); |
|
||||||
for key in &additional_keys { |
|
||||||
result.push(key.clone()); |
|
||||||
if key.to_std_string_escaped().as_str() == "month" { |
|
||||||
result.push(js_string!("monthCode")); |
|
||||||
} else if key.to_std_string_escaped().as_str() == "monthCode" { |
|
||||||
result.push(js_string!("month")); |
|
||||||
} |
|
||||||
} |
|
||||||
result |
|
||||||
} |
|
||||||
|
|
||||||
// NOTE: This is currently not a name that is compliant with
|
|
||||||
// the Temporal proposal. For debugging purposes only.
|
|
||||||
/// Returns the debug name.
|
|
||||||
fn debug_name(&self) -> &str { |
|
||||||
Iso.debug_name() |
|
||||||
} |
|
||||||
} |
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,883 @@ |
|||||||
|
//! Boa's implementation of a user-defined Anonymous Calendar.
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
builtins::temporal::{plain_date, plain_month_day, plain_year_month}, |
||||||
|
property::PropertyKey, |
||||||
|
Context, JsObject, JsString, JsValue, |
||||||
|
}; |
||||||
|
use std::any::Any; |
||||||
|
|
||||||
|
use boa_macros::utf16; |
||||||
|
use boa_temporal::{ |
||||||
|
calendar::{CalendarDateLike, CalendarProtocol}, |
||||||
|
date::Date, |
||||||
|
duration::Duration, |
||||||
|
error::TemporalError, |
||||||
|
fields::TemporalFields, |
||||||
|
month_day::MonthDay, |
||||||
|
options::ArithmeticOverflow, |
||||||
|
year_month::YearMonth, |
||||||
|
TemporalResult, TinyAsciiStr, |
||||||
|
}; |
||||||
|
use num_traits::ToPrimitive; |
||||||
|
|
||||||
|
/// A user-defined, custom calendar that is only known at runtime
|
||||||
|
/// and executed at runtime.
|
||||||
|
///
|
||||||
|
/// A user-defined calendar implements all of the `CalendarProtocolMethods`
|
||||||
|
/// and therefore satisfies the requirements to be used as a calendar.
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub(crate) struct CustomRuntimeCalendar { |
||||||
|
calendar: JsObject, |
||||||
|
} |
||||||
|
|
||||||
|
impl CustomRuntimeCalendar { |
||||||
|
pub(crate) fn new(calendar: &JsObject) -> Self { |
||||||
|
Self { |
||||||
|
calendar: calendar.clone(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl CalendarProtocol for CustomRuntimeCalendar { |
||||||
|
fn date_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Date> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(utf16!("dateFromFields"), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let fields = JsObject::from_temporal_fields(fields, context) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let overflow_obj = JsObject::with_null_proto(); |
||||||
|
|
||||||
|
overflow_obj |
||||||
|
.create_data_property_or_throw( |
||||||
|
utf16!("overflow"), |
||||||
|
JsString::from(overflow.to_string()), |
||||||
|
context, |
||||||
|
) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let value = method |
||||||
|
.as_callable() |
||||||
|
.ok_or_else(|| { |
||||||
|
TemporalError::general("dateFromFields must be implemented as a callable method.") |
||||||
|
})? |
||||||
|
.call( |
||||||
|
&self.calendar.clone().into(), |
||||||
|
&[fields.into(), overflow_obj.into()], |
||||||
|
context, |
||||||
|
) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||||
|
TemporalError::r#type() |
||||||
|
.with_message("datefromFields must return a valid PlainDate object.") |
||||||
|
})?; |
||||||
|
|
||||||
|
let pd = obj.as_plain_date().ok_or_else(|| { |
||||||
|
TemporalError::r#type().with_message("Object returned was not a PlainDate") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(pd.inner.clone()) |
||||||
|
} |
||||||
|
|
||||||
|
fn year_month_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<YearMonth> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(utf16!("yearMonthFromFields"), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let fields = JsObject::from_temporal_fields(fields, context) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let overflow_obj = JsObject::with_null_proto(); |
||||||
|
|
||||||
|
overflow_obj |
||||||
|
.create_data_property_or_throw( |
||||||
|
utf16!("overflow"), |
||||||
|
JsString::from(overflow.to_string()), |
||||||
|
context, |
||||||
|
) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let value = method |
||||||
|
.as_callable() |
||||||
|
.ok_or_else(|| { |
||||||
|
TemporalError::general( |
||||||
|
"yearMonthFromFields must be implemented as a callable method.", |
||||||
|
) |
||||||
|
})? |
||||||
|
.call( |
||||||
|
&self.calendar.clone().into(), |
||||||
|
&[fields.into(), overflow_obj.into()], |
||||||
|
context, |
||||||
|
) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||||
|
TemporalError::r#type() |
||||||
|
.with_message("yearMonthFromFields must return a valid PlainYearMonth object.") |
||||||
|
})?; |
||||||
|
|
||||||
|
let ym = obj.as_plain_year_month().ok_or_else(|| { |
||||||
|
TemporalError::r#type().with_message("Object returned was not a PlainDate") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(ym.inner.clone()) |
||||||
|
} |
||||||
|
|
||||||
|
fn month_day_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<MonthDay> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(utf16!("yearMonthFromFields"), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let fields = JsObject::from_temporal_fields(fields, context) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let overflow_obj = JsObject::with_null_proto(); |
||||||
|
|
||||||
|
overflow_obj |
||||||
|
.create_data_property_or_throw( |
||||||
|
utf16!("overflow"), |
||||||
|
JsString::from(overflow.to_string()), |
||||||
|
context, |
||||||
|
) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let value = method |
||||||
|
.as_callable() |
||||||
|
.ok_or_else(|| { |
||||||
|
TemporalError::general( |
||||||
|
"yearMonthFromFields must be implemented as a callable method.", |
||||||
|
) |
||||||
|
})? |
||||||
|
.call( |
||||||
|
&self.calendar.clone().into(), |
||||||
|
&[fields.into(), overflow_obj.into()], |
||||||
|
context, |
||||||
|
) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string()))?; |
||||||
|
|
||||||
|
let obj = value.as_object().map(JsObject::borrow).ok_or_else(|| { |
||||||
|
TemporalError::r#type() |
||||||
|
.with_message("yearMonthFromFields must return a valid PlainYearMonth object.") |
||||||
|
})?; |
||||||
|
|
||||||
|
let md = obj.as_plain_month_day().ok_or_else(|| { |
||||||
|
TemporalError::r#type().with_message("Object returned was not a PlainDate") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(md.inner.clone()) |
||||||
|
} |
||||||
|
|
||||||
|
fn date_add( |
||||||
|
&self, |
||||||
|
_date: &Date, |
||||||
|
_duration: &Duration, |
||||||
|
_overflow: ArithmeticOverflow, |
||||||
|
_context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Date> { |
||||||
|
// TODO
|
||||||
|
Err(TemporalError::general("Not yet implemented.")) |
||||||
|
} |
||||||
|
|
||||||
|
fn date_until( |
||||||
|
&self, |
||||||
|
_one: &Date, |
||||||
|
_two: &Date, |
||||||
|
_largest_unit: boa_temporal::options::TemporalUnit, |
||||||
|
_context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Duration> { |
||||||
|
// TODO
|
||||||
|
Err(TemporalError::general("Not yet implemented.")) |
||||||
|
} |
||||||
|
|
||||||
|
fn era( |
||||||
|
&self, |
||||||
|
_: &CalendarDateLike, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<Option<TinyAsciiStr<8>>> { |
||||||
|
// Return undefined as custom calendars do not implement -> Currently.
|
||||||
|
Ok(None) |
||||||
|
} |
||||||
|
|
||||||
|
fn era_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<Option<i32>> { |
||||||
|
// Return undefined as custom calendars do not implement -> Currently.
|
||||||
|
Ok(None) |
||||||
|
} |
||||||
|
|
||||||
|
fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<i32> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("year")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("year must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("year return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err(TemporalError::r#type().with_message("year return must be larger than 1.")); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number |
||||||
|
.to_i32() |
||||||
|
.ok_or_else(|| TemporalError::range().with_message("year exceeded a valid range."))?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("month")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("month must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("month return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err(TemporalError::r#type().with_message("month return must be larger than 1.")); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number |
||||||
|
.to_u8() |
||||||
|
.ok_or_else(|| TemporalError::range().with_message("month exceeded a valid range."))?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn month_code( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<TinyAsciiStr<4>> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("monthCode")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
let JsValue::String(result) = val else { |
||||||
|
return Err(TemporalError::r#type().with_message("monthCode return must be a String.")); |
||||||
|
}; |
||||||
|
|
||||||
|
let result = TinyAsciiStr::<4>::from_str(&result.to_std_string_escaped()) |
||||||
|
.map_err(|_| TemporalError::general("Unexpected monthCode value."))?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("day")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("day must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("day return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err(TemporalError::r#type().with_message("day return must be larger than 1.")); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number |
||||||
|
.to_u8() |
||||||
|
.ok_or_else(|| TemporalError::range().with_message("day exceeded a valid range."))?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn day_of_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("dayOfWeek")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("DayOfWeek must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("DayOfWeek return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("DayOfWeek return must be larger than 1.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_u16().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("DayOfWeek exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn day_of_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("dayOfYear")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("dayOfYear must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("dayOfYear return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("dayOfYear return must be larger than 1.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_u16().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("dayOfYear exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn week_of_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("weekOfYear")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("weekOfYear must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("weekOfYear return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("weekOfYear return must be larger than 1.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_u16().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("weekOfYear exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn year_of_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<i32> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("yearOfWeek")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("yearOfWeek must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("yearOfWeek return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_i32().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("yearOfWeek exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn days_in_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("daysInWeek")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("daysInWeek must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("daysInWeek return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("daysInWeek return must be larger than 1.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_u16().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("daysInWeek exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn days_in_month( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("daysInMonth")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("daysInMonth must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("daysInMonth return must be integral.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("daysInMonth return must be larger than 1.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_u16().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("daysInMonth exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn days_in_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("daysInYear")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("daysInYear must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err(TemporalError::r#type().with_message("daysInYear return must be integral.")); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("daysInYear return must be larger than 1.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_u16().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("monthsInYear exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn months_in_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("monthsInYear")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
// Validate the return value.
|
||||||
|
// 3. If Type(result) is not Number, throw a TypeError exception.
|
||||||
|
// 4. If IsIntegralNumber(result) is false, throw a RangeError exception.
|
||||||
|
// 5. If result < 1𝔽, throw a RangeError exception.
|
||||||
|
// 6. Return ℝ(result).
|
||||||
|
|
||||||
|
let Some(number) = val.as_number() else { |
||||||
|
return Err(TemporalError::r#type().with_message("monthsInYear must return a number.")); |
||||||
|
}; |
||||||
|
|
||||||
|
if !number.is_finite() || number.fract() != 0.0 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("monthsInYear return must be integral.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if number < 1f64 { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("monthsInYear return must be larger than 1.") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let result = number.to_u16().ok_or_else(|| { |
||||||
|
TemporalError::range().with_message("monthsInYear exceeded valid range.") |
||||||
|
})?; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
fn in_leap_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<bool> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let date_like = date_like_to_object(date_like, context)?; |
||||||
|
|
||||||
|
let method = self |
||||||
|
.calendar |
||||||
|
.get(PropertyKey::from(utf16!("inLeapYear")), context) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let val = method |
||||||
|
.as_callable() |
||||||
|
.expect("is method") |
||||||
|
.call(&method, &[date_like], context) |
||||||
|
.map_err(|err| TemporalError::general(err.to_string()))?; |
||||||
|
|
||||||
|
let JsValue::Boolean(result) = val else { |
||||||
|
return Err( |
||||||
|
TemporalError::r#type().with_message("inLeapYear must return a valid boolean.") |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
Ok(result) |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Determine fate of fn fields()
|
||||||
|
|
||||||
|
fn field_descriptors( |
||||||
|
&self, |
||||||
|
_: boa_temporal::calendar::CalendarFieldsType, |
||||||
|
) -> Vec<(String, bool)> { |
||||||
|
Vec::default() |
||||||
|
} |
||||||
|
|
||||||
|
fn field_keys_to_ignore(&self, _: Vec<String>) -> Vec<String> { |
||||||
|
Vec::default() |
||||||
|
} |
||||||
|
|
||||||
|
fn resolve_fields( |
||||||
|
&self, |
||||||
|
_: &mut TemporalFields, |
||||||
|
_: boa_temporal::calendar::CalendarFieldsType, |
||||||
|
) -> TemporalResult<()> { |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn identifier(&self, context: &mut dyn Any) -> TemporalResult<String> { |
||||||
|
let context = context |
||||||
|
.downcast_mut::<Context>() |
||||||
|
.expect("Context was not provided for a CustomCalendar."); |
||||||
|
|
||||||
|
let identifier = self |
||||||
|
.calendar |
||||||
|
.__get__( |
||||||
|
&PropertyKey::from(utf16!("id")), |
||||||
|
JsValue::undefined(), |
||||||
|
context, |
||||||
|
) |
||||||
|
.expect("method must exist on a object that implements the CalendarProtocol."); |
||||||
|
|
||||||
|
let JsValue::String(s) = identifier else { |
||||||
|
return Err(TemporalError::range().with_message("Identifier was not a string")); |
||||||
|
}; |
||||||
|
|
||||||
|
Ok(s.to_std_string_escaped()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Utility function for converting `Temporal`'s `CalendarDateLike` to it's `Boa` specific `JsObject`.
|
||||||
|
pub(crate) fn date_like_to_object( |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut Context, |
||||||
|
) -> TemporalResult<JsValue> { |
||||||
|
match date_like { |
||||||
|
CalendarDateLike::Date(d) => plain_date::create_temporal_date(d.clone(), None, context) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string())) |
||||||
|
.map(Into::into), |
||||||
|
CalendarDateLike::DateTime(_dt) => { |
||||||
|
todo!() |
||||||
|
} |
||||||
|
CalendarDateLike::MonthDay(md) => { |
||||||
|
plain_month_day::create_temporal_month_day(md.clone(), None, context) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string())) |
||||||
|
} |
||||||
|
CalendarDateLike::YearMonth(ym) => { |
||||||
|
plain_year_month::create_temporal_year_month(ym.clone(), None, context) |
||||||
|
.map_err(|e| TemporalError::general(e.to_string())) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,107 +0,0 @@ |
|||||||
//! Calendar utility calculations
|
|
||||||
|
|
||||||
// TODO: determine if any of the below are needed.
|
|
||||||
|
|
||||||
use crate::builtins::temporal::{self, date_equations, plain_date::iso::IsoDateRecord}; |
|
||||||
use crate::JsString; |
|
||||||
|
|
||||||
/// 12.2.31 `ISODaysInMonth ( year, month )`
|
|
||||||
pub(crate) fn iso_days_in_month(year: i32, month: i32) -> i32 { |
|
||||||
match month { |
|
||||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, |
|
||||||
4 | 6 | 9 | 11 => 30, |
|
||||||
2 => { |
|
||||||
28 + temporal::date_equations::mathematical_in_leap_year( |
|
||||||
temporal::date_equations::epoch_time_for_year(year), |
|
||||||
) |
|
||||||
} |
|
||||||
_ => unreachable!("an invalid month value is an implementation error."), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// 12.2.32 `ToISOWeekOfYear ( year, month, day )`
|
|
||||||
///
|
|
||||||
/// Takes an `[[IsoYear]]`, `[[IsoMonth]]`, and `[[IsoDay]]` and returns a (week, year) record.
|
|
||||||
#[allow(unused)] |
|
||||||
pub(crate) fn to_iso_week_of_year(year: i32, month: i32, day: i32) -> (i32, i32) { |
|
||||||
// Function constants
|
|
||||||
// 2. Let wednesday be 3.
|
|
||||||
// 3. Let thursday be 4.
|
|
||||||
// 4. Let friday be 5.
|
|
||||||
// 5. Let saturday be 6.
|
|
||||||
// 6. Let daysInWeek be 7.
|
|
||||||
// 7. Let maxWeekNumber be 53.
|
|
||||||
let day_of_year = to_iso_day_of_year(year, month, day); |
|
||||||
let day_of_week = to_iso_day_of_week(year, month, day); |
|
||||||
let week = (day_of_week + 7 - day_of_week + 3) / 7; |
|
||||||
|
|
||||||
if week < 1 { |
|
||||||
let first_day_of_year = to_iso_day_of_week(year, 1, 1); |
|
||||||
if first_day_of_year == 5 { |
|
||||||
return (53, year - 1); |
|
||||||
} else if first_day_of_year == 6 |
|
||||||
&& date_equations::mathematical_in_leap_year(date_equations::epoch_time_for_year( |
|
||||||
year - 1, |
|
||||||
)) == 1 |
|
||||||
{ |
|
||||||
return (52, year - 1); |
|
||||||
} |
|
||||||
return (52, year - 1); |
|
||||||
} else if week == 53 { |
|
||||||
let days_in_year = date_equations::mathematical_days_in_year(year); |
|
||||||
let days_later_in_year = days_in_year - day_of_year; |
|
||||||
let days_after_thursday = 4 - day_of_week; |
|
||||||
if days_later_in_year < days_after_thursday { |
|
||||||
return (1, year - 1); |
|
||||||
} |
|
||||||
} |
|
||||||
(week, year) |
|
||||||
} |
|
||||||
|
|
||||||
/// 12.2.33 `ISOMonthCode ( month )`
|
|
||||||
#[allow(unused)] |
|
||||||
fn iso_month_code(month: i32) -> JsString { |
|
||||||
// TODO: optimize
|
|
||||||
if month < 10 { |
|
||||||
JsString::from(format!("M0{month}")) |
|
||||||
} else { |
|
||||||
JsString::from(format!("M{month}")) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 12.2.34 `ISOResolveMonth ( fields )`
|
|
||||||
// Note: currently implemented on TemporalFields -> implement in this mod?
|
|
||||||
|
|
||||||
// 12.2.35 ISODateFromFields ( fields, overflow )
|
|
||||||
// Note: implemented on IsoDateRecord.
|
|
||||||
|
|
||||||
// 12.2.36 ISOYearMonthFromFields ( fields, overflow )
|
|
||||||
// TODO: implement on a IsoYearMonthRecord
|
|
||||||
|
|
||||||
// 12.2.37 ISOMonthDayFromFields ( fields, overflow )
|
|
||||||
// TODO: implement as method on IsoDateRecord.
|
|
||||||
|
|
||||||
// 12.2.38 IsoFieldKeysToIgnore
|
|
||||||
// TODO: determine usefulness.
|
|
||||||
|
|
||||||
/// 12.2.39 `ToISODayOfYear ( year, month, day )`
|
|
||||||
#[allow(unused)] |
|
||||||
fn to_iso_day_of_year(year: i32, month: i32, day: i32) -> i32 { |
|
||||||
// TODO: update fn parameter to take IsoDateRecord.
|
|
||||||
let iso = IsoDateRecord::new(year, month - 1, day); |
|
||||||
let epoch_days = iso.as_epoch_days(); |
|
||||||
date_equations::epoch_time_to_day_in_year(temporal::epoch_days_to_epoch_ms(epoch_days, 0)) + 1 |
|
||||||
} |
|
||||||
|
|
||||||
/// 12.2.40 `ToISODayOfWeek ( year, month, day )`
|
|
||||||
#[allow(unused)] |
|
||||||
pub(crate) fn to_iso_day_of_week(year: i32, month: i32, day: i32) -> i32 { |
|
||||||
let iso = IsoDateRecord::new(year, month - 1, day); |
|
||||||
let epoch_days = iso.as_epoch_days(); |
|
||||||
let day_of_week = |
|
||||||
date_equations::epoch_time_to_week_day(temporal::epoch_days_to_epoch_ms(epoch_days, 0)); |
|
||||||
if day_of_week == 0 { |
|
||||||
return 7; |
|
||||||
} |
|
||||||
day_of_week |
|
||||||
} |
|
@ -1,121 +0,0 @@ |
|||||||
//! This file represents all equations listed under section 13.4 of the [Temporal Specification][spec]
|
|
||||||
//!
|
|
||||||
//! [spec]: https://tc39.es/proposal-temporal/#sec-date-equations
|
|
||||||
|
|
||||||
use std::ops::Mul; |
|
||||||
|
|
||||||
pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 { |
|
||||||
(t / f64::from(super::MS_PER_DAY)).floor() as i32 |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn mathematical_days_in_year(y: i32) -> i32 { |
|
||||||
if y % 4 != 0 { |
|
||||||
365 |
|
||||||
} else if y % 4 == 0 && y % 100 != 0 { |
|
||||||
366 |
|
||||||
} else if y % 100 == 0 && y % 400 != 0 { |
|
||||||
365 |
|
||||||
} else { |
|
||||||
// Assert that y is divisble by 400 to ensure we are returning the correct result.
|
|
||||||
assert_eq!(y % 400, 0); |
|
||||||
366 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn epoch_day_number_for_year(y: f64) -> f64 { |
|
||||||
365.0f64.mul_add(y - 1970.0, ((y - 1969.0) / 4.0).floor()) - ((y - 1901.0) / 100.0).floor() |
|
||||||
+ ((y - 1601.0) / 400.0).floor() |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn epoch_time_for_year(y: i32) -> f64 { |
|
||||||
f64::from(super::MS_PER_DAY) * epoch_day_number_for_year(f64::from(y)) |
|
||||||
} |
|
||||||
|
|
||||||
// NOTE: The below returns the epoch years (years since 1970). The spec
|
|
||||||
// appears to assume the below returns with the epoch applied.
|
|
||||||
pub(crate) fn epoch_time_to_epoch_year(t: f64) -> i32 { |
|
||||||
// roughly calculate the largest possible year given the time t,
|
|
||||||
// then check and refine the year.
|
|
||||||
let day_count = epoch_time_to_day_number(t); |
|
||||||
let mut year = day_count / 365; |
|
||||||
loop { |
|
||||||
if epoch_time_for_year(year) <= t { |
|
||||||
break; |
|
||||||
} |
|
||||||
year -= 1; |
|
||||||
} |
|
||||||
|
|
||||||
year + 1970 |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns either 1 (true) or 0 (false)
|
|
||||||
pub(crate) fn mathematical_in_leap_year(t: f64) -> i32 { |
|
||||||
mathematical_days_in_year(epoch_time_to_epoch_year(t)) - 365 |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn epoch_time_to_month_in_year(t: f64) -> i32 { |
|
||||||
const DAYS: [i32; 11] = [30, 58, 89, 120, 150, 181, 212, 242, 272, 303, 333]; |
|
||||||
const LEAP_DAYS: [i32; 11] = [30, 59, 90, 121, 151, 182, 213, 242, 272, 303, 334]; |
|
||||||
|
|
||||||
let in_leap_year = mathematical_in_leap_year(t) == 1; |
|
||||||
let day = epoch_time_to_day_in_year(t); |
|
||||||
|
|
||||||
let result = if in_leap_year { |
|
||||||
LEAP_DAYS.binary_search(&day) |
|
||||||
} else { |
|
||||||
DAYS.binary_search(&day) |
|
||||||
}; |
|
||||||
|
|
||||||
match result { |
|
||||||
Ok(i) | Err(i) => i as i32, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn epoch_time_for_month_given_year(m: i32, y: i32) -> f64 { |
|
||||||
let leap_day = mathematical_days_in_year(y) - 365; |
|
||||||
|
|
||||||
let days = match m { |
|
||||||
0 => 1, |
|
||||||
1 => 31, |
|
||||||
2 => 59 + leap_day, |
|
||||||
3 => 90 + leap_day, |
|
||||||
4 => 121 + leap_day, |
|
||||||
5 => 151 + leap_day, |
|
||||||
6 => 182 + leap_day, |
|
||||||
7 => 213 + leap_day, |
|
||||||
8 => 243 + leap_day, |
|
||||||
9 => 273 + leap_day, |
|
||||||
10 => 304 + leap_day, |
|
||||||
11 => 334 + leap_day, |
|
||||||
_ => unreachable!(), |
|
||||||
}; |
|
||||||
|
|
||||||
(super::NS_PER_DAY as f64).mul(f64::from(days)) |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn epoch_time_to_date(t: f64) -> i32 { |
|
||||||
const OFFSETS: [i16; 12] = [ |
|
||||||
1, -30, -58, -89, -119, -150, -180, -211, -242, -272, -303, -333, |
|
||||||
]; |
|
||||||
let day_in_year = epoch_time_to_day_in_year(t); |
|
||||||
let in_leap_year = mathematical_in_leap_year(t); |
|
||||||
let month = epoch_time_to_month_in_year(t); |
|
||||||
|
|
||||||
// Cast from i32 to usize should be safe as the return must be 0-11
|
|
||||||
let mut date = day_in_year + i32::from(OFFSETS[month as usize]); |
|
||||||
|
|
||||||
if month >= 2 { |
|
||||||
date -= in_leap_year; |
|
||||||
} |
|
||||||
|
|
||||||
date |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn epoch_time_to_day_in_year(t: f64) -> i32 { |
|
||||||
epoch_time_to_day_number(t) |
|
||||||
- (epoch_day_number_for_year(f64::from(epoch_time_to_epoch_year(t))) as i32) |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn epoch_time_to_week_day(t: f64) -> i32 { |
|
||||||
(epoch_time_to_day_number(t) + 4) % 7 |
|
||||||
} |
|
@ -0,0 +1,20 @@ |
|||||||
|
use boa_temporal::error::{ErrorKind, TemporalError}; |
||||||
|
|
||||||
|
use crate::{JsError, JsNativeError}; |
||||||
|
|
||||||
|
impl From<TemporalError> for JsNativeError { |
||||||
|
fn from(value: TemporalError) -> Self { |
||||||
|
match value.kind() { |
||||||
|
ErrorKind::Range => JsNativeError::range().with_message(value.message()), |
||||||
|
ErrorKind::Type => JsNativeError::typ().with_message(value.message()), |
||||||
|
ErrorKind::Generic => JsNativeError::error().with_message(value.message()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<TemporalError> for JsError { |
||||||
|
fn from(value: TemporalError) -> Self { |
||||||
|
let native: JsNativeError = value.into(); |
||||||
|
native.into() |
||||||
|
} |
||||||
|
} |
@ -1,587 +1,174 @@ |
|||||||
//! A Rust native implementation of the `fields` object used in `Temporal`.
|
//! A Rust native implementation of the `fields` object used in `Temporal`.
|
||||||
|
|
||||||
|
use std::str::FromStr; |
||||||
|
|
||||||
use crate::{ |
use crate::{ |
||||||
js_string, property::PropertyKey, value::PreferredType, Context, JsNativeError, JsObject, |
js_string, property::PropertyKey, value::PreferredType, Context, JsNativeError, JsObject, |
||||||
JsResult, JsString, JsValue, |
JsResult, JsString, JsValue, |
||||||
}; |
}; |
||||||
|
|
||||||
use super::options::ArithmeticOverflow; |
|
||||||
|
|
||||||
use bitflags::bitflags; |
|
||||||
use rustc_hash::FxHashSet; |
use rustc_hash::FxHashSet; |
||||||
|
|
||||||
bitflags! { |
use boa_temporal::fields::{FieldConversion, FieldValue, TemporalFields}; |
||||||
#[derive(Debug, PartialEq, Eq)] |
|
||||||
pub struct FieldMap: u16 { |
use super::{to_integer_with_truncation, to_positive_integer_with_trunc}; |
||||||
const YEAR = 0b0000_0000_0000_0001; |
|
||||||
const MONTH = 0b0000_0000_0000_0010; |
// TODO: Move extended and required fields into the temporal library?
|
||||||
const MONTH_CODE = 0b0000_0000_0000_0100; |
/// `PrepareTemporalFeilds`
|
||||||
const DAY = 0b0000_0000_0000_1000; |
pub(crate) fn prepare_temporal_fields( |
||||||
const HOUR = 0b0000_0000_0001_0000; |
fields: &JsObject, |
||||||
const MINUTE = 0b0000_0000_0010_0000; |
field_names: &mut Vec<JsString>, |
||||||
const SECOND = 0b0000_0000_0100_0000; |
required_fields: &mut Vec<JsString>, |
||||||
const MILLISECOND = 0b0000_0000_1000_0000; |
extended_fields: Option<Vec<(String, bool)>>, |
||||||
const MICROSECOND = 0b0000_0001_0000_0000; |
partial: bool, |
||||||
const NANOSECOND = 0b0000_0010_0000_0000; |
dup_behaviour: Option<JsString>, |
||||||
const OFFSET = 0b0000_0100_0000_0000; |
context: &mut Context, |
||||||
const ERA = 0b0000_1000_0000_0000; |
) -> JsResult<TemporalFields> { |
||||||
const ERA_YEAR = 0b0001_0000_0000_0000; |
// 1. If duplicateBehaviour is not present, set duplicateBehaviour to throw.
|
||||||
const TIME_ZONE = 0b0010_0000_0000_0000; |
let dup_option = dup_behaviour.unwrap_or_else(|| js_string!("throw")); |
||||||
} |
|
||||||
} |
// 2. Let result be OrdinaryObjectCreate(null).
|
||||||
|
let mut result = TemporalFields::default(); |
||||||
/// The temporal fields are laid out in the Temporal proposal under section 13.46 `PrepareTemporalFields`
|
|
||||||
/// with conversion and defaults laid out by Table 17 (displayed below).
|
// 3. Let any be false.
|
||||||
///
|
let mut any = false; |
||||||
/// `TemporalFields` is meant to act as a native Rust implementation
|
// 4. If extraFieldDescriptors is present, then
|
||||||
/// of the fields.
|
if let Some(extra_fields) = extended_fields { |
||||||
///
|
for (field_name, required) in extra_fields { |
||||||
///
|
// a. For each Calendar Field Descriptor Record desc of extraFieldDescriptors, do
|
||||||
/// ## Table 17: Temporal field requirements
|
// i. Assert: fieldNames does not contain desc.[[Property]].
|
||||||
///
|
// ii. Append desc.[[Property]] to fieldNames.
|
||||||
/// | Property | Conversion | Default |
|
field_names.push(JsString::from(field_name.clone())); |
||||||
/// | -------------|-----------------------------------|------------|
|
|
||||||
/// | "year" | `ToIntegerWithTruncation` | undefined |
|
// iii. If desc.[[Required]] is true and requiredFields is a List, then
|
||||||
/// | "month" | `ToPositiveIntegerWithTruncation` | undefined |
|
if required && !partial { |
||||||
/// | "monthCode" | `ToPrimitiveAndRequireString` | undefined |
|
// 1. Append desc.[[Property]] to requiredFields.
|
||||||
/// | "day" | `ToPositiveIntegerWithTruncation` | undefined |
|
required_fields.push(JsString::from(field_name)); |
||||||
/// | "hour" | `ToIntegerWithTruncation` | +0𝔽 |
|
} |
||||||
/// | "minute" | `ToIntegerWithTruncation` | +0𝔽 |
|
|
||||||
/// | "second" | `ToIntegerWithTruncation` | +0𝔽 |
|
|
||||||
/// | "millisecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
|
||||||
/// | "microsecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
|
||||||
/// | "nanosecond" | `ToIntegerWithTruncation` | +0𝔽 |
|
|
||||||
/// | "offset" | `ToPrimitiveAndRequireString` | undefined |
|
|
||||||
/// | "era" | `ToPrimitiveAndRequireString` | undefined |
|
|
||||||
/// | "eraYear" | `ToIntegerWithTruncation` | undefined |
|
|
||||||
/// | "timeZone" | `None` | undefined |
|
|
||||||
#[derive(Debug)] |
|
||||||
pub(crate) struct TemporalFields { |
|
||||||
bit_map: FieldMap, |
|
||||||
year: Option<i32>, |
|
||||||
month: Option<i32>, |
|
||||||
month_code: Option<JsString>, // TODO: Switch to icu compatible value.
|
|
||||||
day: Option<i32>, |
|
||||||
hour: i32, |
|
||||||
minute: i32, |
|
||||||
second: i32, |
|
||||||
millisecond: i32, |
|
||||||
microsecond: i32, |
|
||||||
nanosecond: i32, |
|
||||||
offset: Option<JsString>, |
|
||||||
era: Option<JsString>, // TODO: switch to icu compatible value.
|
|
||||||
era_year: Option<i32>, // TODO: switch to icu compatible value.
|
|
||||||
time_zone: Option<JsString>, // TODO: figure out the identifier for TimeZone.
|
|
||||||
} |
|
||||||
|
|
||||||
impl Default for TemporalFields { |
|
||||||
fn default() -> Self { |
|
||||||
Self { |
|
||||||
bit_map: FieldMap::empty(), |
|
||||||
year: None, |
|
||||||
month: None, |
|
||||||
month_code: None, |
|
||||||
day: None, |
|
||||||
hour: 0, |
|
||||||
minute: 0, |
|
||||||
second: 0, |
|
||||||
millisecond: 0, |
|
||||||
microsecond: 0, |
|
||||||
nanosecond: 0, |
|
||||||
offset: None, |
|
||||||
era: None, |
|
||||||
era_year: None, |
|
||||||
time_zone: None, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl TemporalFields { |
|
||||||
pub(crate) const fn year(&self) -> Option<i32> { |
|
||||||
self.year |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) const fn month(&self) -> Option<i32> { |
|
||||||
self.month |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) const fn day(&self) -> Option<i32> { |
|
||||||
self.day |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl TemporalFields { |
|
||||||
#[inline] |
|
||||||
fn set_field_value( |
|
||||||
&mut self, |
|
||||||
field: &JsString, |
|
||||||
value: &JsValue, |
|
||||||
context: &mut Context, |
|
||||||
) -> JsResult<()> { |
|
||||||
match field.to_std_string_escaped().as_str() { |
|
||||||
"year" => self.set_year(value, context)?, |
|
||||||
"month" => self.set_month(value, context)?, |
|
||||||
"monthCode" => self.set_month_code(value, context)?, |
|
||||||
"day" => self.set_day(value, context)?, |
|
||||||
"hour" => self.set_hour(value, context)?, |
|
||||||
"minute" => self.set_minute(value, context)?, |
|
||||||
"second" => self.set_second(value, context)?, |
|
||||||
"millisecond" => self.set_milli(value, context)?, |
|
||||||
"microsecond" => self.set_micro(value, context)?, |
|
||||||
"nanosecond" => self.set_nano(value, context)?, |
|
||||||
"offset" => self.set_offset(value, context)?, |
|
||||||
"era" => self.set_era(value, context)?, |
|
||||||
"eraYear" => self.set_era_year(value, context)?, |
|
||||||
"timeZone" => self.set_time_zone(value), |
|
||||||
_ => unreachable!(), |
|
||||||
} |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_year(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let y = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.year = Some(y); |
|
||||||
self.bit_map.set(FieldMap::YEAR, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_month(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let mo = super::to_positive_integer_with_trunc(value, context)?; |
|
||||||
self.year = Some(mo); |
|
||||||
self.bit_map.set(FieldMap::MONTH, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_month_code(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let mc = value.to_primitive(context, PreferredType::String)?; |
|
||||||
if let Some(string) = mc.as_string() { |
|
||||||
self.month_code = Some(string.clone()); |
|
||||||
} else { |
|
||||||
return Err(JsNativeError::typ() |
|
||||||
.with_message("ToPrimativeAndRequireString must be of type String.") |
|
||||||
.into()); |
|
||||||
} |
} |
||||||
|
|
||||||
self.bit_map.set(FieldMap::MONTH_CODE, true); |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
} |
||||||
|
|
||||||
#[inline] |
// 5. Let sortedFieldNames be SortStringListByCodeUnit(fieldNames).
|
||||||
fn set_day(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
// 6. Let previousProperty be undefined.
|
||||||
let d = super::to_positive_integer_with_trunc(value, context)?; |
let mut dups_map = FxHashSet::default(); |
||||||
self.day = Some(d); |
|
||||||
self.bit_map.set(FieldMap::DAY, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_hour(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let h = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.hour = h; |
|
||||||
self.bit_map.set(FieldMap::HOUR, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_minute(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let m = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.minute = m; |
|
||||||
self.bit_map.set(FieldMap::MINUTE, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_second(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let sec = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.second = sec; |
|
||||||
self.bit_map.set(FieldMap::SECOND, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_milli(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let milli = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.millisecond = milli; |
|
||||||
self.bit_map.set(FieldMap::MILLISECOND, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_micro(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let micro = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.microsecond = micro; |
|
||||||
self.bit_map.set(FieldMap::MICROSECOND, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_nano(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let nano = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.nanosecond = nano; |
|
||||||
self.bit_map.set(FieldMap::NANOSECOND, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_offset(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let mc = value.to_primitive(context, PreferredType::String)?; |
|
||||||
if let Some(string) = mc.as_string() { |
|
||||||
self.offset = Some(string.clone()); |
|
||||||
} else { |
|
||||||
return Err(JsNativeError::typ() |
|
||||||
.with_message("ToPrimativeAndRequireString must be of type String.") |
|
||||||
.into()); |
|
||||||
} |
|
||||||
self.bit_map.set(FieldMap::OFFSET, true); |
|
||||||
|
|
||||||
Ok(()) |
// 7. For each property name property of sortedFieldNames, do
|
||||||
} |
for field in &*field_names { |
||||||
|
// a. If property is one of "constructor" or "__proto__", then
|
||||||
#[inline] |
if field.to_std_string_escaped().as_str() == "constructor" |
||||||
fn set_era(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|| field.to_std_string_escaped().as_str() == "__proto__" |
||||||
let mc = value.to_primitive(context, PreferredType::String)?; |
{ |
||||||
if let Some(string) = mc.as_string() { |
// i. Throw a RangeError exception.
|
||||||
self.era = Some(string.clone()); |
return Err(JsNativeError::range() |
||||||
} else { |
.with_message("constructor or proto is out of field range.") |
||||||
return Err(JsNativeError::typ() |
|
||||||
.with_message("ToPrimativeAndRequireString must be of type String.") |
|
||||||
.into()); |
.into()); |
||||||
} |
} |
||||||
self.bit_map.set(FieldMap::ERA, true); |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_era_year(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { |
|
||||||
let ey = super::to_integer_with_truncation(value, context)?; |
|
||||||
self.era_year = Some(ey); |
|
||||||
self.bit_map.set(FieldMap::ERA_YEAR, true); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[inline] |
|
||||||
fn set_time_zone(&mut self, value: &JsValue) { |
|
||||||
let tz = value.as_string().cloned(); |
|
||||||
self.time_zone = tz; |
|
||||||
self.bit_map.set(FieldMap::TIME_ZONE, true); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl TemporalFields { |
|
||||||
// TODO: Shift to JsString or utf16 over String.
|
|
||||||
/// A method for creating a Native representation for `TemporalFields` from
|
|
||||||
/// a `JsObject`.
|
|
||||||
///
|
|
||||||
/// This is the equivalant to Abstract Operation 13.46 `PrepareTemporalFields`
|
|
||||||
pub(crate) fn from_js_object( |
|
||||||
fields: &JsObject, |
|
||||||
field_names: &mut Vec<JsString>, |
|
||||||
required_fields: &mut Vec<JsString>, // None when Partial
|
|
||||||
extended_fields: Option<Vec<(JsString, bool)>>, |
|
||||||
partial: bool, |
|
||||||
dup_behaviour: Option<JsString>, |
|
||||||
context: &mut Context, |
|
||||||
) -> JsResult<Self> { |
|
||||||
// 1. If duplicateBehaviour is not present, set duplicateBehaviour to throw.
|
|
||||||
let dup_option = dup_behaviour.unwrap_or_else(|| js_string!("throw")); |
|
||||||
|
|
||||||
// 2. Let result be OrdinaryObjectCreate(null).
|
|
||||||
let mut result = Self::default(); |
|
||||||
|
|
||||||
// 3. Let any be false.
|
|
||||||
let mut any = false; |
|
||||||
// 4. If extraFieldDescriptors is present, then
|
|
||||||
if let Some(extra_fields) = extended_fields { |
|
||||||
for (field_name, required) in extra_fields { |
|
||||||
// a. For each Calendar Field Descriptor Record desc of extraFieldDescriptors, do
|
|
||||||
// i. Assert: fieldNames does not contain desc.[[Property]].
|
|
||||||
// ii. Append desc.[[Property]] to fieldNames.
|
|
||||||
field_names.push(field_name.clone()); |
|
||||||
|
|
||||||
// iii. If desc.[[Required]] is true and requiredFields is a List, then
|
|
||||||
if required && !partial { |
|
||||||
// 1. Append desc.[[Property]] to requiredFields.
|
|
||||||
required_fields.push(field_name); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 5. Let sortedFieldNames be SortStringListByCodeUnit(fieldNames).
|
|
||||||
// 6. Let previousProperty be undefined.
|
|
||||||
let mut dups_map = FxHashSet::default(); |
|
||||||
|
|
||||||
// 7. For each property name property of sortedFieldNames, do
|
|
||||||
for field in &*field_names { |
|
||||||
// a. If property is one of "constructor" or "__proto__", then
|
|
||||||
if field.to_std_string_escaped().as_str() == "constructor" |
|
||||||
|| field.to_std_string_escaped().as_str() == "__proto__" |
|
||||||
{ |
|
||||||
// i. Throw a RangeError exception.
|
|
||||||
return Err(JsNativeError::range() |
|
||||||
.with_message("constructor or proto is out of field range.") |
|
||||||
.into()); |
|
||||||
} |
|
||||||
|
|
||||||
let new_value = dups_map.insert(field); |
|
||||||
|
|
||||||
// b. If property is not equal to previousProperty, then
|
let new_value = dups_map.insert(field); |
||||||
if new_value { |
|
||||||
// i. Let value be ? Get(fields, property).
|
// b. If property is not equal to previousProperty, then
|
||||||
let value = fields.get(PropertyKey::from(field.clone()), context)?; |
if new_value { |
||||||
// ii. If value is not undefined, then
|
// i. Let value be ? Get(fields, property).
|
||||||
if !value.is_undefined() { |
let value = fields.get(PropertyKey::from(field.clone()), context)?; |
||||||
// 1. Set any to true.
|
// ii. If value is not undefined, then
|
||||||
any = true; |
if !value.is_undefined() { |
||||||
|
// 1. Set any to true.
|
||||||
// 2. If property is in the Property column of Table 17 and there is a Conversion value in the same row, then
|
any = true; |
||||||
// a. Let Conversion be the Conversion value of the same row.
|
|
||||||
// b. If Conversion is ToIntegerWithTruncation, then
|
// 2. If property is in the Property column of Table 17 and there is a Conversion value in the same row, then
|
||||||
// i. Set value to ? ToIntegerWithTruncation(value).
|
// a. Let Conversion be the Conversion value of the same row.
|
||||||
// ii. Set value to 𝔽(value).
|
|
||||||
|
// TODO: Conversion from TemporalError -> JsError
|
||||||
|
let conversion = FieldConversion::from_str(field.to_std_string_escaped().as_str()) |
||||||
|
.map_err(|_| JsNativeError::range().with_message("wrong field value"))?; |
||||||
|
// b. If Conversion is ToIntegerWithTruncation, then
|
||||||
|
let converted_value = match conversion { |
||||||
|
FieldConversion::ToIntegerWithTruncation => { |
||||||
|
// i. Set value to ? ToIntegerWithTruncation(value).
|
||||||
|
let v = to_integer_with_truncation(&value, context)?; |
||||||
|
// ii. Set value to 𝔽(value).
|
||||||
|
FieldValue::Integer(v) |
||||||
|
} |
||||||
// c. Else if Conversion is ToPositiveIntegerWithTruncation, then
|
// c. Else if Conversion is ToPositiveIntegerWithTruncation, then
|
||||||
// i. Set value to ? ToPositiveIntegerWithTruncation(value).
|
FieldConversion::ToPositiveIntegerWithTruncation => { |
||||||
// ii. Set value to 𝔽(value).
|
// i. Set value to ? ToPositiveIntegerWithTruncation(value).
|
||||||
|
let v = to_positive_integer_with_trunc(&value, context)?; |
||||||
|
// ii. Set value to 𝔽(value).
|
||||||
|
FieldValue::Integer(v) |
||||||
|
} |
||||||
// d. Else,
|
// d. Else,
|
||||||
// i. Assert: Conversion is ToPrimitiveAndRequireString.
|
// i. Assert: Conversion is ToPrimitiveAndRequireString.
|
||||||
// ii. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings.
|
FieldConversion::ToPrimativeAndRequireString => { |
||||||
// iii. Set value to ? ToPrimitive(value, string).
|
// ii. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings.
|
||||||
// iv. If value is not a String, throw a TypeError exception.
|
// iii. Set value to ? ToPrimitive(value, string).
|
||||||
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
|
let primitive = value.to_primitive(context, PreferredType::String)?; |
||||||
result.set_field_value(field, &value, context)?; |
// iv. If value is not a String, throw a TypeError exception.
|
||||||
// iii. Else if requiredFields is a List, then
|
FieldValue::String(primitive.to_string(context)?.to_std_string_escaped()) |
||||||
} else if !partial { |
|
||||||
// 1. If requiredFields contains property, then
|
|
||||||
if required_fields.contains(field) { |
|
||||||
// a. Throw a TypeError exception.
|
|
||||||
return Err(JsNativeError::typ() |
|
||||||
.with_message("A required TemporalField was not provided.") |
|
||||||
.into()); |
|
||||||
} |
} |
||||||
|
FieldConversion::None => { |
||||||
// NOTE: Values set to a default on init.
|
unreachable!("todo need to implement conversion handling for tz.") |
||||||
// 2. If property is in the Property column of Table 17, then
|
} |
||||||
// a. Set value to the corresponding Default value of the same row.
|
}; |
||||||
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
|
|
||||||
} |
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
|
||||||
// c. Else if duplicateBehaviour is throw, then
|
result |
||||||
} else if dup_option.to_std_string_escaped() == "throw" { |
.set_field_value(&field.to_std_string_escaped(), &converted_value) |
||||||
// i. Throw a RangeError exception.
|
.expect("FieldConversion enforces the appropriate type"); |
||||||
return Err(JsNativeError::range() |
// iii. Else if requiredFields is a List, then
|
||||||
.with_message("Cannot have a duplicate field") |
} else if !partial { |
||||||
.into()); |
// 1. If requiredFields contains property, then
|
||||||
|
if required_fields.contains(field) { |
||||||
|
// a. Throw a TypeError exception.
|
||||||
|
return Err(JsNativeError::typ() |
||||||
|
.with_message("A required TemporalField was not provided.") |
||||||
|
.into()); |
||||||
|
} |
||||||
|
|
||||||
|
// NOTE: flag that the value is active and the default should be used.
|
||||||
|
// 2. If property is in the Property column of Table 17, then
|
||||||
|
// a. Set value to the corresponding Default value of the same row.
|
||||||
|
// 3. Perform ! CreateDataPropertyOrThrow(result, property, value).
|
||||||
|
result.require_field(&field.to_std_string_escaped()); |
||||||
} |
} |
||||||
// d. Set previousProperty to property.
|
// c. Else if duplicateBehaviour is throw, then
|
||||||
} |
} else if dup_option.to_std_string_escaped() == "throw" { |
||||||
|
// i. Throw a RangeError exception.
|
||||||
// 8. If requiredFields is partial and any is false, then
|
|
||||||
if partial && !any { |
|
||||||
// a. Throw a TypeError exception.
|
|
||||||
return Err(JsNativeError::range() |
return Err(JsNativeError::range() |
||||||
.with_message("requiredFields cannot be partial when any is false") |
.with_message("Cannot have a duplicate field") |
||||||
.into()); |
.into()); |
||||||
} |
} |
||||||
|
// d. Set previousProperty to property.
|
||||||
// 9. Return result.
|
|
||||||
Ok(result) |
|
||||||
} |
} |
||||||
|
|
||||||
/// Convert a `TemporalFields` struct into a `JsObject`.
|
// 8. If requiredFields is partial and any is false, then
|
||||||
pub(crate) fn as_object(&self, context: &mut Context) -> JsResult<JsObject> { |
if partial && !any { |
||||||
let obj = JsObject::with_null_proto(); |
// a. Throw a TypeError exception.
|
||||||
|
return Err(JsNativeError::range() |
||||||
for bit in self.bit_map.iter() { |
.with_message("requiredFields cannot be partial when any is false") |
||||||
match bit { |
.into()); |
||||||
FieldMap::YEAR => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("year"), |
|
||||||
self.year.map_or(JsValue::undefined(), JsValue::from), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::MONTH => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("month"), |
|
||||||
self.month.map_or(JsValue::undefined(), JsValue::from), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::MONTH_CODE => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("monthCode"), |
|
||||||
self.month_code |
|
||||||
.as_ref() |
|
||||||
.map_or(JsValue::undefined(), |f| f.clone().into()), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::DAY => { |
|
||||||
obj.create_data_property( |
|
||||||
js_string!("day"), |
|
||||||
self.day().map_or(JsValue::undefined(), JsValue::from), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::HOUR => { |
|
||||||
obj.create_data_property(js_string!("hour"), self.hour, context)?; |
|
||||||
} |
|
||||||
FieldMap::MINUTE => { |
|
||||||
obj.create_data_property(js_string!("minute"), self.minute, context)?; |
|
||||||
} |
|
||||||
FieldMap::SECOND => { |
|
||||||
obj.create_data_property_or_throw(js_string!("second"), self.second, context)?; |
|
||||||
} |
|
||||||
FieldMap::MILLISECOND => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("millisecond"), |
|
||||||
self.millisecond, |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::MICROSECOND => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("microsecond"), |
|
||||||
self.microsecond, |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::NANOSECOND => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("nanosecond"), |
|
||||||
self.nanosecond, |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::OFFSET => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("offset"), |
|
||||||
self.offset |
|
||||||
.as_ref() |
|
||||||
.map_or(JsValue::undefined(), |s| s.clone().into()), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::ERA => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("era"), |
|
||||||
self.era |
|
||||||
.as_ref() |
|
||||||
.map_or(JsValue::undefined(), |s| s.clone().into()), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::ERA_YEAR => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("eraYear"), |
|
||||||
self.era_year.map_or(JsValue::undefined(), JsValue::from), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
FieldMap::TIME_ZONE => { |
|
||||||
obj.create_data_property_or_throw( |
|
||||||
js_string!("timeZone"), |
|
||||||
self.time_zone |
|
||||||
.as_ref() |
|
||||||
.map_or(JsValue::undefined(), |s| s.clone().into()), |
|
||||||
context, |
|
||||||
)?; |
|
||||||
} |
|
||||||
_ => unreachable!(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Ok(obj) |
|
||||||
} |
} |
||||||
|
|
||||||
// Note placeholder until overflow is implemented on `ICU4x`'s Date<Iso>.
|
// 9. Return result.
|
||||||
/// A function to regulate the current `TemporalFields` according to the overflow value
|
Ok(result) |
||||||
pub(crate) fn regulate(&mut self, overflow: ArithmeticOverflow) -> JsResult<()> { |
} |
||||||
if let (Some(year), Some(month), Some(day)) = (self.year(), self.month(), self.day()) { |
|
||||||
match overflow { |
|
||||||
ArithmeticOverflow::Constrain => { |
|
||||||
let m = month.clamp(1, 12); |
|
||||||
let days_in_month = super::calendar::utils::iso_days_in_month(year, month); |
|
||||||
let d = day.clamp(1, days_in_month); |
|
||||||
|
|
||||||
self.month = Some(m); |
|
||||||
self.day = Some(d); |
|
||||||
} |
|
||||||
ArithmeticOverflow::Reject => { |
|
||||||
return Err(JsNativeError::range() |
|
||||||
.with_message("TemporalFields is out of a valid range.") |
|
||||||
.into()) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) fn regulate_year_month(&mut self, overflow: ArithmeticOverflow) { |
impl JsObject { |
||||||
match self.month { |
pub(crate) fn from_temporal_fields( |
||||||
Some(month) if overflow == ArithmeticOverflow::Constrain => { |
fields: &TemporalFields, |
||||||
let m = month.clamp(1, 12); |
context: &mut Context, |
||||||
self.month = Some(m); |
) -> JsResult<Self> { |
||||||
} |
let obj = JsObject::with_null_proto(); |
||||||
_ => {} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Resolve the month and monthCode on this `TemporalFields`.
|
for (key, value) in fields.active_kvs() { |
||||||
pub(crate) fn iso_resolve_month(&mut self) -> JsResult<()> { |
let js_value = match value { |
||||||
if self.month_code.is_none() { |
FieldValue::Undefined => JsValue::undefined(), |
||||||
if self.month.is_some() { |
FieldValue::Integer(x) => JsValue::Integer(x), |
||||||
return Ok(()); |
FieldValue::String(s) => JsValue::String(s.into()), |
||||||
} |
}; |
||||||
|
|
||||||
return Err(JsNativeError::range() |
obj.create_data_property_or_throw(JsString::from(key), js_value, context)?; |
||||||
.with_message("month and MonthCode values cannot both be undefined.") |
|
||||||
.into()); |
|
||||||
} |
} |
||||||
|
|
||||||
let unresolved_month_code = self |
Ok(obj) |
||||||
.month_code |
|
||||||
.as_ref() |
|
||||||
.expect("monthCode must exist at this point."); |
|
||||||
|
|
||||||
let month_code_integer = month_code_to_integer(unresolved_month_code)?; |
|
||||||
|
|
||||||
let new_month = match self.month { |
|
||||||
Some(month) if month != month_code_integer => { |
|
||||||
return Err(JsNativeError::range() |
|
||||||
.with_message("month and monthCode cannot be resolved.") |
|
||||||
.into()) |
|
||||||
} |
|
||||||
_ => month_code_integer, |
|
||||||
}; |
|
||||||
|
|
||||||
self.month = Some(new_month); |
|
||||||
|
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn month_code_to_integer(mc: &JsString) -> JsResult<i32> { |
|
||||||
match mc.to_std_string_escaped().as_str() { |
|
||||||
"M01" => Ok(1), |
|
||||||
"M02" => Ok(2), |
|
||||||
"M03" => Ok(3), |
|
||||||
"M04" => Ok(4), |
|
||||||
"M05" => Ok(5), |
|
||||||
"M06" => Ok(6), |
|
||||||
"M07" => Ok(7), |
|
||||||
"M08" => Ok(8), |
|
||||||
"M09" => Ok(9), |
|
||||||
"M10" => Ok(10), |
|
||||||
"M11" => Ok(11), |
|
||||||
"M12" => Ok(12), |
|
||||||
"M13" => Ok(13), |
|
||||||
_ => Err(JsNativeError::range() |
|
||||||
.with_message("monthCode is not within the valid values.") |
|
||||||
.into()), |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -1,236 +0,0 @@ |
|||||||
//! An `IsoDateRecord` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
|
|
||||||
|
|
||||||
use crate::{ |
|
||||||
builtins::temporal::{self, options::ArithmeticOverflow, DateDuration, TemporalFields}, |
|
||||||
JsNativeError, JsResult, JsString, |
|
||||||
}; |
|
||||||
|
|
||||||
use icu_calendar::{Date, Iso}; |
|
||||||
|
|
||||||
// TODO: Move ISODateRecord to a more generalized location.
|
|
||||||
|
|
||||||
// TODO: Determine whether month/day should be u8 or i32.
|
|
||||||
|
|
||||||
/// `IsoDateRecord` serves as an record for the `[[ISOYear]]`, `[[ISOMonth]]`,
|
|
||||||
/// and `[[ISODay]]` internal fields.
|
|
||||||
///
|
|
||||||
/// These fields are used for the `Temporal.PlainDate` object, the
|
|
||||||
/// `Temporal.YearMonth` object, and the `Temporal.MonthDay` object.
|
|
||||||
#[derive(Debug, Clone, Copy, Default)] |
|
||||||
pub(crate) struct IsoDateRecord { |
|
||||||
year: i32, |
|
||||||
month: i32, |
|
||||||
day: i32, |
|
||||||
} |
|
||||||
|
|
||||||
// TODO: determine whether the below is neccessary.
|
|
||||||
impl IsoDateRecord { |
|
||||||
pub(crate) const fn year(&self) -> i32 { |
|
||||||
self.year |
|
||||||
} |
|
||||||
pub(crate) const fn month(&self) -> i32 { |
|
||||||
self.month |
|
||||||
} |
|
||||||
pub(crate) const fn day(&self) -> i32 { |
|
||||||
self.day |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl IsoDateRecord { |
|
||||||
// TODO: look into using Date<Iso> across the board...TBD.
|
|
||||||
/// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date<Iso>` struct.
|
|
||||||
pub(crate) fn from_date_iso(date: Date<Iso>) -> Self { |
|
||||||
Self { |
|
||||||
year: date.year().number, |
|
||||||
month: date.month().ordinal as i32, |
|
||||||
day: i32::from(date.days_in_month()), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl IsoDateRecord { |
|
||||||
/// 3.5.2 `CreateISODateRecord`
|
|
||||||
pub(crate) const fn new(year: i32, month: i32, day: i32) -> Self { |
|
||||||
Self { year, month, day } |
|
||||||
} |
|
||||||
|
|
||||||
/// 3.5.6 `RegulateISODate`
|
|
||||||
pub(crate) fn from_unregulated( |
|
||||||
year: i32, |
|
||||||
month: i32, |
|
||||||
day: i32, |
|
||||||
overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<Self> { |
|
||||||
match overflow { |
|
||||||
ArithmeticOverflow::Constrain => { |
|
||||||
let m = month.clamp(1, 12); |
|
||||||
let days_in_month = temporal::calendar::utils::iso_days_in_month(year, month); |
|
||||||
let d = day.clamp(1, days_in_month); |
|
||||||
Ok(Self::new(year, m, d)) |
|
||||||
} |
|
||||||
ArithmeticOverflow::Reject => { |
|
||||||
let date = Self::new(year, month, day); |
|
||||||
if !date.is_valid() { |
|
||||||
return Err(JsNativeError::range() |
|
||||||
.with_message("not a valid ISO date.") |
|
||||||
.into()); |
|
||||||
} |
|
||||||
Ok(date) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// 12.2.35 `ISODateFromFields ( fields, overflow )`
|
|
||||||
///
|
|
||||||
/// Note: fields.month must be resolved prior to using `from_temporal_fields`
|
|
||||||
pub(crate) fn from_temporal_fields( |
|
||||||
fields: &TemporalFields, |
|
||||||
overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<Self> { |
|
||||||
Self::from_unregulated( |
|
||||||
fields.year().expect("Cannot fail per spec"), |
|
||||||
fields.month().expect("cannot fail after resolution"), |
|
||||||
fields.day().expect("cannot fail per spec"), |
|
||||||
overflow, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
/// Create a Month-Day record from a `TemporalFields` object.
|
|
||||||
pub(crate) fn month_day_from_temporal_fields( |
|
||||||
fields: &TemporalFields, |
|
||||||
overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<Self> { |
|
||||||
match fields.year() { |
|
||||||
Some(year) => Self::from_unregulated( |
|
||||||
year, |
|
||||||
fields.month().expect("month must exist."), |
|
||||||
fields.day().expect("cannot fail per spec"), |
|
||||||
overflow, |
|
||||||
), |
|
||||||
None => Self::from_unregulated( |
|
||||||
1972, |
|
||||||
fields.month().expect("cannot fail per spec"), |
|
||||||
fields.day().expect("cannot fail per spec."), |
|
||||||
overflow, |
|
||||||
), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Within `YearMonth` valid limits
|
|
||||||
pub(crate) const fn within_year_month_limits(&self) -> bool { |
|
||||||
if self.year < -271_821 || self.year > 275_760 { |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
if self.year == -271_821 && self.month < 4 { |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
if self.year == 275_760 && self.month > 9 { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
/// 3.5.5 `DifferenceISODate`
|
|
||||||
pub(crate) fn diff_iso_date( |
|
||||||
&self, |
|
||||||
o: &Self, |
|
||||||
largest_unit: &JsString, |
|
||||||
) -> JsResult<temporal::duration::DurationRecord> { |
|
||||||
debug_assert!(self.is_valid()); |
|
||||||
// TODO: Implement on `ICU4X`.
|
|
||||||
|
|
||||||
Err(JsNativeError::range() |
|
||||||
.with_message("not yet implemented.") |
|
||||||
.into()) |
|
||||||
} |
|
||||||
|
|
||||||
/// 3.5.7 `IsValidISODate`
|
|
||||||
pub(crate) fn is_valid(&self) -> bool { |
|
||||||
if self.month < 1 || self.month > 12 { |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
let days_in_month = temporal::calendar::utils::iso_days_in_month(self.year, self.month); |
|
||||||
|
|
||||||
if self.day < 1 || self.day > days_in_month { |
|
||||||
return false; |
|
||||||
} |
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
/// 13.2 `IsoDateToEpochDays`
|
|
||||||
pub(crate) fn as_epoch_days(&self) -> i32 { |
|
||||||
// 1. Let resolvedYear be year + floor(month / 12).
|
|
||||||
let resolved_year = self.year + (f64::from(self.month) / 12_f64).floor() as i32; |
|
||||||
// 2. Let resolvedMonth be month modulo 12.
|
|
||||||
let resolved_month = self.month % 12; |
|
||||||
|
|
||||||
// 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1.
|
|
||||||
let year_t = temporal::date_equations::epoch_time_for_year(resolved_year); |
|
||||||
let month_t = temporal::date_equations::epoch_time_for_month_given_year( |
|
||||||
resolved_month, |
|
||||||
resolved_year, |
|
||||||
); |
|
||||||
|
|
||||||
// 4. Return EpochTimeToDayNumber(t) + date - 1.
|
|
||||||
temporal::date_equations::epoch_time_to_day_number(year_t + month_t) + self.day - 1 |
|
||||||
} |
|
||||||
|
|
||||||
// NOTE: Implementing as mut self so balance is applied to self, but TBD.
|
|
||||||
/// 3.5.8 `BalanceIsoDate`
|
|
||||||
pub(crate) fn balance(&mut self) { |
|
||||||
let epoch_days = self.as_epoch_days(); |
|
||||||
let ms = temporal::epoch_days_to_epoch_ms(epoch_days, 0); |
|
||||||
|
|
||||||
// Balance current values
|
|
||||||
self.year = temporal::date_equations::epoch_time_to_epoch_year(ms); |
|
||||||
self.month = temporal::date_equations::epoch_time_to_month_in_year(ms); |
|
||||||
self.day = temporal::date_equations::epoch_time_to_date(ms); |
|
||||||
} |
|
||||||
|
|
||||||
// NOTE: Used in AddISODate only, so could possibly be deleted in the future.
|
|
||||||
/// 9.5.4 `BalanceISOYearMonth ( year, month )`
|
|
||||||
pub(crate) fn balance_year_month(&mut self) { |
|
||||||
self.year += (self.month - 1) / 12; |
|
||||||
self.month = ((self.month - 1) % 12) + 1; |
|
||||||
} |
|
||||||
|
|
||||||
/// 3.5.11 `AddISODate ( year, month, day, years, months, weeks, days, overflow )`
|
|
||||||
pub(crate) fn add_iso_date( |
|
||||||
&self, |
|
||||||
date_duration: DateDuration, |
|
||||||
overflow: ArithmeticOverflow, |
|
||||||
) -> JsResult<Self> { |
|
||||||
// 1. Assert: year, month, day, years, months, weeks, and days are integers.
|
|
||||||
// 2. Assert: overflow is either "constrain" or "reject".
|
|
||||||
let mut intermediate = Self::new( |
|
||||||
self.year + date_duration.years() as i32, |
|
||||||
self.month + date_duration.months() as i32, |
|
||||||
0, |
|
||||||
); |
|
||||||
|
|
||||||
// 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months).
|
|
||||||
intermediate.balance_year_month(); |
|
||||||
|
|
||||||
// 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow).
|
|
||||||
let mut new_date = Self::from_unregulated( |
|
||||||
intermediate.year(), |
|
||||||
intermediate.month(), |
|
||||||
self.day, |
|
||||||
overflow, |
|
||||||
)?; |
|
||||||
|
|
||||||
// 5. Set days to days + 7 × weeks.
|
|
||||||
// 6. Let d be intermediate.[[Day]] + days.
|
|
||||||
let additional_days = date_duration.days() as i32 + (date_duration.weeks() as i32 * 7); |
|
||||||
new_date.day += additional_days; |
|
||||||
|
|
||||||
// 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
|
|
||||||
new_date.balance(); |
|
||||||
|
|
||||||
Ok(new_date) |
|
||||||
} |
|
||||||
} |
|
@ -1,100 +0,0 @@ |
|||||||
use crate::{ |
|
||||||
builtins::{ |
|
||||||
date::utils, |
|
||||||
temporal::{self, plain_date::iso::IsoDateRecord}, |
|
||||||
}, |
|
||||||
JsBigInt, |
|
||||||
}; |
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)] |
|
||||||
pub(crate) struct IsoDateTimeRecord { |
|
||||||
iso_date: IsoDateRecord, |
|
||||||
hour: i32, |
|
||||||
minute: i32, |
|
||||||
second: i32, |
|
||||||
millisecond: i32, |
|
||||||
microsecond: i32, |
|
||||||
nanosecond: i32, |
|
||||||
} |
|
||||||
|
|
||||||
impl IsoDateTimeRecord { |
|
||||||
pub(crate) const fn iso_date(&self) -> IsoDateRecord { |
|
||||||
self.iso_date |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// ==== `IsoDateTimeRecord` methods ====
|
|
||||||
|
|
||||||
impl IsoDateTimeRecord { |
|
||||||
pub(crate) const fn with_date(mut self, year: i32, month: i32, day: i32) -> Self { |
|
||||||
let iso_date = IsoDateRecord::new(year, month, day); |
|
||||||
self.iso_date = iso_date; |
|
||||||
self |
|
||||||
} |
|
||||||
|
|
||||||
pub(crate) const fn with_time( |
|
||||||
mut self, |
|
||||||
hour: i32, |
|
||||||
minute: i32, |
|
||||||
second: i32, |
|
||||||
ms: i32, |
|
||||||
mis: i32, |
|
||||||
ns: i32, |
|
||||||
) -> Self { |
|
||||||
self.hour = hour; |
|
||||||
self.minute = minute; |
|
||||||
self.second = second; |
|
||||||
self.millisecond = ms; |
|
||||||
self.microsecond = mis; |
|
||||||
self.nanosecond = ns; |
|
||||||
self |
|
||||||
} |
|
||||||
|
|
||||||
/// 5.5.1 `ISODateTimeWithinLimits ( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond )`
|
|
||||||
pub(crate) fn is_valid(&self) -> bool { |
|
||||||
self.iso_date.is_valid(); |
|
||||||
let ns = self.get_utc_epoch_ns(None).to_f64(); |
|
||||||
|
|
||||||
if ns <= temporal::ns_min_instant().to_f64() - (temporal::NS_PER_DAY as f64) |
|
||||||
|| ns >= temporal::ns_max_instant().to_f64() + (temporal::NS_PER_DAY as f64) |
|
||||||
{ |
|
||||||
return false; |
|
||||||
} |
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
/// 14.8.1 `GetUTCEpochNanoseconds`
|
|
||||||
pub(crate) fn get_utc_epoch_ns(&self, offset_ns: Option<i64>) -> JsBigInt { |
|
||||||
let day = utils::make_day( |
|
||||||
i64::from(self.iso_date.year()), |
|
||||||
i64::from(self.iso_date.month()), |
|
||||||
i64::from(self.iso_date.day()), |
|
||||||
) |
|
||||||
.unwrap_or_default(); |
|
||||||
let time = utils::make_time( |
|
||||||
i64::from(self.hour), |
|
||||||
i64::from(self.minute), |
|
||||||
i64::from(self.second), |
|
||||||
i64::from(self.millisecond), |
|
||||||
) |
|
||||||
.unwrap_or_default(); |
|
||||||
|
|
||||||
let ms = utils::make_date(day, time).unwrap_or_default(); |
|
||||||
|
|
||||||
let epoch_ns = match offset_ns { |
|
||||||
Some(offset) if offset != 0 => { |
|
||||||
let ns = (ms * 1_000_000_i64) |
|
||||||
+ (i64::from(self.microsecond) * 1_000_i64) |
|
||||||
+ i64::from(self.nanosecond); |
|
||||||
ns - offset |
|
||||||
} |
|
||||||
_ => { |
|
||||||
(ms * 1_000_000_i64) |
|
||||||
+ (i64::from(self.microsecond) * 1_000_i64) |
|
||||||
+ i64::from(self.nanosecond) |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
JsBigInt::from(epoch_ns) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,23 @@ |
|||||||
|
[package] |
||||||
|
name = "boa_temporal" |
||||||
|
keywords = ["javascript", "js", "compiler", "temporal", "calendar", "date", "time"] |
||||||
|
categories = ["date", "time", "calendars"] |
||||||
|
readme = "./README.md" |
||||||
|
description.workspace = true |
||||||
|
version.workspace = true |
||||||
|
edition.workspace = true |
||||||
|
authors.workspace = true |
||||||
|
license.workspace = true |
||||||
|
repository.workspace = true |
||||||
|
rust-version.workspace = true |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
tinystr = "0.7.4" |
||||||
|
icu_calendar = { workspace = true, default-features = false } |
||||||
|
rustc-hash = { workspace = true, features = ["std"] } |
||||||
|
num-bigint = { workspace = true, features = ["serde"] } |
||||||
|
bitflags.workspace = true |
||||||
|
num-traits.workspace = true |
||||||
|
|
||||||
|
[lints] |
||||||
|
workspace = true |
@ -0,0 +1,11 @@ |
|||||||
|
# Temporal in Rust |
||||||
|
|
||||||
|
Provides a standard API for working with dates and time. |
||||||
|
|
||||||
|
IMPORTANT NOTE: The Temporal Proposal is still in Stage 3. As such, this crate should be viewed |
||||||
|
as highly experimental until the proposal has been completely standardized and released. |
||||||
|
|
||||||
|
## Goal |
||||||
|
|
||||||
|
The intended goal of this crate is to provide an engine agnostic |
||||||
|
implementation of `ECMAScript`'s Temporal algorithms. |
@ -0,0 +1,579 @@ |
|||||||
|
//! Temporal calendar traits and implementations.
|
||||||
|
//!
|
||||||
|
//! The goal of the calendar module of `boa_temporal` is to provide
|
||||||
|
//! Temporal compatible calendar implementations.
|
||||||
|
//!
|
||||||
|
//! The implementation will only be of calendar's prexisting calendars. This library
|
||||||
|
//! does not come with a pre-existing `CustomCalendar` (i.e., an object that implements
|
||||||
|
//! the calendar protocol), but it does aim to provide the necessary tools and API for
|
||||||
|
//! implementing one.
|
||||||
|
|
||||||
|
use std::{any::Any, str::FromStr}; |
||||||
|
|
||||||
|
use crate::{ |
||||||
|
date::Date, |
||||||
|
datetime::DateTime, |
||||||
|
duration::Duration, |
||||||
|
fields::TemporalFields, |
||||||
|
iso::{IsoDate, IsoDateSlots}, |
||||||
|
month_day::MonthDay, |
||||||
|
options::{ArithmeticOverflow, TemporalUnit}, |
||||||
|
year_month::YearMonth, |
||||||
|
TemporalError, TemporalResult, |
||||||
|
}; |
||||||
|
|
||||||
|
use tinystr::TinyAsciiStr; |
||||||
|
|
||||||
|
use self::iso::IsoCalendar; |
||||||
|
|
||||||
|
pub mod iso; |
||||||
|
|
||||||
|
/// The ECMAScript defined protocol methods
|
||||||
|
pub const CALENDAR_PROTOCOL_METHODS: [&str; 21] = [ |
||||||
|
"dateAdd", |
||||||
|
"dateFromFields", |
||||||
|
"dateUntil", |
||||||
|
"day", |
||||||
|
"dayOfWeek", |
||||||
|
"dayOfYear", |
||||||
|
"daysInMonth", |
||||||
|
"daysInWeek", |
||||||
|
"daysInYear", |
||||||
|
"fields", |
||||||
|
"id", |
||||||
|
"inLeapYear", |
||||||
|
"mergeFields", |
||||||
|
"month", |
||||||
|
"monthCode", |
||||||
|
"monthDayFromFields", |
||||||
|
"monthsInYear", |
||||||
|
"weekOfYear", |
||||||
|
"year", |
||||||
|
"yearMonthFromFields", |
||||||
|
"yearOfWeek", |
||||||
|
]; |
||||||
|
|
||||||
|
/// Designate the type of `CalendarFields` needed
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub enum CalendarFieldsType { |
||||||
|
/// Whether the Fields should return for a Date.
|
||||||
|
Date, |
||||||
|
/// Whether the Fields should return for a YearMonth.
|
||||||
|
YearMonth, |
||||||
|
/// Whether the Fields should return for a MonthDay.
|
||||||
|
MonthDay, |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Optimize to TinyStr or &str.
|
||||||
|
impl From<&[String]> for CalendarFieldsType { |
||||||
|
fn from(value: &[String]) -> Self { |
||||||
|
let year_present = value.contains(&"year".to_owned()); |
||||||
|
let day_present = value.contains(&"day".to_owned()); |
||||||
|
|
||||||
|
if year_present && day_present { |
||||||
|
CalendarFieldsType::Date |
||||||
|
} else if year_present { |
||||||
|
CalendarFieldsType::YearMonth |
||||||
|
} else { |
||||||
|
CalendarFieldsType::MonthDay |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `AvailableCalendars` lists the currently implemented `CalendarProtocols`
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub enum AvailableCalendars { |
||||||
|
/// The ISO8601 calendar.
|
||||||
|
Iso, |
||||||
|
} |
||||||
|
|
||||||
|
// NOTE: Should `s` be forced to lowercase or should the user be expected to provide the lowercase.
|
||||||
|
impl FromStr for AvailableCalendars { |
||||||
|
type Err = TemporalError; |
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"iso8601" => Ok(Self::Iso), |
||||||
|
_ => { |
||||||
|
Err(TemporalError::range().with_message("CalendarId is not an available Calendar")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl AvailableCalendars { |
||||||
|
/// Returns the `CalendarProtocol` for the `AvailableCalendar`
|
||||||
|
#[must_use] |
||||||
|
pub fn to_protocol(&self) -> Box<dyn CalendarProtocol> { |
||||||
|
match self { |
||||||
|
Self::Iso => Box::new(IsoCalendar), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The `DateLike` objects that can be provided to the `CalendarProtocol`.
|
||||||
|
#[derive(Debug)] |
||||||
|
pub enum CalendarDateLike { |
||||||
|
/// Represents a `Date` datelike
|
||||||
|
Date(Date), |
||||||
|
/// Represents a `DateTime` datelike
|
||||||
|
DateTime(DateTime), |
||||||
|
/// Represents a `YearMonth` datelike
|
||||||
|
YearMonth(YearMonth), |
||||||
|
/// Represents a `MonthDay` datelike
|
||||||
|
MonthDay(MonthDay), |
||||||
|
} |
||||||
|
|
||||||
|
impl CalendarDateLike { |
||||||
|
/// Retrieves the internal `IsoDate` field.
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub fn as_iso_date(&self) -> IsoDate { |
||||||
|
match self { |
||||||
|
CalendarDateLike::Date(d) => d.iso_date(), |
||||||
|
CalendarDateLike::DateTime(dt) => dt.iso_date(), |
||||||
|
CalendarDateLike::MonthDay(md) => md.iso_date(), |
||||||
|
CalendarDateLike::YearMonth(ym) => ym.iso_date(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ==== CalendarProtocol trait ====
|
||||||
|
|
||||||
|
/// The `CalendarProtocol`'s Clone supertrait.
|
||||||
|
pub trait CalendarProtocolClone { |
||||||
|
/// Clone's the current `CalendarProtocol`
|
||||||
|
fn clone_box(&self) -> Box<dyn CalendarProtocol>; |
||||||
|
} |
||||||
|
|
||||||
|
impl<P> CalendarProtocolClone for P |
||||||
|
where |
||||||
|
P: 'static + CalendarProtocol + Clone, |
||||||
|
{ |
||||||
|
fn clone_box(&self) -> Box<dyn CalendarProtocol> { |
||||||
|
Box::new(self.clone()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Split further into `CalendarProtocol` and `BuiltinCalendar` to better handle
|
||||||
|
// fields and mergeFields.
|
||||||
|
/// A trait for implementing a Builtin Calendar's Calendar Protocol in Rust.
|
||||||
|
pub trait CalendarProtocol: CalendarProtocolClone { |
||||||
|
/// Creates a `Temporal.PlainDate` object from provided fields.
|
||||||
|
fn date_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Date>; |
||||||
|
/// Creates a `Temporal.PlainYearMonth` object from the provided fields.
|
||||||
|
fn year_month_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<YearMonth>; |
||||||
|
/// Creates a `Temporal.PlainMonthDay` object from the provided fields.
|
||||||
|
fn month_day_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<MonthDay>; |
||||||
|
/// Returns a `Temporal.PlainDate` based off an added date.
|
||||||
|
fn date_add( |
||||||
|
&self, |
||||||
|
date: &Date, |
||||||
|
duration: &Duration, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Date>; |
||||||
|
/// Returns a `Temporal.Duration` representing the duration between two dates.
|
||||||
|
fn date_until( |
||||||
|
&self, |
||||||
|
one: &Date, |
||||||
|
two: &Date, |
||||||
|
largest_unit: TemporalUnit, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Duration>; |
||||||
|
/// Returns the era for a given `temporaldatelike`.
|
||||||
|
fn era( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Option<TinyAsciiStr<8>>>; |
||||||
|
/// Returns the era year for a given `temporaldatelike`
|
||||||
|
fn era_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Option<i32>>; |
||||||
|
/// Returns the `year` for a given `temporaldatelike`
|
||||||
|
fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<i32>; |
||||||
|
/// Returns the `month` for a given `temporaldatelike`
|
||||||
|
fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8>; |
||||||
|
// Note: Best practice would probably be to switch to a MonthCode enum after extraction.
|
||||||
|
/// Returns the `monthCode` for a given `temporaldatelike`
|
||||||
|
fn month_code( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<TinyAsciiStr<4>>; |
||||||
|
/// Returns the `day` for a given `temporaldatelike`
|
||||||
|
fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8>; |
||||||
|
/// Returns a value representing the day of the week for a date.
|
||||||
|
fn day_of_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16>; |
||||||
|
/// Returns a value representing the day of the year for a given calendar.
|
||||||
|
fn day_of_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16>; |
||||||
|
/// Returns a value representing the week of the year for a given calendar.
|
||||||
|
fn week_of_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16>; |
||||||
|
/// Returns the year of a given week.
|
||||||
|
fn year_of_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<i32>; |
||||||
|
/// Returns the days in a week for a given calendar.
|
||||||
|
fn days_in_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16>; |
||||||
|
/// Returns the days in a month for a given calendar.
|
||||||
|
fn days_in_month( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16>; |
||||||
|
/// Returns the days in a year for a given calendar.
|
||||||
|
fn days_in_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16>; |
||||||
|
/// Returns the months in a year for a given calendar.
|
||||||
|
fn months_in_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16>; |
||||||
|
/// Returns whether a value is within a leap year according to the designated calendar.
|
||||||
|
fn in_leap_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<bool>; |
||||||
|
/// Resolve the `TemporalFields` for the implemented Calendar
|
||||||
|
fn resolve_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
r#type: CalendarFieldsType, |
||||||
|
) -> TemporalResult<()>; |
||||||
|
/// Return this calendar's a fieldName and whether it is required depending on type (date, day-month).
|
||||||
|
fn field_descriptors(&self, r#type: CalendarFieldsType) -> Vec<(String, bool)>; |
||||||
|
/// Return the fields to ignore for this Calendar based on provided keys.
|
||||||
|
fn field_keys_to_ignore(&self, additional_keys: Vec<String>) -> Vec<String>; |
||||||
|
/// Debug name
|
||||||
|
fn identifier(&self, context: &mut dyn Any) -> TemporalResult<String>; |
||||||
|
} |
||||||
|
|
||||||
|
impl core::fmt::Debug for dyn CalendarProtocol { |
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
||||||
|
write!( |
||||||
|
f, |
||||||
|
"{}", |
||||||
|
self.identifier(&mut ()).unwrap_or_default().as_str() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The `[[Calendar]]` field slot of a Temporal Object.
|
||||||
|
#[derive(Debug)] |
||||||
|
pub enum CalendarSlot { |
||||||
|
/// The calendar identifier string.
|
||||||
|
Identifier(String), |
||||||
|
/// A `CalendarProtocol` implementation.
|
||||||
|
Protocol(Box<dyn CalendarProtocol>), |
||||||
|
} |
||||||
|
|
||||||
|
impl Clone for CalendarSlot { |
||||||
|
fn clone(&self) -> Self { |
||||||
|
match self { |
||||||
|
Self::Identifier(s) => Self::Identifier(s.clone()), |
||||||
|
Self::Protocol(b) => Self::Protocol(b.clone_box()), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Clone for Box<dyn CalendarProtocol + 'static> { |
||||||
|
fn clone(&self) -> Self { |
||||||
|
self.clone_box() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for CalendarSlot { |
||||||
|
fn default() -> Self { |
||||||
|
Self::Identifier("iso8601".to_owned()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Handle `CalendarFields` and `CalendarMergeFields`
|
||||||
|
impl CalendarSlot { |
||||||
|
/// `CalendarDateAdd`
|
||||||
|
///
|
||||||
|
/// TODO: More Docs
|
||||||
|
pub fn date_add( |
||||||
|
&self, |
||||||
|
date: &Date, |
||||||
|
duration: &Duration, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Date> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.date_add(date, duration, overflow, context) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.date_add(date, duration, overflow, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarDateUntil`
|
||||||
|
///
|
||||||
|
/// TODO: More Docs
|
||||||
|
pub fn date_until( |
||||||
|
&self, |
||||||
|
one: &Date, |
||||||
|
two: &Date, |
||||||
|
largest_unit: TemporalUnit, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Duration> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.date_until(one, two, largest_unit, context) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.date_until(one, two, largest_unit, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarYear`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn year(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<i32> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.year(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.year(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarMonth`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn month(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.month(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.month(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarMonthCode`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn month_code( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<TinyAsciiStr<4>> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.month_code(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.month_code(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarDay`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn day(&self, date_like: &CalendarDateLike, context: &mut dyn Any) -> TemporalResult<u8> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.day(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.day(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarDayOfWeek`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn day_of_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.day_of_week(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.day_of_week(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarDayOfYear`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn day_of_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.day_of_year(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.day_of_year(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarWeekOfYear`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn week_of_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.week_of_year(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.week_of_year(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarYearOfWeek`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn year_of_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<i32> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.year_of_week(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.year_of_week(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarDaysInWeek`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn days_in_week( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.days_in_week(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.days_in_week(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarDaysInMonth`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn days_in_month( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.days_in_month(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.days_in_month(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarDaysInYear`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn days_in_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.days_in_year(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.days_in_year(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarMonthsInYear`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn months_in_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<u16> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.months_in_year(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.months_in_year(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `CalendarInLeapYear`
|
||||||
|
///
|
||||||
|
/// TODO: More docs.
|
||||||
|
pub fn in_leap_year( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<bool> { |
||||||
|
match self { |
||||||
|
Self::Identifier(id) => { |
||||||
|
let protocol = AvailableCalendars::from_str(id)?.to_protocol(); |
||||||
|
protocol.in_leap_year(date_like, &mut ()) |
||||||
|
} |
||||||
|
Self::Protocol(protocol) => protocol.in_leap_year(date_like, context), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,285 @@ |
|||||||
|
//! Implementation of the "iso8601" calendar.
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
date::Date, |
||||||
|
duration::Duration, |
||||||
|
error::TemporalError, |
||||||
|
fields::TemporalFields, |
||||||
|
month_day::MonthDay, |
||||||
|
options::{ArithmeticOverflow, TemporalUnit}, |
||||||
|
utils, |
||||||
|
year_month::YearMonth, |
||||||
|
TemporalResult, |
||||||
|
}; |
||||||
|
use std::any::Any; |
||||||
|
|
||||||
|
use tinystr::TinyAsciiStr; |
||||||
|
|
||||||
|
use super::{CalendarDateLike, CalendarFieldsType, CalendarProtocol, CalendarSlot}; |
||||||
|
|
||||||
|
use icu_calendar::week::{RelativeUnit, WeekCalculator}; |
||||||
|
|
||||||
|
/// This represents the implementation of the `ISO8601`
|
||||||
|
/// calendar for Temporal.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub struct IsoCalendar; |
||||||
|
|
||||||
|
impl CalendarProtocol for IsoCalendar { |
||||||
|
/// Temporal 15.8.2.1 `Temporal.prototype.dateFromFields( fields [, options])` - Supercedes 12.5.4
|
||||||
|
///
|
||||||
|
/// This is a basic implementation for an iso8601 calendar's `dateFromFields` method.
|
||||||
|
fn date_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<Date> { |
||||||
|
// NOTE: we are in ISO by default here.
|
||||||
|
// a. Perform ? ISOResolveMonth(fields).
|
||||||
|
// b. Let result be ? ISODateFromFields(fields, overflow).
|
||||||
|
fields.iso_resolve_month()?; |
||||||
|
|
||||||
|
// 9. Return ? CreateDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
|
||||||
|
Date::new( |
||||||
|
fields.year().unwrap_or(0), |
||||||
|
fields.month().unwrap_or(0), |
||||||
|
fields.day().unwrap_or(0), |
||||||
|
CalendarSlot::Identifier("iso8601".to_string()), |
||||||
|
overflow, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// 12.5.5 `Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] )`
|
||||||
|
///
|
||||||
|
/// This is a basic implementation for an iso8601 calendar's `yearMonthFromFields` method.
|
||||||
|
fn year_month_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<YearMonth> { |
||||||
|
// 9. If calendar.[[Identifier]] is "iso8601", then
|
||||||
|
// a. Perform ? ISOResolveMonth(fields).
|
||||||
|
fields.iso_resolve_month()?; |
||||||
|
|
||||||
|
// TODO: Do we even need ISOYearMonthFromFields? YearMonth would should pass as a valid date
|
||||||
|
// b. Let result be ? ISOYearMonthFromFields(fields, overflow).
|
||||||
|
// 10. Return ? CreateYearMonth(result.[[Year]], result.[[Month]], "iso8601", result.[[ReferenceISODay]]).
|
||||||
|
YearMonth::new( |
||||||
|
fields.year().unwrap_or(0), |
||||||
|
fields.month().unwrap_or(0), |
||||||
|
fields.day(), |
||||||
|
CalendarSlot::Identifier("iso8601".to_string()), |
||||||
|
overflow, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// 12.5.6 `Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] )`
|
||||||
|
///
|
||||||
|
/// This is a basic implementation for an iso8601 calendar's `monthDayFromFields` method.
|
||||||
|
fn month_day_from_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<MonthDay> { |
||||||
|
// 8. Perform ? ISOResolveMonth(fields).
|
||||||
|
fields.iso_resolve_month()?; |
||||||
|
|
||||||
|
// TODO: double check error mapping is correct for specifcation/test262.
|
||||||
|
// 9. Let result be ? ISOMonthDayFromFields(fields, overflow).
|
||||||
|
// 10. Return ? CreateMonthDay(result.[[Month]], result.[[Day]], "iso8601", result.[[ReferenceISOYear]]).
|
||||||
|
MonthDay::new( |
||||||
|
fields.month().unwrap_or(0), |
||||||
|
fields.month().unwrap_or(0), |
||||||
|
CalendarSlot::Identifier("iso8601".to_string()), |
||||||
|
overflow, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// 12.5.7 `Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] )`
|
||||||
|
///
|
||||||
|
/// Below implements the basic implementation for an iso8601 calendar's `dateAdd` method.
|
||||||
|
fn date_add( |
||||||
|
&self, |
||||||
|
_date: &Date, |
||||||
|
_duration: &Duration, |
||||||
|
_overflow: ArithmeticOverflow, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<Date> { |
||||||
|
// TODO: Not stable on `ICU4X`. Implement once completed.
|
||||||
|
Err(TemporalError::range().with_message("feature not implemented.")) |
||||||
|
|
||||||
|
// 9. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], balanceResult.[[Days]], overflow).
|
||||||
|
// 10. Return ? CreateDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601").
|
||||||
|
} |
||||||
|
|
||||||
|
/// 12.5.8 `Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] )`
|
||||||
|
///
|
||||||
|
/// Below implements the basic implementation for an iso8601 calendar's `dateUntil` method.
|
||||||
|
fn date_until( |
||||||
|
&self, |
||||||
|
_one: &Date, |
||||||
|
_two: &Date, |
||||||
|
_largest_unit: TemporalUnit, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<Duration> { |
||||||
|
// TODO: Not stable on `ICU4X`. Implement once completed.
|
||||||
|
Err(TemporalError::range().with_message("Feature not yet implemented.")) |
||||||
|
|
||||||
|
// 9. Let result be DifferenceISODate(one.[[ISOYear]], one.[[ISOMonth]], one.[[ISODay]], two.[[ISOYear]], two.[[ISOMonth]], two.[[ISODay]], largestUnit).
|
||||||
|
// 10. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], 0, 0, 0, 0, 0, 0).
|
||||||
|
} |
||||||
|
|
||||||
|
/// `Temporal.Calendar.prototype.era( dateLike )` for iso8601 calendar.
|
||||||
|
fn era( |
||||||
|
&self, |
||||||
|
_: &CalendarDateLike, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<Option<TinyAsciiStr<8>>> { |
||||||
|
// Returns undefined on iso8601.
|
||||||
|
Ok(None) |
||||||
|
} |
||||||
|
|
||||||
|
/// `Temporal.Calendar.prototype.eraYear( dateLike )` for iso8601 calendar.
|
||||||
|
fn era_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<Option<i32>> { |
||||||
|
// Returns undefined on iso8601.
|
||||||
|
Ok(None) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `year` for the `Iso` calendar.
|
||||||
|
fn year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<i32> { |
||||||
|
Ok(date_like.as_iso_date().year()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `month` for the `Iso` calendar.
|
||||||
|
fn month(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u8> { |
||||||
|
Ok(date_like.as_iso_date().month()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `monthCode` for the `Iso` calendar.
|
||||||
|
fn month_code( |
||||||
|
&self, |
||||||
|
date_like: &CalendarDateLike, |
||||||
|
_: &mut dyn Any, |
||||||
|
) -> TemporalResult<TinyAsciiStr<4>> { |
||||||
|
let date = date_like.as_iso_date().as_icu4x()?; |
||||||
|
Ok(date.month().code.0) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `day` for the `Iso` calendar.
|
||||||
|
fn day(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u8> { |
||||||
|
Ok(date_like.as_iso_date().day()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `dayOfWeek` for the `Iso` calendar.
|
||||||
|
fn day_of_week(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> { |
||||||
|
let date = date_like.as_iso_date().as_icu4x()?; |
||||||
|
Ok(date.day_of_week() as u16) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `dayOfYear` for the `Iso` calendar.
|
||||||
|
fn day_of_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> { |
||||||
|
let date = date_like.as_iso_date().as_icu4x()?; |
||||||
|
Ok(date.day_of_year_info().day_of_year) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `weekOfYear` for the `Iso` calendar.
|
||||||
|
fn week_of_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> { |
||||||
|
let date = date_like.as_iso_date().as_icu4x()?; |
||||||
|
|
||||||
|
let week_calculator = WeekCalculator::default(); |
||||||
|
|
||||||
|
let week_of = date |
||||||
|
.week_of_year(&week_calculator) |
||||||
|
.map_err(|err| TemporalError::range().with_message(err.to_string()))?; |
||||||
|
|
||||||
|
Ok(week_of.week) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `yearOfWeek` for the `Iso` calendar.
|
||||||
|
fn year_of_week(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<i32> { |
||||||
|
let date = date_like.as_iso_date().as_icu4x()?; |
||||||
|
|
||||||
|
let week_calculator = WeekCalculator::default(); |
||||||
|
|
||||||
|
let week_of = date |
||||||
|
.week_of_year(&week_calculator) |
||||||
|
.map_err(|err| TemporalError::range().with_message(err.to_string()))?; |
||||||
|
|
||||||
|
// TODO: Reach out and see about RelativeUnit starting at -1
|
||||||
|
// Ok(date.year().number - week_of.unit)
|
||||||
|
match week_of.unit { |
||||||
|
RelativeUnit::Previous => Ok(date.year().number - 1), |
||||||
|
RelativeUnit::Current => Ok(date.year().number), |
||||||
|
RelativeUnit::Next => Ok(date.year().number + 1), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `daysInWeek` value for the `Iso` calendar.
|
||||||
|
fn days_in_week(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> { |
||||||
|
Ok(7) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `daysInMonth` value for the `Iso` calendar.
|
||||||
|
fn days_in_month(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> { |
||||||
|
let date = date_like.as_iso_date().as_icu4x()?; |
||||||
|
Ok(u16::from(date.days_in_month())) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `daysInYear` value for the `Iso` calendar.
|
||||||
|
fn days_in_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> { |
||||||
|
let date = date_like.as_iso_date().as_icu4x()?; |
||||||
|
Ok(date.days_in_year()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Return the amount of months in an ISO Calendar.
|
||||||
|
fn months_in_year(&self, _: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<u16> { |
||||||
|
Ok(12) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns whether provided date is in a leap year according to this calendar.
|
||||||
|
fn in_leap_year(&self, date_like: &CalendarDateLike, _: &mut dyn Any) -> TemporalResult<bool> { |
||||||
|
// `ICU4X`'s `CalendarArithmetic` is currently private.
|
||||||
|
Ok(utils::mathematical_days_in_year(date_like.as_iso_date().year()) == 366) |
||||||
|
} |
||||||
|
|
||||||
|
// Resolve the fields for the iso calendar.
|
||||||
|
fn resolve_fields( |
||||||
|
&self, |
||||||
|
fields: &mut TemporalFields, |
||||||
|
_: CalendarFieldsType, |
||||||
|
) -> TemporalResult<()> { |
||||||
|
fields.iso_resolve_month()?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the ISO field descriptors, which is not called for the iso8601 calendar.
|
||||||
|
fn field_descriptors(&self, _: CalendarFieldsType) -> Vec<(String, bool)> { |
||||||
|
// NOTE(potential improvement): look into implementing field descriptors and call
|
||||||
|
// ISO like any other calendar?
|
||||||
|
// Field descriptors is unused on ISO8601.
|
||||||
|
unreachable!() |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the `CalendarFieldKeysToIgnore` implementation for ISO.
|
||||||
|
fn field_keys_to_ignore(&self, additional_keys: Vec<String>) -> Vec<String> { |
||||||
|
let mut result = Vec::new(); |
||||||
|
for key in &additional_keys { |
||||||
|
result.push(key.clone()); |
||||||
|
if key.as_str() == "month" { |
||||||
|
result.push("monthCode".to_string()); |
||||||
|
} else if key.as_str() == "monthCode" { |
||||||
|
result.push("month".to_string()); |
||||||
|
} |
||||||
|
} |
||||||
|
result |
||||||
|
} |
||||||
|
|
||||||
|
// NOTE: This is currently not a name that is compliant with
|
||||||
|
// the Temporal proposal. For debugging purposes only.
|
||||||
|
/// Returns the debug name.
|
||||||
|
fn identifier(&self, _: &mut dyn Any) -> TemporalResult<String> { |
||||||
|
Ok("iso8601".to_string()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,220 @@ |
|||||||
|
//! The `PlainDate` representation.
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
calendar::CalendarSlot, |
||||||
|
datetime::DateTime, |
||||||
|
duration::{DateDuration, Duration}, |
||||||
|
iso::{IsoDate, IsoDateSlots}, |
||||||
|
options::{ArithmeticOverflow, TemporalUnit}, |
||||||
|
TemporalResult, |
||||||
|
}; |
||||||
|
use std::any::Any; |
||||||
|
|
||||||
|
/// The `Temporal.PlainDate` equivalent
|
||||||
|
#[derive(Debug, Default, Clone)] |
||||||
|
pub struct Date { |
||||||
|
iso: IsoDate, |
||||||
|
calendar: CalendarSlot, |
||||||
|
} |
||||||
|
|
||||||
|
// ==== Private API ====
|
||||||
|
|
||||||
|
impl Date { |
||||||
|
/// Create a new `Date` with the date values and calendar slot.
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self { |
||||||
|
Self { iso, calendar } |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
/// Returns a new moved date and the days associated with that adjustment
|
||||||
|
pub(crate) fn move_relative_date( |
||||||
|
&self, |
||||||
|
duration: &Duration, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<(Self, f64)> { |
||||||
|
let new_date = |
||||||
|
self.contextual_add_date(duration, ArithmeticOverflow::Constrain, context)?; |
||||||
|
let days = f64::from(self.days_until(&new_date)); |
||||||
|
Ok((new_date, days)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ==== Public API ====
|
||||||
|
|
||||||
|
impl Date { |
||||||
|
/// Creates a new `Date` while checking for validity.
|
||||||
|
pub fn new( |
||||||
|
year: i32, |
||||||
|
month: i32, |
||||||
|
day: i32, |
||||||
|
calendar: CalendarSlot, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
let iso = IsoDate::new(year, month, day, overflow)?; |
||||||
|
Ok(Self::new_unchecked(iso, calendar)) |
||||||
|
} |
||||||
|
|
||||||
|
#[must_use] |
||||||
|
/// Creates a `Date` from a `DateTime`.
|
||||||
|
pub fn from_datetime(dt: &DateTime) -> Self { |
||||||
|
Self { |
||||||
|
iso: dt.iso_date(), |
||||||
|
calendar: dt.calendar().clone(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns this `Date`'s year value.
|
||||||
|
pub const fn year(&self) -> i32 { |
||||||
|
self.iso.year() |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns this `Date`'s month value.
|
||||||
|
pub const fn month(&self) -> u8 { |
||||||
|
self.iso.month() |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns this `Date`'s day value.
|
||||||
|
pub const fn day(&self) -> u8 { |
||||||
|
self.iso.day() |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns the `Date`'s inner `IsoDate` record.
|
||||||
|
pub const fn iso_date(&self) -> IsoDate { |
||||||
|
self.iso |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns a reference to this `Date`'s calendar slot.
|
||||||
|
pub fn calendar(&self) -> &CalendarSlot { |
||||||
|
&self.calendar |
||||||
|
} |
||||||
|
|
||||||
|
/// 3.5.7 `IsValidISODate`
|
||||||
|
///
|
||||||
|
/// Checks if the current date is a valid `ISODate`.
|
||||||
|
#[must_use] |
||||||
|
pub fn is_valid(&self) -> bool { |
||||||
|
self.iso.is_valid() |
||||||
|
} |
||||||
|
|
||||||
|
/// `DaysUntil`
|
||||||
|
///
|
||||||
|
/// Calculates the epoch days between two `Date`s
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub fn days_until(&self, other: &Self) -> i32 { |
||||||
|
other.iso.to_epoch_days() - self.iso.to_epoch_days() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl IsoDateSlots for Date { |
||||||
|
/// Returns the structs `IsoDate`
|
||||||
|
fn iso_date(&self) -> IsoDate { |
||||||
|
self.iso |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ==== Context based API ====
|
||||||
|
|
||||||
|
impl Date { |
||||||
|
/// Returns the date after adding the given duration to date with a provided context.
|
||||||
|
///
|
||||||
|
/// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )`
|
||||||
|
#[inline] |
||||||
|
pub fn contextual_add_date( |
||||||
|
&self, |
||||||
|
duration: &Duration, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
// 1. If options is not present, set options to undefined.
|
||||||
|
// 2. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then
|
||||||
|
if duration.date().years() != 0.0 |
||||||
|
|| duration.date().months() != 0.0 |
||||||
|
|| duration.date().weeks() != 0.0 |
||||||
|
{ |
||||||
|
// a. If dateAdd is not present, then
|
||||||
|
// i. Set dateAdd to unused.
|
||||||
|
// ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd").
|
||||||
|
// b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd).
|
||||||
|
return self.calendar().date_add(self, duration, overflow, context); |
||||||
|
} |
||||||
|
|
||||||
|
// 3. Let overflow be ? ToTemporalOverflow(options).
|
||||||
|
// 4. Let days be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").[[Days]].
|
||||||
|
let (days, _) = duration.balance_time_duration(TemporalUnit::Day)?; |
||||||
|
|
||||||
|
// 5. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow).
|
||||||
|
let result = self |
||||||
|
.iso |
||||||
|
.add_iso_date(&DateDuration::new(0f64, 0f64, 0f64, days), overflow)?; |
||||||
|
|
||||||
|
Ok(Self::new_unchecked(result, self.calendar().clone())) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the date after adding the given duration to date.
|
||||||
|
///
|
||||||
|
/// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )`
|
||||||
|
#[inline] |
||||||
|
pub fn add_date( |
||||||
|
&self, |
||||||
|
duration: &Duration, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
self.contextual_add_date(duration, overflow, &mut ()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns a duration representing the difference between the dates one and two with a provided context.
|
||||||
|
///
|
||||||
|
/// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )`
|
||||||
|
#[inline] |
||||||
|
pub fn contextual_difference_date( |
||||||
|
&self, |
||||||
|
other: &Self, |
||||||
|
largest_unit: TemporalUnit, |
||||||
|
context: &mut dyn Any, |
||||||
|
) -> TemporalResult<Duration> { |
||||||
|
if self.iso.year() == other.iso.year() |
||||||
|
&& self.iso.month() == other.iso.month() |
||||||
|
&& self.iso.day() == other.iso.day() |
||||||
|
{ |
||||||
|
return Ok(Duration::default()); |
||||||
|
} |
||||||
|
|
||||||
|
if largest_unit == TemporalUnit::Day { |
||||||
|
let days = self.days_until(other); |
||||||
|
return Ok(Duration::from_date_duration(DateDuration::new( |
||||||
|
0f64, |
||||||
|
0f64, |
||||||
|
0f64, |
||||||
|
f64::from(days), |
||||||
|
))); |
||||||
|
} |
||||||
|
|
||||||
|
self.calendar() |
||||||
|
.date_until(self, other, largest_unit, context) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns a duration representing the difference between the dates one and two.
|
||||||
|
///
|
||||||
|
/// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )`
|
||||||
|
#[inline] |
||||||
|
pub fn difference_date( |
||||||
|
&self, |
||||||
|
other: &Self, |
||||||
|
largest_unit: TemporalUnit, |
||||||
|
) -> TemporalResult<Duration> { |
||||||
|
self.contextual_difference_date(other, largest_unit, &mut ()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,95 @@ |
|||||||
|
//! Temporal implementation of `DateTime`
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
calendar::CalendarSlot, |
||||||
|
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime}, |
||||||
|
options::ArithmeticOverflow, |
||||||
|
TemporalResult, |
||||||
|
}; |
||||||
|
|
||||||
|
/// The `DateTime` struct.
|
||||||
|
#[derive(Debug, Default, Clone)] |
||||||
|
pub struct DateTime { |
||||||
|
iso: IsoDateTime, |
||||||
|
calendar: CalendarSlot, |
||||||
|
} |
||||||
|
|
||||||
|
// ==== Private DateTime API ====
|
||||||
|
|
||||||
|
impl DateTime { |
||||||
|
/// Creates a new unchecked `DateTime`.
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub(crate) fn new_unchecked(date: IsoDate, time: IsoTime, calendar: CalendarSlot) -> Self { |
||||||
|
Self { |
||||||
|
iso: IsoDateTime::new_unchecked(date, time), |
||||||
|
calendar, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Utility function for validating `IsoDate`s
|
||||||
|
fn validate_iso(iso: IsoDate) -> bool { |
||||||
|
IsoDateTime::new_unchecked(iso, IsoTime::noon()).is_within_limits() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ==== Public DateTime API ====
|
||||||
|
|
||||||
|
impl DateTime { |
||||||
|
/// Creates a new validated `DateTime`.
|
||||||
|
#[inline] |
||||||
|
#[allow(clippy::too_many_arguments)] |
||||||
|
pub fn new( |
||||||
|
year: i32, |
||||||
|
month: i32, |
||||||
|
day: i32, |
||||||
|
hour: i32, |
||||||
|
minute: i32, |
||||||
|
second: i32, |
||||||
|
millisecond: i32, |
||||||
|
microsecond: i32, |
||||||
|
nanosecond: i32, |
||||||
|
calendar: CalendarSlot, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
let iso_date = IsoDate::new(year, month, day, ArithmeticOverflow::Reject)?; |
||||||
|
let iso_time = IsoTime::new( |
||||||
|
hour, |
||||||
|
minute, |
||||||
|
second, |
||||||
|
millisecond, |
||||||
|
microsecond, |
||||||
|
nanosecond, |
||||||
|
ArithmeticOverflow::Reject, |
||||||
|
)?; |
||||||
|
Ok(Self::new_unchecked(iso_date, iso_time, calendar)) |
||||||
|
} |
||||||
|
|
||||||
|
/// Validates whether ISO date slots are within iso limits at noon.
|
||||||
|
#[inline] |
||||||
|
pub fn validate<T: IsoDateSlots>(target: &T) -> bool { |
||||||
|
Self::validate_iso(target.iso_date()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the inner `IsoDate` value.
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub fn iso_date(&self) -> IsoDate { |
||||||
|
self.iso.iso_date() |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the inner `IsoTime` value.
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub fn iso_time(&self) -> IsoTime { |
||||||
|
self.iso.iso_time() |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the Calendar value.
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub fn calendar(&self) -> &CalendarSlot { |
||||||
|
&self.calendar |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,98 @@ |
|||||||
|
//! An error type for Temporal Errors.
|
||||||
|
|
||||||
|
use core::fmt; |
||||||
|
|
||||||
|
/// `TemporalError`'s error type.
|
||||||
|
#[derive(Debug, Default, Clone, Copy)] |
||||||
|
pub enum ErrorKind { |
||||||
|
/// Error.
|
||||||
|
#[default] |
||||||
|
Generic, |
||||||
|
/// TypeError
|
||||||
|
Type, |
||||||
|
/// RangeError
|
||||||
|
Range, |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for ErrorKind { |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Generic => "Error", |
||||||
|
Self::Type => "TypeError", |
||||||
|
Self::Range => "RangeError", |
||||||
|
} |
||||||
|
.fmt(f) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The error type for `boa_temporal`.
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub struct TemporalError { |
||||||
|
kind: ErrorKind, |
||||||
|
msg: Box<str>, |
||||||
|
} |
||||||
|
|
||||||
|
impl TemporalError { |
||||||
|
fn new(kind: ErrorKind) -> Self { |
||||||
|
Self { |
||||||
|
kind, |
||||||
|
msg: Box::default(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a generic error
|
||||||
|
#[must_use] |
||||||
|
pub fn general<S>(msg: S) -> Self |
||||||
|
where |
||||||
|
S: Into<Box<str>>, |
||||||
|
{ |
||||||
|
Self::new(ErrorKind::Generic).with_message(msg) |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a range error.
|
||||||
|
#[must_use] |
||||||
|
pub fn range() -> Self { |
||||||
|
Self::new(ErrorKind::Range) |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a type error.
|
||||||
|
#[must_use] |
||||||
|
pub fn r#type() -> Self { |
||||||
|
Self::new(ErrorKind::Type) |
||||||
|
} |
||||||
|
|
||||||
|
/// Add a message to the error.
|
||||||
|
#[must_use] |
||||||
|
pub fn with_message<S>(mut self, msg: S) -> Self |
||||||
|
where |
||||||
|
S: Into<Box<str>>, |
||||||
|
{ |
||||||
|
self.msg = msg.into(); |
||||||
|
self |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns this error's kind.
|
||||||
|
#[must_use] |
||||||
|
pub fn kind(&self) -> ErrorKind { |
||||||
|
self.kind |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the error message.
|
||||||
|
#[must_use] |
||||||
|
pub fn message(&self) -> &str { |
||||||
|
&self.msg |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for TemporalError { |
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||||
|
write!(f, "{}", self.kind)?; |
||||||
|
|
||||||
|
let msg = self.msg.trim(); |
||||||
|
if !msg.is_empty() { |
||||||
|
write!(f, ": {msg}")?; |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,487 @@ |
|||||||
|
//! `TemporalFields` native Rust representation.
|
||||||
|
|
||||||
|
use std::str::FromStr; |
||||||
|
|
||||||
|
use crate::{error::TemporalError, TemporalResult}; |
||||||
|
|
||||||
|
use bitflags::bitflags; |
||||||
|
// use rustc_hash::FxHashSet;
|
||||||
|
use tinystr::{TinyAsciiStr, TinyStr16, TinyStr4}; |
||||||
|
|
||||||
|
bitflags! { |
||||||
|
/// FieldMap maps the currently active fields on the `TemporalField`
|
||||||
|
#[derive(Debug, PartialEq, Eq)] |
||||||
|
pub struct FieldMap: u16 { |
||||||
|
/// Represents an active `year` field
|
||||||
|
const YEAR = 0b0000_0000_0000_0001; |
||||||
|
/// Represents an active `month` field
|
||||||
|
const MONTH = 0b0000_0000_0000_0010; |
||||||
|
/// Represents an active `monthCode` field
|
||||||
|
const MONTH_CODE = 0b0000_0000_0000_0100; |
||||||
|
/// Represents an active `day` field
|
||||||
|
const DAY = 0b0000_0000_0000_1000; |
||||||
|
/// Represents an active `hour` field
|
||||||
|
const HOUR = 0b0000_0000_0001_0000; |
||||||
|
/// Represents an active `minute` field
|
||||||
|
const MINUTE = 0b0000_0000_0010_0000; |
||||||
|
/// Represents an active `second` field
|
||||||
|
const SECOND = 0b0000_0000_0100_0000; |
||||||
|
/// Represents an active `millisecond` field
|
||||||
|
const MILLISECOND = 0b0000_0000_1000_0000; |
||||||
|
/// Represents an active `microsecond` field
|
||||||
|
const MICROSECOND = 0b0000_0001_0000_0000; |
||||||
|
/// Represents an active `nanosecond` field
|
||||||
|
const NANOSECOND = 0b0000_0010_0000_0000; |
||||||
|
/// Represents an active `offset` field
|
||||||
|
const OFFSET = 0b0000_0100_0000_0000; |
||||||
|
/// Represents an active `era` field
|
||||||
|
const ERA = 0b0000_1000_0000_0000; |
||||||
|
/// Represents an active `eraYear` field
|
||||||
|
const ERA_YEAR = 0b0001_0000_0000_0000; |
||||||
|
/// Represents an active `timeZone` field
|
||||||
|
const TIME_ZONE = 0b0010_0000_0000_0000; |
||||||
|
// NOTE(nekevss): Two bits preserved if needed.
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The post conversion field value.
|
||||||
|
#[derive(Debug)] |
||||||
|
#[allow(variant_size_differences)] |
||||||
|
pub enum FieldValue { |
||||||
|
/// Designates the values as an integer.
|
||||||
|
Integer(i32), |
||||||
|
/// Designates that the value is undefined.
|
||||||
|
Undefined, |
||||||
|
/// Designates the value as a string.
|
||||||
|
String(String), |
||||||
|
} |
||||||
|
|
||||||
|
/// The Conversion type of a field.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub enum FieldConversion { |
||||||
|
/// Designates the Conversion type is `ToIntegerWithTruncation`
|
||||||
|
ToIntegerWithTruncation, |
||||||
|
/// Designates the Conversion type is `ToPositiveIntegerWithTruncation`
|
||||||
|
ToPositiveIntegerWithTruncation, |
||||||
|
/// Designates the Conversion type is `ToPrimitiveRequireString`
|
||||||
|
ToPrimativeAndRequireString, |
||||||
|
/// Designates the Conversion type is nothing
|
||||||
|
None, |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for FieldConversion { |
||||||
|
type Err = TemporalError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"year" | "hour" | "minute" | "second" | "millisecond" | "microsecond" |
||||||
|
| "nanosecond" => Ok(Self::ToIntegerWithTruncation), |
||||||
|
"month" | "day" => Ok(Self::ToPositiveIntegerWithTruncation), |
||||||
|
"monthCode" | "offset" | "eraYear" => Ok(Self::ToPrimativeAndRequireString), |
||||||
|
_ => Err(TemporalError::range() |
||||||
|
.with_message(format!("{s} is not a valid TemporalField Property"))), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The temporal fields are laid out in the Temporal proposal under section 13.46 `PrepareTemporalFields`
|
||||||
|
/// with conversion and defaults laid out by Table 17 (displayed below).
|
||||||
|
///
|
||||||
|
/// `TemporalFields` is meant to act as a native Rust implementation
|
||||||
|
/// of the fields.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ## Table 17: Temporal field requirements
|
||||||
|
///
|
||||||
|
/// | Property | Conversion | Default |
|
||||||
|
/// | -------------|-----------------------------------|------------|
|
||||||
|
/// | "year" | `ToIntegerWithTruncation` | undefined |
|
||||||
|
/// | "month" | `ToPositiveIntegerWithTruncation` | undefined |
|
||||||
|
/// | "monthCode" | `ToPrimitiveAndRequireString` | undefined |
|
||||||
|
/// | "day" | `ToPositiveIntegerWithTruncation` | undefined |
|
||||||
|
/// | "hour" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||||
|
/// | "minute" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||||
|
/// | "second" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||||
|
/// | "millisecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
||||||
|
/// | "microsecond"| `ToIntegerWithTruncation` | +0𝔽 |
|
||||||
|
/// | "nanosecond" | `ToIntegerWithTruncation` | +0𝔽 |
|
||||||
|
/// | "offset" | `ToPrimitiveAndRequireString` | undefined |
|
||||||
|
/// | "era" | `ToPrimitiveAndRequireString` | undefined |
|
||||||
|
/// | "eraYear" | `ToIntegerWithTruncation` | undefined |
|
||||||
|
/// | "timeZone" | `None` | undefined |
|
||||||
|
#[derive(Debug)] |
||||||
|
pub struct TemporalFields { |
||||||
|
bit_map: FieldMap, |
||||||
|
year: Option<i32>, |
||||||
|
month: Option<i32>, |
||||||
|
month_code: Option<TinyStr4>, // TODO: Switch to icu compatible value.
|
||||||
|
day: Option<i32>, |
||||||
|
hour: i32, |
||||||
|
minute: i32, |
||||||
|
second: i32, |
||||||
|
millisecond: i32, |
||||||
|
microsecond: i32, |
||||||
|
nanosecond: i32, |
||||||
|
offset: Option<String>, // TODO: Switch to tinystr?
|
||||||
|
era: Option<TinyStr16>, // TODO: switch to icu compatible value.
|
||||||
|
era_year: Option<i32>, // TODO: switch to icu compatible value.
|
||||||
|
time_zone: Option<String>, // TODO: figure out the identifier for TimeZone.
|
||||||
|
} |
||||||
|
|
||||||
|
impl Default for TemporalFields { |
||||||
|
fn default() -> Self { |
||||||
|
Self { |
||||||
|
bit_map: FieldMap::empty(), |
||||||
|
year: None, |
||||||
|
month: None, |
||||||
|
month_code: None, |
||||||
|
day: None, |
||||||
|
hour: 0, |
||||||
|
minute: 0, |
||||||
|
second: 0, |
||||||
|
millisecond: 0, |
||||||
|
microsecond: 0, |
||||||
|
nanosecond: 0, |
||||||
|
offset: None, |
||||||
|
era: None, |
||||||
|
era_year: None, |
||||||
|
time_zone: None, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl TemporalFields { |
||||||
|
pub(crate) const fn year(&self) -> Option<i32> { |
||||||
|
self.year |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) const fn month(&self) -> Option<i32> { |
||||||
|
self.month |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) const fn day(&self) -> Option<i32> { |
||||||
|
self.day |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Update the below.
|
||||||
|
impl TemporalFields { |
||||||
|
/// Flags a field as being required.
|
||||||
|
#[inline] |
||||||
|
pub fn require_field(&mut self, field: &str) { |
||||||
|
match field { |
||||||
|
"year" => self.bit_map.set(FieldMap::YEAR, true), |
||||||
|
"month" => self.bit_map.set(FieldMap::MONTH, true), |
||||||
|
"monthCode" => self.bit_map.set(FieldMap::MONTH_CODE, true), |
||||||
|
"day" => self.bit_map.set(FieldMap::DAY, true), |
||||||
|
"hour" => self.bit_map.set(FieldMap::HOUR, true), |
||||||
|
"minute" => self.bit_map.set(FieldMap::MINUTE, true), |
||||||
|
"second" => self.bit_map.set(FieldMap::SECOND, true), |
||||||
|
"millisecond" => self.bit_map.set(FieldMap::MILLISECOND, true), |
||||||
|
"microsecond" => self.bit_map.set(FieldMap::MICROSECOND, true), |
||||||
|
"nanosecond" => self.bit_map.set(FieldMap::NANOSECOND, true), |
||||||
|
"offset" => self.bit_map.set(FieldMap::OFFSET, true), |
||||||
|
"era" => self.bit_map.set(FieldMap::ERA, true), |
||||||
|
"eraYear" => self.bit_map.set(FieldMap::ERA_YEAR, true), |
||||||
|
"timeZone" => self.bit_map.set(FieldMap::TIME_ZONE, true), |
||||||
|
_ => {} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
/// A generic field setter for `TemporalFields`
|
||||||
|
///
|
||||||
|
/// This method will not run any `JsValue` conversion. `FieldValue` is
|
||||||
|
/// expected to contain a preconverted value.
|
||||||
|
pub fn set_field_value(&mut self, field: &str, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
match field { |
||||||
|
"year" => self.set_year(value)?, |
||||||
|
"month" => self.set_month(value)?, |
||||||
|
"monthCode" => self.set_month_code(value)?, |
||||||
|
"day" => self.set_day(value)?, |
||||||
|
"hour" => self.set_hour(value)?, |
||||||
|
"minute" => self.set_minute(value)?, |
||||||
|
"second" => self.set_second(value)?, |
||||||
|
"millisecond" => self.set_milli(value)?, |
||||||
|
"microsecond" => self.set_micro(value)?, |
||||||
|
"nanosecond" => self.set_nano(value)?, |
||||||
|
"offset" => self.set_offset(value)?, |
||||||
|
"era" => self.set_era(value)?, |
||||||
|
"eraYear" => self.set_era_year(value)?, |
||||||
|
"timeZone" => self.set_time_zone(value)?, |
||||||
|
_ => unreachable!(), |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_year(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(y) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("Year must be an integer.")); |
||||||
|
}; |
||||||
|
self.year = Some(*y); |
||||||
|
self.bit_map.set(FieldMap::YEAR, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_month(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(mo) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("Month must be an integer.")); |
||||||
|
}; |
||||||
|
self.year = Some(*mo); |
||||||
|
self.bit_map.set(FieldMap::MONTH, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_month_code(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::String(mc) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("monthCode must be string.")); |
||||||
|
}; |
||||||
|
self.month_code = |
||||||
|
Some(TinyStr4::from_bytes(mc.as_bytes()).expect("monthCode must be less than 4 chars")); |
||||||
|
self.bit_map.set(FieldMap::MONTH_CODE, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_day(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(d) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("day must be an integer.")); |
||||||
|
}; |
||||||
|
self.day = Some(*d); |
||||||
|
self.bit_map.set(FieldMap::DAY, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_hour(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(h) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("hour must be an integer.")); |
||||||
|
}; |
||||||
|
self.hour = *h; |
||||||
|
self.bit_map.set(FieldMap::HOUR, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_minute(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(min) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("minute must be an integer.")); |
||||||
|
}; |
||||||
|
self.minute = *min; |
||||||
|
self.bit_map.set(FieldMap::MINUTE, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_second(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(sec) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("Second must be an integer.")); |
||||||
|
}; |
||||||
|
self.second = *sec; |
||||||
|
self.bit_map.set(FieldMap::SECOND, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_milli(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(milli) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("Second must be an integer.")); |
||||||
|
}; |
||||||
|
self.millisecond = *milli; |
||||||
|
self.bit_map.set(FieldMap::MILLISECOND, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_micro(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(micro) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("microsecond must be an integer.")); |
||||||
|
}; |
||||||
|
self.microsecond = *micro; |
||||||
|
self.bit_map.set(FieldMap::MICROSECOND, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_nano(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(nano) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("nanosecond must be an integer.")); |
||||||
|
}; |
||||||
|
self.nanosecond = *nano; |
||||||
|
self.bit_map.set(FieldMap::NANOSECOND, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_offset(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::String(offset) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("offset must be string.")); |
||||||
|
}; |
||||||
|
self.offset = Some(offset.to_string()); |
||||||
|
self.bit_map.set(FieldMap::OFFSET, true); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_era(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::String(era) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("era must be string.")); |
||||||
|
}; |
||||||
|
self.era = |
||||||
|
Some(TinyStr16::from_bytes(era.as_bytes()).expect("era should not exceed 16 bytes.")); |
||||||
|
self.bit_map.set(FieldMap::ERA, true); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_era_year(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::Integer(era_year) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("eraYear must be an integer.")); |
||||||
|
}; |
||||||
|
self.era_year = Some(*era_year); |
||||||
|
self.bit_map.set(FieldMap::ERA_YEAR, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn set_time_zone(&mut self, value: &FieldValue) -> TemporalResult<()> { |
||||||
|
let FieldValue::String(tz) = value else { |
||||||
|
return Err(TemporalError::r#type().with_message("tz must be string.")); |
||||||
|
}; |
||||||
|
self.time_zone = Some(tz.to_string()); |
||||||
|
self.bit_map.set(FieldMap::TIME_ZONE, true); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: optimize into iter.
|
||||||
|
impl TemporalFields { |
||||||
|
/// Returns a vector filled with the key-value pairs marked as active.
|
||||||
|
pub fn active_kvs(&self) -> Vec<(String, FieldValue)> { |
||||||
|
let mut result = Vec::default(); |
||||||
|
|
||||||
|
for field in self.bit_map.iter() { |
||||||
|
match field { |
||||||
|
FieldMap::YEAR => result.push(( |
||||||
|
"year".to_owned(), |
||||||
|
self.year.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||||
|
)), |
||||||
|
FieldMap::MONTH => result.push(( |
||||||
|
"month".to_owned(), |
||||||
|
self.month |
||||||
|
.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||||
|
)), |
||||||
|
FieldMap::MONTH_CODE => result.push(( |
||||||
|
"monthCode".to_owned(), |
||||||
|
self.month_code |
||||||
|
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())), |
||||||
|
)), |
||||||
|
FieldMap::DAY => result.push(( |
||||||
|
"day".to_owned(), |
||||||
|
self.day.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||||
|
)), |
||||||
|
FieldMap::HOUR => result.push(("hour".to_owned(), FieldValue::Integer(self.hour))), |
||||||
|
FieldMap::MINUTE => { |
||||||
|
result.push(("minute".to_owned(), FieldValue::Integer(self.minute))); |
||||||
|
} |
||||||
|
FieldMap::SECOND => { |
||||||
|
result.push(("second".to_owned(), FieldValue::Integer(self.second))); |
||||||
|
} |
||||||
|
FieldMap::MILLISECOND => result.push(( |
||||||
|
"millisecond".to_owned(), |
||||||
|
FieldValue::Integer(self.millisecond), |
||||||
|
)), |
||||||
|
FieldMap::MICROSECOND => result.push(( |
||||||
|
"microsecond".to_owned(), |
||||||
|
FieldValue::Integer(self.microsecond), |
||||||
|
)), |
||||||
|
FieldMap::NANOSECOND => result.push(( |
||||||
|
"nanosecond".to_owned(), |
||||||
|
FieldValue::Integer(self.nanosecond), |
||||||
|
)), |
||||||
|
FieldMap::OFFSET => result.push(( |
||||||
|
"offset".to_owned(), |
||||||
|
self.offset |
||||||
|
.clone() |
||||||
|
.map_or(FieldValue::Undefined, FieldValue::String), |
||||||
|
)), |
||||||
|
FieldMap::ERA => result.push(( |
||||||
|
"era".to_owned(), |
||||||
|
self.era |
||||||
|
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())), |
||||||
|
)), |
||||||
|
FieldMap::ERA_YEAR => result.push(( |
||||||
|
"eraYear".to_owned(), |
||||||
|
self.era_year |
||||||
|
.map_or(FieldValue::Undefined, FieldValue::Integer), |
||||||
|
)), |
||||||
|
FieldMap::TIME_ZONE => result.push(( |
||||||
|
"timeZone".to_owned(), |
||||||
|
self.time_zone |
||||||
|
.clone() |
||||||
|
.map_or(FieldValue::Undefined, FieldValue::String), |
||||||
|
)), |
||||||
|
_ => {} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
result |
||||||
|
} |
||||||
|
|
||||||
|
/// Resolve `TemporalFields` month and monthCode fields.
|
||||||
|
pub(crate) fn iso_resolve_month(&mut self) -> TemporalResult<()> { |
||||||
|
if self.month_code.is_none() { |
||||||
|
if self.month.is_some() { |
||||||
|
return Ok(()); |
||||||
|
} |
||||||
|
|
||||||
|
return Err(TemporalError::range() |
||||||
|
.with_message("month and MonthCode values cannot both be undefined.")); |
||||||
|
} |
||||||
|
|
||||||
|
let unresolved_month_code = self |
||||||
|
.month_code |
||||||
|
.as_ref() |
||||||
|
.expect("monthCode must exist at this point."); |
||||||
|
|
||||||
|
let month_code_integer = month_code_to_integer(*unresolved_month_code)?; |
||||||
|
|
||||||
|
let new_month = match self.month { |
||||||
|
Some(month) if month != month_code_integer => { |
||||||
|
return Err( |
||||||
|
TemporalError::range().with_message("month and monthCode cannot be resolved.") |
||||||
|
) |
||||||
|
} |
||||||
|
_ => month_code_integer, |
||||||
|
}; |
||||||
|
|
||||||
|
self.month = Some(new_month); |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn month_code_to_integer(mc: TinyAsciiStr<4>) -> TemporalResult<i32> { |
||||||
|
match mc.as_str() { |
||||||
|
"M01" => Ok(1), |
||||||
|
"M02" => Ok(2), |
||||||
|
"M03" => Ok(3), |
||||||
|
"M04" => Ok(4), |
||||||
|
"M05" => Ok(5), |
||||||
|
"M06" => Ok(6), |
||||||
|
"M07" => Ok(7), |
||||||
|
"M08" => Ok(8), |
||||||
|
"M09" => Ok(9), |
||||||
|
"M10" => Ok(10), |
||||||
|
"M11" => Ok(11), |
||||||
|
"M12" => Ok(12), |
||||||
|
"M13" => Ok(13), |
||||||
|
_ => Err(TemporalError::range().with_message("monthCode is not within the valid values.")), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,341 @@ |
|||||||
|
//! The `ISO` module implements the internal ISO field slots.
|
||||||
|
//!
|
||||||
|
//! The three main types of slots are:
|
||||||
|
//! - `IsoDateTime`
|
||||||
|
//! - `IsoDate`
|
||||||
|
//! - `IsoTime`
|
||||||
|
//!
|
||||||
|
//! An `IsoDate` that represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
|
||||||
|
//! An `IsoTime` that represents the `[[ISOHour]]`, `[[ISOMinute]]`, `[[ISOsecond]]`, `[[ISOmillisecond]]`,
|
||||||
|
//! `[[ISOmicrosecond]]`, and `[[ISOnanosecond]]` internal slots.
|
||||||
|
//! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`.
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
duration::DateDuration, error::TemporalError, options::ArithmeticOverflow, utils, |
||||||
|
TemporalResult, |
||||||
|
}; |
||||||
|
use icu_calendar::{Date as IcuDate, Iso}; |
||||||
|
use num_bigint::BigInt; |
||||||
|
use num_traits::cast::FromPrimitive; |
||||||
|
|
||||||
|
/// `IsoDateTime` is the Temporal internal representation of
|
||||||
|
/// a `DateTime` record
|
||||||
|
#[derive(Debug, Default, Clone, Copy)] |
||||||
|
pub struct IsoDateTime { |
||||||
|
date: IsoDate, |
||||||
|
time: IsoTime, |
||||||
|
} |
||||||
|
|
||||||
|
impl IsoDateTime { |
||||||
|
/// Creates a new `IsoDateTime` without any validaiton.
|
||||||
|
pub(crate) fn new_unchecked(date: IsoDate, time: IsoTime) -> Self { |
||||||
|
Self { date, time } |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns whether the `IsoDateTime` is within valid limits.
|
||||||
|
pub(crate) fn is_within_limits(&self) -> bool { |
||||||
|
let Some(ns) = self.to_utc_epoch_nanoseconds(0f64) else { |
||||||
|
return false; |
||||||
|
}; |
||||||
|
|
||||||
|
let max = BigInt::from(crate::NS_MAX_INSTANT + i128::from(crate::NS_PER_DAY)); |
||||||
|
let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(crate::NS_PER_DAY)); |
||||||
|
|
||||||
|
min < ns && max > ns |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the UTC epoch nanoseconds for this `IsoDateTime`.
|
||||||
|
pub(crate) fn to_utc_epoch_nanoseconds(self, offset: f64) -> Option<BigInt> { |
||||||
|
let day = self.date.to_epoch_days(); |
||||||
|
let time = self.time.to_epoch_ms(); |
||||||
|
let epoch_ms = utils::epoch_days_to_epoch_ms(day, time); |
||||||
|
|
||||||
|
let epoch_nanos = epoch_ms.mul_add( |
||||||
|
1_000_000f64, |
||||||
|
f64::from(self.time.microsecond).mul_add(1_000f64, f64::from(self.time.nanosecond)), |
||||||
|
); |
||||||
|
|
||||||
|
BigInt::from_f64(epoch_nanos - offset) |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn iso_date(&self) -> IsoDate { |
||||||
|
self.date |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn iso_time(&self) -> IsoTime { |
||||||
|
self.time |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ==== `IsoDate` section ====
|
||||||
|
|
||||||
|
// TODO: Figure out `ICU4X` interop / replacement?
|
||||||
|
|
||||||
|
/// A trait for accessing the `IsoDate` across the various Temporal objects
|
||||||
|
pub trait IsoDateSlots { |
||||||
|
/// Returns the target's internal `IsoDate`.
|
||||||
|
fn iso_date(&self) -> IsoDate; |
||||||
|
} |
||||||
|
|
||||||
|
/// `IsoDate` serves as a record for the `[[ISOYear]]`, `[[ISOMonth]]`,
|
||||||
|
/// and `[[ISODay]]` internal fields.
|
||||||
|
///
|
||||||
|
/// These fields are used for the `Temporal.PlainDate` object, the
|
||||||
|
/// `Temporal.YearMonth` object, and the `Temporal.MonthDay` object.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)] |
||||||
|
pub struct IsoDate { |
||||||
|
year: i32, |
||||||
|
month: u8, |
||||||
|
day: u8, |
||||||
|
} |
||||||
|
|
||||||
|
impl IsoDate { |
||||||
|
/// Creates a new `IsoDate` without determining the validity.
|
||||||
|
pub(crate) const fn new_unchecked(year: i32, month: u8, day: u8) -> Self { |
||||||
|
Self { year, month, day } |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn new( |
||||||
|
year: i32, |
||||||
|
month: i32, |
||||||
|
day: i32, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
match overflow { |
||||||
|
ArithmeticOverflow::Constrain => { |
||||||
|
let m = month.clamp(1, 12); |
||||||
|
let days_in_month = utils::iso_days_in_month(year, month); |
||||||
|
let d = day.clamp(1, days_in_month); |
||||||
|
Ok(Self::new_unchecked(year, m as u8, d as u8)) |
||||||
|
} |
||||||
|
ArithmeticOverflow::Reject => { |
||||||
|
if !is_valid_date(year, month, day) { |
||||||
|
return Err(TemporalError::range().with_message("not a valid ISO date.")); |
||||||
|
} |
||||||
|
// NOTE: Values have been verified to be in a u8 range.
|
||||||
|
Ok(Self::new_unchecked(year, month as u8, day as u8)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a balanced `IsoDate`
|
||||||
|
///
|
||||||
|
/// Equivalent to `BalanceISODate`.
|
||||||
|
fn balance(year: i32, month: i32, day: i32) -> Self { |
||||||
|
let epoch_days = iso_date_to_epoch_days(year, month - 1, day); |
||||||
|
let ms = utils::epoch_days_to_epoch_ms(epoch_days, 0f64); |
||||||
|
Self::new_unchecked( |
||||||
|
utils::epoch_time_to_epoch_year(ms), |
||||||
|
utils::epoch_time_to_month_in_year(ms) + 1, |
||||||
|
utils::epoch_time_to_date(ms), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the year field
|
||||||
|
pub(crate) const fn year(self) -> i32 { |
||||||
|
self.year |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the month field
|
||||||
|
pub(crate) const fn month(self) -> u8 { |
||||||
|
self.month |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the day field
|
||||||
|
pub(crate) const fn day(self) -> u8 { |
||||||
|
self.day |
||||||
|
} |
||||||
|
|
||||||
|
/// Functionally the same as Date's abstract operation `MakeDay`
|
||||||
|
///
|
||||||
|
/// Equivalent to `IsoDateToEpochDays`
|
||||||
|
pub(crate) fn to_epoch_days(self) -> i32 { |
||||||
|
iso_date_to_epoch_days(self.year, self.month.into(), self.day.into()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns if the current `IsoDate` is valid.
|
||||||
|
pub(crate) fn is_valid(self) -> bool { |
||||||
|
is_valid_date(self.year, self.month.into(), self.day.into()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the resulting `IsoDate` from adding a provided `Duration` to this `IsoDate`
|
||||||
|
pub(crate) fn add_iso_date( |
||||||
|
self, |
||||||
|
duration: &DateDuration, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
// 1. Assert: year, month, day, years, months, weeks, and days are integers.
|
||||||
|
// 2. Assert: overflow is either "constrain" or "reject".
|
||||||
|
// 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months).
|
||||||
|
let mut intermediate_year = self.year + duration.years() as i32; |
||||||
|
let mut intermediate_month = i32::from(self.month) + duration.months() as i32; |
||||||
|
|
||||||
|
intermediate_year += (intermediate_month - 1) / 12; |
||||||
|
intermediate_month = (intermediate_month - 1) % 12 + 1; |
||||||
|
|
||||||
|
// 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow).
|
||||||
|
let intermediate = Self::new( |
||||||
|
intermediate_year, |
||||||
|
intermediate_month, |
||||||
|
i32::from(self.day), |
||||||
|
overflow, |
||||||
|
)?; |
||||||
|
|
||||||
|
// 5. Set days to days + 7 × weeks.
|
||||||
|
// 6. Let d be intermediate.[[Day]] + days.
|
||||||
|
let additional_days = duration.days() as i32 + (duration.weeks() as i32 * 7); |
||||||
|
let d = i32::from(intermediate.day) + additional_days; |
||||||
|
|
||||||
|
// 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d).
|
||||||
|
Ok(Self::balance( |
||||||
|
intermediate.year, |
||||||
|
intermediate.month.into(), |
||||||
|
d, |
||||||
|
)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl IsoDate { |
||||||
|
/// Creates `[[ISOYear]]`, `[[isoMonth]]`, `[[isoDay]]` fields from `ICU4X`'s `Date<Iso>` struct.
|
||||||
|
pub(crate) fn as_icu4x(self) -> TemporalResult<IcuDate<Iso>> { |
||||||
|
IcuDate::try_new_iso_date(self.year, self.month, self.day) |
||||||
|
.map_err(|e| TemporalError::range().with_message(e.to_string())) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ==== `IsoTime` section ====
|
||||||
|
|
||||||
|
/// An `IsoTime` record that contains `Temporal`'s
|
||||||
|
/// time slots.
|
||||||
|
#[derive(Debug, Default, Clone, Copy)] |
||||||
|
pub struct IsoTime { |
||||||
|
hour: i32, // 0..=23
|
||||||
|
minute: i32, // 0..=59
|
||||||
|
second: i32, // 0..=59
|
||||||
|
millisecond: i32, // 0..=999
|
||||||
|
microsecond: i32, // 0..=999
|
||||||
|
nanosecond: i32, // 0..=999
|
||||||
|
} |
||||||
|
|
||||||
|
impl IsoTime { |
||||||
|
/// Creates a new `IsoTime` without any validation.
|
||||||
|
pub(crate) fn new_unchecked( |
||||||
|
hour: i32, |
||||||
|
minute: i32, |
||||||
|
second: i32, |
||||||
|
millisecond: i32, |
||||||
|
microsecond: i32, |
||||||
|
nanosecond: i32, |
||||||
|
) -> Self { |
||||||
|
Self { |
||||||
|
hour, |
||||||
|
minute, |
||||||
|
second, |
||||||
|
millisecond, |
||||||
|
microsecond, |
||||||
|
nanosecond, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Creates a new regulated `IsoTime`.
|
||||||
|
pub fn new( |
||||||
|
hour: i32, |
||||||
|
minute: i32, |
||||||
|
second: i32, |
||||||
|
millisecond: i32, |
||||||
|
microsecond: i32, |
||||||
|
nanosecond: i32, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
) -> TemporalResult<IsoTime> { |
||||||
|
match overflow { |
||||||
|
ArithmeticOverflow::Constrain => { |
||||||
|
let h = hour.clamp(0, 23); |
||||||
|
let min = minute.clamp(0, 59); |
||||||
|
let sec = second.clamp(0, 59); |
||||||
|
let milli = millisecond.clamp(0, 999); |
||||||
|
let micro = microsecond.clamp(0, 999); |
||||||
|
let nano = nanosecond.clamp(0, 999); |
||||||
|
Ok(Self::new_unchecked(h, min, sec, milli, micro, nano)) |
||||||
|
} |
||||||
|
ArithmeticOverflow::Reject => { |
||||||
|
// TODO: Invert structure validation and update fields to u16.
|
||||||
|
let time = |
||||||
|
Self::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond); |
||||||
|
if !time.is_valid() { |
||||||
|
return Err(TemporalError::range().with_message("IsoTime is not valid")); |
||||||
|
} |
||||||
|
Ok(time) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns an `IsoTime` set to 12:00:00
|
||||||
|
pub(crate) const fn noon() -> Self { |
||||||
|
Self { |
||||||
|
hour: 12, |
||||||
|
minute: 0, |
||||||
|
second: 0, |
||||||
|
millisecond: 0, |
||||||
|
microsecond: 0, |
||||||
|
nanosecond: 0, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Checks if the time is a valid `IsoTime`
|
||||||
|
pub(crate) fn is_valid(&self) -> bool { |
||||||
|
if !(0..=23).contains(&self.hour) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
let min_sec = 0..=59; |
||||||
|
if !min_sec.contains(&self.minute) || !min_sec.contains(&self.second) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
let sub_second = 0..=999; |
||||||
|
sub_second.contains(&self.millisecond) |
||||||
|
&& sub_second.contains(&self.microsecond) |
||||||
|
&& sub_second.contains(&self.nanosecond) |
||||||
|
} |
||||||
|
|
||||||
|
/// `IsoTimeToEpochMs`
|
||||||
|
///
|
||||||
|
/// Note: This method is library specific and not in spec
|
||||||
|
///
|
||||||
|
/// Functionally the same as Date's `MakeTime`
|
||||||
|
pub(crate) fn to_epoch_ms(self) -> f64 { |
||||||
|
f64::from(self.hour).mul_add( |
||||||
|
utils::MS_PER_HOUR, |
||||||
|
f64::from(self.minute) * utils::MS_PER_MINUTE, |
||||||
|
) + f64::from(self.second).mul_add(1000f64, f64::from(self.millisecond)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ==== `IsoDate` specific utiltiy functions ====
|
||||||
|
|
||||||
|
/// Returns the Epoch days based off the given year, month, and day.
|
||||||
|
#[inline] |
||||||
|
fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 { |
||||||
|
// 1. Let resolvedYear be year + floor(month / 12).
|
||||||
|
let resolved_year = year + (f64::from(month) / 12_f64).floor() as i32; |
||||||
|
// 2. Let resolvedMonth be month modulo 12.
|
||||||
|
let resolved_month = month % 12; |
||||||
|
|
||||||
|
// 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1.
|
||||||
|
let year_t = utils::epoch_time_for_year(resolved_year); |
||||||
|
let month_t = utils::epoch_time_for_month_given_year(resolved_month, resolved_year); |
||||||
|
|
||||||
|
// 4. Return EpochTimeToDayNumber(t) + date - 1.
|
||||||
|
utils::epoch_time_to_day_number(year_t + month_t) + day - 1 |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
// Determines if the month and day are valid for the given year.
|
||||||
|
fn is_valid_date(year: i32, month: i32, day: i32) -> bool { |
||||||
|
if !(1..=12).contains(&month) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
let days_in_month = utils::iso_days_in_month(year, month); |
||||||
|
(1..=days_in_month).contains(&day) |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
//! Boa's `boa_temporal` crate is intended to serve as an engine agnostic
|
||||||
|
//! implementation the ECMAScript's Temporal builtin and algorithm.
|
||||||
|
#![doc = include_str!("../../ABOUT.md")] |
||||||
|
#![doc(
|
||||||
|
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg", |
||||||
|
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg" |
||||||
|
)] |
||||||
|
#![cfg_attr(not(test), forbid(clippy::unwrap_used))] |
||||||
|
#![allow(
|
||||||
|
// Currently throws a false positive regarding dependencies that are only used in benchmarks.
|
||||||
|
unused_crate_dependencies, |
||||||
|
clippy::module_name_repetitions, |
||||||
|
clippy::redundant_pub_crate, |
||||||
|
clippy::too_many_lines, |
||||||
|
clippy::cognitive_complexity, |
||||||
|
clippy::missing_errors_doc, |
||||||
|
clippy::let_unit_value, |
||||||
|
clippy::option_if_let_else, |
||||||
|
|
||||||
|
// It may be worth to look if we can fix the issues highlighted by these lints.
|
||||||
|
clippy::cast_possible_truncation, |
||||||
|
clippy::cast_sign_loss, |
||||||
|
clippy::cast_precision_loss, |
||||||
|
clippy::cast_possible_wrap, |
||||||
|
|
||||||
|
// Add temporarily - Needs addressing
|
||||||
|
clippy::missing_panics_doc, |
||||||
|
)] |
||||||
|
|
||||||
|
pub mod calendar; |
||||||
|
pub mod date; |
||||||
|
pub mod datetime; |
||||||
|
pub mod duration; |
||||||
|
pub mod error; |
||||||
|
pub mod fields; |
||||||
|
pub mod iso; |
||||||
|
pub mod month_day; |
||||||
|
pub mod options; |
||||||
|
pub mod time; |
||||||
|
pub(crate) mod utils; |
||||||
|
pub mod year_month; |
||||||
|
pub mod zoneddatetime; |
||||||
|
|
||||||
|
// TODO: evaluate positives and negatives of using tinystr.
|
||||||
|
// Re-exporting tinystr as a convenience, as it is currently tied into the API.
|
||||||
|
pub use tinystr::TinyAsciiStr; |
||||||
|
|
||||||
|
pub use error::TemporalError; |
||||||
|
|
||||||
|
/// The `Temporal` result type
|
||||||
|
pub type TemporalResult<T> = Result<T, TemporalError>; |
||||||
|
|
||||||
|
// Relevant numeric constants
|
||||||
|
/// Nanoseconds per day constant: 8.64e+13
|
||||||
|
pub(crate) const NS_PER_DAY: i64 = 86_400_000_000_000; |
||||||
|
/// Milliseconds per day constant: 8.64e+7
|
||||||
|
pub(crate) const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000; |
||||||
|
/// Max Instant nanosecond constant
|
||||||
|
pub(crate) const NS_MAX_INSTANT: i128 = NS_PER_DAY as i128 * 100_000_000i128; |
||||||
|
/// Min Instant nanosecond constant
|
||||||
|
pub(crate) const NS_MIN_INSTANT: i128 = -NS_MAX_INSTANT; |
@ -0,0 +1,51 @@ |
|||||||
|
//! `MonthDay`
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
calendar::CalendarSlot, |
||||||
|
iso::{IsoDate, IsoDateSlots}, |
||||||
|
options::ArithmeticOverflow, |
||||||
|
TemporalResult, |
||||||
|
}; |
||||||
|
|
||||||
|
/// The `MonthDay` struct
|
||||||
|
#[derive(Debug, Default, Clone)] |
||||||
|
pub struct MonthDay { |
||||||
|
iso: IsoDate, |
||||||
|
calendar: CalendarSlot, |
||||||
|
} |
||||||
|
|
||||||
|
impl MonthDay { |
||||||
|
/// Creates a new unchecked `MonthDay`
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self { |
||||||
|
Self { iso, calendar } |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
/// Creates a new valid `MonthDay`.
|
||||||
|
pub fn new( |
||||||
|
month: i32, |
||||||
|
day: i32, |
||||||
|
calendar: CalendarSlot, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
let iso = IsoDate::new(1972, month, day, overflow)?; |
||||||
|
Ok(Self::new_unchecked(iso, calendar)) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns a reference to `MonthDay`'s `CalendarSlot`
|
||||||
|
pub fn calendar(&self) -> &CalendarSlot { |
||||||
|
&self.calendar |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl IsoDateSlots for MonthDay { |
||||||
|
#[inline] |
||||||
|
/// Returns this structs `IsoDate`.
|
||||||
|
fn iso_date(&self) -> IsoDate { |
||||||
|
self.iso |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,413 @@ |
|||||||
|
//! Temporal Options
|
||||||
|
|
||||||
|
use core::{fmt, str::FromStr}; |
||||||
|
|
||||||
|
use crate::TemporalError; |
||||||
|
|
||||||
|
/// The relevant unit that should be used for the operation that
|
||||||
|
/// this option is provided as a value.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
||||||
|
pub enum TemporalUnit { |
||||||
|
/// The `Auto` unit
|
||||||
|
Auto = 0, |
||||||
|
/// The `Nanosecond` unit
|
||||||
|
Nanosecond, |
||||||
|
/// The `Microsecond` unit
|
||||||
|
Microsecond, |
||||||
|
/// The `Millisecond` unit
|
||||||
|
Millisecond, |
||||||
|
/// The `Second` unit
|
||||||
|
Second, |
||||||
|
/// The `Minute` unit
|
||||||
|
Minute, |
||||||
|
/// The `Hour` unit
|
||||||
|
Hour, |
||||||
|
/// The `Day` unit
|
||||||
|
Day, |
||||||
|
/// The `Week` unit
|
||||||
|
Week, |
||||||
|
/// The `Month` unit
|
||||||
|
Month, |
||||||
|
/// The `Year` unit
|
||||||
|
Year, |
||||||
|
} |
||||||
|
|
||||||
|
impl TemporalUnit { |
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns the `MaximumRoundingIncrement` for the current `TemporalUnit`.
|
||||||
|
pub fn to_maximum_rounding_increment(self) -> Option<u16> { |
||||||
|
use TemporalUnit::{ |
||||||
|
Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week, |
||||||
|
Year, |
||||||
|
}; |
||||||
|
// 1. If unit is "year", "month", "week", or "day", then
|
||||||
|
// a. Return undefined.
|
||||||
|
// 2. If unit is "hour", then
|
||||||
|
// a. Return 24.
|
||||||
|
// 3. If unit is "minute" or "second", then
|
||||||
|
// a. Return 60.
|
||||||
|
// 4. Assert: unit is one of "millisecond", "microsecond", or "nanosecond".
|
||||||
|
// 5. Return 1000.
|
||||||
|
match self { |
||||||
|
Year | Month | Week | Day => None, |
||||||
|
Hour => Some(24), |
||||||
|
Minute | Second => Some(60), |
||||||
|
Millisecond | Microsecond | Nanosecond => Some(1000), |
||||||
|
Auto => unreachable!(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// A parsing error for `TemporalUnit`
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub struct ParseTemporalUnitError; |
||||||
|
|
||||||
|
impl fmt::Display for ParseTemporalUnitError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not a valid TemporalUnit") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for TemporalUnit { |
||||||
|
type Err = ParseTemporalUnitError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"auto" => Ok(Self::Auto), |
||||||
|
"year" | "years" => Ok(Self::Year), |
||||||
|
"month" | "months" => Ok(Self::Month), |
||||||
|
"week" | "weeks" => Ok(Self::Week), |
||||||
|
"day" | "days" => Ok(Self::Day), |
||||||
|
"hour" | "hours" => Ok(Self::Hour), |
||||||
|
"minute" | "minutes" => Ok(Self::Minute), |
||||||
|
"second" | "seconds" => Ok(Self::Second), |
||||||
|
"millisecond" | "milliseconds" => Ok(Self::Millisecond), |
||||||
|
"microsecond" | "microseconds" => Ok(Self::Microsecond), |
||||||
|
"nanosecond" | "nanoseconds" => Ok(Self::Nanosecond), |
||||||
|
_ => Err(ParseTemporalUnitError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for TemporalUnit { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Auto => "auto", |
||||||
|
Self::Year => "constrain", |
||||||
|
Self::Month => "month", |
||||||
|
Self::Week => "week", |
||||||
|
Self::Day => "day", |
||||||
|
Self::Hour => "hour", |
||||||
|
Self::Minute => "minute", |
||||||
|
Self::Second => "second", |
||||||
|
Self::Millisecond => "millsecond", |
||||||
|
Self::Microsecond => "microsecond", |
||||||
|
Self::Nanosecond => "nanosecond", |
||||||
|
} |
||||||
|
.fmt(f) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `ArithmeticOverflow` can also be used as an
|
||||||
|
/// assignment overflow and consists of the "constrain"
|
||||||
|
/// and "reject" options.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||||
|
pub enum ArithmeticOverflow { |
||||||
|
/// Constrain option
|
||||||
|
Constrain, |
||||||
|
/// Constrain option
|
||||||
|
Reject, |
||||||
|
} |
||||||
|
|
||||||
|
/// A parsing error for `ArithemeticOverflow`
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub struct ParseArithmeticOverflowError; |
||||||
|
|
||||||
|
impl fmt::Display for ParseArithmeticOverflowError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not a valid overflow value") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for ArithmeticOverflow { |
||||||
|
type Err = ParseArithmeticOverflowError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"constrain" => Ok(Self::Constrain), |
||||||
|
"reject" => Ok(Self::Reject), |
||||||
|
_ => Err(ParseArithmeticOverflowError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for ArithmeticOverflow { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Constrain => "constrain", |
||||||
|
Self::Reject => "reject", |
||||||
|
} |
||||||
|
.fmt(f) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// `Duration` overflow options.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub enum DurationOverflow { |
||||||
|
/// Constrain option
|
||||||
|
Constrain, |
||||||
|
/// Balance option
|
||||||
|
Balance, |
||||||
|
} |
||||||
|
|
||||||
|
/// A parsing error for `DurationOverflow`.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub struct ParseDurationOverflowError; |
||||||
|
|
||||||
|
impl fmt::Display for ParseDurationOverflowError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not a valid duration overflow value") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for DurationOverflow { |
||||||
|
type Err = ParseDurationOverflowError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"constrain" => Ok(Self::Constrain), |
||||||
|
"balance" => Ok(Self::Balance), |
||||||
|
_ => Err(ParseDurationOverflowError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for DurationOverflow { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Constrain => "constrain", |
||||||
|
Self::Balance => "balance", |
||||||
|
} |
||||||
|
.fmt(f) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The disambiguation options for an instant.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub enum InstantDisambiguation { |
||||||
|
/// Compatible option
|
||||||
|
Compatible, |
||||||
|
/// Earlier option
|
||||||
|
Earlier, |
||||||
|
/// Later option
|
||||||
|
Later, |
||||||
|
/// Reject option
|
||||||
|
Reject, |
||||||
|
} |
||||||
|
|
||||||
|
/// A parsing error on `InstantDisambiguation` options.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub struct ParseInstantDisambiguationError; |
||||||
|
|
||||||
|
impl fmt::Display for ParseInstantDisambiguationError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not a valid instant disambiguation value") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for InstantDisambiguation { |
||||||
|
type Err = ParseInstantDisambiguationError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"compatible" => Ok(Self::Compatible), |
||||||
|
"earlier" => Ok(Self::Earlier), |
||||||
|
"later" => Ok(Self::Later), |
||||||
|
"reject" => Ok(Self::Reject), |
||||||
|
_ => Err(ParseInstantDisambiguationError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for InstantDisambiguation { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Compatible => "compatible", |
||||||
|
Self::Earlier => "earlier", |
||||||
|
Self::Later => "later", |
||||||
|
Self::Reject => "reject", |
||||||
|
} |
||||||
|
.fmt(f) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Offset disambiguation options.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub enum OffsetDisambiguation { |
||||||
|
/// Use option
|
||||||
|
Use, |
||||||
|
/// Prefer option
|
||||||
|
Prefer, |
||||||
|
/// Ignore option
|
||||||
|
Ignore, |
||||||
|
/// Reject option
|
||||||
|
Reject, |
||||||
|
} |
||||||
|
|
||||||
|
/// A parsing error for `OffsetDisambiguation` parsing.
|
||||||
|
#[derive(Debug, Clone, Copy)] |
||||||
|
pub struct ParseOffsetDisambiguationError; |
||||||
|
|
||||||
|
impl fmt::Display for ParseOffsetDisambiguationError { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.write_str("provided string was not a valid offset disambiguation value") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for OffsetDisambiguation { |
||||||
|
type Err = ParseOffsetDisambiguationError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"use" => Ok(Self::Use), |
||||||
|
"prefer" => Ok(Self::Prefer), |
||||||
|
"ignore" => Ok(Self::Ignore), |
||||||
|
"reject" => Ok(Self::Reject), |
||||||
|
_ => Err(ParseOffsetDisambiguationError), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for OffsetDisambiguation { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Use => "use", |
||||||
|
Self::Prefer => "prefer", |
||||||
|
Self::Ignore => "ignore", |
||||||
|
Self::Reject => "reject", |
||||||
|
} |
||||||
|
.fmt(f) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Figure out what to do with intl's RoundingMode
|
||||||
|
|
||||||
|
/// Declares the specified `RoundingMode` for the operation.
|
||||||
|
#[derive(Debug, Copy, Clone, Default)] |
||||||
|
pub enum TemporalRoundingMode { |
||||||
|
/// Ceil RoundingMode
|
||||||
|
Ceil, |
||||||
|
/// Floor RoundingMode
|
||||||
|
Floor, |
||||||
|
/// Expand RoundingMode
|
||||||
|
Expand, |
||||||
|
/// Truncate RoundingMode
|
||||||
|
Trunc, |
||||||
|
/// HalfCeil RoundingMode
|
||||||
|
HalfCeil, |
||||||
|
/// HalfFloor RoundingMode
|
||||||
|
HalfFloor, |
||||||
|
/// HalfExpand RoundingMode - Default
|
||||||
|
#[default] |
||||||
|
HalfExpand, |
||||||
|
/// HalfTruncate RoundingMode
|
||||||
|
HalfTrunc, |
||||||
|
/// HalfEven RoundingMode
|
||||||
|
HalfEven, |
||||||
|
} |
||||||
|
|
||||||
|
/// The `UnsignedRoundingMode`
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
||||||
|
pub enum TemporalUnsignedRoundingMode { |
||||||
|
/// `Infinity` `RoundingMode`
|
||||||
|
Infinity, |
||||||
|
/// `Zero` `RoundingMode`
|
||||||
|
Zero, |
||||||
|
/// `HalfInfinity` `RoundingMode`
|
||||||
|
HalfInfinity, |
||||||
|
/// `HalfZero` `RoundingMode`
|
||||||
|
HalfZero, |
||||||
|
/// `HalfEven` `RoundingMode`
|
||||||
|
HalfEven, |
||||||
|
} |
||||||
|
|
||||||
|
impl TemporalRoundingMode { |
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Negates the current `RoundingMode`.
|
||||||
|
pub const fn negate(self) -> Self { |
||||||
|
use TemporalRoundingMode::{ |
||||||
|
Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, |
||||||
|
}; |
||||||
|
|
||||||
|
match self { |
||||||
|
Ceil => Self::Floor, |
||||||
|
Floor => Self::Ceil, |
||||||
|
HalfCeil => Self::HalfFloor, |
||||||
|
HalfFloor => Self::HalfCeil, |
||||||
|
Trunc => Self::Trunc, |
||||||
|
Expand => Self::Expand, |
||||||
|
HalfTrunc => Self::HalfTrunc, |
||||||
|
HalfExpand => Self::HalfExpand, |
||||||
|
HalfEven => Self::HalfEven, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns the `UnsignedRoundingMode`
|
||||||
|
pub const fn get_unsigned_round_mode(self, is_negative: bool) -> TemporalUnsignedRoundingMode { |
||||||
|
use TemporalRoundingMode::{ |
||||||
|
Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, |
||||||
|
}; |
||||||
|
|
||||||
|
match self { |
||||||
|
Ceil if !is_negative => TemporalUnsignedRoundingMode::Infinity, |
||||||
|
Ceil => TemporalUnsignedRoundingMode::Zero, |
||||||
|
Floor if !is_negative => TemporalUnsignedRoundingMode::Zero, |
||||||
|
Floor | Trunc | Expand => TemporalUnsignedRoundingMode::Infinity, |
||||||
|
HalfCeil if !is_negative => TemporalUnsignedRoundingMode::HalfInfinity, |
||||||
|
HalfCeil | HalfTrunc => TemporalUnsignedRoundingMode::HalfZero, |
||||||
|
HalfFloor if !is_negative => TemporalUnsignedRoundingMode::HalfZero, |
||||||
|
HalfFloor | HalfExpand => TemporalUnsignedRoundingMode::HalfInfinity, |
||||||
|
HalfEven => TemporalUnsignedRoundingMode::HalfEven, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl FromStr for TemporalRoundingMode { |
||||||
|
type Err = TemporalError; |
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> { |
||||||
|
match s { |
||||||
|
"ceil" => Ok(Self::Ceil), |
||||||
|
"floor" => Ok(Self::Floor), |
||||||
|
"expand" => Ok(Self::Expand), |
||||||
|
"trunc" => Ok(Self::Trunc), |
||||||
|
"halfCeil" => Ok(Self::HalfCeil), |
||||||
|
"halfFloor" => Ok(Self::HalfFloor), |
||||||
|
"halfExpand" => Ok(Self::HalfExpand), |
||||||
|
"halfTrunc" => Ok(Self::HalfTrunc), |
||||||
|
"halfEven" => Ok(Self::HalfEven), |
||||||
|
_ => Err(TemporalError::range().with_message("RoundingMode not an accepted value.")), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl fmt::Display for TemporalRoundingMode { |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
match self { |
||||||
|
Self::Ceil => "ceil", |
||||||
|
Self::Floor => "floor", |
||||||
|
Self::Expand => "expand", |
||||||
|
Self::Trunc => "trunc", |
||||||
|
Self::HalfCeil => "halfCeil", |
||||||
|
Self::HalfFloor => "halfFloor", |
||||||
|
Self::HalfExpand => "halfExpand", |
||||||
|
Self::HalfTrunc => "halfTrunc", |
||||||
|
Self::HalfEven => "halfEven", |
||||||
|
} |
||||||
|
.fmt(f) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
//! Temporal Time Representation.
|
||||||
|
|
||||||
|
use crate::iso::IsoTime; |
||||||
|
|
||||||
|
/// The Temporal `PlainTime` object.
|
||||||
|
#[derive(Debug, Default, Clone, Copy)] |
||||||
|
#[allow(dead_code)] |
||||||
|
pub struct Time { |
||||||
|
iso: IsoTime, |
||||||
|
} |
||||||
|
|
||||||
|
// ==== Private API ====
|
||||||
|
|
||||||
|
impl Time { |
||||||
|
#[allow(dead_code)] |
||||||
|
pub(crate) fn new_unchecked( |
||||||
|
hour: i32, |
||||||
|
minute: i32, |
||||||
|
second: i32, |
||||||
|
millisecond: i32, |
||||||
|
microsecond: i32, |
||||||
|
nanosecond: i32, |
||||||
|
) -> Self { |
||||||
|
Self { |
||||||
|
iso: IsoTime::new_unchecked(hour, minute, second, millisecond, microsecond, nanosecond), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns true if a valid `Time`.
|
||||||
|
#[allow(dead_code)] |
||||||
|
pub(crate) fn is_valid(&self) -> bool { |
||||||
|
self.iso.is_valid() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,284 @@ |
|||||||
|
//! Utility equations for Temporal
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
options::{TemporalRoundingMode, TemporalUnsignedRoundingMode}, |
||||||
|
MS_PER_DAY, |
||||||
|
}; |
||||||
|
|
||||||
|
use std::ops::Mul; |
||||||
|
|
||||||
|
// NOTE: Review the below for optimizations and add ALOT of tests.
|
||||||
|
|
||||||
|
fn apply_unsigned_rounding_mode( |
||||||
|
x: f64, |
||||||
|
r1: f64, |
||||||
|
r2: f64, |
||||||
|
unsigned_rounding_mode: TemporalUnsignedRoundingMode, |
||||||
|
) -> f64 { |
||||||
|
// 1. If x is equal to r1, return r1.
|
||||||
|
if (x - r1).abs() == 0.0 { |
||||||
|
return r1; |
||||||
|
}; |
||||||
|
// 2. Assert: r1 < x < r2.
|
||||||
|
assert!(r1 < x && x < r2); |
||||||
|
// 3. Assert: unsignedRoundingMode is not undefined.
|
||||||
|
|
||||||
|
// 4. If unsignedRoundingMode is zero, return r1.
|
||||||
|
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::Zero { |
||||||
|
return r1; |
||||||
|
}; |
||||||
|
// 5. If unsignedRoundingMode is infinity, return r2.
|
||||||
|
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::Infinity { |
||||||
|
return r2; |
||||||
|
}; |
||||||
|
|
||||||
|
// 6. Let d1 be x – r1.
|
||||||
|
let d1 = x - r1; |
||||||
|
// 7. Let d2 be r2 – x.
|
||||||
|
let d2 = r2 - x; |
||||||
|
// 8. If d1 < d2, return r1.
|
||||||
|
if d1 < d2 { |
||||||
|
return r1; |
||||||
|
} |
||||||
|
// 9. If d2 < d1, return r2.
|
||||||
|
if d2 < d1 { |
||||||
|
return r2; |
||||||
|
} |
||||||
|
// 10. Assert: d1 is equal to d2.
|
||||||
|
assert!((d1 - d2).abs() == 0.0); |
||||||
|
|
||||||
|
// 11. If unsignedRoundingMode is half-zero, return r1.
|
||||||
|
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfZero { |
||||||
|
return r1; |
||||||
|
}; |
||||||
|
// 12. If unsignedRoundingMode is half-infinity, return r2.
|
||||||
|
if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfInfinity { |
||||||
|
return r2; |
||||||
|
}; |
||||||
|
// 13. Assert: unsignedRoundingMode is half-even.
|
||||||
|
assert!(unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfEven); |
||||||
|
// 14. Let cardinality be (r1 / (r2 – r1)) modulo 2.
|
||||||
|
let cardinality = (r1 / (r2 - r1)) % 2.0; |
||||||
|
// 15. If cardinality is 0, return r1.
|
||||||
|
if cardinality == 0.0 { |
||||||
|
return r1; |
||||||
|
} |
||||||
|
// 16. Return r2.
|
||||||
|
r2 |
||||||
|
} |
||||||
|
|
||||||
|
/// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )`
|
||||||
|
pub(crate) fn round_number_to_increment( |
||||||
|
x: f64, |
||||||
|
increment: f64, |
||||||
|
rounding_mode: TemporalRoundingMode, |
||||||
|
) -> f64 { |
||||||
|
// 1. Let quotient be x / increment.
|
||||||
|
let mut quotient = x / increment; |
||||||
|
|
||||||
|
// 2. If quotient < 0, then
|
||||||
|
let is_negative = if quotient < 0_f64 { |
||||||
|
// a. Let isNegative be true.
|
||||||
|
// b. Set quotient to -quotient.
|
||||||
|
quotient = -quotient; |
||||||
|
true |
||||||
|
// 3. Else,
|
||||||
|
} else { |
||||||
|
// a. Let isNegative be false.
|
||||||
|
false |
||||||
|
}; |
||||||
|
|
||||||
|
// 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative).
|
||||||
|
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative); |
||||||
|
// 5. Let r1 be the largest integer such that r1 ≤ quotient.
|
||||||
|
let r1 = quotient.ceil(); |
||||||
|
// 6. Let r2 be the smallest integer such that r2 > quotient.
|
||||||
|
let r2 = quotient.floor(); |
||||||
|
// 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
|
||||||
|
let mut rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); |
||||||
|
// 8. If isNegative is true, set rounded to -rounded.
|
||||||
|
if is_negative { |
||||||
|
rounded = -rounded; |
||||||
|
}; |
||||||
|
// 9. Return rounded × increment.
|
||||||
|
rounded * increment |
||||||
|
} |
||||||
|
|
||||||
|
// ==== Begin Date Equations ====
|
||||||
|
|
||||||
|
pub(crate) const MS_PER_HOUR: f64 = 3_600_000f64; |
||||||
|
pub(crate) const MS_PER_MINUTE: f64 = 60_000f64; |
||||||
|
|
||||||
|
/// `EpochDaysToEpochMS`
|
||||||
|
///
|
||||||
|
/// Functionally the same as Date's abstract operation `MakeDate`
|
||||||
|
pub(crate) fn epoch_days_to_epoch_ms(day: i32, time: f64) -> f64 { |
||||||
|
f64::from(day).mul_add(f64::from(MS_PER_DAY), time).floor() |
||||||
|
} |
||||||
|
|
||||||
|
/// `EpochTimeToDayNumber`
|
||||||
|
///
|
||||||
|
/// This equation is the equivalent to `ECMAScript`'s `Date(t)`
|
||||||
|
pub(crate) fn epoch_time_to_day_number(t: f64) -> i32 { |
||||||
|
(t / f64::from(MS_PER_DAY)).floor() as i32 |
||||||
|
} |
||||||
|
|
||||||
|
/// Mathematically determine the days in a year.
|
||||||
|
pub(crate) fn mathematical_days_in_year(y: i32) -> i32 { |
||||||
|
if y % 4 != 0 { |
||||||
|
365 |
||||||
|
} else if y % 4 == 0 && y % 100 != 0 { |
||||||
|
366 |
||||||
|
} else if y % 100 == 0 && y % 400 != 0 { |
||||||
|
365 |
||||||
|
} else { |
||||||
|
// Assert that y is divisble by 400 to ensure we are returning the correct result.
|
||||||
|
assert_eq!(y % 400, 0); |
||||||
|
366 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the epoch day number for a given year.
|
||||||
|
pub(crate) fn epoch_day_number_for_year(y: f64) -> f64 { |
||||||
|
365.0f64.mul_add(y - 1970.0, ((y - 1969.0) / 4.0).floor()) - ((y - 1901.0) / 100.0).floor() |
||||||
|
+ ((y - 1601.0) / 400.0).floor() |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn epoch_time_for_year(y: i32) -> f64 { |
||||||
|
f64::from(MS_PER_DAY) * epoch_day_number_for_year(f64::from(y)) |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn epoch_time_to_epoch_year(t: f64) -> i32 { |
||||||
|
// roughly calculate the largest possible year given the time t,
|
||||||
|
// then check and refine the year.
|
||||||
|
let day_count = epoch_time_to_day_number(t); |
||||||
|
let mut year = (day_count / 365) + 1970; |
||||||
|
loop { |
||||||
|
if epoch_time_for_year(year) <= t { |
||||||
|
break; |
||||||
|
} |
||||||
|
year -= 1; |
||||||
|
} |
||||||
|
|
||||||
|
year |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns either 1 (true) or 0 (false)
|
||||||
|
pub(crate) fn mathematical_in_leap_year(t: f64) -> i32 { |
||||||
|
mathematical_days_in_year(epoch_time_to_epoch_year(t)) - 365 |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn epoch_time_to_month_in_year(t: f64) -> u8 { |
||||||
|
const DAYS: [i32; 11] = [30, 58, 89, 120, 150, 181, 212, 242, 272, 303, 333]; |
||||||
|
const LEAP_DAYS: [i32; 11] = [30, 59, 90, 121, 151, 182, 213, 242, 272, 303, 334]; |
||||||
|
|
||||||
|
let in_leap_year = mathematical_in_leap_year(t) == 1; |
||||||
|
let day = epoch_time_to_day_in_year(t); |
||||||
|
|
||||||
|
let result = if in_leap_year { |
||||||
|
LEAP_DAYS.binary_search(&day) |
||||||
|
} else { |
||||||
|
DAYS.binary_search(&day) |
||||||
|
}; |
||||||
|
|
||||||
|
match result { |
||||||
|
Ok(i) | Err(i) => i as u8, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn epoch_time_for_month_given_year(m: i32, y: i32) -> f64 { |
||||||
|
let leap_day = mathematical_days_in_year(y) - 365; |
||||||
|
|
||||||
|
let days = match m { |
||||||
|
0 => 1, |
||||||
|
1 => 31, |
||||||
|
2 => 59 + leap_day, |
||||||
|
3 => 90 + leap_day, |
||||||
|
4 => 121 + leap_day, |
||||||
|
5 => 151 + leap_day, |
||||||
|
6 => 182 + leap_day, |
||||||
|
7 => 213 + leap_day, |
||||||
|
8 => 243 + leap_day, |
||||||
|
9 => 273 + leap_day, |
||||||
|
10 => 304 + leap_day, |
||||||
|
11 => 334 + leap_day, |
||||||
|
_ => unreachable!(), |
||||||
|
}; |
||||||
|
|
||||||
|
f64::from(MS_PER_DAY).mul(f64::from(days)) |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn epoch_time_to_date(t: f64) -> u8 { |
||||||
|
const OFFSETS: [i16; 12] = [ |
||||||
|
1, -30, -58, -89, -119, -150, -180, -211, -242, -272, -303, -333, |
||||||
|
]; |
||||||
|
let day_in_year = epoch_time_to_day_in_year(t); |
||||||
|
let in_leap_year = mathematical_in_leap_year(t); |
||||||
|
let month = epoch_time_to_month_in_year(t); |
||||||
|
|
||||||
|
// Cast from i32 to usize should be safe as the return must be 0-11
|
||||||
|
let mut date = day_in_year + i32::from(OFFSETS[month as usize]); |
||||||
|
|
||||||
|
if month >= 2 { |
||||||
|
date -= in_leap_year; |
||||||
|
} |
||||||
|
|
||||||
|
// This return of date should be < 31.
|
||||||
|
date as u8 |
||||||
|
} |
||||||
|
|
||||||
|
pub(crate) fn epoch_time_to_day_in_year(t: f64) -> i32 { |
||||||
|
epoch_time_to_day_number(t) |
||||||
|
- (epoch_day_number_for_year(f64::from(epoch_time_to_epoch_year(t))) as i32) |
||||||
|
} |
||||||
|
|
||||||
|
// EpochTimeTOWeekDay -> REMOVED
|
||||||
|
|
||||||
|
// ==== End Date Equations ====
|
||||||
|
|
||||||
|
// ==== Begin Calendar Equations ====
|
||||||
|
|
||||||
|
// NOTE: below was the iso methods in temporal::calendar -> Need to be reassessed.
|
||||||
|
|
||||||
|
/// 12.2.31 `ISODaysInMonth ( year, month )`
|
||||||
|
pub(crate) fn iso_days_in_month(year: i32, month: i32) -> i32 { |
||||||
|
match month { |
||||||
|
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, |
||||||
|
4 | 6 | 9 | 11 => 30, |
||||||
|
2 => 28 + mathematical_in_leap_year(epoch_time_for_year(year)), |
||||||
|
_ => unreachable!("an invalid month value is an implementation error."), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// The below calendar abstract equations/utilities were removed for being unused.
|
||||||
|
// 12.2.32 `ToISOWeekOfYear ( year, month, day )`
|
||||||
|
// 12.2.33 `ISOMonthCode ( month )`
|
||||||
|
// 12.2.39 `ToISODayOfYear ( year, month, day )`
|
||||||
|
// 12.2.40 `ToISODayOfWeek ( year, month, day )`
|
||||||
|
|
||||||
|
// ==== End Calendar Equations ====
|
||||||
|
|
||||||
|
// ==== Tests =====
|
||||||
|
|
||||||
|
// TODO(nekevss): Add way more to the below.
|
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use super::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn time_to_month() { |
||||||
|
let oct_2023 = 1_696_459_917_000_f64; |
||||||
|
let mar_1_2020 = 1_583_020_800_000_f64; |
||||||
|
let feb_29_2020 = 1_582_934_400_000_f64; |
||||||
|
let mar_1_2021 = 1_614_556_800_000_f64; |
||||||
|
|
||||||
|
assert_eq!(epoch_time_to_month_in_year(oct_2023), 9); |
||||||
|
assert_eq!(epoch_time_to_month_in_year(mar_1_2020), 2); |
||||||
|
assert_eq!(mathematical_in_leap_year(mar_1_2020), 1); |
||||||
|
assert_eq!(epoch_time_to_month_in_year(feb_29_2020), 1); |
||||||
|
assert_eq!(mathematical_in_leap_year(feb_29_2020), 1); |
||||||
|
assert_eq!(epoch_time_to_month_in_year(mar_1_2021), 2); |
||||||
|
assert_eq!(mathematical_in_leap_year(mar_1_2021), 0); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
//! `YearMonth`
|
||||||
|
|
||||||
|
use crate::{ |
||||||
|
calendar::CalendarSlot, |
||||||
|
iso::{IsoDate, IsoDateSlots}, |
||||||
|
options::ArithmeticOverflow, |
||||||
|
TemporalResult, |
||||||
|
}; |
||||||
|
|
||||||
|
/// The `YearMonth` struct
|
||||||
|
#[derive(Debug, Default, Clone)] |
||||||
|
pub struct YearMonth { |
||||||
|
iso: IsoDate, |
||||||
|
calendar: CalendarSlot, |
||||||
|
} |
||||||
|
|
||||||
|
impl YearMonth { |
||||||
|
/// Creates an unvalidated `YearMonth`.
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot) -> Self { |
||||||
|
Self { iso, calendar } |
||||||
|
} |
||||||
|
|
||||||
|
/// Creates a new valid `YearMonth`.
|
||||||
|
#[inline] |
||||||
|
pub fn new( |
||||||
|
year: i32, |
||||||
|
month: i32, |
||||||
|
reference_day: Option<i32>, |
||||||
|
calendar: CalendarSlot, |
||||||
|
overflow: ArithmeticOverflow, |
||||||
|
) -> TemporalResult<Self> { |
||||||
|
let day = reference_day.unwrap_or(1); |
||||||
|
let iso = IsoDate::new(year, month, day, overflow)?; |
||||||
|
Ok(Self::new_unchecked(iso, calendar)) |
||||||
|
} |
||||||
|
|
||||||
|
#[inline] |
||||||
|
#[must_use] |
||||||
|
/// Returns a reference to `YearMonth`'s `CalendarSlot`
|
||||||
|
pub fn calendar(&self) -> &CalendarSlot { |
||||||
|
&self.calendar |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl IsoDateSlots for YearMonth { |
||||||
|
#[inline] |
||||||
|
/// Returns this `YearMonth`'s `IsoDate`
|
||||||
|
fn iso_date(&self) -> IsoDate { |
||||||
|
self.iso |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue