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