Browse Source

Migrate to `temporal_rs` crate (#3694)

* Migrate to temporal_rs crate

* Disable Calendar.prototype.dateUntil
pull/3692/head
Kevin 9 months ago committed by GitHub
parent
commit
5ce93b2086
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 28
      Cargo.lock
  2. 1
      Cargo.toml
  3. 3
      core/engine/Cargo.toml
  4. 28
      core/engine/src/builtins/temporal/calendar/mod.rs
  5. 14
      core/engine/src/builtins/temporal/calendar/object.rs
  6. 250
      core/engine/src/builtins/temporal/duration/mod.rs
  7. 2
      core/engine/src/builtins/temporal/error.rs
  8. 2
      core/engine/src/builtins/temporal/fields.rs
  9. 2
      core/engine/src/builtins/temporal/instant/mod.rs
  10. 60
      core/engine/src/builtins/temporal/mod.rs
  11. 2
      core/engine/src/builtins/temporal/options.rs
  12. 6
      core/engine/src/builtins/temporal/plain_date/mod.rs
  13. 2
      core/engine/src/builtins/temporal/plain_date_time/mod.rs
  14. 4
      core/engine/src/builtins/temporal/plain_month_day/mod.rs
  15. 2
      core/engine/src/builtins/temporal/plain_time/mod.rs
  16. 4
      core/engine/src/builtins/temporal/plain_year_month/mod.rs
  17. 4
      core/engine/src/builtins/temporal/time_zone/custom.rs
  18. 4
      core/engine/src/builtins/temporal/time_zone/mod.rs
  19. 2
      core/engine/src/builtins/temporal/zoned_date_time/mod.rs
  20. 33
      core/temporal/ABOUT.md
  21. 23
      core/temporal/Cargo.toml
  22. 11
      core/temporal/README.md
  23. 1011
      core/temporal/src/components/calendar.rs
  24. 433
      core/temporal/src/components/date.rs
  25. 447
      core/temporal/src/components/datetime.rs
  26. 1022
      core/temporal/src/components/duration.rs
  27. 486
      core/temporal/src/components/duration/date.rs
  28. 596
      core/temporal/src/components/duration/time.rs
  29. 318
      core/temporal/src/components/instant.rs
  30. 46
      core/temporal/src/components/mod.rs
  31. 92
      core/temporal/src/components/month_day.rs
  32. 274
      core/temporal/src/components/time.rs
  33. 118
      core/temporal/src/components/tz.rs
  34. 96
      core/temporal/src/components/year_month.rs
  35. 234
      core/temporal/src/components/zoneddatetime.rs
  36. 121
      core/temporal/src/error.rs
  37. 640
      core/temporal/src/fields.rs
  38. 654
      core/temporal/src/iso.rs
  39. 72
      core/temporal/src/lib.rs
  40. 437
      core/temporal/src/options.rs
  41. 180
      core/temporal/src/parser/annotations.rs
  42. 311
      core/temporal/src/parser/datetime.rs
  43. 247
      core/temporal/src/parser/duration.rs
  44. 136
      core/temporal/src/parser/grammar.rs
  45. 290
      core/temporal/src/parser/mod.rs
  46. 90
      core/temporal/src/parser/nodes.rs
  47. 244
      core/temporal/src/parser/tests.rs
  48. 134
      core/temporal/src/parser/time.rs
  49. 229
      core/temporal/src/parser/time_zone.rs
  50. 341
      core/temporal/src/utils.rs

28
Cargo.lock generated

@ -402,7 +402,6 @@ dependencies = [
"boa_macros", "boa_macros",
"boa_parser", "boa_parser",
"boa_profiler", "boa_profiler",
"boa_temporal",
"bytemuck", "bytemuck",
"cfg-if", "cfg-if",
"criterion", "criterion",
@ -447,6 +446,7 @@ dependencies = [
"static_assertions", "static_assertions",
"sys-locale", "sys-locale",
"tap", "tap",
"temporal_rs",
"textwrap", "textwrap",
"thin-vec", "thin-vec",
"thiserror", "thiserror",
@ -571,18 +571,6 @@ dependencies = [
"textwrap", "textwrap",
] ]
[[package]]
name = "boa_temporal"
version = "0.17.0"
dependencies = [
"bitflags 2.4.2",
"icu_calendar",
"num-bigint",
"num-traits",
"rustc-hash",
"tinystr",
]
[[package]] [[package]]
name = "boa_tester" name = "boa_tester"
version = "0.17.0" version = "0.17.0"
@ -3641,6 +3629,20 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "temporal_rs"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0437239e22d804a1ea7a83e001ae86812afbfc77a5dc12a9dfc579bfa0d9bc"
dependencies = [
"bitflags 2.4.2",
"icu_calendar",
"num-bigint",
"num-traits",
"rustc-hash",
"tinystr",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.4.1" version = "1.4.1"

1
Cargo.toml

@ -41,7 +41,6 @@ boa_macros = { version = "~0.17.0", path = "core/macros" }
boa_parser = { version = "~0.17.0", path = "core/parser" } boa_parser = { version = "~0.17.0", path = "core/parser" }
boa_profiler = { version = "~0.17.0", path = "core/profiler" } boa_profiler = { version = "~0.17.0", path = "core/profiler" }
boa_runtime = { version = "~0.17.0", path = "core/runtime" } boa_runtime = { version = "~0.17.0", path = "core/runtime" }
boa_temporal = { version = "~0.17.0", path = "core/temporal" }
# Shared deps # Shared deps
arbitrary = "1" arbitrary = "1"

3
core/engine/Cargo.toml

@ -64,7 +64,6 @@ boa_profiler.workspace = true
boa_macros.workspace = true boa_macros.workspace = true
boa_ast.workspace = true boa_ast.workspace = true
boa_parser.workspace = true boa_parser.workspace = true
boa_temporal = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] } serde = { workspace = true, features = ["derive", "rc"] }
serde_json.workspace = true serde_json.workspace = true
rand = "0.8.5" rand = "0.8.5"
@ -117,6 +116,8 @@ zerofrom = { workspace = true, optional = true }
fixed_decimal = { workspace = true, features = ["ryu", "experimental"], optional = true } fixed_decimal = { workspace = true, features = ["ryu", "experimental"], optional = true }
tinystr = { workspace = true, optional = true } tinystr = { workspace = true, optional = true }
# temporal deps
temporal_rs = "0.0.1"
[target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies] [target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies]
web-time = { version = "1.0.0", optional = true } web-time = { version = "1.0.0", optional = true }

28
core/engine/src/builtins/temporal/calendar/mod.rs

@ -3,9 +3,9 @@
use std::str::FromStr; use std::str::FromStr;
use super::{ use super::{
create_temporal_date, create_temporal_duration, create_temporal_month_day, create_temporal_date, create_temporal_month_day, create_temporal_year_month, fields,
create_temporal_year_month, fields, options::TemporalUnitGroup, PlainDate, PlainDateTime, options::TemporalUnitGroup, PlainDate, PlainDateTime, PlainMonthDay, PlainYearMonth,
PlainMonthDay, PlainYearMonth, ZonedDateTime, ZonedDateTime,
}; };
use crate::{ use crate::{
builtins::{ builtins::{
@ -23,7 +23,7 @@ use crate::{
}; };
use boa_gc::{custom_trace, Finalize, Trace}; use boa_gc::{custom_trace, Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{ use temporal_rs::{
components::calendar::{ components::calendar::{
CalendarDateLike, CalendarFieldsType, CalendarProtocol, CalendarSlot, CalendarDateLike, CalendarFieldsType, CalendarProtocol, CalendarSlot,
CALENDAR_PROTOCOL_METHODS, CALENDAR_PROTOCOL_METHODS,
@ -442,9 +442,8 @@ impl Calendar {
let overflow = get_option(&options_obj, utf16!("overflow"), context)? let overflow = get_option(&options_obj, utf16!("overflow"), context)?
.unwrap_or(ArithmeticOverflow::Constrain); .unwrap_or(ArithmeticOverflow::Constrain);
// 8. Let balanceResult be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day"). // 8. Let balanceResult be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]],
duration.balance_time_duration(TemporalUnit::Day)?; // duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").
let result = calendar let result = calendar
.slot .slot
.date_add(&date.inner, &duration, overflow, context)?; .date_add(&date.inner, &duration, overflow, context)?;
@ -457,7 +456,7 @@ impl Calendar {
// 1. Let calendar be the this value. // 1. Let calendar be the this value.
// 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]). // 2. Perform ? RequireInternalSlot(calendar, [[InitializedTemporalCalendar]]).
// 3. Assert: calendar.[[Identifier]] is "iso8601". // 3. Assert: calendar.[[Identifier]] is "iso8601".
let calendar = this let _calendar = this
.as_object() .as_object()
.and_then(JsObject::downcast_ref::<Self>) .and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| { .ok_or_else(|| {
@ -466,16 +465,16 @@ impl Calendar {
})?; })?;
// 4. Set one to ? ToTemporalDate(one). // 4. Set one to ? ToTemporalDate(one).
let one = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?; let _one = temporal::plain_date::to_temporal_date(args.get_or_undefined(0), None, context)?;
// 5. Set two to ? ToTemporalDate(two). // 5. Set two to ? ToTemporalDate(two).
let two = temporal::plain_date::to_temporal_date(args.get_or_undefined(1), None, context)?; let _two = temporal::plain_date::to_temporal_date(args.get_or_undefined(1), None, context)?;
// 6. Set options to ? GetOptionsObject(options). // 6. Set options to ? GetOptionsObject(options).
let options = get_options_object(args.get_or_undefined(2))?; let options = get_options_object(args.get_or_undefined(2))?;
// 7. Let largestUnit be ? GetTemporalUnit(options, "largestUnit", date, "auto"). // 7. Let largestUnit be ? GetTemporalUnit(options, "largestUnit", date, "auto").
// 8. If largestUnit is "auto", set largestUnit to "day". // 8. If largestUnit is "auto", set largestUnit to "day".
let largest_unit = super::options::get_temporal_unit( let _largest_unit = super::options::get_temporal_unit(
&options, &options,
utf16!("largestUnit"), utf16!("largestUnit"),
TemporalUnitGroup::Date, TemporalUnitGroup::Date,
@ -484,11 +483,18 @@ impl Calendar {
)? )?
.unwrap_or(TemporalUnit::Day); .unwrap_or(TemporalUnit::Day);
// TODO: Fix temporal_rs `dateUntil` loop
/*
let result = calendar let result = calendar
.slot .slot
.date_until(&one.inner, &two.inner, largest_unit, context)?; .date_until(&one.inner, &two.inner, largest_unit, context)?;
create_temporal_duration(result, None, context).map(Into::into) create_temporal_duration(result, None, context).map(Into::into)
*/
Err(JsNativeError::range()
.with_message("dateUntil not yet implemented.")
.into())
} }
/// 15.8.2.6 `Temporal.Calendar.prototype.era ( temporalDateLike )` /// 15.8.2.6 `Temporal.Calendar.prototype.era ( temporalDateLike )`

14
core/engine/src/builtins/temporal/calendar/object.rs

@ -14,7 +14,12 @@ use crate::{
}; };
use boa_macros::utf16; use boa_macros::utf16;
use boa_temporal::{ use num_traits::ToPrimitive;
use plain_date::PlainDate;
use plain_date_time::PlainDateTime;
use plain_month_day::PlainMonthDay;
use plain_year_month::PlainYearMonth;
use temporal_rs::{
components::{ components::{
calendar::{CalendarDateLike, CalendarProtocol}, calendar::{CalendarDateLike, CalendarProtocol},
Date, Duration, MonthDay, YearMonth, Date, Duration, MonthDay, YearMonth,
@ -22,11 +27,6 @@ use boa_temporal::{
options::ArithmeticOverflow, options::ArithmeticOverflow,
TemporalError, TemporalFields, TemporalResult, TinyAsciiStr, TemporalError, TemporalFields, TemporalResult, TinyAsciiStr,
}; };
use num_traits::ToPrimitive;
use plain_date::PlainDate;
use plain_date_time::PlainDateTime;
use plain_month_day::PlainMonthDay;
use plain_year_month::PlainYearMonth;
impl CalendarProtocol for JsObject { impl CalendarProtocol for JsObject {
type Date = JsObject<PlainDate>; type Date = JsObject<PlainDate>;
@ -194,7 +194,7 @@ impl CalendarProtocol for JsObject {
&self, &self,
_one: &Date<JsObject>, _one: &Date<JsObject>,
_two: &Date<JsObject>, _two: &Date<JsObject>,
_largest_unit: boa_temporal::options::TemporalUnit, _largest_unit: temporal_rs::options::TemporalUnit,
_context: &mut Context, _context: &mut Context,
) -> TemporalResult<Duration> { ) -> TemporalResult<Duration> {
// TODO // TODO

250
core/engine/src/builtins/temporal/duration/mod.rs

@ -3,7 +3,6 @@
use crate::{ use crate::{
builtins::{ builtins::{
options::{get_option, get_options_object, RoundingMode}, options::{get_option, get_options_object, RoundingMode},
temporal::validate_temporal_rounding_increment,
BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject,
}, },
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
@ -16,12 +15,11 @@ use crate::{
}; };
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{components::Duration as InnerDuration, options::TemporalUnit}; use temporal_rs::{components::Duration as InnerDuration, options::TemporalUnit};
use super::{ use super::{
options::{get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup}, options::{get_temporal_rounding_increment, get_temporal_unit, TemporalUnitGroup},
plain_date::PlainDate, to_integer_if_integral, DateTimeValues,
to_integer_if_integral, DateTimeValues, PlainDateTime,
}; };
#[cfg(test)] #[cfg(test)]
@ -307,16 +305,16 @@ impl Duration {
let inner = &duration.inner; let inner = &duration.inner;
match field { match field {
DateTimeValues::Year => Ok(JsValue::Rational(inner.date().years())), DateTimeValues::Year => Ok(JsValue::Rational(inner.years())),
DateTimeValues::Month => Ok(JsValue::Rational(inner.date().months())), DateTimeValues::Month => Ok(JsValue::Rational(inner.months())),
DateTimeValues::Week => Ok(JsValue::Rational(inner.date().weeks())), DateTimeValues::Week => Ok(JsValue::Rational(inner.weeks())),
DateTimeValues::Day => Ok(JsValue::Rational(inner.date().days())), DateTimeValues::Day => Ok(JsValue::Rational(inner.days())),
DateTimeValues::Hour => Ok(JsValue::Rational(inner.time().hours())), DateTimeValues::Hour => Ok(JsValue::Rational(inner.hours())),
DateTimeValues::Minute => Ok(JsValue::Rational(inner.time().minutes())), DateTimeValues::Minute => Ok(JsValue::Rational(inner.minutes())),
DateTimeValues::Second => Ok(JsValue::Rational(inner.time().seconds())), DateTimeValues::Second => Ok(JsValue::Rational(inner.seconds())),
DateTimeValues::Millisecond => Ok(JsValue::Rational(inner.time().milliseconds())), DateTimeValues::Millisecond => Ok(JsValue::Rational(inner.milliseconds())),
DateTimeValues::Microsecond => Ok(JsValue::Rational(inner.time().microseconds())), DateTimeValues::Microsecond => Ok(JsValue::Rational(inner.microseconds())),
DateTimeValues::Nanosecond => Ok(JsValue::Rational(inner.time().nanoseconds())), DateTimeValues::Nanosecond => Ok(JsValue::Rational(inner.nanoseconds())),
DateTimeValues::MonthCode => unreachable!( DateTimeValues::MonthCode => unreachable!(
"Any other DateTimeValue fields on Duration would be an implementation error." "Any other DateTimeValue fields on Duration would be an implementation error."
), ),
@ -387,7 +385,7 @@ impl Duration {
// 3. Return 𝔽(! DurationSign(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], // 3. Return 𝔽(! DurationSign(duration.[[Years]], duration.[[Months]], duration.[[Weeks]],
// duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], // duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]],
// duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]])). // duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]])).
Ok(duration.inner.duration_sign().into()) Ok(duration.inner.sign().into())
} }
/// 7.3.14 get Temporal.Duration.prototype.blank /// 7.3.14 get Temporal.Duration.prototype.blank
@ -404,14 +402,9 @@ impl Duration {
// 3. Let sign be ! DurationSign(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], // 3. Let sign be ! DurationSign(duration.[[Years]], duration.[[Months]], duration.[[Weeks]],
// duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], // duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]],
// duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). // duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]).
let sign = duration.inner.duration_sign();
// 4. If sign = 0, return true. // 4. If sign = 0, return true.
// 5. Return false. // 5. Return false.
match sign { Ok(duration.inner.is_zero().into())
0 => Ok(true.into()),
_ => Ok(false.into()),
}
} }
} }
@ -441,100 +434,100 @@ impl Duration {
// a. Let years be temporalDurationLike.[[Years]]. // a. Let years be temporalDurationLike.[[Years]].
// 5. Else, // 5. Else,
// a. Let years be duration.[[Years]]. // a. Let years be duration.[[Years]].
let years = if temporal_duration_like.date().years().is_nan() { let years = if temporal_duration_like.years().is_nan() {
duration.inner.date().years() duration.inner.years()
} else { } else {
temporal_duration_like.date().years() temporal_duration_like.years()
}; };
// 6. If temporalDurationLike.[[Months]] is not undefined, then // 6. If temporalDurationLike.[[Months]] is not undefined, then
// a. Let months be temporalDurationLike.[[Months]]. // a. Let months be temporalDurationLike.[[Months]].
// 7. Else, // 7. Else,
// a. Let months be duration.[[Months]]. // a. Let months be duration.[[Months]].
let months = if temporal_duration_like.date().months().is_nan() { let months = if temporal_duration_like.months().is_nan() {
duration.inner.date().months() duration.inner.months()
} else { } else {
temporal_duration_like.date().months() temporal_duration_like.months()
}; };
// 8. If temporalDurationLike.[[Weeks]] is not undefined, then // 8. If temporalDurationLike.[[Weeks]] is not undefined, then
// a. Let weeks be temporalDurationLike.[[Weeks]]. // a. Let weeks be temporalDurationLike.[[Weeks]].
// 9. Else, // 9. Else,
// a. Let weeks be duration.[[Weeks]]. // a. Let weeks be duration.[[Weeks]].
let weeks = if temporal_duration_like.date().weeks().is_nan() { let weeks = if temporal_duration_like.weeks().is_nan() {
duration.inner.date().weeks() duration.inner.weeks()
} else { } else {
temporal_duration_like.date().weeks() temporal_duration_like.weeks()
}; };
// 10. If temporalDurationLike.[[Days]] is not undefined, then // 10. If temporalDurationLike.[[Days]] is not undefined, then
// a. Let days be temporalDurationLike.[[Days]]. // a. Let days be temporalDurationLike.[[Days]].
// 11. Else, // 11. Else,
// a. Let days be duration.[[Days]]. // a. Let days be duration.[[Days]].
let days = if temporal_duration_like.date().days().is_nan() { let days = if temporal_duration_like.days().is_nan() {
duration.inner.date().days() duration.inner.days()
} else { } else {
temporal_duration_like.date().days() temporal_duration_like.days()
}; };
// 12. If temporalDurationLike.[[Hours]] is not undefined, then // 12. If temporalDurationLike.[[Hours]] is not undefined, then
// a. Let hours be temporalDurationLike.[[Hours]]. // a. Let hours be temporalDurationLike.[[Hours]].
// 13. Else, // 13. Else,
// a. Let hours be duration.[[Hours]]. // a. Let hours be duration.[[Hours]].
let hours = if temporal_duration_like.time().hours().is_nan() { let hours = if temporal_duration_like.hours().is_nan() {
duration.inner.time().hours() duration.inner.hours()
} else { } else {
temporal_duration_like.time().hours() temporal_duration_like.hours()
}; };
// 14. If temporalDurationLike.[[Minutes]] is not undefined, then // 14. If temporalDurationLike.[[Minutes]] is not undefined, then
// a. Let minutes be temporalDurationLike.[[Minutes]]. // a. Let minutes be temporalDurationLike.[[Minutes]].
// 15. Else, // 15. Else,
// a. Let minutes be duration.[[Minutes]]. // a. Let minutes be duration.[[Minutes]].
let minutes = if temporal_duration_like.time().minutes().is_nan() { let minutes = if temporal_duration_like.minutes().is_nan() {
duration.inner.time().minutes() duration.inner.minutes()
} else { } else {
temporal_duration_like.time().minutes() temporal_duration_like.minutes()
}; };
// 16. If temporalDurationLike.[[Seconds]] is not undefined, then // 16. If temporalDurationLike.[[Seconds]] is not undefined, then
// a. Let seconds be temporalDurationLike.[[Seconds]]. // a. Let seconds be temporalDurationLike.[[Seconds]].
// 17. Else, // 17. Else,
// a. Let seconds be duration.[[Seconds]]. // a. Let seconds be duration.[[Seconds]].
let seconds = if temporal_duration_like.time().seconds().is_nan() { let seconds = if temporal_duration_like.seconds().is_nan() {
duration.inner.time().seconds() duration.inner.seconds()
} else { } else {
temporal_duration_like.time().seconds() temporal_duration_like.seconds()
}; };
// 18. If temporalDurationLike.[[Milliseconds]] is not undefined, then // 18. If temporalDurationLike.[[Milliseconds]] is not undefined, then
// a. Let milliseconds be temporalDurationLike.[[Milliseconds]]. // a. Let milliseconds be temporalDurationLike.[[Milliseconds]].
// 19. Else, // 19. Else,
// a. Let milliseconds be duration.[[Milliseconds]]. // a. Let milliseconds be duration.[[Milliseconds]].
let milliseconds = if temporal_duration_like.time().milliseconds().is_nan() { let milliseconds = if temporal_duration_like.milliseconds().is_nan() {
duration.inner.time().milliseconds() duration.inner.milliseconds()
} else { } else {
temporal_duration_like.time().milliseconds() temporal_duration_like.milliseconds()
}; };
// 20. If temporalDurationLike.[[Microseconds]] is not undefined, then // 20. If temporalDurationLike.[[Microseconds]] is not undefined, then
// a. Let microseconds be temporalDurationLike.[[Microseconds]]. // a. Let microseconds be temporalDurationLike.[[Microseconds]].
// 21. Else, // 21. Else,
// a. Let microseconds be duration.[[Microseconds]]. // a. Let microseconds be duration.[[Microseconds]].
let microseconds = if temporal_duration_like.time().microseconds().is_nan() { let microseconds = if temporal_duration_like.microseconds().is_nan() {
duration.inner.time().microseconds() duration.inner.microseconds()
} else { } else {
temporal_duration_like.time().microseconds() temporal_duration_like.microseconds()
}; };
// 22. If temporalDurationLike.[[Nanoseconds]] is not undefined, then // 22. If temporalDurationLike.[[Nanoseconds]] is not undefined, then
// a. Let nanoseconds be temporalDurationLike.[[Nanoseconds]]. // a. Let nanoseconds be temporalDurationLike.[[Nanoseconds]].
// 23. Else, // 23. Else,
// a. Let nanoseconds be duration.[[Nanoseconds]]. // a. Let nanoseconds be duration.[[Nanoseconds]].
let nanoseconds = if temporal_duration_like.time().nanoseconds().is_nan() { let nanoseconds = if temporal_duration_like.nanoseconds().is_nan() {
duration.inner.time().nanoseconds() duration.inner.nanoseconds()
} else { } else {
temporal_duration_like.time().nanoseconds() temporal_duration_like.nanoseconds()
}; };
// 24. Return ? CreateTemporalDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). // 24. Return ? CreateTemporalDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
@ -598,7 +591,7 @@ impl Duration {
.into()) .into())
} }
// TODO: Update needed. // TODO: Migrate to `temporal_rs's Duration::round`
/// 7.3.20 `Temporal.Duration.prototype.round ( roundTo )` /// 7.3.20 `Temporal.Duration.prototype.round ( roundTo )`
pub(crate) fn round( pub(crate) fn round(
this: &JsValue, this: &JsValue,
@ -607,7 +600,7 @@ impl Duration {
) -> JsResult<JsValue> { ) -> JsResult<JsValue> {
// 1. Let duration be the this value. // 1. Let duration be the this value.
// 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]). // 2. Perform ? RequireInternalSlot(duration, [[InitializedTemporalDuration]]).
let duration = this let _duration = this
.as_object() .as_object()
.and_then(JsObject::downcast_ref::<Self>) .and_then(JsObject::downcast_ref::<Self>)
.ok_or_else(|| { .ok_or_else(|| {
@ -659,11 +652,11 @@ impl Duration {
// 10. Let relativeToRecord be ? ToRelativeTemporalObject(roundTo). // 10. Let relativeToRecord be ? ToRelativeTemporalObject(roundTo).
// 11. Let zonedRelativeTo be relativeToRecord.[[ZonedRelativeTo]]. // 11. Let zonedRelativeTo be relativeToRecord.[[ZonedRelativeTo]].
// 12. Let plainRelativeTo be relativeToRecord.[[PlainRelativeTo]]. // 12. Let plainRelativeTo be relativeToRecord.[[PlainRelativeTo]].
let (plain_relative_to, zoned_relative_to) = let (_plain_relative_to, _zoned_relative_to) =
super::to_relative_temporal_object(&round_to, context)?; super::to_relative_temporal_object(&round_to, context)?;
// 13. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo). // 13. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo).
let rounding_increment = get_temporal_rounding_increment(&round_to, context)?; let _rounding_increment = get_temporal_rounding_increment(&round_to, context)?;
// 14. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). // 14. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand").
let _rounding_mode = get_option(&round_to, utf16!("roundingMode"), context)? let _rounding_mode = get_option(&round_to, utf16!("roundingMode"), context)?
@ -687,133 +680,7 @@ impl Duration {
.into()); .into());
} }
// 16. If smallestUnit is undefined, then // NOTE:
let smallest_unit = if let Some(unit) = smallest_unit {
unit
} else {
// a. Set smallestUnitPresent to false.
// b. Set smallestUnit to "nanosecond".
TemporalUnit::Nanosecond
};
// 17. Let existingLargestUnit be ! DefaultTemporalLargestUnit(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]]).
let existing_largest_unit = duration.inner.default_temporal_largest_unit();
// 18. Set defaultLargestUnit to ! LargerOfTwoTemporalUnits(defaultLargestUnit, smallestUnit).
let default_largest_unit = core::cmp::max(existing_largest_unit, smallest_unit);
// 19. If largestUnit is undefined, then
let largest_unit = match largest_unit {
// 20. Else if largestUnit is "auto", then
// a. Set largestUnit to defaultLargestUnit.
Some(TemporalUnit::Auto) => default_largest_unit,
Some(u) => u,
None => {
// a. Set largestUnitPresent to false.
// b. Set largestUnit to defaultLargestUnit.
default_largest_unit
}
};
// 22. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception.
if core::cmp::max(largest_unit, smallest_unit) != largest_unit {
return Err(JsNativeError::range()
.with_message("largestUnit must be larger than smallestUnit")
.into());
}
// 23. Let maximum be ! MaximumTemporalDurationRoundingIncrement(smallestUnit).
let maximum = smallest_unit.to_maximum_rounding_increment();
// 24. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false).
if let Some(max) = maximum {
validate_temporal_rounding_increment(rounding_increment.into(), max.into(), false)?;
}
// 25. Let hoursToDaysConversionMayOccur be false.
// 26. If duration.[[Days]] ≠ 0 and zonedRelativeTo is not undefined, set hoursToDaysConversionMayOccur to true.
// 27. Else if abs(duration.[[Hours]]) ≥ 24, set hoursToDaysConversionMayOccur to true.
let conversion_may_occur =
if duration.inner.date().days() != 0.0 && zoned_relative_to.is_some() {
true
} else {
24f64 <= duration.inner.time().hours().abs()
};
// 28. If smallestUnit is "nanosecond" and roundingIncrement = 1, let roundingGranularityIsNoop be true; else let roundingGranularityIsNoop be false.
let is_noop = smallest_unit == TemporalUnit::Nanosecond && rounding_increment == 1;
// 29. If duration.[[Years]] = 0 and duration.[[Months]] = 0 and duration.[[Weeks]] = 0, let calendarUnitsPresent be false; else let calendarUnitsPresent be true.
let calendar_units_present = !(duration.inner.date().years() == 0f64
|| duration.inner.date().months() == 0f64
|| duration.inner.date().weeks() == 0f64);
// 30. If roundingGranularityIsNoop is true, and largestUnit is existingLargestUnit,
// and calendarUnitsPresent is false, and hoursToDaysConversionMayOccur is false,
// and abs(duration.[[Minutes]]) < 60, and abs(duration.[[Seconds]]) < 60,
// and abs(duration.[[Milliseconds]]) < 1000, and abs(duration.[[Microseconds]]) < 1000,
// and abs(duration.[[Nanoseconds]]) < 1000, then
if is_noop
&& largest_unit == existing_largest_unit
&& !calendar_units_present
&& !conversion_may_occur
&& duration.inner.is_time_within_range()
{
// a. NOTE: The above conditions mean that the operation will have no effect: the smallest unit and
// rounding increment will leave the total duration unchanged, and it can be determined without
// calling a calendar or time zone method that no balancing will take place.
// b. Return ! CreateTemporalDuration(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]).
}
// 31. Let precalculatedPlainDateTime be undefined.
// let mut precalc_datetime = None;
// 32. If roundingGranularityIsNoop is false, or largestUnit is "year", or largestUnit is "month",
// or largestUnit is "week", or largestUnit is "day", or calendarUnitsPresent is true, or duration.[[Days]] ≠ 0,
// let plainDateTimeOrRelativeToWillBeUsed be true; else let plainDateTimeOrRelativeToWillBeUsed be false.
let pdt_or_rel_will_be_used = !is_noop
|| largest_unit == TemporalUnit::Year
|| largest_unit == TemporalUnit::Month
|| largest_unit == TemporalUnit::Week
|| largest_unit == TemporalUnit::Day
|| calendar_units_present
|| duration.inner.date().days() != 0f64;
// 33. If zonedRelativeTo is not undefined and plainDateTimeOrRelativeToWillBeUsed is true, then
let (_plain_relative_to, _precalc_pdt) = if zoned_relative_to.is_some()
&& pdt_or_rel_will_be_used
{
// TODO(TimeZone): Implement GetPlainDateTimeFor
// TODO(ZonedDateTime): Implement ZonedDateTime related methods.
return Err(JsNativeError::range()
.with_message("not yet implemented.")
.into());
// a. NOTE: The above conditions mean that the corresponding Temporal.PlainDateTime or Temporal.PlainDate for zonedRelativeTo will be used in one of the operations below.
// b. Let instant be ! CreateTemporalInstant(zonedRelativeTo.[[Nanoseconds]]).
// c. Set precalculatedPlainDateTime to ? GetPlainDateTimeFor(zonedRelativeTo.[[TimeZone]], instant, zonedRelativeTo.[[Calendar]]).
// d. Set plainRelativeTo to ! CreateTemporalDate(precalculatedPlainDateTime.[[ISOYear]], precalculatedPlainDateTime.[[ISOMonth]], precalculatedPlainDateTime.[[ISODay]], zonedRelativeTo.[[Calendar]]).
} else {
// TODO: remove after ZonedDateTime is implemented
let non_zoned: (Option<PlainDate>, Option<PlainDateTime>) = (plain_relative_to, None);
non_zoned
};
// 34. Let unbalanceResult be ? UnbalanceDateDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], largestUnit, plainRelativeTo).
// 35. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]],
// unbalanceResult.[[Weeks]], unbalanceResult.[[Days]], duration.[[Hours]], duration.[[Minutes]],
// duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]],
// roundingIncrement, smallestUnit, roundingMode, plainRelativeTo, zonedRelativeTo, precalculatedPlainDateTime).
// 36. Let roundResult be roundRecord.[[DurationRecord]].
// 37. If zonedRelativeTo is not undefined, then
// a. Set roundResult to ? AdjustRoundedDurationDays(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], roundingIncrement, smallestUnit, roundingMode, zonedRelativeTo, precalculatedPlainDateTime).
// b. Let balanceResult be ? BalanceTimeDurationRelative(roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], largestUnit, zonedRelativeTo, precalculatedPlainDateTime).
// 38. Else,
// a. Let balanceResult be ? BalanceTimeDuration(roundResult.[[Days]], roundResult.[[Hours]], roundResult.[[Minutes]], roundResult.[[Seconds]], roundResult.[[Milliseconds]], roundResult.[[Microseconds]], roundResult.[[Nanoseconds]], largestUnit).
// 39. Let result be ? BalanceDateDurationRelative(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], balanceResult.[[Days]], largestUnit, plainRelativeTo).
// 40. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], result.[[Days]], balanceResult.[[Hours]], balanceResult.[[Minutes]], balanceResult.[[Seconds]], balanceResult.[[Milliseconds]], balanceResult.[[Microseconds]], balanceResult.[[Nanoseconds]]).
// NOTE: Below is currently incorrect: Handling of zonedRelativeTo and precalculatedPlainDateTime is needed.
Err(JsNativeError::range() Err(JsNativeError::range()
.with_message("not yet implemented.") .with_message("not yet implemented.")
.into()) .into())
@ -1099,10 +966,23 @@ pub(crate) fn to_temporal_partial_duration(
result.set_days(f64::from(to_integer_if_integral(&years, context)?)); result.set_days(f64::from(to_integer_if_integral(&years, context)?));
} }
// 24. If years is undefined, and months is undefined, and weeks is undefined, and days is undefined, and hours is undefined, and minutes is undefined, and seconds is undefined, and milliseconds is undefined, and microseconds is undefined, and nanoseconds is undefined, throw a TypeError exception. // TODO: Implement this functionality better in `temporal_rs`.
if result.into_iter().all(f64::is_nan) { // 24. If years is undefined, and months is undefined, and weeks is undefined, and days
// is undefined, and hours is undefined, and minutes is undefined, and seconds is
// undefined, and milliseconds is undefined, and microseconds is undefined, and
// nanoseconds is undefined, throw a TypeError exception.
if result.years().is_nan()
&& result.months().is_nan()
&& result.weeks().is_nan()
&& result.days().is_nan()
&& result.minutes().is_nan()
&& result.seconds().is_nan()
&& result.milliseconds().is_nan()
&& result.microseconds().is_nan()
&& result.nanoseconds().is_nan()
{
return Err(JsNativeError::typ() return Err(JsNativeError::typ()
.with_message("no valid Duration fields on temporalDurationLike.") .with_message("PartialDurationRecord must have a defined field.")
.into()); .into());
} }

2
core/engine/src/builtins/temporal/error.rs

@ -1,4 +1,4 @@
use boa_temporal::error::{ErrorKind, TemporalError}; use temporal_rs::error::{ErrorKind, TemporalError};
use crate::{JsError, JsNativeError}; use crate::{JsError, JsNativeError};

2
core/engine/src/builtins/temporal/fields.rs

@ -9,7 +9,7 @@ use crate::{
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use boa_temporal::fields::{FieldConversion, FieldValue, TemporalFields}; use temporal_rs::fields::{FieldConversion, FieldValue, TemporalFields};
use super::{to_integer_with_truncation, to_positive_integer_with_trunc}; use super::{to_integer_with_truncation, to_positive_integer_with_trunc};

2
core/engine/src/builtins/temporal/instant/mod.rs

@ -20,7 +20,7 @@ use crate::{
}; };
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{ use temporal_rs::{
components::Instant as InnerInstant, components::Instant as InnerInstant,
options::{TemporalRoundingMode, TemporalUnit}, options::{TemporalRoundingMode, TemporalUnit},
}; };

60
core/engine/src/builtins/temporal/mod.rs

@ -38,14 +38,14 @@ use crate::{
Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
}; };
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::NS_PER_DAY; use temporal_rs::NS_PER_DAY;
// TODO: Remove in favor of `boa_temporal` // TODO: Remove in favor of `temporal_rs`
pub(crate) fn ns_max_instant() -> JsBigInt { pub(crate) fn ns_max_instant() -> JsBigInt {
JsBigInt::from(i128::from(NS_PER_DAY) * 100_000_000_i128) JsBigInt::from(i128::from(NS_PER_DAY) * 100_000_000_i128)
} }
// TODO: Remove in favor of `boa_temporal` // TODO: Remove in favor of `temporal_rs`
pub(crate) fn ns_min_instant() -> JsBigInt { pub(crate) fn ns_min_instant() -> JsBigInt {
JsBigInt::from(i128::from(NS_PER_DAY) * -100_000_000_i128) JsBigInt::from(i128::from(NS_PER_DAY) * -100_000_000_i128)
} }
@ -211,11 +211,8 @@ pub(crate) fn _iterator_to_list_of_types(
Ok(values) Ok(values)
} }
/// 13.2 `ISODateToEpochDays ( year, month, date )`
// Note: implemented on IsoDateRecord.
// Abstract Operation 13.3 `EpochDaysToEpochMs` // Abstract Operation 13.3 `EpochDaysToEpochMs`
// Migrated to `boa_temporal` // Migrated to `temporal_rs`
// 13.4 Date Equations // 13.4 Date Equations
// implemented in temporal/date_equations.rs // implemented in temporal/date_equations.rs
@ -238,41 +235,8 @@ pub(crate) fn _iterator_to_list_of_types(
// 13.16 `ToTemporalRoundingIncrement ( normalizedOptions )` // 13.16 `ToTemporalRoundingIncrement ( normalizedOptions )`
// Now implemented in temporal/options.rs // Now implemented in temporal/options.rs
/// 13.17 `ValidateTemporalRoundingIncrement ( increment, dividend, inclusive )` // 13.17 `ValidateTemporalRoundingIncrement ( increment, dividend, inclusive )`
#[inline] // Moved to temporal_rs
pub(crate) fn validate_temporal_rounding_increment(
increment: u64,
dividend: u64,
inclusive: bool,
) -> JsResult<()> {
// 1. If inclusive is true, then
let maximum = if inclusive {
// a. Let maximum be dividend.
dividend
// 2. Else,
} else {
// a. Assert: dividend > 1.
assert!(dividend > 1);
// b. Let maximum be dividend - 1.
dividend - 1
};
// 3. If increment > maximum, throw a RangeError exception.
if increment > maximum {
return Err(JsNativeError::range()
.with_message("increment is exceeds the range of the allowed maximum.")
.into());
}
// 4. If dividend modulo increment ≠ 0, then
if dividend % increment != 0 {
// a. Throw a RangeError exception.
return Err(JsNativeError::range()
.with_message("Temporal rounding increment is not valid.")
.into());
}
// 5. Return unused.
Ok(())
}
/// 13.21 `ToRelativeTemporalObject ( options )` /// 13.21 `ToRelativeTemporalObject ( options )`
pub(crate) fn to_relative_temporal_object( pub(crate) fn to_relative_temporal_object(
@ -294,13 +258,13 @@ pub(crate) fn to_relative_temporal_object(
// Implemented on RoundingMode in builtins/options.rs // Implemented on RoundingMode in builtins/options.rs
// 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )` // 13.27 `ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode )`
// Migrated to `boa_temporal` // Migrated to `temporal_rs`
// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )` // 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )`
// Migrated to `boa_temporal` // Migrated to `temporal_rs`
// 13.29 `RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode )` // 13.29 `RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode )`
// Migrated to `boa_temporal` // Migrated to `temporal_rs`
/// 13.43 `ToPositiveIntegerWithTruncation ( argument )` /// 13.43 `ToPositiveIntegerWithTruncation ( argument )`
#[inline] #[inline]
@ -355,12 +319,12 @@ pub(crate) fn to_integer_if_integral(arg: &JsValue, context: &mut Context) -> Js
// NOTE: op -> true == until | false == since // NOTE: op -> true == until | false == since
// 13.47 `GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )` // 13.47 `GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit )`
// Migrated to `boa_temporal` // Migrated to `temporal_rs`
// NOTE: used for MergeFields methods. Potentially can be omitted in favor of `TemporalFields`. // NOTE: used for MergeFields methods. Potentially can be omitted in favor of `TemporalFields`.
// 14.6 `CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] )` // 14.6 `CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] )`
// Migrated or repurposed to `boa_temporal`/`fields.rs` // Migrated or repurposed to `temporal_rs`/`fields.rs`
// Note: Deviates from Proposal spec -> proto appears to be always null across the specification. // Note: Deviates from Proposal spec -> proto appears to be always null across the specification.
// 14.7 `SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] )` // 14.7 `SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] )`
// Migrated or repurposed to `boa_temporal`/`fields.rs` // Migrated or repurposed to `temporal_rs`/`fields.rs`

2
core/engine/src/builtins/temporal/options.rs

@ -12,7 +12,7 @@ use crate::{
builtins::options::{get_option, ParsableOptionType}, builtins::options::{get_option, ParsableOptionType},
js_string, Context, JsNativeError, JsObject, JsResult, js_string, Context, JsNativeError, JsObject, JsResult,
}; };
use boa_temporal::options::{ use temporal_rs::options::{
ArithmeticOverflow, DurationOverflow, InstantDisambiguation, OffsetDisambiguation, ArithmeticOverflow, DurationOverflow, InstantDisambiguation, OffsetDisambiguation,
TemporalRoundingMode, TemporalUnit, TemporalRoundingMode, TemporalUnit,
}; };

6
core/engine/src/builtins/temporal/plain_date/mod.rs

@ -18,7 +18,7 @@ use crate::{
}; };
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{ use temporal_rs::{
components::{ components::{
calendar::{CalendarSlot, GetCalendarSlot}, calendar::{CalendarSlot, GetCalendarSlot},
Date as InnerDate, DateTime, Date as InnerDate, DateTime,
@ -43,8 +43,8 @@ impl PlainDate {
} }
impl IsoDateSlots for JsObject<PlainDate> { impl IsoDateSlots for JsObject<PlainDate> {
fn iso_date(&self) -> boa_temporal::iso::IsoDate { fn iso_date(&self) -> temporal_rs::iso::IsoDate {
self.borrow().data().inner.iso() self.borrow().data().inner.iso_date()
} }
} }

2
core/engine/src/builtins/temporal/plain_date_time/mod.rs

@ -20,7 +20,7 @@ use boa_profiler::Profiler;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
use boa_temporal::{ use temporal_rs::{
components::{ components::{
calendar::{CalendarSlot, GetCalendarSlot}, calendar::{CalendarSlot, GetCalendarSlot},
DateTime as InnerDateTime, DateTime as InnerDateTime,

4
core/engine/src/builtins/temporal/plain_month_day/mod.rs

@ -12,7 +12,7 @@ use crate::{
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{ use temporal_rs::{
components::{ components::{
calendar::{CalendarSlot, GetCalendarSlot}, calendar::{CalendarSlot, GetCalendarSlot},
DateTime, MonthDay as InnerMonthDay, DateTime, MonthDay as InnerMonthDay,
@ -34,7 +34,7 @@ impl PlainMonthDay {
} }
impl IsoDateSlots for JsObject<PlainMonthDay> { impl IsoDateSlots for JsObject<PlainMonthDay> {
fn iso_date(&self) -> boa_temporal::iso::IsoDate { fn iso_date(&self) -> temporal_rs::iso::IsoDate {
self.borrow().data().inner.iso_date() self.borrow().data().inner.iso_date()
} }
} }

2
core/engine/src/builtins/temporal/plain_time/mod.rs

@ -16,7 +16,7 @@ use crate::{
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_macros::utf16; use boa_macros::utf16;
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::{ use temporal_rs::{
components::Time, components::Time,
options::{ArithmeticOverflow, TemporalRoundingMode}, options::{ArithmeticOverflow, TemporalRoundingMode},
}; };

4
core/engine/src/builtins/temporal/plain_year_month/mod.rs

@ -15,7 +15,7 @@ use boa_profiler::Profiler;
use super::calendar::to_temporal_calendar_slot_value; use super::calendar::to_temporal_calendar_slot_value;
use boa_temporal::{ use temporal_rs::{
iso::IsoDateSlots, iso::IsoDateSlots,
{ {
components::{ components::{
@ -40,7 +40,7 @@ impl PlainYearMonth {
} }
impl IsoDateSlots for JsObject<PlainYearMonth> { impl IsoDateSlots for JsObject<PlainYearMonth> {
fn iso_date(&self) -> boa_temporal::iso::IsoDate { fn iso_date(&self) -> temporal_rs::iso::IsoDate {
self.borrow().data().inner.iso_date() self.borrow().data().inner.iso_date()
} }
} }

4
core/engine/src/builtins/temporal/time_zone/custom.rs

@ -2,11 +2,11 @@
use crate::{property::PropertyKey, string::utf16, Context, JsObject, JsValue}; use crate::{property::PropertyKey, string::utf16, Context, JsObject, JsValue};
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
use boa_temporal::{ use num_bigint::BigInt;
use temporal_rs::{
components::{tz::TzProtocol, Instant}, components::{tz::TzProtocol, Instant},
TemporalError, TemporalResult, TemporalError, TemporalResult,
}; };
use num_bigint::BigInt;
#[derive(Debug, Clone, Trace, Finalize)] #[derive(Debug, Clone, Trace, Finalize)]
pub(crate) struct JsCustomTimeZone { pub(crate) struct JsCustomTimeZone {

4
core/engine/src/builtins/temporal/time_zone/mod.rs

@ -16,7 +16,7 @@ use crate::{
}; };
use boa_gc::{custom_trace, Finalize, Trace}; use boa_gc::{custom_trace, Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::components::tz::TimeZoneSlot; use temporal_rs::components::tz::TimeZoneSlot;
mod custom; mod custom;
@ -348,7 +348,7 @@ pub(super) fn create_temporal_time_zone(
/// [spec]: https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring /// [spec]: https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring
#[allow(clippy::unnecessary_wraps, unused)] #[allow(clippy::unnecessary_wraps, unused)]
fn parse_timezone_offset_string(offset_string: &str, context: &mut Context) -> JsResult<i64> { fn parse_timezone_offset_string(offset_string: &str, context: &mut Context) -> JsResult<i64> {
use boa_temporal::parser::{Cursor, TemporalTimeZoneString}; use temporal_rs::parser::{Cursor, TemporalTimeZoneString};
// 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset). // 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset).
let parse_result = TemporalTimeZoneString::parse(&mut Cursor::new(offset_string))?; let parse_result = TemporalTimeZoneString::parse(&mut Cursor::new(offset_string))?;

2
core/engine/src/builtins/temporal/zoned_date_time/mod.rs

@ -9,7 +9,7 @@ use crate::{
}; };
use boa_gc::{custom_trace, Finalize, Trace}; use boa_gc::{custom_trace, Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use boa_temporal::components::{ use temporal_rs::components::{
calendar::CalendarSlot, tz::TimeZoneSlot, Duration as TemporalDuration, calendar::CalendarSlot, tz::TimeZoneSlot, Duration as TemporalDuration,
ZonedDateTime as InnerZdt, ZonedDateTime as InnerZdt,
}; };

33
core/temporal/ABOUT.md

@ -1,33 +0,0 @@
# About Boa
Boa is an open-source, experimental ECMAScript Engine written in Rust for
lexing, parsing and executing ECMAScript/JavaScript. Currently, Boa supports some
of the [language][boa-conformance]. More information can be viewed at [Boa's
website][boa-web].
Try out the most recent release with Boa's live demo
[playground][boa-playground].
# Boa Crates
- [**`boa_ast`**][ast] - Boa's ECMAScript Abstract Syntax Tree.
- [**`boa_engine`**][engine] - Boa's implementation of ECMAScript builtin objects and
execution.
- [**`boa_gc`**][gc] - Boa's garbage collector.
- [**`boa_interner`**][interner] - Boa's string interner.
- [**`boa_parser`**][parser] - Boa's lexer and parser.
- [**`boa_profiler`**][profiler] - Boa's code profiler.
- [**`boa_icu_provider`**][icu] - Boa's ICU4X data provider.
- [**`boa_runtime`**][runtime] - Boa's WebAPI features.
[boa-conformance]: https://boajs.dev/boa/test262/
[boa-web]: https://boajs.dev/
[boa-playground]: https://boajs.dev/boa/playground/
[ast]: https://boajs.dev/boa/doc/boa_ast/index.html
[engine]: https://boajs.dev/boa/doc/boa_engine/index.html
[gc]: https://boajs.dev/boa/doc/boa_gc/index.html
[interner]: https://boajs.dev/boa/doc/boa_interner/index.html
[parser]: https://boajs.dev/boa/doc/boa_parser/index.html
[profiler]: https://boajs.dev/boa/doc/boa_profiler/index.html
[icu]: https://boajs.dev/boa/doc/boa_icu_provider/index.html
[runtime]: https://boajs.dev/boa/doc/boa_runtime/index.html

23
core/temporal/Cargo.toml

@ -1,23 +0,0 @@
[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 = true }
rustc-hash = { workspace = true, features = ["std"] }
num-bigint = { workspace = true, features = ["serde"] }
bitflags.workspace = true
num-traits.workspace = true
[lints]
workspace = true

11
core/temporal/README.md

@ -1,11 +0,0 @@
# 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.

1011
core/temporal/src/components/calendar.rs

File diff suppressed because it is too large Load Diff

433
core/temporal/src/components/date.rs

@ -1,433 +0,0 @@
//! This module implements `Date` and any directly related algorithms.
use tinystr::TinyAsciiStr;
use crate::{
components::{
calendar::{CalendarProtocol, CalendarSlot},
duration::DateDuration,
DateTime, Duration,
},
iso::{IsoDate, IsoDateSlots},
options::{ArithmeticOverflow, TemporalUnit},
parser::parse_date_time,
TemporalError, TemporalResult,
};
use std::str::FromStr;
use super::{
calendar::{CalendarDateLike, GetCalendarSlot},
duration::TimeDuration,
};
/// The native Rust implementation of `Temporal.PlainDate`.
#[derive(Debug, Default, Clone)]
pub struct Date<C: CalendarProtocol> {
iso: IsoDate,
calendar: CalendarSlot<C>,
}
// ==== Private API ====
impl<C: CalendarProtocol> Date<C> {
/// Create a new `Date` with the date values and calendar slot.
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot<C>) -> 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 C::Context,
) -> 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<C: CalendarProtocol> Date<C> {
/// Creates a new `Date` while checking for validity.
pub fn new(
year: i32,
month: i32,
day: i32,
calendar: CalendarSlot<C>,
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<C>) -> Self {
Self {
iso: dt.iso_date(),
calendar: dt.calendar().clone(),
}
}
#[inline]
#[must_use]
/// Returns this `Date`'s ISO year value.
pub const fn iso_year(&self) -> i32 {
self.iso.year
}
#[inline]
#[must_use]
/// Returns this `Date`'s ISO month value.
pub const fn iso_month(&self) -> u8 {
self.iso.month
}
#[inline]
#[must_use]
/// Returns this `Date`'s ISO day value.
pub const fn iso_day(&self) -> u8 {
self.iso.day
}
#[inline]
#[must_use]
/// Returns the `Date`'s inner `IsoDate` record.
pub const fn iso(&self) -> IsoDate {
self.iso
}
#[inline]
#[must_use]
/// Returns a reference to this `Date`'s calendar slot.
pub fn calendar(&self) -> &CalendarSlot<C> {
&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()
}
}
// ==== Calendar-derived Public API ====
impl Date<()> {
/// Returns the calendar year value.
pub fn year(&self) -> TemporalResult<i32> {
self.calendar
.year(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar month value.
pub fn month(&self) -> TemporalResult<u8> {
self.calendar
.month(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar month code value.
pub fn month_code(&self) -> TemporalResult<TinyAsciiStr<4>> {
self.calendar
.month_code(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar day value.
pub fn day(&self) -> TemporalResult<u8> {
self.calendar
.day(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar day of week value.
pub fn day_of_week(&self) -> TemporalResult<u16> {
self.calendar
.day_of_week(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar day of year value.
pub fn day_of_year(&self) -> TemporalResult<u16> {
self.calendar
.day_of_year(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar week of year value.
pub fn week_of_year(&self) -> TemporalResult<u16> {
self.calendar
.week_of_year(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar year of week value.
pub fn year_of_week(&self) -> TemporalResult<i32> {
self.calendar
.year_of_week(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar days in week value.
pub fn days_in_week(&self) -> TemporalResult<u16> {
self.calendar
.days_in_week(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar days in month value.
pub fn days_in_month(&self) -> TemporalResult<u16> {
self.calendar
.days_in_month(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar days in year value.
pub fn days_in_year(&self) -> TemporalResult<u16> {
self.calendar
.days_in_year(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns the calendar months in year value.
pub fn months_in_year(&self) -> TemporalResult<u16> {
self.calendar
.months_in_year(&CalendarDateLike::Date(self.clone()), &mut ())
}
/// Returns returns whether the date in a leap year for the given calendar.
pub fn in_leap_year(&self) -> TemporalResult<bool> {
self.calendar
.in_leap_year(&CalendarDateLike::Date(self.clone()), &mut ())
}
}
// NOTE(nekevss): The clone below should ideally not change the memory address, but that may
// not be true across all cases. I.e., it should be fine as long as the clone is simply a
// reference count increment. Need to test.
impl<C: CalendarProtocol> Date<C> {
/// Returns the calendar year value with provided context.
pub fn contextual_year(this: &C::Date, context: &mut C::Context) -> TemporalResult<i32> {
this.get_calendar()
.year(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar month value with provided context.
pub fn contextual_month(this: &C::Date, context: &mut C::Context) -> TemporalResult<u8> {
this.get_calendar()
.month(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar month code value with provided context.
pub fn contextual_month_code(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<TinyAsciiStr<4>> {
this.get_calendar()
.month_code(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar day value with provided context.
pub fn contextual_day(this: &C::Date, context: &mut C::Context) -> TemporalResult<u8> {
this.get_calendar()
.day(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar day of week value with provided context.
pub fn contextual_day_of_week(this: &C::Date, context: &mut C::Context) -> TemporalResult<u16> {
this.get_calendar()
.day_of_week(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar day of year value with provided context.
pub fn contextual_day_of_year(this: &C::Date, context: &mut C::Context) -> TemporalResult<u16> {
this.get_calendar()
.day_of_year(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar week of year value with provided context.
pub fn contextual_week_of_year(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.week_of_year(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar year of week value with provided context.
pub fn contextual_year_of_week(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<i32> {
this.get_calendar()
.year_of_week(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar days in week value with provided context.
pub fn contextual_days_in_week(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.days_in_week(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar days in month value with provided context.
pub fn contextual_days_in_month(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.days_in_month(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar days in year value with provided context.
pub fn contextual_days_in_year(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.days_in_year(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns the calendar months in year value with provided context.
pub fn contextual_months_in_year(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.months_in_year(&CalendarDateLike::CustomDate(this.clone()), context)
}
/// Returns whether the date is in a leap year for the given calendar with provided context.
pub fn contextual_in_leap_year(
this: &C::Date,
context: &mut C::Context,
) -> TemporalResult<bool> {
this.get_calendar()
.in_leap_year(&CalendarDateLike::CustomDate(this.clone()), context)
}
}
impl<C: CalendarProtocol> GetCalendarSlot<C> for Date<C> {
fn get_calendar(&self) -> CalendarSlot<C> {
self.calendar.clone()
}
}
impl<C: CalendarProtocol> IsoDateSlots for Date<C> {
/// Returns the structs `IsoDate`
fn iso_date(&self) -> IsoDate {
self.iso
}
}
// ==== Context based API ====
impl<C: CalendarProtocol> Date<C> {
/// 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 C::Context,
) -> 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, _) = TimeDuration::new_unchecked(
duration.hours(),
duration.minutes(),
duration.seconds(),
duration.milliseconds(),
duration.microseconds(),
duration.nanoseconds(),
)
.balance(duration.days(), 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 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 C::Context,
) -> 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)
}
}
// ==== Trait impls ====
impl<C: CalendarProtocol> FromStr for Date<C> {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse_record = parse_date_time(s)?;
let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned());
let date = IsoDate::new(
parse_record.date.year,
parse_record.date.month,
parse_record.date.day,
ArithmeticOverflow::Reject,
)?;
Ok(Self::new_unchecked(
date,
CalendarSlot::from_str(&calendar)?,
))
}
}

447
core/temporal/src/components/datetime.rs

@ -1,447 +0,0 @@
//! This module implements `DateTime` any directly related algorithms.
use crate::{
components::{
calendar::{CalendarProtocol, CalendarSlot},
Instant,
},
iso::{IsoDate, IsoDateSlots, IsoDateTime, IsoTime},
options::ArithmeticOverflow,
parser::parse_date_time,
TemporalError, TemporalResult,
};
use std::str::FromStr;
use tinystr::TinyAsciiStr;
use super::calendar::{CalendarDateLike, GetCalendarSlot};
/// The native Rust implementation of `Temporal.PlainDateTime`
#[derive(Debug, Default, Clone)]
pub struct DateTime<C: CalendarProtocol> {
iso: IsoDateTime,
calendar: CalendarSlot<C>,
}
// ==== Private DateTime API ====
impl<C: CalendarProtocol> DateTime<C> {
/// Creates a new unchecked `DateTime`.
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoDateTime, calendar: CalendarSlot<C>) -> Self {
Self { iso, 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()
}
/// Create a new `DateTime` from an `Instant`.
#[inline]
pub(crate) fn from_instant(
instant: &Instant,
offset: f64,
calendar: CalendarSlot<C>,
) -> TemporalResult<Self> {
let iso = IsoDateTime::from_epoch_nanos(&instant.nanos, offset)?;
Ok(Self { iso, calendar })
}
}
// ==== Public DateTime API ====
impl<C: CalendarProtocol> DateTime<C> {
/// 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<C>,
) -> 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(
IsoDateTime::new(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 this `Date`'s ISO year value.
#[inline]
#[must_use]
pub const fn iso_year(&self) -> i32 {
self.iso.date().year
}
/// Returns this `Date`'s ISO month value.
#[inline]
#[must_use]
pub const fn iso_month(&self) -> u8 {
self.iso.date().month
}
/// Returns this `Date`'s ISO day value.
#[inline]
#[must_use]
pub const fn iso_day(&self) -> u8 {
self.iso.date().day
}
/// Returns the hour value
#[inline]
#[must_use]
pub fn hour(&self) -> u8 {
self.iso.time().hour
}
/// Returns the minute value
#[inline]
#[must_use]
pub fn minute(&self) -> u8 {
self.iso.time().minute
}
/// Returns the second value
#[inline]
#[must_use]
pub fn second(&self) -> u8 {
self.iso.time().second
}
/// Returns the `millisecond` value
#[inline]
#[must_use]
pub fn millisecond(&self) -> u16 {
self.iso.time().millisecond
}
/// Returns the `microsecond` value
#[inline]
#[must_use]
pub fn microsecond(&self) -> u16 {
self.iso.time().microsecond
}
/// Returns the `nanosecond` value
#[inline]
#[must_use]
pub fn nanosecond(&self) -> u16 {
self.iso.time().nanosecond
}
/// Returns the Calendar value.
#[inline]
#[must_use]
pub fn calendar(&self) -> &CalendarSlot<C> {
&self.calendar
}
}
// ==== Calendar-derived public API ====
// TODO: Revert to `DateTime<C>`.
impl DateTime<()> {
/// Returns the calendar year value.
pub fn year(&self) -> TemporalResult<i32> {
self.calendar
.year(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar month value.
pub fn month(&self) -> TemporalResult<u8> {
self.calendar
.month(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar month code value.
pub fn month_code(&self) -> TemporalResult<TinyAsciiStr<4>> {
self.calendar
.month_code(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar day value.
pub fn day(&self) -> TemporalResult<u8> {
self.calendar
.day(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar day of week value.
pub fn day_of_week(&self) -> TemporalResult<u16> {
self.calendar
.day_of_week(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar day of year value.
pub fn day_of_year(&self) -> TemporalResult<u16> {
self.calendar
.day_of_year(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar week of year value.
pub fn week_of_year(&self) -> TemporalResult<u16> {
self.calendar
.week_of_year(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar year of week value.
pub fn year_of_week(&self) -> TemporalResult<i32> {
self.calendar
.year_of_week(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar days in week value.
pub fn days_in_week(&self) -> TemporalResult<u16> {
self.calendar
.days_in_week(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar days in month value.
pub fn days_in_month(&self) -> TemporalResult<u16> {
self.calendar
.days_in_month(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar days in year value.
pub fn days_in_year(&self) -> TemporalResult<u16> {
self.calendar
.days_in_year(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns the calendar months in year value.
pub fn months_in_year(&self) -> TemporalResult<u16> {
self.calendar
.months_in_year(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
/// Returns returns whether the date in a leap year for the given calendar.
pub fn in_leap_year(&self) -> TemporalResult<bool> {
self.calendar
.in_leap_year(&CalendarDateLike::DateTime(self.clone()), &mut ())
}
}
impl<C: CalendarProtocol> DateTime<C> {
/// Returns the calendar year value with provided context.
pub fn contextual_year(this: &C::DateTime, context: &mut C::Context) -> TemporalResult<i32> {
this.get_calendar()
.year(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar month value with provided context.
pub fn contextual_month(this: &C::DateTime, context: &mut C::Context) -> TemporalResult<u8> {
this.get_calendar()
.month(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar month code value with provided context.
pub fn contextual_month_code(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<TinyAsciiStr<4>> {
this.get_calendar()
.month_code(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar day value with provided context.
pub fn contextual_day(this: &C::DateTime, context: &mut C::Context) -> TemporalResult<u8> {
this.get_calendar()
.day(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar day of week value with provided context.
pub fn contextual_day_of_week(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.day_of_week(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar day of year value with provided context.
pub fn contextual_day_of_year(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.day_of_year(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar week of year value with provided context.
pub fn contextual_week_of_year(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.week_of_year(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar year of week value with provided context.
pub fn contextual_year_of_week(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<i32> {
this.get_calendar()
.year_of_week(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar days in week value with provided context.
pub fn contextual_days_in_week(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.days_in_week(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar days in month value with provided context.
pub fn contextual_days_in_month(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.days_in_month(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar days in year value with provided context.
pub fn contextual_days_in_year(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.days_in_year(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns the calendar months in year value with provided context.
pub fn contextual_months_in_year(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<u16> {
this.get_calendar()
.months_in_year(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
/// Returns whether the date is in a leap year for the given calendar with provided context.
pub fn contextual_in_leap_year(
this: &C::DateTime,
context: &mut C::Context,
) -> TemporalResult<bool> {
this.get_calendar()
.in_leap_year(&CalendarDateLike::CustomDateTime(this.clone()), context)
}
}
// ==== Trait impls ====
impl<C: CalendarProtocol> GetCalendarSlot<C> for DateTime<C> {
fn get_calendar(&self) -> CalendarSlot<C> {
self.calendar.clone()
}
}
impl<C: CalendarProtocol> IsoDateSlots for DateTime<C> {
fn iso_date(&self) -> IsoDate {
*self.iso.date()
}
}
impl<C: CalendarProtocol> FromStr for DateTime<C> {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse_record = parse_date_time(s)?;
let calendar = parse_record.calendar.unwrap_or("iso8601".to_owned());
let time = if let Some(time) = parse_record.time {
IsoTime::from_components(
i32::from(time.hour),
i32::from(time.minute),
i32::from(time.second),
time.fraction,
)?
} else {
IsoTime::default()
};
let date = IsoDate::new(
parse_record.date.year,
parse_record.date.month,
parse_record.date.day,
ArithmeticOverflow::Reject,
)?;
Ok(Self::new_unchecked(
IsoDateTime::new(date, time)?,
CalendarSlot::from_str(&calendar)?,
))
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::components::calendar::CalendarSlot;
use super::DateTime;
#[test]
#[allow(clippy::float_cmp)]
fn plain_date_time_limits() {
// This test is primarily to assert that the `expect` in the epoch methods is
// valid, i.e., a valid instant is within the range of an f64.
let negative_limit = DateTime::<()>::new(
-271_821,
4,
19,
0,
0,
0,
0,
0,
0,
CalendarSlot::from_str("iso8601").unwrap(),
);
let positive_limit = DateTime::<()>::new(
275_760,
9,
14,
0,
0,
0,
0,
0,
0,
CalendarSlot::from_str("iso8601").unwrap(),
);
assert!(negative_limit.is_err());
assert!(positive_limit.is_err());
}
}

1022
core/temporal/src/components/duration.rs

File diff suppressed because it is too large Load Diff

486
core/temporal/src/components/duration/date.rs

@ -1,486 +0,0 @@
//! Implementation of a `DateDuration`
use crate::{
components::{
calendar::CalendarProtocol, duration::TimeDuration, tz::TzProtocol, Date, DateTime,
Duration, ZonedDateTime,
},
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit},
utils, TemporalError, TemporalResult, NS_PER_DAY,
};
/// `DateDuration` represents the [date duration record][spec] of the `Duration.`
///
/// These fields are laid out in the [Temporal Proposal][field spec] as 64-bit floating point numbers.
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-date-duration-records
/// [field spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances
#[derive(Debug, Default, Clone, Copy)]
pub struct DateDuration {
pub(crate) years: f64,
pub(crate) months: f64,
pub(crate) weeks: f64,
pub(crate) days: f64,
}
impl DateDuration {
/// Creates a new, non-validated `DateDuration`.
#[inline]
#[must_use]
pub(crate) const fn new_unchecked(years: f64, months: f64, weeks: f64, days: f64) -> Self {
Self {
years,
months,
weeks,
days,
}
}
}
impl DateDuration {
/// Creates a new `DateDuration` with provided values.
pub fn new(years: f64, months: f64, weeks: f64, days: f64) -> TemporalResult<Self> {
let result = Self::new_unchecked(years, months, weeks, days);
if !super::is_valid_duration(&result.into_iter().collect()) {
return Err(TemporalError::range().with_message("Invalid DateDuration."));
}
Ok(result)
}
/// Returns a `PartialDateDuration` with all fields set to `NaN`.
#[must_use]
pub const fn partial() -> Self {
Self {
years: f64::NAN,
months: f64::NAN,
weeks: f64::NAN,
days: f64::NAN,
}
}
/// Creates a `DateDuration` from a provided partial `DateDuration`.
#[must_use]
pub fn from_partial(partial: &DateDuration) -> Self {
Self {
years: if partial.years.is_nan() {
0.0
} else {
partial.years
},
months: if partial.months.is_nan() {
0.0
} else {
partial.months
},
weeks: if partial.weeks.is_nan() {
0.0
} else {
partial.weeks
},
days: if partial.days.is_nan() {
0.0
} else {
partial.days
},
}
}
/// Returns a new `DateDuration` representing the absolute value of the current.
#[inline]
#[must_use]
pub fn abs(&self) -> Self {
Self {
years: self.years.abs(),
months: self.months.abs(),
weeks: self.weeks.abs(),
days: self.days.abs(),
}
}
/// Returns the `[[years]]` value.
#[must_use]
pub const fn years(&self) -> f64 {
self.years
}
/// Returns the `[[months]]` value.
#[must_use]
pub const fn months(&self) -> f64 {
self.months
}
/// Returns the `[[weeks]]` value.
#[must_use]
pub const fn weeks(&self) -> f64 {
self.weeks
}
/// Returns the `[[days]]` value.
#[must_use]
pub const fn days(&self) -> f64 {
self.days
}
/// Returns the iterator for `DateDuration`
#[must_use]
pub fn iter(&self) -> DateIter<'_> {
<&Self as IntoIterator>::into_iter(self)
}
}
// ==== DateDuration Operations ====
impl DateDuration {
/// Rounds the current `DateDuration` returning a tuple of the rounded `DateDuration` and
/// the `total` value of the smallest unit prior to rounding.
#[allow(clippy::type_complexity, clippy::let_and_return)]
pub fn round<C: CalendarProtocol, Z: TzProtocol>(
&self,
additional_time: Option<TimeDuration>,
increment: f64,
unit: TemporalUnit,
rounding_mode: TemporalRoundingMode,
relative_targets: (
Option<&Date<C>>,
Option<&ZonedDateTime<C, Z>>,
Option<&DateTime<C>>,
),
context: &mut C::Context,
) -> TemporalResult<(Self, f64)> {
// 1. If plainRelativeTo is not present, set plainRelativeTo to undefined.
let plain_relative_to = relative_targets.0;
// 2. If zonedRelativeTo is not present, set zonedRelativeTo to undefined.
let zoned_relative_to = relative_targets.1;
// 3. If precalculatedPlainDateTime is not present, set precalculatedPlainDateTime to undefined.
let _ = relative_targets.2;
let mut fractional_days = match unit {
// 4. If unit is "year", "month", or "week", and plainRelativeTo is undefined, then
TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week
if plain_relative_to.is_none() =>
{
// a. Throw a RangeError exception.
return Err(TemporalError::range()
.with_message("plainRelativeTo canot be undefined with given TemporalUnit"));
}
// 5. If unit is one of "year", "month", "week", or "day", then
TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week | TemporalUnit::Day => {
// a. Let nanoseconds be TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
let nanoseconds = additional_time.unwrap_or_default().as_nanos();
// b. If zonedRelativeTo is not undefined, then
// i. Let intermediate be ? MoveRelativeZonedDateTime(zonedRelativeTo, years, months, weeks, days, precalculatedPlainDateTime).
// ii. Let result be ? NanosecondsToDays(nanoseconds, intermediate).
// iii. Let fractionalDays be days + result.[[Days]] + result.[[Nanoseconds]] / result.[[DayLength]].
// c. Else,
// i. Let fractionalDays be days + nanoseconds / nsPerDay.
// d. Set days, hours, minutes, seconds, milliseconds, microseconds, and nanoseconds to 0.
// e. Assert: fractionalSeconds is not used below.
if zoned_relative_to.is_none() {
self.days + nanoseconds / NS_PER_DAY as f64
} else {
// implementation of b: i-iii needed.
return Err(TemporalError::range().with_message("Not yet implemented."));
}
}
_ => {
return Err(TemporalError::range()
.with_message("Invalid TemporalUnit provided to DateDuration.round"))
}
};
// 7. let total be unset.
// We begin matching against unit and return the remainder value.
match unit {
// 8. If unit is "year", then
TemporalUnit::Year => {
let plain_relative_to = plain_relative_to.expect("this must exist.");
// a. Let calendar be plainRelativeTo.[[Calendar]].
let calendar = plain_relative_to.calendar();
// b. Let yearsDuration be ! CreateTemporalDuration(years, 0, 0, 0, 0, 0, 0, 0, 0, 0).
let years = DateDuration::new_unchecked(self.years, 0.0, 0.0, 0.0);
let years_duration = Duration::new_unchecked(years, TimeDuration::default());
// c. If calendar is an Object, then
// i. Let dateAdd be ? GetMethod(calendar, "dateAdd").
// d. Else,
// i. Let dateAdd be unused.
// e. Let yearsLater be ? AddDate(calendar, plainRelativeTo, yearsDuration, undefined, dateAdd).
let years_later = plain_relative_to.contextual_add_date(
&years_duration,
ArithmeticOverflow::Constrain,
context,
)?;
// f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0).
let years_months_weeks = Duration::new_unchecked(
Self::new_unchecked(self.years, self.months, self.weeks, 0.0),
TimeDuration::default(),
);
// g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd).
let years_months_weeks_later = plain_relative_to.contextual_add_date(
&years_months_weeks,
ArithmeticOverflow::Constrain,
context,
)?;
// h. Let monthsWeeksInDays be DaysUntil(yearsLater, yearsMonthsWeeksLater).
let months_weeks_in_days = years_later.days_until(&years_months_weeks_later);
// i. Set plainRelativeTo to yearsLater.
let plain_relative_to = years_later;
// j. Set fractionalDays to fractionalDays + monthsWeeksInDays.
fractional_days += f64::from(months_weeks_in_days);
// k. Let isoResult be ! AddISODate(plainRelativeTo.[[ISOYear]]. plainRelativeTo.[[ISOMonth]], plainRelativeTo.[[ISODay]], 0, 0, 0, truncate(fractionalDays), "constrain").
let iso_result = plain_relative_to.iso().add_iso_date(
&DateDuration::new_unchecked(0.0, 0.0, 0.0, fractional_days.trunc()),
ArithmeticOverflow::Constrain,
)?;
// l. Let wholeDaysLater be ? CreateDate(isoResult.[[Year]], isoResult.[[Month]], isoResult.[[Day]], calendar).
let whole_days_later = Date::new_unchecked(iso_result, calendar.clone());
// m. Let untilOptions be OrdinaryObjectCreate(null).
// n. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "year").
// o. Let timePassed be ? DifferenceDate(calendar, plainRelativeTo, wholeDaysLater, untilOptions).
let time_passed = plain_relative_to.contextual_difference_date(
&whole_days_later,
TemporalUnit::Year,
context,
)?;
// p. Let yearsPassed be timePassed.[[Years]].
let years_passed = time_passed.date.years();
// q. Set years to years + yearsPassed.
let years = self.years() + years_passed;
// r. Let yearsDuration be ! CreateTemporalDuration(yearsPassed, 0, 0, 0, 0, 0, 0, 0, 0, 0).
let years_duration = Duration::one_year(years_passed);
// s. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, yearsDuration, dateAdd).
// t. Set plainRelativeTo to moveResult.[[RelativeTo]].
// u. Let daysPassed be moveResult.[[Days]].
let (plain_relative_to, days_passed) =
plain_relative_to.move_relative_date(&years_duration, context)?;
// v. Set fractionalDays to fractionalDays - daysPassed.
fractional_days -= days_passed;
// w. If fractionalDays < 0, let sign be -1; else, let sign be 1.
let sign = if fractional_days < 0.0 { -1 } else { 1 };
// x. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0).
let one_year = Duration::one_year(f64::from(sign));
// y. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd).
// z. Let oneYearDays be moveResult.[[Days]].
let (_, one_year_days) =
plain_relative_to.move_relative_date(&one_year, context)?;
// aa. Let fractionalYears be years + fractionalDays / abs(oneYearDays).
let frac_years = years + (fractional_days / one_year_days.abs());
// ab. Set years to RoundNumberToIncrement(fractionalYears, increment, roundingMode).
let rounded_years =
utils::round_number_to_increment(frac_years, increment, rounding_mode);
// ac. Set total to fractionalYears.
// ad. Set months and weeks to 0.
let result = Self::new(rounded_years, 0f64, 0f64, 0f64)?;
Ok((result, frac_years))
}
// 9. Else if unit is "month", then
TemporalUnit::Month => {
// a. Let calendar be plainRelativeTo.[[Calendar]].
let plain_relative_to = plain_relative_to.expect("this must exist.");
// b. Let yearsMonths be ! CreateTemporalDuration(years, months, 0, 0, 0, 0, 0, 0, 0, 0).
let years_months = Duration::from_date_duration(DateDuration::new_unchecked(
self.years(),
self.months(),
0.0,
0.0,
));
// c. If calendar is an Object, then
// i. Let dateAdd be ? GetMethod(calendar, "dateAdd").
// d. Else,
// i. Let dateAdd be unused.
// e. Let yearsMonthsLater be ? AddDate(calendar, plainRelativeTo, yearsMonths, undefined, dateAdd).
let years_months_later = plain_relative_to.contextual_add_date(
&years_months,
ArithmeticOverflow::Constrain,
context,
)?;
// f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0).
let years_months_weeks = Duration::from_date_duration(DateDuration::new_unchecked(
self.years(),
self.months(),
self.weeks(),
0.0,
));
// g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd).
let years_months_weeks_later = plain_relative_to.contextual_add_date(
&years_months_weeks,
ArithmeticOverflow::Constrain,
context,
)?;
// h. Let weeksInDays be DaysUntil(yearsMonthsLater, yearsMonthsWeeksLater).
let weeks_in_days = years_months_later.days_until(&years_months_weeks_later);
// i. Set plainRelativeTo to yearsMonthsLater.
let plain_relative_to = years_months_later;
// j. Set fractionalDays to fractionalDays + weeksInDays.
fractional_days += f64::from(weeks_in_days);
// k. If fractionalDays < 0, let sign be -1; else, let sign be 1.
let sign = if fractional_days < 0.0 { -1f64 } else { 1f64 };
// l. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0).
let one_month = Duration::one_month(sign);
// m. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd).
// n. Set plainRelativeTo to moveResult.[[RelativeTo]].
// o. Let oneMonthDays be moveResult.[[Days]].
let (mut plain_relative_to, mut one_month_days) =
plain_relative_to.move_relative_date(&one_month, context)?;
let mut months = self.months;
// p. Repeat, while abs(fractionalDays) ≥ abs(oneMonthDays),
while fractional_days.abs() >= one_month_days.abs() {
// i. Set months to months + sign.
months += sign;
// ii. Set fractionalDays to fractionalDays - oneMonthDays.
fractional_days -= one_month_days;
// iii. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd).
let move_result = plain_relative_to.move_relative_date(&one_month, context)?;
// iv. Set plainRelativeTo to moveResult.[[RelativeTo]].
plain_relative_to = move_result.0;
// v. Set oneMonthDays to moveResult.[[Days]].
one_month_days = move_result.1;
}
// q. Let fractionalMonths be months + fractionalDays / abs(oneMonthDays).
let frac_months = months + fractional_days / one_month_days.abs();
// r. Set months to RoundNumberToIncrement(fractionalMonths, increment, roundingMode).
let rounded_months =
utils::round_number_to_increment(frac_months, increment, rounding_mode);
// s. Set total to fractionalMonths.
// t. Set weeks to 0.
let result = Self::new(self.years, rounded_months, 0f64, 0f64)?;
Ok((result, frac_months))
}
// 10. Else if unit is "week", then
TemporalUnit::Week => {
// a. Let calendar be plainRelativeTo.[[Calendar]].
let plain_relative_to = plain_relative_to.expect("date must exist given Week");
// b. If fractionalDays < 0, let sign be -1; else, let sign be 1.
let sign = if fractional_days < 0.0 { -1f64 } else { 1f64 };
// c. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0).
let one_week = Duration::one_week(sign);
// d. If calendar is an Object, then
// i. Let dateAdd be ? GetMethod(calendar, "dateAdd").
// e. Else,
// i. Let dateAdd be unused.
// f. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneWeek, dateAdd).
// g. Set plainRelativeTo to moveResult.[[RelativeTo]].
// h. Let oneWeekDays be moveResult.[[Days]].
let (mut plain_relative_to, mut one_week_days) =
plain_relative_to.move_relative_date(&one_week, context)?;
let mut weeks = self.weeks;
// i. Repeat, while abs(fractionalDays) ≥ abs(oneWeekDays),
while fractional_days.abs() >= one_week_days.abs() {
// i. Set weeks to weeks + sign.
weeks += sign;
// ii. Set fractionalDays to fractionalDays - oneWeekDays.
fractional_days -= one_week_days;
// iii. Set moveResult to ? MoveRelativeDate(calendar, plainRelativeTo, oneWeek, dateAdd).
let move_result = plain_relative_to.move_relative_date(&one_week, context)?;
// iv. Set plainRelativeTo to moveResult.[[RelativeTo]].
plain_relative_to = move_result.0;
// v. Set oneWeekDays to moveResult.[[Days]].
one_week_days = move_result.1;
}
// j. Let fractionalWeeks be weeks + fractionalDays / abs(oneWeekDays).
let frac_weeks = weeks + fractional_days / one_week_days.abs();
// k. Set weeks to RoundNumberToIncrement(fractionalWeeks, increment, roundingMode).
let rounded_weeks =
utils::round_number_to_increment(frac_weeks, increment, rounding_mode);
// l. Set total to fractionalWeeks.
let result = Self::new(self.years, self.months, rounded_weeks, 0f64)?;
Ok((result, frac_weeks))
}
// 11. Else if unit is "day", then
TemporalUnit::Day => {
// a. Set days to RoundNumberToIncrement(fractionalDays, increment, roundingMode).
let rounded_days =
utils::round_number_to_increment(fractional_days, increment, rounding_mode);
// b. Set total to fractionalDays.
let result = Self::new(self.years, self.months, self.weeks, rounded_days)?;
Ok((result, fractional_days))
}
_ => unreachable!("All other TemporalUnits were returned early as invalid."),
}
}
}
impl<'a> IntoIterator for &'a DateDuration {
type Item = f64;
type IntoIter = DateIter<'a>;
fn into_iter(self) -> Self::IntoIter {
DateIter {
date: self,
index: 0,
}
}
}
/// An iterator over the `DateDuration`
#[derive(Debug)]
pub struct DateIter<'a> {
date: &'a DateDuration,
index: usize,
}
impl Iterator for DateIter<'_> {
type Item = f64;
fn next(&mut self) -> Option<Self::Item> {
let result = match self.index {
0 => Some(self.date.years),
1 => Some(self.date.months),
2 => Some(self.date.weeks),
3 => Some(self.date.days),
_ => None,
};
self.index += 1;
result
}
}

596
core/temporal/src/components/duration/time.rs

@ -1,596 +0,0 @@
//! An implementation of `TimeDuration` and it's methods.
use crate::{
options::{TemporalRoundingMode, TemporalUnit},
utils, TemporalError, TemporalResult,
};
use super::is_valid_duration;
/// `TimeDuration` represents the [Time Duration record][spec] of the `Duration.`
///
/// These fields are laid out in the [Temporal Proposal][field spec] as 64-bit floating point numbers.
///
/// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-time-duration-records
/// [field spec]: https://tc39.es/proposal-temporal/#sec-properties-of-temporal-duration-instances
#[derive(Debug, Default, Clone, Copy)]
pub struct TimeDuration {
pub(crate) hours: f64,
pub(crate) minutes: f64,
pub(crate) seconds: f64,
pub(crate) milliseconds: f64,
pub(crate) microseconds: f64,
pub(crate) nanoseconds: f64,
}
// ==== TimeDuration Private API ====
impl TimeDuration {
/// Creates a new `TimeDuration`.
#[must_use]
pub(crate) const fn new_unchecked(
hours: f64,
minutes: f64,
seconds: f64,
milliseconds: f64,
microseconds: f64,
nanoseconds: f64,
) -> Self {
Self {
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
}
}
/// Returns the current `TimeDuration` as nanoseconds.
#[inline]
pub(crate) fn as_nanos(&self) -> f64 {
self.hours
.mul_add(60_f64, self.minutes)
.mul_add(60_f64, self.seconds)
.mul_add(1_000_f64, self.milliseconds)
.mul_add(1_000_f64, self.microseconds)
.mul_add(1_000_f64, self.nanoseconds)
}
/// Abstract Operation 7.5.18 `BalancePossiblyInfiniteDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit )`
///
/// This function will balance the current `TimeDuration`. It returns the balanced `day` and `TimeDuration` value.
#[allow(clippy::too_many_arguments)]
pub(crate) fn balance_possibly_infinite_time_duration(
days: f64,
hours: f64,
minutes: f64,
seconds: f64,
milliseconds: f64,
microseconds: f64,
nanoseconds: f64,
largest_unit: TemporalUnit,
) -> TemporalResult<(f64, Option<Self>)> {
// 1. Set hours to hours + days × 24.
let hours = hours + (days * 24f64);
// 2. Set nanoseconds to TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds).
let mut nanoseconds = Self::new_unchecked(
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
)
.as_nanos();
// 3. Set days, hours, minutes, seconds, milliseconds, and microseconds to 0.
let mut days = 0f64;
let mut hours = 0f64;
let mut minutes = 0f64;
let mut seconds = 0f64;
let mut milliseconds = 0f64;
let mut microseconds = 0f64;
// 4. If nanoseconds < 0, let sign be -1; else, let sign be 1.
let sign = if nanoseconds < 0f64 { -1 } else { 1 };
// 5. Set nanoseconds to abs(nanoseconds).
nanoseconds = nanoseconds.abs();
match largest_unit {
// 9. If largestUnit is "year", "month", "week", "day", or "hour", then
TemporalUnit::Year
| TemporalUnit::Month
| TemporalUnit::Week
| TemporalUnit::Day
| TemporalUnit::Hour => {
// a. Set microseconds to floor(nanoseconds / 1000).
microseconds = (nanoseconds / 1000f64).floor();
// b. Set nanoseconds to nanoseconds modulo 1000.
nanoseconds %= 1000f64;
// c. Set milliseconds to floor(microseconds / 1000).
milliseconds = (microseconds / 1000f64).floor();
// d. Set microseconds to microseconds modulo 1000.
microseconds %= 1000f64;
// e. Set seconds to floor(milliseconds / 1000).
seconds = (milliseconds / 1000f64).floor();
// f. Set milliseconds to milliseconds modulo 1000.
milliseconds %= 1000f64;
// g. Set minutes to floor(seconds / 60).
minutes = (seconds / 60f64).floor();
// h. Set seconds to seconds modulo 60.
seconds %= 60f64;
// i. Set hours to floor(minutes / 60).
hours = (minutes / 60f64).floor();
// j. Set minutes to minutes modulo 60.
minutes %= 60f64;
// k. Set days to floor(hours / 24).
days = (hours / 24f64).floor();
// l. Set hours to hours modulo 24.
hours %= 24f64;
}
// 10. Else if largestUnit is "minute", then
TemporalUnit::Minute => {
// a. Set microseconds to floor(nanoseconds / 1000).
// b. Set nanoseconds to nanoseconds modulo 1000.
microseconds = (nanoseconds / 1000f64).floor();
nanoseconds %= 1000f64;
// c. Set milliseconds to floor(microseconds / 1000).
// d. Set microseconds to microseconds modulo 1000.
milliseconds = (microseconds / 1000f64).floor();
microseconds %= 1000f64;
// e. Set seconds to floor(milliseconds / 1000).
// f. Set milliseconds to milliseconds modulo 1000.
seconds = (milliseconds / 1000f64).floor();
milliseconds %= 1000f64;
// g. Set minutes to floor(seconds / 60).
// h. Set seconds to seconds modulo 60.
minutes = (seconds / 60f64).floor();
seconds %= 60f64;
}
// 11. Else if largestUnit is "second", then
TemporalUnit::Second => {
// a. Set microseconds to floor(nanoseconds / 1000).
// b. Set nanoseconds to nanoseconds modulo 1000.
microseconds = (nanoseconds / 1000f64).floor();
nanoseconds %= 1000f64;
// c. Set milliseconds to floor(microseconds / 1000).
// d. Set microseconds to microseconds modulo 1000.
milliseconds = (microseconds / 1000f64).floor();
microseconds %= 1000f64;
// e. Set seconds to floor(milliseconds / 1000).
// f. Set milliseconds to milliseconds modulo 1000.
seconds = (milliseconds / 1000f64).floor();
milliseconds %= 1000f64;
}
// 12. Else if largestUnit is "millisecond", then
TemporalUnit::Millisecond => {
// a. Set microseconds to floor(nanoseconds / 1000).
// b. Set nanoseconds to nanoseconds modulo 1000.
microseconds = (nanoseconds / 1000f64).floor();
nanoseconds %= 1000f64;
// c. Set milliseconds to floor(microseconds / 1000).
// d. Set microseconds to microseconds modulo 1000.
milliseconds = (microseconds / 1000f64).floor();
microseconds %= 1000f64;
}
// 13. Else if largestUnit is "microsecond", then
TemporalUnit::Microsecond => {
// a. Set microseconds to floor(nanoseconds / 1000).
// b. Set nanoseconds to nanoseconds modulo 1000.
microseconds = (nanoseconds / 1000f64).floor();
nanoseconds %= 1000f64;
}
// 14. Else,
// a. Assert: largestUnit is "nanosecond".
_ => debug_assert!(largest_unit == TemporalUnit::Nanosecond),
}
let result_values = Vec::from(&[
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
]);
// 15. For each value v of « days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds », do
for value in result_values {
// a. If 𝔽(v) is not finite, then
if !value.is_finite() {
// i. If sign = 1, then
if sign == 1 {
// 1. Return positive overflow.
return Ok((f64::INFINITY, None));
}
// ii. Else if sign = -1, then
// 1. Return negative overflow.
return Ok((f64::NEG_INFINITY, None));
}
}
let sign = f64::from(sign);
// 16. Return ? CreateTimeDurationRecord(days, hours × sign, minutes × sign, seconds × sign, milliseconds × sign, microseconds × sign, nanoseconds × sign).
let result = Self::new(
hours * sign,
minutes * sign,
seconds * sign,
milliseconds * sign,
microseconds * sign,
nanoseconds * sign,
)?;
Ok((days, Some(result)))
}
}
// ==== TimeDuration's public API ====
impl TimeDuration {
/// Creates a new validated `TimeDuration`.
pub fn new(
hours: f64,
minutes: f64,
seconds: f64,
milliseconds: f64,
microseconds: f64,
nanoseconds: f64,
) -> TemporalResult<Self> {
let result = Self::new_unchecked(
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
);
if !is_valid_duration(&result.into_iter().collect()) {
return Err(
TemporalError::range().with_message("Attempted to create an invalid TimeDuration.")
);
}
Ok(result)
}
/// Creates a partial `TimeDuration` with all values set to `NaN`.
#[must_use]
pub const fn partial() -> Self {
Self {
hours: f64::NAN,
minutes: f64::NAN,
seconds: f64::NAN,
milliseconds: f64::NAN,
microseconds: f64::NAN,
nanoseconds: f64::NAN,
}
}
/// Creates a `TimeDuration` from a provided partial `TimeDuration`.
#[must_use]
pub fn from_partial(partial: &TimeDuration) -> Self {
Self {
hours: if partial.hours.is_nan() {
0.0
} else {
partial.hours
},
minutes: if partial.minutes.is_nan() {
0.0
} else {
partial.minutes
},
seconds: if partial.seconds.is_nan() {
0.0
} else {
partial.seconds
},
milliseconds: if partial.milliseconds.is_nan() {
0.0
} else {
partial.milliseconds
},
microseconds: if partial.microseconds.is_nan() {
0.0
} else {
partial.microseconds
},
nanoseconds: if partial.nanoseconds.is_nan() {
0.0
} else {
partial.nanoseconds
},
}
}
/// Returns a new `TimeDuration` representing the absolute value of the current.
#[inline]
#[must_use]
pub fn abs(&self) -> Self {
Self {
hours: self.hours.abs(),
minutes: self.minutes.abs(),
seconds: self.seconds.abs(),
milliseconds: self.milliseconds.abs(),
microseconds: self.microseconds.abs(),
nanoseconds: self.nanoseconds.abs(),
}
}
/// Returns a negated `TimeDuration`.
#[inline]
#[must_use]
pub fn neg(&self) -> Self {
Self {
hours: self.hours * -1f64,
minutes: self.minutes * -1f64,
seconds: self.seconds * -1f64,
milliseconds: self.milliseconds * -1f64,
microseconds: self.microseconds * -1f64,
nanoseconds: self.nanoseconds * -1f64,
}
}
/// Balances a `TimeDuration` given a day value and the largest unit. `balance` will return
/// the balanced `day` and `TimeDuration`.
///
/// # Errors:
/// - Will error if provided duration is invalid
pub fn balance(&self, days: f64, largest_unit: TemporalUnit) -> TemporalResult<(f64, Self)> {
let result = Self::balance_possibly_infinite_time_duration(
days,
self.hours,
self.minutes,
self.seconds,
self.milliseconds,
self.microseconds,
self.nanoseconds,
largest_unit,
)?;
let Some(time_duration) = result.1 else {
return Err(TemporalError::range().with_message("Invalid balance TimeDuration."));
};
Ok((result.0, time_duration))
}
/// Utility function for returning if values in a valid range.
#[inline]
#[must_use]
pub fn is_within_range(&self) -> bool {
self.hours.abs() < 24f64
&& self.minutes.abs() < 60f64
&& self.seconds.abs() < 60f64
&& self.milliseconds.abs() < 1000f64
&& self.milliseconds.abs() < 1000f64
&& self.milliseconds.abs() < 1000f64
}
/// Returns the `[[hours]]` value.
#[must_use]
pub const fn hours(&self) -> f64 {
self.hours
}
/// Returns the `[[minutes]]` value.
#[must_use]
pub const fn minutes(&self) -> f64 {
self.minutes
}
/// Returns the `[[seconds]]` value.
#[must_use]
pub const fn seconds(&self) -> f64 {
self.seconds
}
/// Returns the `[[milliseconds]]` value.
#[must_use]
pub const fn milliseconds(&self) -> f64 {
self.milliseconds
}
/// Returns the `[[microseconds]]` value.
#[must_use]
pub const fn microseconds(&self) -> f64 {
self.microseconds
}
/// Returns the `[[nanoseconds]]` value.
#[must_use]
pub const fn nanoseconds(&self) -> f64 {
self.nanoseconds
}
/// Returns the `TimeDuration`'s iterator.
#[must_use]
pub fn iter(&self) -> TimeIter<'_> {
<&Self as IntoIterator>::into_iter(self)
}
}
// ==== TimeDuration method impls ====
impl TimeDuration {
/// Rounds the current `TimeDuration` given a rounding increment, unit and rounding mode. `round` will return a tuple of the rounded `TimeDuration` and
/// the `total` value of the smallest unit prior to rounding.
#[inline]
pub fn round(
&self,
increment: f64,
unit: TemporalUnit,
rounding_mode: TemporalRoundingMode,
) -> TemporalResult<(Self, f64)> {
let fraction_seconds = match unit {
TemporalUnit::Year
| TemporalUnit::Month
| TemporalUnit::Week
| TemporalUnit::Day
| TemporalUnit::Auto => {
return Err(TemporalError::r#type()
.with_message("Invalid unit provided to for TimeDuration to round."))
}
_ => self.nanoseconds().mul_add(
1_000_000_000f64,
self.microseconds().mul_add(
1_000_000f64,
self.milliseconds().mul_add(1000f64, self.seconds()),
),
),
};
match unit {
// 12. Else if unit is "hour", then
TemporalUnit::Hour => {
// a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours.
let frac_hours = (fraction_seconds / 60f64 + self.minutes) / 60f64 + self.hours;
// b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode).
let rounded_hours =
utils::round_number_to_increment(frac_hours, increment, rounding_mode);
// c. Set total to fractionalHours.
// d. Set minutes, seconds, milliseconds, microseconds, and nanoseconds to 0.
let result = Self::new(rounded_hours, 0f64, 0f64, 0f64, 0f64, 0f64)?;
Ok((result, frac_hours))
}
// 13. Else if unit is "minute", then
TemporalUnit::Minute => {
// a. Let fractionalMinutes be fractionalSeconds / 60 + minutes.
let frac_minutes = fraction_seconds / 60f64 + self.minutes;
// b. Set minutes to RoundNumberToIncrement(fractionalMinutes, increment, roundingMode).
let rounded_minutes =
utils::round_number_to_increment(frac_minutes, increment, rounding_mode);
// c. Set total to fractionalMinutes.
// d. Set seconds, milliseconds, microseconds, and nanoseconds to 0.
let result = Self::new(self.hours, rounded_minutes, 0f64, 0f64, 0f64, 0f64)?;
Ok((result, frac_minutes))
}
// 14. Else if unit is "second", then
TemporalUnit::Second => {
// a. Set seconds to RoundNumberToIncrement(fractionalSeconds, increment, roundingMode).
let rounded_seconds =
utils::round_number_to_increment(fraction_seconds, increment, rounding_mode);
// b. Set total to fractionalSeconds.
// c. Set milliseconds, microseconds, and nanoseconds to 0.
let result =
Self::new(self.hours, self.minutes, rounded_seconds, 0f64, 0f64, 0f64)?;
Ok((result, fraction_seconds))
}
// 15. Else if unit is "millisecond", then
TemporalUnit::Millisecond => {
// a. Let fractionalMilliseconds be nanoseconds × 10-6 + microseconds × 10-3 + milliseconds.
let fraction_millis = self.nanoseconds.mul_add(
1_000_000f64,
self.microseconds.mul_add(1_000f64, self.milliseconds),
);
// b. Set milliseconds to RoundNumberToIncrement(fractionalMilliseconds, increment, roundingMode).
let rounded_millis =
utils::round_number_to_increment(fraction_millis, increment, rounding_mode);
// c. Set total to fractionalMilliseconds.
// d. Set microseconds and nanoseconds to 0.
let result = Self::new(
self.hours,
self.minutes,
self.seconds,
rounded_millis,
0f64,
0f64,
)?;
Ok((result, fraction_millis))
}
// 16. Else if unit is "microsecond", then
TemporalUnit::Microsecond => {
// a. Let fractionalMicroseconds be nanoseconds × 10-3 + microseconds.
let frac_micros = self.nanoseconds.mul_add(1_000f64, self.microseconds);
// b. Set microseconds to RoundNumberToIncrement(fractionalMicroseconds, increment, roundingMode).
let rounded_micros =
utils::round_number_to_increment(frac_micros, increment, rounding_mode);
// c. Set total to fractionalMicroseconds.
// d. Set nanoseconds to 0.
let result = Self::new(
self.hours,
self.minutes,
self.seconds,
self.milliseconds,
rounded_micros,
0f64,
)?;
Ok((result, frac_micros))
}
// 17. Else,
TemporalUnit::Nanosecond => {
// a. Assert: unit is "nanosecond".
// b. Set total to nanoseconds.
let total = self.nanoseconds;
// c. Set nanoseconds to RoundNumberToIncrement(nanoseconds, increment, roundingMode).
let rounded_nanos =
utils::round_number_to_increment(self.nanoseconds, increment, rounding_mode);
let result = Self::new(
self.hours,
self.minutes,
self.seconds,
self.milliseconds,
self.microseconds,
rounded_nanos,
)?;
Ok((result, total))
}
_ => unreachable!("All other units early return error."),
}
}
}
impl<'a> IntoIterator for &'a TimeDuration {
type Item = f64;
type IntoIter = TimeIter<'a>;
fn into_iter(self) -> Self::IntoIter {
TimeIter {
time: self,
index: 0,
}
}
}
/// An iterator over a `TimeDuration`.
#[derive(Debug, Clone)]
pub struct TimeIter<'a> {
time: &'a TimeDuration,
index: usize,
}
impl Iterator for TimeIter<'_> {
type Item = f64;
fn next(&mut self) -> Option<Self::Item> {
let result = match self.index {
0 => Some(self.time.hours),
1 => Some(self.time.minutes),
2 => Some(self.time.seconds),
3 => Some(self.time.milliseconds),
4 => Some(self.time.microseconds),
5 => Some(self.time.nanoseconds),
_ => None,
};
self.index += 1;
result
}
}

318
core/temporal/src/components/instant.rs

@ -1,318 +0,0 @@
//! An implementation of the Temporal Instant.
use crate::{
components::{duration::TimeDuration, Duration},
options::{TemporalRoundingMode, TemporalUnit},
utils, TemporalError, TemporalResult, MS_PER_DAY, NS_PER_DAY,
};
use num_bigint::BigInt;
use num_traits::{FromPrimitive, ToPrimitive};
const NANOSECONDS_PER_SECOND: f64 = 1e9;
const NANOSECONDS_PER_MINUTE: f64 = 60f64 * NANOSECONDS_PER_SECOND;
const NANOSECONDS_PER_HOUR: f64 = 60f64 * NANOSECONDS_PER_MINUTE;
/// The native Rust implementation of `Temporal.Instant`
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Instant {
pub(crate) nanos: BigInt,
}
// ==== Private API ====
impl Instant {
/// Adds a `TimeDuration` to the current `Instant`.
///
/// Temporal-Proposal equivalent: `AddDurationToOrSubtractDurationFrom`.
pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult<Self> {
let result = self.epoch_nanoseconds()
+ duration.nanoseconds
+ (duration.microseconds * 1000f64)
+ (duration.milliseconds * 1_000_000f64)
+ (duration.seconds * NANOSECONDS_PER_SECOND)
+ (duration.minutes * NANOSECONDS_PER_MINUTE)
+ (duration.hours * NANOSECONDS_PER_HOUR);
let nanos = BigInt::from_f64(result).ok_or_else(|| {
TemporalError::range().with_message("Duration added to instant exceeded valid range.")
})?;
Self::new(nanos)
}
// TODO: Add test for `diff_instant`.
// NOTE(nekevss): As the below is internal, op will be left as a boolean
// with a `since` op being true and `until` being false.
/// Internal operation to handle `since` and `until` difference ops.
#[allow(unused)]
pub(crate) fn diff_instant(
&self,
op: bool,
other: &Self,
rounding_mode: Option<TemporalRoundingMode>,
rounding_increment: Option<f64>,
largest_unit: Option<TemporalUnit>,
smallest_unit: Option<TemporalUnit>,
) -> TemporalResult<TimeDuration> {
// diff the instant and determine its component values.
let diff = self.to_f64() - other.to_f64();
let nanos = diff.rem_euclid(1000f64);
let micros = (diff / 1000f64).trunc().rem_euclid(1000f64);
let millis = (diff / 1_000_000f64).trunc().rem_euclid(1000f64);
let secs = (diff / NANOSECONDS_PER_SECOND).trunc();
// Handle the settings provided to `diff_instant`
let rounding_increment = rounding_increment.unwrap_or(1.0);
let rounding_mode = if op {
rounding_mode
.unwrap_or(TemporalRoundingMode::Trunc)
.negate()
} else {
rounding_mode.unwrap_or(TemporalRoundingMode::Trunc)
};
let smallest_unit = smallest_unit.unwrap_or(TemporalUnit::Nanosecond);
// Use the defaultlargestunit which is max smallestlargestdefault and smallestunit
let largest_unit = largest_unit.unwrap_or(smallest_unit.max(TemporalUnit::Second));
// TODO: validate roundingincrement
// Steps 11-13 of 13.47 GetDifferenceSettings
if smallest_unit == TemporalUnit::Nanosecond {
let (_, result) = TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos)
.balance(0f64, largest_unit)?;
return Ok(result);
}
let (round_result, _) = TimeDuration::new(0f64, 0f64, secs, millis, micros, nanos)?.round(
rounding_increment,
smallest_unit,
rounding_mode,
)?;
let (_, result) = round_result.balance(0f64, largest_unit)?;
Ok(result)
}
/// Rounds a current `Instant` given the resolved options, returning a `BigInt` result.
pub(crate) fn round_instant(
&self,
increment: f64,
unit: TemporalUnit,
rounding_mode: TemporalRoundingMode,
) -> TemporalResult<BigInt> {
let increment_nanos = match unit {
TemporalUnit::Hour => increment * NANOSECONDS_PER_HOUR,
TemporalUnit::Minute => increment * NANOSECONDS_PER_MINUTE,
TemporalUnit::Second => increment * NANOSECONDS_PER_SECOND,
TemporalUnit::Millisecond => increment * 1_000_000f64,
TemporalUnit::Microsecond => increment * 1_000f64,
TemporalUnit::Nanosecond => increment,
_ => {
return Err(TemporalError::range()
.with_message("Invalid unit provided for Instant::round."))
}
};
let rounded = utils::round_number_to_increment_as_if_positive(
self.to_f64(),
increment_nanos,
rounding_mode,
);
BigInt::from_f64(rounded)
.ok_or_else(|| TemporalError::range().with_message("Invalid rounded Instant value."))
}
/// Utility for converting `Instant` to f64.
///
/// # Panics
///
/// This function will panic if called on an invalid `Instant`.
pub(crate) fn to_f64(&self) -> f64 {
self.nanos
.to_f64()
.expect("A valid instant is representable by f64.")
}
}
// ==== Public API ====
impl Instant {
/// Create a new validated `Instant`.
#[inline]
pub fn new(nanos: BigInt) -> TemporalResult<Self> {
if !is_valid_epoch_nanos(&nanos) {
return Err(TemporalError::range()
.with_message("Instant nanoseconds are not within a valid epoch range."));
}
Ok(Self { nanos })
}
/// Adds a `Duration` to the current `Instant`, returning an error if the `Duration`
/// contains a `DateDuration`.
#[inline]
pub fn add(&self, duration: Duration) -> TemporalResult<Self> {
if !duration.is_time_duration() {
return Err(TemporalError::range()
.with_message("DateDuration values cannot be added to instant."));
}
self.add_time_duration(duration.time())
}
/// Adds a `TimeDuration` to `Instant`.
#[inline]
pub fn add_time_duration(&self, duration: &TimeDuration) -> TemporalResult<Self> {
self.add_to_instant(duration)
}
/// Subtract a `Duration` to the current `Instant`, returning an error if the `Duration`
/// contains a `DateDuration`.
#[inline]
pub fn subtract(&self, duration: Duration) -> TemporalResult<Self> {
if !duration.is_time_duration() {
return Err(TemporalError::range()
.with_message("DateDuration values cannot be added to instant."));
}
self.subtract_time_duration(duration.time())
}
/// Subtracts a `TimeDuration` to `Instant`.
#[inline]
pub fn subtract_time_duration(&self, duration: &TimeDuration) -> TemporalResult<Self> {
self.add_to_instant(&duration.neg())
}
/// Returns a `TimeDuration` representing the duration since provided `Instant`
#[inline]
pub fn since(
&self,
other: &Self,
rounding_mode: Option<TemporalRoundingMode>,
rounding_increment: Option<f64>,
largest_unit: Option<TemporalUnit>,
smallest_unit: Option<TemporalUnit>,
) -> TemporalResult<TimeDuration> {
self.diff_instant(
true,
other,
rounding_mode,
rounding_increment,
largest_unit,
smallest_unit,
)
}
/// Returns a `TimeDuration` representing the duration until provided `Instant`
#[inline]
pub fn until(
&self,
other: &Self,
rounding_mode: Option<TemporalRoundingMode>,
rounding_increment: Option<f64>,
largest_unit: Option<TemporalUnit>,
smallest_unit: Option<TemporalUnit>,
) -> TemporalResult<TimeDuration> {
self.diff_instant(
false,
other,
rounding_mode,
rounding_increment,
largest_unit,
smallest_unit,
)
}
/// Returns an `Instant` by rounding the current `Instant` according to the provided settings.
pub fn round(
&self,
increment: Option<f64>,
unit: TemporalUnit, // smallestUnit is required on Instant::round
rounding_mode: Option<TemporalRoundingMode>,
) -> TemporalResult<Self> {
let increment = utils::to_rounding_increment(increment)?;
let mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand);
let maximum = match unit {
TemporalUnit::Hour => 24u64,
TemporalUnit::Minute => 24 * 60,
TemporalUnit::Second => 24 * 3600,
TemporalUnit::Millisecond => MS_PER_DAY as u64,
TemporalUnit::Microsecond => MS_PER_DAY as u64 * 1000,
TemporalUnit::Nanosecond => NS_PER_DAY as u64,
_ => return Err(TemporalError::range().with_message("Invalid roundTo unit provided.")),
};
// NOTE: to_rounding_increment returns an f64 within a u32 range.
utils::validate_temporal_rounding_increment(increment as u32, maximum, true)?;
let round_result = self.round_instant(increment, unit, mode)?;
Self::new(round_result)
}
/// Returns the `epochSeconds` value for this `Instant`.
#[must_use]
pub fn epoch_seconds(&self) -> f64 {
(&self.nanos / BigInt::from(1_000_000_000))
.to_f64()
.expect("A validated Instant should be within a valid f64")
.floor()
}
/// Returns the `epochMilliseconds` value for this `Instant`.
#[must_use]
pub fn epoch_milliseconds(&self) -> f64 {
(&self.nanos / BigInt::from(1_000_000))
.to_f64()
.expect("A validated Instant should be within a valid f64")
.floor()
}
/// Returns the `epochMicroseconds` value for this `Instant`.
#[must_use]
pub fn epoch_microseconds(&self) -> f64 {
(&self.nanos / BigInt::from(1_000))
.to_f64()
.expect("A validated Instant should be within a valid f64")
.floor()
}
/// Returns the `epochNanoseconds` value for this `Instant`.
#[must_use]
pub fn epoch_nanoseconds(&self) -> f64 {
self.to_f64()
}
}
// ==== Utility Functions ====
/// Utility for determining if the nanos are within a valid range.
#[inline]
#[must_use]
pub(crate) fn is_valid_epoch_nanos(nanos: &BigInt) -> bool {
nanos <= &BigInt::from(crate::NS_MAX_INSTANT) && nanos >= &BigInt::from(crate::NS_MIN_INSTANT)
}
// ==== Instant Tests ====
#[cfg(test)]
mod tests {
use crate::{components::Instant, NS_MAX_INSTANT, NS_MIN_INSTANT};
use num_bigint::BigInt;
use num_traits::ToPrimitive;
#[test]
#[allow(clippy::float_cmp)]
fn max_and_minimum_instant_bounds() {
// This test is primarily to assert that the `expect` in the epoch methods is
// valid, i.e., a valid instant is within the range of an f64.
let max = BigInt::from(NS_MAX_INSTANT);
let min = BigInt::from(NS_MIN_INSTANT);
let max_instant = Instant::new(max.clone()).unwrap();
let min_instant = Instant::new(min.clone()).unwrap();
assert_eq!(max_instant.epoch_nanoseconds(), max.to_f64().unwrap());
assert_eq!(min_instant.epoch_nanoseconds(), min.to_f64().unwrap());
let max_plus_one = BigInt::from(NS_MAX_INSTANT + 1);
let min_minus_one = BigInt::from(NS_MIN_INSTANT - 1);
assert!(Instant::new(max_plus_one).is_err());
assert!(Instant::new(min_minus_one).is_err());
}
}

46
core/temporal/src/components/mod.rs

@ -1,46 +0,0 @@
//! The primary date-time components provided by Temporal.
//!
//! The below components are the main primitives of the `Temporal` specification:
//! - `Date` -> `PlainDate`
//! - `DateTime` -> `PlainDateTime`
//! - `Time` -> `PlainTime`
//! - `Duration` -> `Duration`
//! - `Instant` -> `Instant`
//! - `MonthDay` -> `PlainMonthDay`
//! - `YearMonth` -> `PlainYearMonth`
//! - `ZonedDateTime` -> `ZonedDateTime`
//!
//! The Temporal specification, along with this implementation aims to provide
//! full support for time zones and non-gregorian calendars that are compliant
//! with standards like ISO 8601, RFC 3339, and RFC 5545.
// TODO: Expand upon above introduction.
pub mod calendar;
pub mod duration;
pub mod tz;
mod date;
mod datetime;
mod instant;
mod month_day;
mod time;
mod year_month;
mod zoneddatetime;
#[doc(inline)]
pub use date::Date;
#[doc(inline)]
pub use datetime::DateTime;
#[doc(inline)]
pub use duration::Duration;
#[doc(inline)]
pub use instant::Instant;
#[doc(inline)]
pub use month_day::MonthDay;
#[doc(inline)]
pub use time::Time;
#[doc(inline)]
pub use year_month::YearMonth;
#[doc(inline)]
pub use zoneddatetime::ZonedDateTime;

92
core/temporal/src/components/month_day.rs

@ -1,92 +0,0 @@
//! This module implements `MonthDay` and any directly related algorithms.
use std::str::FromStr;
use crate::{
components::calendar::CalendarSlot,
iso::{IsoDate, IsoDateSlots},
options::ArithmeticOverflow,
TemporalError, TemporalResult,
};
use super::calendar::{CalendarProtocol, GetCalendarSlot};
/// The native Rust implementation of `Temporal.PlainMonthDay`
#[derive(Debug, Default, Clone)]
pub struct MonthDay<C: CalendarProtocol> {
iso: IsoDate,
calendar: CalendarSlot<C>,
}
impl<C: CalendarProtocol> MonthDay<C> {
/// Creates a new unchecked `MonthDay`
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot<C>) -> Self {
Self { iso, calendar }
}
/// Creates a new valid `MonthDay`.
#[inline]
pub fn new(
month: i32,
day: i32,
calendar: CalendarSlot<C>,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
let iso = IsoDate::new(1972, month, day, overflow)?;
Ok(Self::new_unchecked(iso, calendar))
}
/// Returns the `month` value of `MonthDay`.
#[inline]
#[must_use]
pub fn month(&self) -> u8 {
self.iso.month
}
/// Returns the `day` value of `MonthDay`.
#[inline]
#[must_use]
pub fn day(&self) -> u8 {
self.iso.day
}
/// Returns a reference to `MonthDay`'s `CalendarSlot`
#[inline]
#[must_use]
pub fn calendar(&self) -> &CalendarSlot<C> {
&self.calendar
}
}
impl<C: CalendarProtocol> GetCalendarSlot<C> for MonthDay<C> {
fn get_calendar(&self) -> CalendarSlot<C> {
self.calendar.clone()
}
}
impl<C: CalendarProtocol> IsoDateSlots for MonthDay<C> {
#[inline]
/// Returns this structs `IsoDate`.
fn iso_date(&self) -> IsoDate {
self.iso
}
}
impl<C: CalendarProtocol> FromStr for MonthDay<C> {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let record = crate::parser::parse_month_day(s)?;
let calendar = record.calendar.unwrap_or("iso8601".into());
Self::new(
record.date.month,
record.date.day,
CalendarSlot::from_str(&calendar)?,
ArithmeticOverflow::Reject,
)
}
}

274
core/temporal/src/components/time.rs

@ -1,274 +0,0 @@
//! This module implements `Time` and any directly related algorithms.
use crate::{
components::{duration::TimeDuration, Duration},
iso::IsoTime,
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit},
utils, TemporalError, TemporalResult,
};
/// The native Rust implementation of `Temporal.PlainTime`.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
iso: IsoTime,
}
// ==== Private API ====
impl Time {
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoTime) -> Self {
Self { iso }
}
/// Returns true if a valid `Time`.
#[allow(dead_code)]
pub(crate) fn is_valid(&self) -> bool {
self.iso.is_valid()
}
/// Adds a `TimeDuration` to the current `Time`.
///
/// Spec Equivalent: `AddDurationToOrSubtractDurationFromPlainTime` AND `AddTime`.
pub(crate) fn add_to_time(&self, duration: &TimeDuration) -> Self {
let (_, result) = IsoTime::balance(
f64::from(self.hour()) + duration.hours(),
f64::from(self.minute()) + duration.minutes(),
f64::from(self.second()) + duration.seconds(),
f64::from(self.millisecond()) + duration.milliseconds(),
f64::from(self.microsecond()) + duration.microseconds(),
f64::from(self.nanosecond()) + duration.nanoseconds(),
);
// NOTE (nekevss): IsoTime::balance should never return an invalid `IsoTime`
Self::new_unchecked(result)
}
}
// ==== Public API ====
impl Time {
/// Creates a new `IsoTime` value.
pub fn new(
hour: i32,
minute: i32,
second: i32,
millisecond: i32,
microsecond: i32,
nanosecond: i32,
overflow: ArithmeticOverflow,
) -> TemporalResult<Self> {
let time = IsoTime::new(
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
overflow,
)?;
Ok(Self::new_unchecked(time))
}
/// Returns the internal `hour` field.
#[inline]
#[must_use]
pub const fn hour(&self) -> u8 {
self.iso.hour
}
/// Returns the internal `minute` field.
#[inline]
#[must_use]
pub const fn minute(&self) -> u8 {
self.iso.minute
}
/// Returns the internal `second` field.
#[inline]
#[must_use]
pub const fn second(&self) -> u8 {
self.iso.second
}
/// Returns the internal `millisecond` field.
#[inline]
#[must_use]
pub const fn millisecond(&self) -> u16 {
self.iso.millisecond
}
/// Returns the internal `microsecond` field.
#[inline]
#[must_use]
pub const fn microsecond(&self) -> u16 {
self.iso.microsecond
}
/// Returns the internal `nanosecond` field.
#[inline]
#[must_use]
pub const fn nanosecond(&self) -> u16 {
self.iso.nanosecond
}
/// Add a `Duration` to the current `Time`.
pub fn add(&self, duration: &Duration) -> TemporalResult<Self> {
if !duration.is_time_duration() {
return Err(TemporalError::range()
.with_message("DateDuration values cannot be added to `Time`."));
}
Ok(self.add_time_duration(duration.time()))
}
/// Adds a `TimeDuration` to the current `Time`.
#[inline]
#[must_use]
pub fn add_time_duration(&self, duration: &TimeDuration) -> Self {
self.add_to_time(duration)
}
/// Subtract a `Duration` to the current `Time`.
pub fn subtract(&self, duration: &Duration) -> TemporalResult<Self> {
if !duration.is_time_duration() {
return Err(TemporalError::range()
.with_message("DateDuration values cannot be added to `Time` component."));
}
Ok(self.add_time_duration(duration.time()))
}
/// Adds a `TimeDuration` to the current `Time`.
#[inline]
#[must_use]
pub fn subtract_time_duration(&self, duration: &TimeDuration) -> Self {
self.add_to_time(&duration.neg())
}
// TODO (nekevss): optimize and test rounding_increment type (f64 vs. u64).
/// Rounds the current `Time` according to provided options.
pub fn round(
&self,
smallest_unit: TemporalUnit,
rounding_increment: Option<f64>,
rounding_mode: Option<TemporalRoundingMode>,
) -> TemporalResult<Self> {
let increment = utils::to_rounding_increment(rounding_increment)?;
let mode = rounding_mode.unwrap_or(TemporalRoundingMode::HalfExpand);
let max = smallest_unit
.to_maximum_rounding_increment()
.ok_or_else(|| {
TemporalError::range().with_message("smallestUnit must be a time value.")
})?;
// Safety (nekevss): to_rounding_increment returns a value in the range of a u32.
utils::validate_temporal_rounding_increment(increment as u32, u64::from(max), false)?;
let (_, result) = self.iso.round(increment, smallest_unit, mode, None)?;
Ok(Self::new_unchecked(result))
}
}
// ==== Test land ====
#[cfg(test)]
mod tests {
use crate::{components::Duration, iso::IsoTime, options::TemporalUnit};
use super::Time;
fn assert_time(result: Time, values: (u8, u8, u8, u16, u16, u16)) {
assert!(result.hour() == values.0);
assert!(result.minute() == values.1);
assert!(result.second() == values.2);
assert!(result.millisecond() == values.3);
assert!(result.microsecond() == values.4);
assert!(result.nanosecond() == values.5);
}
#[test]
fn time_round_millisecond() {
let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321));
let result_1 = base
.round(TemporalUnit::Millisecond, Some(1.0), None)
.unwrap();
assert_time(result_1, (3, 34, 56, 988, 0, 0));
let result_2 = base
.round(TemporalUnit::Millisecond, Some(2.0), None)
.unwrap();
assert_time(result_2, (3, 34, 56, 988, 0, 0));
let result_3 = base
.round(TemporalUnit::Millisecond, Some(4.0), None)
.unwrap();
assert_time(result_3, (3, 34, 56, 988, 0, 0));
let result_4 = base
.round(TemporalUnit::Millisecond, Some(5.0), None)
.unwrap();
assert_time(result_4, (3, 34, 56, 990, 0, 0));
}
#[test]
fn time_round_microsecond() {
let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321));
let result_1 = base
.round(TemporalUnit::Microsecond, Some(1.0), None)
.unwrap();
assert_time(result_1, (3, 34, 56, 987, 654, 0));
let result_2 = base
.round(TemporalUnit::Microsecond, Some(2.0), None)
.unwrap();
assert_time(result_2, (3, 34, 56, 987, 654, 0));
let result_3 = base
.round(TemporalUnit::Microsecond, Some(4.0), None)
.unwrap();
assert_time(result_3, (3, 34, 56, 987, 656, 0));
let result_4 = base
.round(TemporalUnit::Microsecond, Some(5.0), None)
.unwrap();
assert_time(result_4, (3, 34, 56, 987, 655, 0));
}
#[test]
fn time_round_nanoseconds() {
let base = Time::new_unchecked(IsoTime::new_unchecked(3, 34, 56, 987, 654, 321));
let result_1 = base
.round(TemporalUnit::Nanosecond, Some(1.0), None)
.unwrap();
assert_time(result_1, (3, 34, 56, 987, 654, 321));
let result_2 = base
.round(TemporalUnit::Nanosecond, Some(2.0), None)
.unwrap();
assert_time(result_2, (3, 34, 56, 987, 654, 322));
let result_3 = base
.round(TemporalUnit::Nanosecond, Some(4.0), None)
.unwrap();
assert_time(result_3, (3, 34, 56, 987, 654, 320));
let result_4 = base
.round(TemporalUnit::Nanosecond, Some(5.0), None)
.unwrap();
assert_time(result_4, (3, 34, 56, 987, 654, 320));
}
#[test]
fn add_duration_basic() {
let base = Time::new_unchecked(IsoTime::new_unchecked(15, 23, 30, 123, 456, 789));
let result = base.add(&"PT16H".parse::<Duration>().unwrap()).unwrap();
assert_time(result, (7, 23, 30, 123, 456, 789));
}
}

118
core/temporal/src/components/tz.rs

@ -1,118 +0,0 @@
//! This module implements the Temporal `TimeZone` and components.
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use crate::{
components::{calendar::CalendarSlot, DateTime, Instant},
TemporalError, TemporalResult,
};
use super::calendar::CalendarProtocol;
/// Any object that implements the `TzProtocol` must implement the below methods/properties.
pub const TIME_ZONE_PROPERTIES: [&str; 3] =
["getOffsetNanosecondsFor", "getPossibleInstantsFor", "id"];
/// The Time Zone Protocol that must be implemented for time zones.
pub trait TzProtocol: Clone {
/// The context passed to every method of the `TzProtocol`.
type Context;
/// Get the Offset nanoseconds for this `TimeZone`
fn get_offset_nanos_for(&self, context: &mut Self::Context) -> TemporalResult<BigInt>;
/// Get the possible Instant for this `TimeZone`
fn get_possible_instant_for(&self, context: &mut Self::Context)
-> TemporalResult<Vec<Instant>>; // TODO: Implement Instant
/// Get the `TimeZone`'s identifier.
fn id(&self, context: &mut Self::Context) -> TemporalResult<String>;
}
/// A Temporal `TimeZone`.
#[derive(Debug, Clone)]
#[allow(unused)]
pub struct TimeZone {
pub(crate) iana: Option<String>, // TODO: ICU4X IANA TimeZone support.
pub(crate) offset: Option<i16>,
}
/// The `TimeZoneSlot` represents a `[[TimeZone]]` internal slot value.
#[derive(Clone)]
pub enum TimeZoneSlot<Z: TzProtocol> {
/// A native `TimeZone` representation.
Tz(TimeZone),
/// A Custom `TimeZone` that implements the `TzProtocol`.
Protocol(Z),
}
impl<Z: TzProtocol> core::fmt::Debug for TimeZoneSlot<Z> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tz(tz) => write!(f, "{tz:?}"),
Self::Protocol(_) => write!(f, "TzProtocol"),
}
}
}
impl<Z: TzProtocol> TimeZoneSlot<Z> {
pub(crate) fn get_datetime_for<C: CalendarProtocol>(
&self,
instant: &Instant,
calendar: &CalendarSlot<C>,
context: &mut Z::Context,
) -> TemporalResult<DateTime<C>> {
let nanos = self.get_offset_nanos_for(context)?;
DateTime::from_instant(instant, nanos.to_f64().unwrap_or(0.0), calendar.clone())
}
}
impl<Z: TzProtocol> TimeZoneSlot<Z> {
/// Get the offset for this current `TimeZoneSlot`.
pub fn get_offset_nanos_for(&self, context: &mut Z::Context) -> TemporalResult<BigInt> {
// 1. Let timeZone be the this value.
// 2. Perform ? RequireInternalSlot(timeZone, [[InitializedTemporalTimeZone]]).
// 3. Set instant to ? ToTemporalInstant(instant).
match self {
Self::Tz(tz) => {
// 4. If timeZone.[[OffsetMinutes]] is not empty, return 𝔽(timeZone.[[OffsetMinutes]] × (60 × 10^9)).
if let Some(offset) = &tz.offset {
return Ok(BigInt::from(i64::from(*offset) * 60_000_000_000i64));
}
// 5. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(timeZone.[[Identifier]], instant.[[Nanoseconds]])).
Err(TemporalError::range().with_message("IANA TimeZone names not yet implemented."))
}
// Call any custom implemented TimeZone.
Self::Protocol(p) => p.get_offset_nanos_for(context),
}
}
/// Get the possible `Instant`s for this `TimeZoneSlot`.
pub fn get_possible_instant_for(
&self,
_context: &mut Z::Context,
) -> TemporalResult<Vec<Instant>> {
Err(TemporalError::general("Not yet implemented."))
}
/// Returns the current `TimeZoneSlot`'s identifier.
pub fn id(&self, context: &mut Z::Context) -> TemporalResult<String> {
match self {
Self::Tz(_) => Err(TemporalError::range().with_message("Not yet implemented.")), // TODO: Implement Display for Time Zone.
Self::Protocol(tz) => tz.id(context),
}
}
}
impl TzProtocol for () {
type Context = ();
fn get_offset_nanos_for(&self, (): &mut ()) -> TemporalResult<BigInt> {
unreachable!()
}
fn get_possible_instant_for(&self, (): &mut ()) -> TemporalResult<Vec<Instant>> {
unreachable!()
}
fn id(&self, (): &mut ()) -> TemporalResult<String> {
Ok("() TimeZone".to_owned())
}
}

96
core/temporal/src/components/year_month.rs

@ -1,96 +0,0 @@
//! This module implements `YearMonth` and any directly related algorithms.
use std::str::FromStr;
use crate::{
components::calendar::CalendarSlot,
iso::{IsoDate, IsoDateSlots},
options::ArithmeticOverflow,
TemporalError, TemporalResult,
};
use super::calendar::{CalendarProtocol, GetCalendarSlot};
/// The native Rust implementation of `Temporal.YearMonth`.
#[derive(Debug, Default, Clone)]
pub struct YearMonth<C: CalendarProtocol> {
iso: IsoDate,
calendar: CalendarSlot<C>,
}
impl<C: CalendarProtocol> YearMonth<C> {
/// Creates an unvalidated `YearMonth`.
#[inline]
#[must_use]
pub(crate) fn new_unchecked(iso: IsoDate, calendar: CalendarSlot<C>) -> Self {
Self { iso, calendar }
}
/// Creates a new valid `YearMonth`.
#[inline]
pub fn new(
year: i32,
month: i32,
reference_day: Option<i32>,
calendar: CalendarSlot<C>,
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))
}
/// Returns the `year` value for this `YearMonth`.
#[inline]
#[must_use]
pub fn year(&self) -> i32 {
self.iso.year
}
/// Returns the `month` value for this `YearMonth`.
#[inline]
#[must_use]
pub fn month(&self) -> u8 {
self.iso.month
}
/// Returns the Calendar value.
#[inline]
#[must_use]
pub fn calendar(&self) -> &CalendarSlot<C> {
&self.calendar
}
}
impl<C: CalendarProtocol> GetCalendarSlot<C> for YearMonth<C> {
/// Returns a reference to `YearMonth`'s `CalendarSlot`
fn get_calendar(&self) -> CalendarSlot<C> {
self.calendar.clone()
}
}
impl<C: CalendarProtocol> IsoDateSlots for YearMonth<C> {
#[inline]
/// Returns this `YearMonth`'s `IsoDate`
fn iso_date(&self) -> IsoDate {
self.iso
}
}
impl<C: CalendarProtocol> FromStr for YearMonth<C> {
type Err = TemporalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let record = crate::parser::parse_year_month(s)?;
let calendar = record.calendar.unwrap_or("iso8601".into());
Self::new(
record.date.year,
record.date.month,
None,
CalendarSlot::from_str(&calendar)?,
ArithmeticOverflow::Reject,
)
}
}

234
core/temporal/src/components/zoneddatetime.rs

@ -1,234 +0,0 @@
//! This module implements `ZonedDateTime` and any directly related algorithms.
use num_bigint::BigInt;
use tinystr::TinyStr4;
use crate::{
components::{
calendar::{CalendarDateLike, CalendarProtocol, CalendarSlot},
tz::TimeZoneSlot,
Instant,
},
TemporalResult,
};
use super::tz::TzProtocol;
/// The native Rust implementation of `Temporal.ZonedDateTime`.
#[derive(Debug, Clone)]
pub struct ZonedDateTime<C: CalendarProtocol, Z: TzProtocol> {
instant: Instant,
calendar: CalendarSlot<C>,
tz: TimeZoneSlot<Z>,
}
// ==== Private API ====
impl<C: CalendarProtocol, Z: TzProtocol> ZonedDateTime<C, Z> {
/// Creates a `ZonedDateTime` without validating the input.
#[inline]
#[must_use]
pub(crate) fn new_unchecked(
instant: Instant,
calendar: CalendarSlot<C>,
tz: TimeZoneSlot<Z>,
) -> Self {
Self {
instant,
calendar,
tz,
}
}
}
// ==== Public API ====
impl<C: CalendarProtocol, Z: TzProtocol> ZonedDateTime<C, Z> {
/// Creates a new valid `ZonedDateTime`.
#[inline]
pub fn new(
nanos: BigInt,
calendar: CalendarSlot<C>,
tz: TimeZoneSlot<Z>,
) -> TemporalResult<Self> {
let instant = Instant::new(nanos)?;
Ok(Self::new_unchecked(instant, calendar, tz))
}
/// Returns `ZonedDateTime`'s Calendar.
#[inline]
#[must_use]
pub fn calendar(&self) -> &CalendarSlot<C> {
&self.calendar
}
/// Returns `ZonedDateTime`'s `TimeZone` slot.
#[inline]
#[must_use]
pub fn tz(&self) -> &TimeZoneSlot<Z> {
&self.tz
}
/// Returns the `epochSeconds` value of this `ZonedDateTime`.
#[must_use]
pub fn epoch_seconds(&self) -> f64 {
self.instant.epoch_seconds()
}
/// Returns the `epochMilliseconds` value of this `ZonedDateTime`.
#[must_use]
pub fn epoch_milliseconds(&self) -> f64 {
self.instant.epoch_milliseconds()
}
/// Returns the `epochMicroseconds` value of this `ZonedDateTime`.
#[must_use]
pub fn epoch_microseconds(&self) -> f64 {
self.instant.epoch_microseconds()
}
/// Returns the `epochNanoseconds` value of this `ZonedDateTime`.
#[must_use]
pub fn epoch_nanoseconds(&self) -> f64 {
self.instant.epoch_nanoseconds()
}
}
// ==== Context based API ====
impl<C, Z: TzProtocol> ZonedDateTime<C, Z>
where
C: CalendarProtocol<Context = Z::Context>,
{
/// Returns the `year` value for this `ZonedDateTime`.
#[inline]
pub fn contextual_year(&self, context: &mut C::Context) -> TemporalResult<i32> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
self.calendar.year(&CalendarDateLike::DateTime(dt), context)
}
/// Returns the `month` value for this `ZonedDateTime`.
pub fn contextual_month(&self, context: &mut C::Context) -> TemporalResult<u8> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
self.calendar
.month(&CalendarDateLike::DateTime(dt), context)
}
/// Returns the `monthCode` value for this `ZonedDateTime`.
pub fn contextual_month_code(&self, context: &mut C::Context) -> TemporalResult<TinyStr4> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
self.calendar
.month_code(&CalendarDateLike::DateTime(dt), context)
}
/// Returns the `day` value for this `ZonedDateTime`.
pub fn contextual_day(&self, context: &mut C::Context) -> TemporalResult<u8> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
self.calendar.day(&CalendarDateLike::DateTime(dt), context)
}
/// Returns the `hour` value for this `ZonedDateTime`.
pub fn contextual_hour(&self, context: &mut C::Context) -> TemporalResult<u8> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
Ok(dt.hour())
}
/// Returns the `minute` value for this `ZonedDateTime`.
pub fn contextual_minute(&self, context: &mut C::Context) -> TemporalResult<u8> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
Ok(dt.minute())
}
/// Returns the `second` value for this `ZonedDateTime`.
pub fn contextual_second(&self, context: &mut C::Context) -> TemporalResult<u8> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
Ok(dt.second())
}
/// Returns the `millisecond` value for this `ZonedDateTime`.
pub fn contextual_millisecond(&self, context: &mut C::Context) -> TemporalResult<u16> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
Ok(dt.millisecond())
}
/// Returns the `microsecond` value for this `ZonedDateTime`.
pub fn contextual_microsecond(&self, context: &mut C::Context) -> TemporalResult<u16> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
Ok(dt.millisecond())
}
/// Returns the `nanosecond` value for this `ZonedDateTime`.
pub fn contextual_nanosecond(&self, context: &mut C::Context) -> TemporalResult<u16> {
let dt = self
.tz
.get_datetime_for(&self.instant, &self.calendar, context)?;
Ok(dt.nanosecond())
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::components::tz::TimeZone;
use num_bigint::BigInt;
use super::{CalendarSlot, TimeZoneSlot, ZonedDateTime};
#[test]
fn basic_zdt_test() {
let nov_30_2023_utc = BigInt::from(1_701_308_952_000_000_000i64);
let zdt = ZonedDateTime::<(), ()>::new(
nov_30_2023_utc.clone(),
CalendarSlot::from_str("iso8601").unwrap(),
TimeZoneSlot::Tz(TimeZone {
iana: None,
offset: Some(0),
}),
)
.unwrap();
assert_eq!(zdt.contextual_year(&mut ()).unwrap(), 2023);
assert_eq!(zdt.contextual_month(&mut ()).unwrap(), 11);
assert_eq!(zdt.contextual_day(&mut ()).unwrap(), 30);
assert_eq!(zdt.contextual_hour(&mut ()).unwrap(), 1);
assert_eq!(zdt.contextual_minute(&mut ()).unwrap(), 49);
assert_eq!(zdt.contextual_second(&mut ()).unwrap(), 12);
let zdt_minus_five = ZonedDateTime::<(), ()>::new(
nov_30_2023_utc,
CalendarSlot::from_str("iso8601").unwrap(),
TimeZoneSlot::Tz(TimeZone {
iana: None,
offset: Some(-300),
}),
)
.unwrap();
assert_eq!(zdt_minus_five.contextual_year(&mut ()).unwrap(), 2023);
assert_eq!(zdt_minus_five.contextual_month(&mut ()).unwrap(), 11);
assert_eq!(zdt_minus_five.contextual_day(&mut ()).unwrap(), 29);
assert_eq!(zdt_minus_five.contextual_hour(&mut ()).unwrap(), 20);
assert_eq!(zdt_minus_five.contextual_minute(&mut ()).unwrap(), 49);
assert_eq!(zdt_minus_five.contextual_second(&mut ()).unwrap(), 12);
}
}

121
core/temporal/src/error.rs

@ -1,121 +0,0 @@
//! This module implements `TemporalError`.
use core::fmt;
use icu_calendar::CalendarError;
/// `TemporalError`'s error type.
#[derive(Debug, Default, Clone, Copy)]
pub enum ErrorKind {
/// Error.
#[default]
Generic,
/// TypeError
Type,
/// RangeError
Range,
/// SyntaxError
Syntax,
}
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",
Self::Syntax => "SyntaxError",
}
.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)
}
/// Create a syntax error.
#[must_use]
pub fn syntax() -> Self {
Self::new(ErrorKind::Syntax)
}
/// Create an abrupt end error.
#[must_use]
pub fn abrupt_end() -> Self {
Self::syntax().with_message("Abrupt end to parsing target.")
}
/// 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(())
}
}
impl From<CalendarError> for TemporalError {
fn from(value: CalendarError) -> Self {
TemporalError::general(value.to_string())
}
}

640
core/temporal/src/fields.rs

@ -1,640 +0,0 @@
//! This module implements a native Rust `TemporalField` and components.
use std::{fmt, str::FromStr};
use crate::{
components::calendar::{CalendarProtocol, CalendarSlot},
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),
}
impl From<i32> for FieldValue {
fn from(value: i32) -> Self {
FieldValue::Integer(value)
}
}
/// 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"))),
}
}
}
/// `TemporalFields` acts as a native Rust implementation of the `fields` object
///
/// 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).
///
/// ## 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) fn era(&self) -> TinyAsciiStr<16> {
self.era.unwrap_or("default".parse().expect("less than 8"))
}
pub(crate) const fn year(&self) -> Option<i32> {
self.year
}
pub(crate) const fn month(&self) -> Option<i32> {
self.month
}
pub(crate) fn month_code(&self) -> TinyAsciiStr<4> {
// Passing along an invalid MonthCode to ICU...might be better to figure out a different approach...TBD.
self.month_code
.unwrap_or("M00".parse().expect("less than 4"))
}
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(())
}
/// Retrieves a field value if set, else None.
pub fn get(&self, field: &str) -> Option<FieldValue> {
if !self.is_set_field(field) {
return None;
}
match field {
"year" => self.year.map(FieldValue::Integer),
"month" => self.month.map(FieldValue::Integer),
"monthCode" => self.month_code.map(|s| FieldValue::String(s.to_string())),
"day" => self.day.map(FieldValue::from),
"hour" => Some(FieldValue::Integer(self.hour)),
"minute" => Some(FieldValue::Integer(self.minute)),
"second" => Some(FieldValue::Integer(self.second)),
"millisecond" => Some(FieldValue::Integer(self.millisecond)),
"microsecond" => Some(FieldValue::Integer(self.microsecond)),
"nanosecond" => Some(FieldValue::Integer(self.nanosecond)),
"offset" => self.offset.as_ref().map(|s| FieldValue::String(s.clone())),
"era" => self.era.map(|s| FieldValue::String(s.to_string())),
"eraYear" => self.era_year.map(FieldValue::Integer),
"timeZone" => self
.time_zone
.as_ref()
.map(|s| FieldValue::String(s.clone())),
_ => unreachable!(),
}
}
fn is_set_field(&self, field: &str) -> bool {
match field {
"year" => self.bit_map.contains(FieldMap::YEAR),
"month" => self.bit_map.contains(FieldMap::MONTH),
"monthCode" => self.bit_map.contains(FieldMap::MONTH_CODE),
"day" => self.bit_map.contains(FieldMap::DAY),
"hour" => self.bit_map.contains(FieldMap::HOUR),
"minute" => self.bit_map.contains(FieldMap::MINUTE),
"second" => self.bit_map.contains(FieldMap::SECOND),
"millisecond" => self.bit_map.contains(FieldMap::MILLISECOND),
"microsecond" => self.bit_map.contains(FieldMap::MICROSECOND),
"nanosecond" => self.bit_map.contains(FieldMap::NANOSECOND),
"offset" => self.bit_map.contains(FieldMap::OFFSET),
"era" => self.bit_map.contains(FieldMap::ERA),
"eraYear" => self.bit_map.contains(FieldMap::ERA_YEAR),
"timeZone" => self.bit_map.contains(FieldMap::TIME_ZONE),
_ => unreachable!(),
}
}
#[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(())
}
}
impl TemporalFields {
/// Returns a vector filled with the key-value pairs marked as active.
#[must_use]
pub fn active_kvs(&self) -> Vec<(String, FieldValue)> {
self.keys().zip(self.values()).collect()
}
/// Returns an iterator over the current keys.
#[must_use]
pub fn keys(&self) -> Keys {
Keys {
iter: self.bit_map.iter(),
}
}
/// Returns an iterator over the current values.
#[must_use]
pub fn values(&self) -> Values<'_> {
Values {
fields: self,
iter: self.bit_map.iter(),
}
}
/// 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(())
}
/// Merges two `TemporalFields` values given a specific `CalendarSlot`.
pub fn merge_fields<C: CalendarProtocol>(
&self,
other: &Self,
calendar: &CalendarSlot<C>,
) -> TemporalResult<Self> {
let add_keys = other.keys().collect::<Vec<_>>();
let overridden_keys = calendar.field_keys_to_ignore(&add_keys)?;
let mut result = Self::default();
for key in self.keys() {
let value = if overridden_keys.contains(&key) {
other.get(&key)
} else {
self.get(&key)
};
if let Some(value) = value {
result.set_field_value(&key, &value)?;
}
}
Ok(result)
}
}
/// Iterator over `TemporalFields` keys.
pub struct Keys {
iter: bitflags::iter::Iter<FieldMap>,
}
impl fmt::Debug for Keys {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TemporalFields KeyIterator")
}
}
impl Iterator for Keys {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
let Some(field) = self.iter.next() else {
return None;
};
match field {
FieldMap::YEAR => Some("year".to_owned()),
FieldMap::MONTH => Some("month".to_owned()),
FieldMap::MONTH_CODE => Some("monthCode".to_owned()),
FieldMap::DAY => Some("day".to_owned()),
FieldMap::HOUR => Some("hour".to_owned()),
FieldMap::MINUTE => Some("minute".to_owned()),
FieldMap::SECOND => Some("second".to_owned()),
FieldMap::MILLISECOND => Some("millisecond".to_owned()),
FieldMap::MICROSECOND => Some("microsecond".to_owned()),
FieldMap::NANOSECOND => Some("nanosecond".to_owned()),
FieldMap::OFFSET => Some("offset".to_owned()),
FieldMap::ERA => Some("era".to_owned()),
FieldMap::ERA_YEAR => Some("eraYear".to_owned()),
FieldMap::TIME_ZONE => Some("timeZone".to_owned()),
_ => None,
}
}
}
/// An iterator over `TemporalFields`'s values.
pub struct Values<'a> {
fields: &'a TemporalFields,
iter: bitflags::iter::Iter<FieldMap>,
}
impl fmt::Debug for Values<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TemporalFields Values Iterator")
}
}
impl Iterator for Values<'_> {
type Item = FieldValue;
fn next(&mut self) -> Option<Self::Item> {
let Some(field) = self.iter.next() else {
return None;
};
match field {
FieldMap::YEAR => Some(
self.fields
.year
.map_or(FieldValue::Undefined, FieldValue::Integer),
),
FieldMap::MONTH => Some(
self.fields
.month
.map_or(FieldValue::Undefined, FieldValue::Integer),
),
FieldMap::MONTH_CODE => Some(
self.fields
.month_code
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())),
),
FieldMap::DAY => Some(
self.fields
.day
.map_or(FieldValue::Undefined, FieldValue::Integer),
),
FieldMap::HOUR => Some(FieldValue::Integer(self.fields.hour)),
FieldMap::MINUTE => Some(FieldValue::Integer(self.fields.minute)),
FieldMap::SECOND => Some(FieldValue::Integer(self.fields.second)),
FieldMap::MILLISECOND => Some(FieldValue::Integer(self.fields.millisecond)),
FieldMap::MICROSECOND => Some(FieldValue::Integer(self.fields.microsecond)),
FieldMap::NANOSECOND => Some(FieldValue::Integer(self.fields.nanosecond)),
FieldMap::OFFSET => Some(
self.fields
.offset
.clone()
.map_or(FieldValue::Undefined, FieldValue::String),
),
FieldMap::ERA => Some(
self.fields
.era
.map_or(FieldValue::Undefined, |s| FieldValue::String(s.to_string())),
),
FieldMap::ERA_YEAR => Some(
self.fields
.era_year
.map_or(FieldValue::Undefined, FieldValue::Integer),
),
FieldMap::TIME_ZONE => Some(
self.fields
.time_zone
.clone()
.map_or(FieldValue::Undefined, FieldValue::String),
),
_ => None,
}
}
}
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.")),
}
}

654
core/temporal/src/iso.rs

@ -1,654 +0,0 @@
//! This module implements the internal ISO field slots.
//!
//! The three main types of slots are:
//! - `IsoDateTime`
//! - `IsoDate`
//! - `IsoTime`
//!
//! An `IsoDate` represents the `[[ISOYear]]`, `[[ISOMonth]]`, and `[[ISODay]]` internal slots.
//!
//! An `IsoTime` 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::{
components::duration::DateDuration,
error::TemporalError,
options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit},
utils, TemporalResult, NS_PER_DAY,
};
use icu_calendar::{Date as IcuDate, Iso};
use num_bigint::BigInt;
use num_traits::{cast::FromPrimitive, ToPrimitive};
/// `IsoDateTime` is the record of the `IsoDate` and `IsoTime` internal slots.
#[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 }
}
/// Creates a new validated `IsoDateTime` that is within valid limits.
pub(crate) fn new(date: IsoDate, time: IsoTime) -> TemporalResult<Self> {
if !iso_dt_within_valid_limits(date, &time) {
return Err(
TemporalError::range().with_message("IsoDateTime not within a valid range.")
);
}
Ok(Self::new_unchecked(date, time))
}
// NOTE: The below assumes that nanos is from an `Instant` and thus in a valid range. -> Needs validation.
/// Creates an `IsoDateTime` from a `BigInt` of epochNanoseconds.
pub(crate) fn from_epoch_nanos(nanos: &BigInt, offset: f64) -> TemporalResult<Self> {
// Skip the assert as nanos should be validated by Instant.
// TODO: Determine whether value needs to be validated as integral.
// Get the component ISO parts
let mathematical_nanos = nanos.to_f64().ok_or_else(|| {
TemporalError::range().with_message("nanos was not within a valid range.")
})?;
// 2. Let remainderNs be epochNanoseconds modulo 10^6.
let remainder_nanos = mathematical_nanos % 1_000_000f64;
// 3. Let epochMilliseconds be 𝔽((epochNanoseconds - remainderNs) / 10^6).
let epoch_millis = ((mathematical_nanos - remainder_nanos) / 1_000_000f64).floor();
let year = utils::epoch_time_to_epoch_year(epoch_millis);
let month = utils::epoch_time_to_month_in_year(epoch_millis) + 1;
let day = utils::epoch_time_to_date(epoch_millis);
// 7. Let hour be ℝ(! HourFromTime(epochMilliseconds)).
let hour = (epoch_millis / 3_600_000f64).floor() % 24f64;
// 8. Let minute be ℝ(! MinFromTime(epochMilliseconds)).
let minute = (epoch_millis / 60_000f64).floor() % 60f64;
// 9. Let second be ℝ(! SecFromTime(epochMilliseconds)).
let second = (epoch_millis / 1000f64).floor() % 60f64;
// 10. Let millisecond be ℝ(! msFromTime(epochMilliseconds)).
let millis = (epoch_millis % 1000f64).floor() % 1000f64;
// 11. Let microsecond be floor(remainderNs / 1000).
let micros = (remainder_nanos / 1000f64).floor();
// 12. Assert: microsecond < 1000.
debug_assert!(micros < 1000f64);
// 13. Let nanosecond be remainderNs modulo 1000.
let nanos = (remainder_nanos % 1000f64).floor();
Ok(Self::balance(
year,
i32::from(month),
i32::from(day),
hour,
minute,
second,
millis,
micros,
nanos + offset,
))
}
#[allow(clippy::too_many_arguments)]
fn balance(
year: i32,
month: i32,
day: i32,
hour: f64,
minute: f64,
second: f64,
millisecond: f64,
microsecond: f64,
nanosecond: f64,
) -> Self {
let (overflow_day, time) =
IsoTime::balance(hour, minute, second, millisecond, microsecond, nanosecond);
let date = IsoDate::balance(year, month, day + overflow_day);
Self::new_unchecked(date, time)
}
/// Returns whether the `IsoDateTime` is within valid limits.
pub(crate) fn is_within_limits(&self) -> bool {
iso_dt_within_valid_limits(self.date, &self.time)
}
pub(crate) const fn date(&self) -> &IsoDate {
&self.date
}
pub(crate) const fn 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 {
pub(crate) year: i32,
pub(crate) month: u8,
pub(crate) 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 month = month.clamp(1, 12);
let days_in_month = utils::iso_days_in_month(year, month);
let d = day.clamp(1, days_in_month);
// NOTE: Values are clamped in a u8 range.
Ok(Self::new_unchecked(year, month 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),
)
}
/// 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, PartialEq, Eq, PartialOrd, Ord)]
pub struct IsoTime {
pub(crate) hour: u8, // 0..=23
pub(crate) minute: u8, // 0..=59
pub(crate) second: u8, // 0..=59
pub(crate) millisecond: u16, // 0..=999
pub(crate) microsecond: u16, // 0..=999
pub(crate) nanosecond: u16, // 0..=999
}
impl IsoTime {
/// Creates a new `IsoTime` without any validation.
pub(crate) fn new_unchecked(
hour: u8,
minute: u8,
second: u8,
millisecond: u16,
microsecond: u16,
nanosecond: u16,
) -> 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) as u8;
let min = minute.clamp(0, 59) as u8;
let sec = second.clamp(0, 59) as u8;
let milli = millisecond.clamp(0, 999) as u16;
let micro = microsecond.clamp(0, 999) as u16;
let nano = nanosecond.clamp(0, 999) as u16;
Ok(Self::new_unchecked(h, min, sec, milli, micro, nano))
}
ArithmeticOverflow::Reject => {
if !is_valid_time(hour, minute, second, millisecond, microsecond, nanosecond) {
return Err(TemporalError::range().with_message("IsoTime is not valid"));
};
Ok(Self::new_unchecked(
hour as u8,
minute as u8,
second as u8,
millisecond as u16,
microsecond as u16,
nanosecond as u16,
))
}
}
}
/// 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,
}
}
/// Returns an `IsoTime` based off parse components.
pub(crate) fn from_components(
hour: i32,
minute: i32,
second: i32,
fraction: f64,
) -> TemporalResult<Self> {
let millisecond = fraction * 1000f64;
let micros = millisecond.rem_euclid(1f64) * 1000f64;
let nanos = micros.rem_euclid(1f64).mul_add(1000f64, 0.5).floor();
Self::new(
hour,
minute,
second,
millisecond as i32,
micros as i32,
nanos as i32,
ArithmeticOverflow::Reject,
)
}
// NOTE(nekevss): f64 is needed here as values could exceed i32 when input.
/// Balances and creates a new `IsoTime` with `day` overflow from the provided values.
pub(crate) fn balance(
hour: f64,
minute: f64,
second: f64,
millisecond: f64,
microsecond: f64,
nanosecond: f64,
) -> (i32, Self) {
// 1. Set microsecond to microsecond + floor(nanosecond / 1000).
// 2. Set nanosecond to nanosecond modulo 1000.
let (quotient, nanosecond) = div_mod(nanosecond, 1000f64);
let microsecond = microsecond + quotient;
// 3. Set millisecond to millisecond + floor(microsecond / 1000).
// 4. Set microsecond to microsecond modulo 1000.
let (quotient, microsecond) = div_mod(microsecond, 1000f64);
let millisecond = millisecond + quotient;
// 5. Set second to second + floor(millisecond / 1000).
// 6. Set millisecond to millisecond modulo 1000.
let (quotient, millisecond) = div_mod(millisecond, 1000f64);
let second = second + quotient;
// 7. Set minute to minute + floor(second / 60).
// 8. Set second to second modulo 60.
let (quotient, second) = div_mod(second, 60f64);
let minute = minute + quotient;
// 9. Set hour to hour + floor(minute / 60).
// 10. Set minute to minute modulo 60.
let (quotient, minute) = div_mod(minute, 60f64);
let hour = hour + quotient;
// 11. Let days be floor(hour / 24).
// 12. Set hour to hour modulo 24.
let (days, hour) = div_mod(hour, 24f64);
let time = Self::new_unchecked(
hour as u8,
minute as u8,
second as u8,
millisecond as u16,
microsecond as u16,
nanosecond as u16,
);
(days as i32, time)
}
// NOTE (nekevss): Specification seemed to be off / not entirely working, so the below was adapted from the
// temporal-polyfill
// TODO: DayLengthNS can probably be a u64, but keep as is for now and optimize.
/// Rounds the current `IsoTime` according to the provided settings.
pub(crate) fn round(
&self,
increment: f64,
unit: TemporalUnit,
mode: TemporalRoundingMode,
day_length_ns: Option<i64>,
) -> TemporalResult<(i32, Self)> {
// 1. Let fractionalSecond be nanosecond × 10-9 + microsecond × 10-6 + millisecond × 10-3 + second.
let quantity = match unit {
// 2. If unit is "day", then
// a. If dayLengthNs is not present, set dayLengthNs to nsPerDay.
// b. Let quantity be (((((hour × 60 + minute) × 60 + second) × 1000 + millisecond) × 1000 + microsecond) × 1000 + nanosecond) / dayLengthNs.
// 3. Else if unit is "hour", then
// a. Let quantity be (fractionalSecond / 60 + minute) / 60 + hour.
TemporalUnit::Hour | TemporalUnit::Day => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
+ u64::from(self.second) * 1_000_000_000
+ u64::from(self.minute) * 60 * 1_000_000_000
+ u64::from(self.hour) * 60 * 60 * 1_000_000_000
}
// 4. Else if unit is "minute", then
// a. Let quantity be fractionalSecond / 60 + minute.
TemporalUnit::Minute => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
+ u64::from(self.second) * 1_000_000_000
+ u64::from(self.minute) * 60
}
// 5. Else if unit is "second", then
// a. Let quantity be fractionalSecond.
TemporalUnit::Second => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
+ u64::from(self.second) * 1_000_000_000
}
// 6. Else if unit is "millisecond", then
// a. Let quantity be nanosecond × 10-6 + microsecond × 10-3 + millisecond.
TemporalUnit::Millisecond => {
u64::from(self.nanosecond)
+ u64::from(self.microsecond) * 1_000
+ u64::from(self.millisecond) * 1_000_000
}
// 7. Else if unit is "microsecond", then
// a. Let quantity be nanosecond × 10-3 + microsecond.
TemporalUnit::Microsecond => {
u64::from(self.nanosecond) + 1_000 * u64::from(self.microsecond)
}
// 8. Else,
// a. Assert: unit is "nanosecond".
// b. Let quantity be nanosecond.
TemporalUnit::Nanosecond => u64::from(self.nanosecond),
_ => {
return Err(TemporalError::range()
.with_message("Invalid temporal unit provided to Time.round."))
}
};
let ns_per_unit = if unit == TemporalUnit::Day {
day_length_ns.unwrap_or(NS_PER_DAY) as f64
} else {
unit.as_nanoseconds().expect("Only valid time values are ")
};
// TODO: Verify validity of cast or handle better.
// 9. Let result be RoundNumberToIncrement(quantity, increment, roundingMode).
let result =
utils::round_number_to_increment(quantity as f64, ns_per_unit * increment, mode)
/ ns_per_unit;
let result = match unit {
// 10. If unit is "day", then
// a. Return the Record { [[Days]]: result, [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }.
TemporalUnit::Day => (result as i32, IsoTime::default()),
// 11. If unit is "hour", then
// a. Return BalanceTime(result, 0, 0, 0, 0, 0).
TemporalUnit::Hour => IsoTime::balance(result, 0.0, 0.0, 0.0, 0.0, 0.0),
// 12. If unit is "minute", then
// a. Return BalanceTime(hour, result, 0, 0, 0, 0).
TemporalUnit::Minute => {
IsoTime::balance(f64::from(self.hour), result, 0.0, 0.0, 0.0, 0.0)
}
// 13. If unit is "second", then
// a. Return BalanceTime(hour, minute, result, 0, 0, 0).
TemporalUnit::Second => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
result,
0.0,
0.0,
0.0,
),
// 14. If unit is "millisecond", then
// a. Return BalanceTime(hour, minute, second, result, 0, 0).
TemporalUnit::Millisecond => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
f64::from(self.second),
result,
0.0,
0.0,
),
// 15. If unit is "microsecond", then
// a. Return BalanceTime(hour, minute, second, millisecond, result, 0).
TemporalUnit::Microsecond => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
f64::from(self.second),
f64::from(self.millisecond),
result,
0.0,
),
// 16. Assert: unit is "nanosecond".
// 17. Return BalanceTime(hour, minute, second, millisecond, microsecond, result).
TemporalUnit::Nanosecond => IsoTime::balance(
f64::from(self.hour),
f64::from(self.minute),
f64::from(self.second),
f64::from(self.millisecond),
f64::from(self.microsecond),
result,
),
_ => unreachable!("Error is thrown in previous match."),
};
Ok(result)
}
/// 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) * utils::MS_PER_HOUR
+ f64::from(self.minute) * utils::MS_PER_MINUTE)
+ f64::from(self.second) * 1000f64)
+ f64::from(self.millisecond)
}
}
// ==== `IsoDateTime` specific utility functions ====
#[inline]
/// Utility function to determine if a `DateTime`'s components create a `DateTime` within valid limits
fn iso_dt_within_valid_limits(date: IsoDate, time: &IsoTime) -> bool {
if iso_date_to_epoch_days(date.year, (date.month).into(), date.day.into()).abs() > 100_000_001 {
return false;
}
let Some(ns) = utc_epoch_nanos(date, time, 0.0) else {
return false;
};
let max = BigInt::from(crate::NS_MAX_INSTANT + i128::from(NS_PER_DAY));
let min = BigInt::from(crate::NS_MIN_INSTANT - i128::from(NS_PER_DAY));
min < ns && max > ns
}
#[inline]
/// Utility function to convert a `IsoDate` and `IsoTime` values into epoch nanoseconds
fn utc_epoch_nanos(date: IsoDate, time: &IsoTime, offset: f64) -> Option<BigInt> {
let ms = time.to_epoch_ms();
let epoch_ms = utils::epoch_days_to_epoch_ms(date.to_epoch_days(), ms);
let epoch_nanos = epoch_ms.mul_add(
1_000_000f64,
f64::from(time.microsecond).mul_add(1_000f64, f64::from(time.nanosecond)),
);
BigInt::from_f64(epoch_nanos - offset)
}
// ==== `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.abs() + month_t).copysign(year_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)
}
// ==== `IsoTime` specific utilities ====
#[inline]
fn is_valid_time(hour: i32, minute: i32, second: i32, ms: i32, mis: i32, ns: i32) -> bool {
if !(0..=23).contains(&hour) {
return false;
}
let min_sec = 0..=59;
if !min_sec.contains(&minute) || !min_sec.contains(&second) {
return false;
}
let sub_second = 0..=999;
sub_second.contains(&ms) && sub_second.contains(&mis) && sub_second.contains(&ns)
}
// NOTE(nekevss): Considering the below: Balance can probably be altered from f64.
#[inline]
fn div_mod(dividend: f64, divisor: f64) -> (f64, f64) {
(dividend.div_euclid(divisor), dividend.rem_euclid(divisor))
}

72
core/temporal/src/lib.rs

@ -1,72 +0,0 @@
//! Boa's `boa_temporal` crate is an engine agnostic implementation of ECMAScript's Temporal.
//!
//! IMPORTANT NOTE: Please note that this library is actively being developed and is very
//! much experimental and NOT STABLE.
//!
//! [`Temporal`][proposal] is the Stage 3 proposal for ECMAScript that provides new JS objects and functions
//! for working with dates and times that fully supports time zones and non-gregorian calendars.
//!
//! This library's primary source is the Temporal Proposal [specification][spec].
//!
//! [proposal]: https://github.com/tc39/proposal-temporal
//! [spec]: https://tc39.es/proposal-temporal/
#![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 components;
pub mod error;
pub mod fields;
pub mod iso;
pub mod options;
pub mod parser;
#[doc(hidden)]
pub(crate) mod utils;
// 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;
#[doc(inline)]
pub use error::TemporalError;
#[doc(inline)]
pub use fields::TemporalFields;
/// The `Temporal` result type
pub type TemporalResult<T> = Result<T, TemporalError>;
// Relevant numeric constants
/// Nanoseconds per day constant: 8.64e+13
pub const NS_PER_DAY: i64 = MS_PER_DAY as i64 * 1_000_000;
/// Milliseconds per day constant: 8.64e+7
pub const MS_PER_DAY: i32 = 24 * 60 * 60 * 1000;
/// Max Instant nanosecond constant
#[doc(hidden)]
pub(crate) const NS_MAX_INSTANT: i128 = NS_PER_DAY as i128 * 100_000_000i128;
/// Min Instant nanosecond constant
#[doc(hidden)]
pub(crate) const NS_MIN_INSTANT: i128 = -NS_MAX_INSTANT;

437
core/temporal/src/options.rs

@ -1,437 +0,0 @@
//! Native implementation of the `Temporal` options.
//!
//! Temporal has various instances where user's can define options for how an
//! operation may be completed.
use core::{fmt, str::FromStr};
use crate::TemporalError;
// ==== Options enums and methods ====
/// 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!(),
}
}
// TODO: potentiall use a u64
/// Returns the `Nanosecond amount for any given value.`
#[must_use]
pub fn as_nanoseconds(&self) -> Option<f64> {
use TemporalUnit::{
Auto, Day, Hour, Microsecond, Millisecond, Minute, Month, Nanosecond, Second, Week,
Year,
};
match self {
Year | Month | Week | Day | Auto => None,
Hour => Some(3600e9),
Minute => Some(60e9),
Second => Some(1e9),
Millisecond => Some(1e6),
Microsecond => Some(1e3),
Nanosecond => Some(1f64),
}
}
}
/// 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)
}
}

180
core/temporal/src/parser/annotations.rs

@ -1,180 +0,0 @@
/// Parsing for Temporal's `Annotations`.
use crate::{
assert_syntax,
parser::{
grammar::{
is_a_key_char, is_a_key_leading_char, is_annotation_close,
is_annotation_key_value_separator, is_annotation_value_component, is_critical_flag,
},
time_zone,
time_zone::TimeZoneAnnotation,
Cursor,
},
TemporalError, TemporalResult,
};
use super::grammar::{is_annotation_open, is_hyphen};
/// A `KeyValueAnnotation` Parse Node.
#[derive(Debug, Clone)]
pub(crate) struct KeyValueAnnotation {
/// An `Annotation`'s Key.
pub(crate) key: String,
/// An `Annotation`'s value.
pub(crate) value: String,
/// Whether the annotation was flagged as critical.
pub(crate) critical: bool,
}
/// Strictly a Parsing Intermediary for the checking the common annotation backing.
pub(crate) struct AnnotationSet {
pub(crate) tz: Option<TimeZoneAnnotation>,
pub(crate) calendar: Option<String>,
}
/// Parse a `TimeZoneAnnotation` `Annotations` set
pub(crate) fn parse_annotation_set(
zoned: bool,
cursor: &mut Cursor,
) -> TemporalResult<AnnotationSet> {
// Parse the first annotation.
let tz_annotation = time_zone::parse_ambiguous_tz_annotation(cursor)?;
if tz_annotation.is_none() && zoned {
return Err(
TemporalError::syntax().with_message("ZonedDateTime must have a TimeZone annotation.")
);
}
// Parse any `Annotations`
let annotations = cursor.check_or(false, is_annotation_open);
if annotations {
let annotations = parse_annotations(cursor)?;
return Ok(AnnotationSet {
tz: tz_annotation,
calendar: annotations.calendar,
});
}
Ok(AnnotationSet {
tz: tz_annotation,
calendar: None,
})
}
/// An internal crate type to house any recognized annotations that are found.
#[derive(Default)]
pub(crate) struct RecognizedAnnotations {
pub(crate) calendar: Option<String>,
}
/// Parse any number of `KeyValueAnnotation`s
pub(crate) fn parse_annotations(cursor: &mut Cursor) -> TemporalResult<RecognizedAnnotations> {
let mut annotations = RecognizedAnnotations::default();
let mut calendar_crit = false;
while cursor.check_or(false, is_annotation_open) {
let kv = parse_kv_annotation(cursor)?;
if &kv.key == "u-ca" {
if annotations.calendar.is_none() {
annotations.calendar = Some(kv.value);
calendar_crit = kv.critical;
continue;
}
if calendar_crit || kv.critical {
return Err(TemporalError::syntax().with_message(
"Cannot have critical flag with duplicate calendar annotations",
));
}
} else if kv.critical {
return Err(TemporalError::syntax().with_message("Unrecognized critical annotation."));
}
}
Ok(annotations)
}
/// Parse an annotation with an `AnnotationKey`=`AnnotationValue` pair.
fn parse_kv_annotation(cursor: &mut Cursor) -> TemporalResult<KeyValueAnnotation> {
assert_syntax!(
is_annotation_open(cursor.abrupt_next()?),
"Invalid annotation open character."
);
let critical = cursor.check_or(false, is_critical_flag);
cursor.advance_if(critical);
// Parse AnnotationKey.
let annotation_key = parse_annotation_key(cursor)?;
assert_syntax!(
is_annotation_key_value_separator(cursor.abrupt_next()?),
"Invalid annotation key-value separator"
);
// Parse AnnotationValue.
let annotation_value = parse_annotation_value(cursor)?;
assert_syntax!(
is_annotation_close(cursor.abrupt_next()?),
"Invalid annotion closing character"
);
Ok(KeyValueAnnotation {
key: annotation_key,
value: annotation_value,
critical,
})
}
/// Parse an `AnnotationKey`.
fn parse_annotation_key(cursor: &mut Cursor) -> TemporalResult<String> {
let key_start = cursor.pos();
assert_syntax!(
is_a_key_leading_char(cursor.abrupt_next()?),
"Invalid key leading character."
);
while let Some(potential_key_char) = cursor.next() {
// End of key.
if cursor.check_or(false, is_annotation_key_value_separator) {
// Return found key
return Ok(cursor.slice(key_start, cursor.pos()));
}
assert_syntax!(
is_a_key_char(potential_key_char),
"Invalid annotation key character."
);
}
Err(TemporalError::abrupt_end())
}
/// Parse an `AnnotationValue`.
fn parse_annotation_value(cursor: &mut Cursor) -> TemporalResult<String> {
let value_start = cursor.pos();
cursor.advance();
while let Some(potential_value_char) = cursor.next() {
if cursor.check_or(false, is_annotation_close) {
// Return the determined AnnotationValue.
return Ok(cursor.slice(value_start, cursor.pos()));
}
if is_hyphen(potential_value_char) {
assert_syntax!(
cursor.peek().map_or(false, is_annotation_value_component),
"Missing annotation value compoenent after '-'"
);
cursor.advance();
continue;
}
assert_syntax!(
is_annotation_value_component(potential_value_char),
"Invalid annotation value component character."
);
}
Err(TemporalError::abrupt_end())
}

311
core/temporal/src/parser/datetime.rs

@ -1,311 +0,0 @@
//! Parsing for Temporal's ISO8601 `Date` and `DateTime`.
use crate::{
assert_syntax,
parser::{
annotations,
grammar::{is_date_time_separator, is_sign, is_utc_designator},
nodes::TimeZone,
time,
time::TimeSpec,
time_zone, Cursor, IsoParseRecord,
},
TemporalError, TemporalResult,
};
use super::grammar::{is_annotation_open, is_hyphen};
use bitflags::bitflags;
#[derive(Debug, Default, Clone)]
/// A `DateTime` Parse Node that contains the date, time, and offset info.
pub(crate) struct DateTimeRecord {
/// Date
pub(crate) date: DateRecord,
/// Time
pub(crate) time: Option<TimeSpec>,
/// Tz Offset
pub(crate) time_zone: Option<TimeZone>,
}
#[derive(Default, Debug, Clone, Copy)]
/// The record of a parsed date.
pub(crate) struct DateRecord {
/// Date Year
pub(crate) year: i32,
/// Date Month
pub(crate) month: i32,
/// Date Day
pub(crate) day: i32,
}
bitflags! {
/// Parsing flags for `AnnotatedDateTime` parsing.
#[derive(Debug, Clone, Copy)]
pub struct DateTimeFlags: u8 {
const ZONED = 0b0000_0001;
const TIME_REQ = 0b0000_0010;
const UTC_REQ = 0b0000_0100;
}
}
/// This function handles parsing for [`AnnotatedDateTime`][datetime],
/// [`AnnotatedDateTimeTimeRequred`][time], and
/// [`TemporalInstantString.`][instant] according to the requirements
/// provided via Spec.
///
/// [datetime]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTime
/// [time]: https://tc39.es/proposal-temporal/#prod-AnnotatedDateTimeTimeRequired
/// [instant]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
pub(crate) fn parse_annotated_date_time(
flags: DateTimeFlags,
cursor: &mut Cursor,
) -> TemporalResult<IsoParseRecord> {
let date_time = parse_date_time(
flags.contains(DateTimeFlags::TIME_REQ),
flags.contains(DateTimeFlags::UTC_REQ),
cursor,
)?;
// Peek Annotation presence
// Throw error if annotation does not exist and zoned is true, else return.
if !cursor.check_or(false, is_annotation_open) {
if flags.contains(DateTimeFlags::ZONED) {
return Err(TemporalError::syntax()
.with_message("ZonedDateTime must have a TimeZoneAnnotation."));
}
cursor.close()?;
return Ok(IsoParseRecord {
date: date_time.date,
time: date_time.time,
tz: date_time.time_zone,
calendar: None,
});
}
let mut tz = TimeZone::default();
if let Some(tz_info) = date_time.time_zone {
tz = tz_info;
}
let annotation_set =
annotations::parse_annotation_set(flags.contains(DateTimeFlags::ZONED), cursor)?;
if let Some(annotated_tz) = annotation_set.tz {
tz = annotated_tz.tz;
}
let tz = if tz.name.is_some() || tz.offset.is_some() {
Some(tz)
} else {
None
};
cursor.close()?;
Ok(IsoParseRecord {
date: date_time.date,
time: date_time.time,
tz,
calendar: annotation_set.calendar,
})
}
/// Parses a `DateTime` record.
fn parse_date_time(
time_required: bool,
utc_required: bool,
cursor: &mut Cursor,
) -> TemporalResult<DateTimeRecord> {
let date = parse_date(cursor)?;
// If there is no `DateTimeSeparator`, return date early.
if !cursor.check_or(false, is_date_time_separator) {
if time_required {
return Err(TemporalError::syntax().with_message("Missing a required Time target."));
}
return Ok(DateTimeRecord {
date,
time: None,
time_zone: None,
});
}
cursor.advance();
let time = time::parse_time_spec(cursor)?;
let time_zone = if cursor.check_or(false, |ch| is_sign(ch) || is_utc_designator(ch)) {
Some(time_zone::parse_date_time_utc(cursor)?)
} else {
if utc_required {
return Err(TemporalError::syntax().with_message("DateTimeUTCOffset is required."));
}
None
};
Ok(DateTimeRecord {
date,
time: Some(time),
time_zone,
})
}
/// Parses `Date` record.
fn parse_date(cursor: &mut Cursor) -> TemporalResult<DateRecord> {
let year = parse_date_year(cursor)?;
let hyphenated = cursor
.check(is_hyphen)
.ok_or_else(TemporalError::abrupt_end)?;
cursor.advance_if(hyphenated);
let month = parse_date_month(cursor)?;
if hyphenated {
assert_syntax!(cursor.check_or(false, is_hyphen), "Invalid hyphen usage.");
}
cursor.advance_if(cursor.check_or(false, is_hyphen));
let day = parse_date_day(cursor)?;
Ok(DateRecord { year, month, day })
}
// ==== `YearMonth` and `MonthDay` parsing functions ====
/// Parses a `DateSpecYearMonth`
pub(crate) fn parse_year_month(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> {
let year = parse_date_year(cursor)?;
cursor.advance_if(cursor.check_or(false, is_hyphen));
let month = parse_date_month(cursor)?;
assert_syntax!(
cursor.check_or(true, is_annotation_open),
"Expected an end or AnnotationOpen"
);
Ok((year, month))
}
/// Parses a `DateSpecMonthDay`
pub(crate) fn parse_month_day(cursor: &mut Cursor) -> TemporalResult<(i32, i32)> {
let dash_one = cursor
.check(is_hyphen)
.ok_or_else(TemporalError::abrupt_end)?;
let dash_two = cursor
.peek()
.map(is_hyphen)
.ok_or_else(TemporalError::abrupt_end)?;
if dash_two && dash_one {
cursor.advance_n(2);
} else if dash_two && !dash_one {
return Err(TemporalError::syntax().with_message("MonthDay requires two dashes"));
}
let month = parse_date_month(cursor)?;
cursor.advance_if(cursor.check_or(false, is_hyphen));
let day = parse_date_day(cursor)?;
assert_syntax!(
cursor.check_or(true, is_annotation_open),
"Expected an end or AnnotationOpen"
);
Ok((month, day))
}
// ==== Unit Parsers ====
fn parse_date_year(cursor: &mut Cursor) -> TemporalResult<i32> {
if cursor.check_or(false, is_sign) {
let sign = if cursor.expect_next() == '+' { 1 } else { -1 };
let year_start = cursor.pos();
for _ in 0..6 {
let year_digit = cursor.abrupt_next()?;
assert_syntax!(
year_digit.is_ascii_digit(),
"Year must be made up of digits."
);
}
let year_value = cursor
.slice(year_start, cursor.pos())
.parse::<i32>()
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
// 13.30.1 Static Semantics: Early Errors
//
// It is a Syntax Error if DateYear is "-000000" or "−000000" (U+2212 MINUS SIGN followed by 000000).
if sign == -1 && year_value == 0 {
return Err(TemporalError::syntax().with_message("Cannot have negative 0 years."));
}
let year = sign * year_value;
if !(-271_820..=275_760).contains(&year) {
return Err(TemporalError::range()
.with_message("Year is outside of the minimum supported range."));
}
return Ok(year);
}
let year_start = cursor.pos();
for _ in 0..4 {
let year_digit = cursor.abrupt_next()?;
assert_syntax!(
year_digit.is_ascii_digit(),
"Year must be made up of digits."
);
}
let year_value = cursor
.slice(year_start, cursor.pos())
.parse::<i32>()
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
Ok(year_value)
}
fn parse_date_month(cursor: &mut Cursor) -> TemporalResult<i32> {
let start = cursor.pos();
for _ in 0..2 {
let digit = cursor.abrupt_next()?;
assert_syntax!(digit.is_ascii_digit(), "Month must be a digit");
}
let month_value = cursor
.slice(start, cursor.pos())
.parse::<i32>()
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
if !(1..=12).contains(&month_value) {
return Err(TemporalError::syntax().with_message("DateMonth must be in a range of 1-12"));
}
Ok(month_value)
}
fn parse_date_day(cursor: &mut Cursor) -> TemporalResult<i32> {
let start = cursor.pos();
for _ in 0..2 {
let digit = cursor.abrupt_next()?;
assert_syntax!(digit.is_ascii_digit(), "Date must be a digit");
}
let day_value = cursor
.slice(start, cursor.pos())
.parse::<i32>()
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
if !(1..=31).contains(&day_value) {
return Err(TemporalError::syntax().with_message("DateDay must be in a range of 1-31"));
}
Ok(day_value)
}

247
core/temporal/src/parser/duration.rs

@ -1,247 +0,0 @@
use crate::{
assert_syntax,
parser::{
grammar::{
is_day_designator, is_decimal_separator, is_duration_designator, is_hour_designator,
is_minute_designator, is_month_designator, is_second_designator, is_sign,
is_time_designator, is_week_designator, is_year_designator,
},
time::parse_fraction,
Cursor,
},
TemporalError, TemporalResult,
};
/// An ISO8601 `DurationRecord` Parse Node.
#[derive(Debug, Clone, Copy)]
pub(crate) struct DurationParseRecord {
/// Duration Sign
pub(crate) sign: bool,
/// A `DateDuration` record.
pub(crate) date: DateDuration,
/// A `TimeDuration` record.
pub(crate) time: TimeDuration,
}
/// A `DateDuration` Parse Node.
#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct DateDuration {
/// Years value.
pub(crate) years: i32,
/// Months value.
pub(crate) months: i32,
/// Weeks value.
pub(crate) weeks: i32,
/// Days value.
pub(crate) days: i32,
}
/// A `TimeDuration` Parse Node
#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct TimeDuration {
/// Hours value.
pub(crate) hours: i32,
/// Hours fraction value.
pub(crate) fhours: f64,
/// Minutes value with fraction.
pub(crate) minutes: i32,
/// Minutes fraction value.
pub(crate) fminutes: f64,
/// Seconds value with fraction.
pub(crate) seconds: i32,
/// Seconds fraction value,
pub(crate) fseconds: f64,
}
pub(crate) fn parse_duration(cursor: &mut Cursor) -> TemporalResult<DurationParseRecord> {
let sign = if cursor
.check(is_sign)
.ok_or_else(TemporalError::abrupt_end)?
{
cursor.expect_next() == '+'
} else {
true
};
assert_syntax!(
is_duration_designator(cursor.abrupt_next()?),
"DurationDisgnator is missing."
);
let date = if cursor.check_or(false, is_time_designator) {
Some(DateDuration::default())
} else {
Some(parse_date_duration(cursor)?)
};
let time = if cursor.check_or(false, is_time_designator) {
cursor.advance();
Some(parse_time_duration(cursor)?)
} else {
None
};
cursor.close()?;
Ok(DurationParseRecord {
sign,
date: date.unwrap_or_default(),
time: time.unwrap_or_default(),
})
}
#[derive(PartialEq, PartialOrd, Eq, Ord)]
enum DateUnit {
None = 0,
Year,
Month,
Week,
Day,
}
pub(crate) fn parse_date_duration(cursor: &mut Cursor) -> TemporalResult<DateDuration> {
let mut date = DateDuration::default();
let mut previous_unit = DateUnit::None;
while cursor.check_or(false, |ch| ch.is_ascii_digit()) {
let digit_start = cursor.pos();
while cursor.next().is_some() {
if !cursor.check_or(false, |ch| ch.is_ascii_digit()) {
break;
}
}
let value = cursor
.slice(digit_start, cursor.pos())
.parse::<i32>()
.map_err(|err| TemporalError::syntax().with_message(err.to_string()))?;
match cursor.next() {
Some(ch) if is_year_designator(ch) => {
if previous_unit > DateUnit::Year {
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.years = value;
previous_unit = DateUnit::Year;
}
Some(ch) if is_month_designator(ch) => {
if previous_unit > DateUnit::Month {
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.months = value;
previous_unit = DateUnit::Month;
}
Some(ch) if is_week_designator(ch) => {
if previous_unit > DateUnit::Week {
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.weeks = value;
previous_unit = DateUnit::Week;
}
Some(ch) if is_day_designator(ch) => {
if previous_unit > DateUnit::Day {
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
date.days = value;
previous_unit = DateUnit::Day;
}
Some(_) | None => return Err(TemporalError::abrupt_end()),
}
}
Ok(date)
}
#[derive(PartialEq, PartialOrd, Eq, Ord)]
enum TimeUnit {
None = 0,
Hour,
Minute,
Second,
}
pub(crate) fn parse_time_duration(cursor: &mut Cursor) -> TemporalResult<TimeDuration> {
let mut time = TimeDuration::default();
assert_syntax!(
cursor.check_or(false, |ch| ch.is_ascii_digit()),
"TimeDuration designator must have values after."
);
let mut previous_unit = TimeUnit::None;
let mut fraction_present = false;
while cursor.check_or(false, |ch| ch.is_ascii_digit()) {
let digit_start = cursor.pos();
while cursor.next().is_some() {
if !cursor.check_or(false, |ch| ch.is_ascii_digit()) {
break;
}
}
let value = cursor
.slice(digit_start, cursor.pos())
.parse::<i32>()
.map_err(|err| TemporalError::syntax().with_message(err.to_string()))?;
let fraction = if cursor.check_or(false, is_decimal_separator) {
fraction_present = true;
parse_fraction(cursor)?
} else {
0.0
};
match cursor.next() {
Some(ch) if is_hour_designator(ch) => {
if previous_unit > TimeUnit::Hour {
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
time.hours = value;
time.fhours = fraction;
previous_unit = TimeUnit::Hour;
}
Some(ch) if is_minute_designator(ch) => {
if previous_unit > TimeUnit::Minute {
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
time.minutes = value;
time.fminutes = fraction;
previous_unit = TimeUnit::Minute;
}
Some(ch) if is_second_designator(ch) => {
if previous_unit > TimeUnit::Second {
return Err(
TemporalError::syntax().with_message("Not a valid DateDuration order")
);
}
time.seconds = value;
time.fseconds = fraction;
previous_unit = TimeUnit::Second;
}
Some(_) | None => return Err(TemporalError::abrupt_end()),
}
if fraction_present {
assert_syntax!(
cursor.check_or(true, |ch| !ch.is_ascii_digit()),
"Invalid duration value provided after fraction."
);
break;
}
}
Ok(time)
}

136
core/temporal/src/parser/grammar.rs

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

290
core/temporal/src/parser/mod.rs

@ -1,290 +0,0 @@
//! This module implements parsing for ISO 8601 grammar.
use crate::{TemporalError, TemporalResult};
use datetime::DateRecord;
use nodes::{IsoDate, IsoDateTime, IsoTime, TimeZone};
use time::TimeSpec;
mod annotations;
pub(crate) mod datetime;
pub(crate) mod duration;
mod grammar;
mod nodes;
mod time;
pub(crate) mod time_zone;
use self::{datetime::DateTimeFlags, grammar::is_annotation_open};
#[cfg(test)]
mod tests;
// TODO: optimize where possible.
/// `assert_syntax!` is a parser specific utility macro for asserting a syntax test, and returning a
/// `SyntaxError` with the provided message if the test fails.
#[macro_export]
macro_rules! assert_syntax {
($cond:expr, $msg:literal) => {
if !$cond {
return Err(TemporalError::syntax().with_message($msg));
}
};
}
/// A utility function for parsing a `DateTime` string
pub(crate) fn parse_date_time(target: &str) -> TemporalResult<IsoParseRecord> {
datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut Cursor::new(target))
}
/// A utility function for parsing an `Instant` string
#[allow(unused)]
pub(crate) fn parse_instant(target: &str) -> TemporalResult<IsoParseRecord> {
datetime::parse_annotated_date_time(
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ,
&mut Cursor::new(target),
)
}
/// A utility function for parsing a `YearMonth` string
pub(crate) fn parse_year_month(target: &str) -> TemporalResult<IsoParseRecord> {
let mut cursor = Cursor::new(target);
let ym = datetime::parse_year_month(&mut cursor);
let Ok(year_month) = ym else {
cursor.pos = 0;
return datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut cursor);
};
let calendar = if cursor.check_or(false, is_annotation_open) {
let set = annotations::parse_annotation_set(false, &mut cursor)?;
set.calendar
} else {
None
};
cursor.close()?;
Ok(IsoParseRecord {
date: DateRecord {
year: year_month.0,
month: year_month.1,
day: 1,
},
time: None,
tz: None,
calendar,
})
}
/// A utilty function for parsing a `MonthDay` String.
pub(crate) fn parse_month_day(target: &str) -> TemporalResult<IsoParseRecord> {
let mut cursor = Cursor::new(target);
let md = datetime::parse_month_day(&mut cursor);
let Ok(month_day) = md else {
cursor.pos = 0;
return datetime::parse_annotated_date_time(DateTimeFlags::empty(), &mut cursor);
};
let calendar = if cursor.check_or(false, is_annotation_open) {
let set = annotations::parse_annotation_set(false, &mut cursor)?;
set.calendar
} else {
None
};
cursor.close()?;
Ok(IsoParseRecord {
date: DateRecord {
year: 0,
month: month_day.0,
day: month_day.1,
},
time: None,
tz: None,
calendar,
})
}
/// An `IsoParseRecord` is an intermediary record returned by ISO parsing functions.
///
/// `IsoParseRecord` is converted into the ISO AST Nodes.
#[derive(Default, Debug)]
pub(crate) struct IsoParseRecord {
/// Parsed Date Record
pub(crate) date: DateRecord,
/// Parsed Time
pub(crate) time: Option<TimeSpec>,
/// Parsed `TimeZone` data (UTCOffset | IANA name)
pub(crate) tz: Option<TimeZone>,
/// The parsed calendar value.
pub(crate) calendar: Option<String>,
}
// TODO: Phase out the below and integrate parsing with Temporal components.
/// Parse a [`TemporalTimeZoneString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalTimeZoneString
#[derive(Debug, Clone, Copy)]
pub struct TemporalTimeZoneString;
impl TemporalTimeZoneString {
/// Parses a targeted string as a `TimeZone`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut Cursor) -> TemporalResult<TimeZone> {
time_zone::parse_time_zone(cursor)
}
}
/// Parser for a [`TemporalInstantString`][proposal].
///
/// [proposal]: https://tc39.es/proposal-temporal/#prod-TemporalInstantString
#[derive(Debug, Clone, Copy)]
pub struct TemporalInstantString;
impl TemporalInstantString {
/// Parses a targeted string as an `Instant`.
///
/// # Errors
///
/// The parse will error if the provided target is not valid
/// Iso8601 grammar.
pub fn parse(cursor: &mut Cursor) -> TemporalResult<IsoDateTime> {
let parse_record = datetime::parse_annotated_date_time(
DateTimeFlags::UTC_REQ | DateTimeFlags::TIME_REQ,
cursor,
)?;
let date = IsoDate {
year: parse_record.date.year,
month: parse_record.date.month,
day: parse_record.date.day,
calendar: parse_record.calendar,
};
let time = parse_record.time.map_or_else(IsoTime::default, |time| {
IsoTime::from_components(time.hour, time.minute, time.second, time.fraction)
});
Ok(IsoDateTime {
date,
time,
tz: parse_record.tz,
})
}
}
// ==== Mini cursor implementation for Iso8601 targets ====
/// `Cursor` is a small cursor implementation for parsing Iso8601 grammar.
#[derive(Debug)]
pub struct Cursor {
pos: u32,
source: Vec<char>,
}
impl Cursor {
/// Create a new cursor from a source `String` value.
#[must_use]
pub fn new(source: &str) -> Self {
Self {
pos: 0,
source: source.chars().collect(),
}
}
/// Returns a string value from a slice of the cursor.
fn slice(&self, start: u32, end: u32) -> String {
self.source[start as usize..end as usize].iter().collect()
}
/// Get current position
const fn pos(&self) -> u32 {
self.pos
}
/// Peek the value at next position (current + 1).
fn peek(&self) -> Option<char> {
self.peek_n(1)
}
/// Peek the value at n len from current.
fn peek_n(&self, n: u32) -> Option<char> {
let target = (self.pos + n) as usize;
if target < self.source.len() {
Some(self.source[target])
} else {
None
}
}
/// Runs the provided check on the current position.
fn check<F>(&self, f: F) -> Option<bool>
where
F: FnOnce(char) -> bool,
{
self.peek_n(0).map(f)
}
/// Runs the provided check on current position returns the default value if None.
fn check_or<F>(&self, default: bool, f: F) -> bool
where
F: FnOnce(char) -> bool,
{
self.peek_n(0).map_or(default, f)
}
/// Returns `Cursor`'s current char and advances to the next position.
fn next(&mut self) -> Option<char> {
let result = self.peek_n(0);
self.advance();
result
}
/// Utility method that returns next charactor unwrapped char
///
/// # Panics
///
/// This will panic if the next value has not been confirmed to exist.
fn expect_next(&mut self) -> char {
self.next().expect("Invalid use of expect_next.")
}
/// A utility next method that returns an `AbruptEnd` error if invalid.
fn abrupt_next(&mut self) -> TemporalResult<char> {
self.next().ok_or_else(TemporalError::abrupt_end)
}
/// Advances the cursor's position by 1.
fn advance(&mut self) {
self.pos += 1;
}
/// Utility function to advance when a condition is true
fn advance_if(&mut self, condition: bool) {
if condition {
self.advance();
}
}
/// Advances the cursor's position by `n`.
fn advance_n(&mut self, n: u32) {
self.pos += n;
}
/// Closes the current cursor by checking if all contents have been consumed. If not, returns an error for invalid syntax.
fn close(&mut self) -> TemporalResult<()> {
if (self.pos as usize) < self.source.len() {
return Err(TemporalError::syntax()
.with_message("Unexpected syntax at the end of an ISO target."));
}
Ok(())
}
}

90
core/temporal/src/parser/nodes.rs

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

244
core/temporal/src/parser/tests.rs

@ -1,244 +0,0 @@
use std::str::FromStr;
use crate::{
components::{DateTime, Duration, MonthDay, YearMonth},
parser::{parse_date_time, Cursor, TemporalInstantString},
};
#[test]
fn temporal_parser_basic() {
let basic = "20201108";
let basic_separated = "2020-11-08";
let basic_result = basic.parse::<DateTime<()>>().unwrap();
let sep_result = basic_separated.parse::<DateTime<()>>().unwrap();
assert_eq!(basic_result.iso_year(), 2020);
assert_eq!(basic_result.iso_month(), 11);
assert_eq!(basic_result.iso_day(), 8);
assert_eq!(basic_result.iso_year(), sep_result.iso_year());
assert_eq!(basic_result.iso_month(), sep_result.iso_month());
assert_eq!(basic_result.iso_day(), sep_result.iso_day());
}
#[test]
#[allow(clippy::cast_possible_truncation)]
fn temporal_date_time_max() {
// Fractions not accurate, but for testing purposes.
let date_time =
"+002020-11-08T12:28:32.329402834[!America/Argentina/ComodRivadavia][!u-ca=iso8601]";
let result = date_time.parse::<DateTime<()>>().unwrap();
assert_eq!(result.hour(), 12);
assert_eq!(result.minute(), 28);
assert_eq!(result.second(), 32);
assert_eq!(result.millisecond(), 329);
assert_eq!(result.microsecond(), 402);
assert_eq!(result.nanosecond(), 834);
}
#[test]
fn temporal_year_parsing() {
let long = "+002020-11-08";
let bad_year = "-000000-11-08";
let result_good = long.parse::<DateTime<()>>().unwrap();
assert_eq!(result_good.iso_year(), 2020);
let err_result = bad_year.parse::<DateTime<()>>();
assert!(
err_result.is_err(),
"Invalid extended year parsing: \"{bad_year}\" should fail to parse."
);
}
#[test]
fn temporal_annotated_date_time() {
let basic = "2020-11-08[America/Argentina/ComodRivadavia][u-ca=iso8601][foo=bar]";
let omitted = "+0020201108[u-ca=iso8601][f-1a2b=a0sa-2l4s]";
let result = parse_date_time(basic).unwrap();
let tz = &result.tz.unwrap().name.unwrap();
assert_eq!(tz, "America/Argentina/ComodRivadavia");
assert_eq!(&result.calendar, &Some("iso8601".to_string()));
let omit_result = parse_date_time(omitted).unwrap();
assert!(&omit_result.tz.is_none());
assert_eq!(&omit_result.calendar, &Some("iso8601".to_string()));
}
#[test]
fn temporal_year_month() {
let possible_year_months = [
"+002020-11",
"2020-11[u-ca=iso8601]",
"+00202011",
"202011[u-ca=iso8601]",
"+002020-11-07T12:28:32[!u-ca=iso8601]",
];
for ym in possible_year_months {
let result = ym.parse::<YearMonth<()>>().unwrap();
assert_eq!(result.year(), 2020);
assert_eq!(result.month(), 11);
}
}
#[test]
fn temporal_month_day() {
let possible_month_day = [
"11-07",
"1107[+04:00]",
"--11-07",
"--1107[+04:00]",
"+002020-11-07T12:28:32[!u-ca=iso8601]",
];
for md in possible_month_day {
let result = md.parse::<MonthDay<()>>().unwrap();
assert_eq!(result.month(), 11);
assert_eq!(result.day(), 7);
}
}
#[test]
fn temporal_invalid_annotations() {
let invalid_annotations = [
"2020-11-11[!u-ca=iso8601][u-ca=iso8601]",
"2020-11-11[u-ca=iso8601][!u-ca=iso8601]",
"2020-11-11[u-ca=iso8601][!rip=this-invalid-annotation]",
];
for invalid in invalid_annotations {
let err_result = invalid.parse::<MonthDay<()>>();
assert!(
err_result.is_err(),
"Invalid ISO annotation parsing: \"{invalid}\" should fail parsing."
);
}
}
#[test]
fn temporal_valid_instant_strings() {
let instants = [
"1970-01-01T00:00+00:00[!Africa/Abidjan]",
"1970-01-01T00:00+00:00[UTC]",
"1970-01-01T00:00Z[!Europe/Vienna]",
];
for test in instants {
let result = TemporalInstantString::parse(&mut Cursor::new(test));
assert!(result.is_ok());
}
}
#[test]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::float_cmp)]
fn temporal_duration_parsing() {
let durations = [
"p1y1m1dt1h1m1s",
"P1Y1M1W1DT1H1M1.1S",
"-P1Y1M1W1DT1H1M1.123456789S",
"-P1Y3wT0,5H",
];
for dur in durations {
let ok_result = Duration::from_str(dur);
assert!(
ok_result.is_ok(),
"Failing to parse a valid ISO 8601 target: \"{dur}\" should pass."
);
}
let sub_second = durations[2].parse::<Duration>().unwrap();
assert_eq!(sub_second.time().milliseconds(), -123.0);
assert_eq!(sub_second.time().microseconds(), -456.0);
assert_eq!(sub_second.time().nanoseconds(), -789.0);
let test_result = durations[3].parse::<Duration>().unwrap();
assert_eq!(test_result.date().years(), -1f64);
assert_eq!(test_result.date().weeks(), -3f64);
assert_eq!(test_result.time().minutes(), -30.0);
}
#[test]
fn temporal_invalid_durations() {
let invalids = [
"P1Y1M1W0,5D",
"P1Y1M1W1DT1H1M1.123456789123S",
"+PT",
"P1Y1M1W1DT1H0.5M0.5S",
];
for test in invalids {
let err = test.parse::<Duration>();
assert!(
err.is_err(),
"Invalid ISO8601 Duration target: \"{test}\" should fail duration parsing."
);
}
}
#[test]
fn temporal_invalid_iso_datetime_strings() {
// NOTE: The below tests were initially pulled from test262's `argument-string-invalid`
const INVALID_DATETIME_STRINGS: [&str; 34] = [
"", // 1
"invalid iso8601",
"2020-01-00",
"2020-01-32",
"2020-02-30",
"2021-02-29",
"2020-00-01",
"2020-13-01",
"2020-01-01T",
"2020-01-01T25:00:00",
"2020-01-01T01:60:00",
"2020-01-01T01:60:61",
"2020-01-01junk",
"2020-01-01T00:00:00junk",
"2020-01-01T00:00:00+00:00junk",
"2020-01-01T00:00:00+00:00[UTC]junk",
"2020-01-01T00:00:00+00:00[UTC][u-ca=iso8601]junk",
"02020-01-01",
"2020-001-01",
"2020-01-001",
"2020-01-01T001",
"2020-01-01T01:001",
"2020-01-01T01:01:001",
"2020-W01-1",
"2020-001",
"+0002020-01-01",
// TODO: Add the non-existent calendar test back to the test cases.
// may be valid in other contexts, but insufficient information for PlainDate:
"2020-01",
"+002020-01",
"01-01",
"2020-W01",
"P1Y",
"-P12Y",
// valid, but outside the supported range:
"-999999-01-01",
"+999999-01-01",
];
for invalid_target in INVALID_DATETIME_STRINGS {
let error_result = invalid_target.parse::<DateTime<()>>();
assert!(
error_result.is_err(),
"Invalid ISO8601 `DateTime` target: \"{invalid_target}\" should fail parsing."
);
}
}

134
core/temporal/src/parser/time.rs

@ -1,134 +0,0 @@
//! Parsing of ISO8601 Time Values
use super::{
grammar::{is_decimal_separator, is_time_separator},
Cursor,
};
use crate::{assert_syntax, TemporalError, TemporalResult};
/// Parsed Time info
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct TimeSpec {
/// An hour
pub(crate) hour: u8,
/// A minute value
pub(crate) minute: u8,
/// A second value.
pub(crate) second: u8,
/// A floating point number representing the sub-second values
pub(crate) fraction: f64,
}
/// Parse `TimeSpec`
pub(crate) fn parse_time_spec(cursor: &mut Cursor) -> TemporalResult<TimeSpec> {
let hour = parse_hour(cursor)?;
if !cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) {
return Ok(TimeSpec {
hour,
minute: 0,
second: 0,
fraction: 0.0,
});
}
let separator_present = cursor.check_or(false, is_time_separator);
cursor.advance_if(separator_present);
let minute = parse_minute_second(cursor, false)?;
if !cursor.check_or(false, |ch| is_time_separator(ch) || ch.is_ascii_digit()) {
return Ok(TimeSpec {
hour,
minute,
second: 0,
fraction: 0.0,
});
} else if cursor.check_or(false, is_time_separator) && !separator_present {
return Err(TemporalError::syntax().with_message("Invalid TimeSeparator"));
}
cursor.advance_if(separator_present);
let second = parse_minute_second(cursor, true)?;
let fraction = if cursor.check_or(false, is_decimal_separator) {
parse_fraction(cursor)?
} else {
0.0
};
Ok(TimeSpec {
hour,
minute,
second,
fraction,
})
}
pub(crate) fn parse_hour(cursor: &mut Cursor) -> TemporalResult<u8> {
let start = cursor.pos();
for _ in 0..2 {
let digit = cursor.abrupt_next()?;
assert_syntax!(digit.is_ascii_digit(), "Hour must be a digit.");
}
let hour_value = cursor
.slice(start, cursor.pos())
.parse::<u8>()
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
if !(0..=23).contains(&hour_value) {
return Err(TemporalError::syntax().with_message("Hour must be in a range of 0-23"));
}
Ok(hour_value)
}
// NOTE: `TimeSecond` is a 60 inclusive `MinuteSecond`.
/// Parse `MinuteSecond`
pub(crate) fn parse_minute_second(cursor: &mut Cursor, inclusive: bool) -> TemporalResult<u8> {
let start = cursor.pos();
for _ in 0..2 {
let digit = cursor.abrupt_next()?;
assert_syntax!(digit.is_ascii_digit(), "MinuteSecond must be a digit.");
}
let min_sec_value = cursor
.slice(start, cursor.pos())
.parse::<u8>()
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
let valid_range = if inclusive { 0..=60 } else { 0..=59 };
if !valid_range.contains(&min_sec_value) {
return Err(TemporalError::syntax().with_message("MinuteSecond must be in a range of 0-59"));
}
Ok(min_sec_value)
}
/// Parse a `Fraction` value
///
/// This is primarily used in ISO8601 to add percision past
/// a second.
pub(crate) fn parse_fraction(cursor: &mut Cursor) -> TemporalResult<f64> {
let mut fraction_components = Vec::default();
// Assert that the first char provided is a decimal separator.
assert_syntax!(
is_decimal_separator(cursor.abrupt_next()?),
"fraction must begin with a valid decimal separator."
);
fraction_components.push('.');
while cursor.check_or(false, |ch| ch.is_ascii_digit()) {
fraction_components.push(cursor.abrupt_next()?);
}
assert_syntax!(
fraction_components.len() <= 10,
"Fraction component cannot exceed 9 digits."
);
let fraction_value = fraction_components
.iter()
.collect::<String>()
.parse::<f64>()
.map_err(|e| TemporalError::syntax().with_message(e.to_string()))?;
Ok(fraction_value)
}

229
core/temporal/src/parser/time_zone.rs

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

341
core/temporal/src/utils.rs

@ -1,341 +0,0 @@
//! Utility date and time equations for Temporal
use crate::{
options::{TemporalRoundingMode, TemporalUnsignedRoundingMode},
TemporalError, TemporalResult, MS_PER_DAY,
};
// NOTE: Review the below for optimizations and add ALOT of tests.
/// Converts and validates an `Option<f64>` rounding increment value into a valid increment result.
pub(crate) fn to_rounding_increment(increment: Option<f64>) -> TemporalResult<f64> {
let inc = increment.unwrap_or(1.0);
if !inc.is_finite() {
return Err(TemporalError::range().with_message("roundingIncrement must be finite."));
}
let integer = inc.trunc();
if !(1.0..=1_000_000_000f64).contains(&integer) {
return Err(
TemporalError::range().with_message("roundingIncrement is not within a valid range.")
);
}
Ok(integer)
}
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.floor();
// 6. Let r2 be the smallest integer such that r2 > quotient.
let r2 = quotient.ceil();
// 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
}
/// Rounds provided number assuming that the increment is greater than 0.
pub(crate) fn round_number_to_increment_as_if_positive(
nanos: f64,
increment_nanos: f64,
rounding_mode: TemporalRoundingMode,
) -> f64 {
// 1. Let quotient be x / increment.
let quotient = nanos / increment_nanos;
// 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false).
let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false);
// 3. Let r1 be the largest integer such that r1 ≤ quotient.
let r1 = quotient.floor();
// 4. Let r2 be the smallest integer such that r2 > quotient.
let r2 = quotient.ceil();
// 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode).
let rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode);
// 6. Return rounded × increment.
rounded * increment_nanos
}
pub(crate) fn validate_temporal_rounding_increment(
increment: u32,
dividend: u64,
inclusive: bool,
) -> TemporalResult<()> {
let max = if inclusive { dividend } else { dividend - 1 };
if u64::from(increment) > max {
return Err(TemporalError::range().with_message("roundingIncrement exceeds maximum."));
}
if dividend.rem_euclid(u64::from(increment)) != 0 {
return Err(
TemporalError::range().with_message("dividend is not divisble by roundingIncrement.")
);
}
Ok(())
}
// ==== 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) * 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!("ISODaysInMonth panicking 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);
}
}
Loading…
Cancel
Save