diff --git a/Cargo.lock b/Cargo.lock index 2a9f262250..4ba83bfabf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,21 +49,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anes" version = "0.1.6" @@ -403,7 +388,6 @@ dependencies = [ "boa_temporal", "bytemuck", "cfg-if", - "chrono", "criterion", "dashmap", "fast-float", @@ -448,6 +432,7 @@ dependencies = [ "textwrap", "thin-vec", "thiserror", + "time 0.3.31", "web-time", "writeable", "yoke", @@ -464,9 +449,9 @@ dependencies = [ "boa_interner", "boa_parser", "boa_runtime", - "chrono", "futures-util", "smol", + "time 0.3.31", ] [[package]] @@ -600,6 +585,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_yaml", + "time 0.3.31", "toml 0.8.8", ] @@ -608,7 +594,6 @@ name = "boa_wasm" version = "0.17.0" dependencies = [ "boa_engine", - "chrono", "console_error_panic_hook", "getrandom", "wasm-bindgen", @@ -710,20 +695,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets 0.48.5", -] - [[package]] name = "ciborium" version = "0.2.1" @@ -886,12 +857,6 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - [[package]] name = "core_maths" version = "0.1.0" @@ -1569,29 +1534,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "iana-time-zone" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_calendar" version = "1.4.0" @@ -3193,7 +3135,7 @@ checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" dependencies = [ "colored", "log", - "time 0.3.30", + "time 0.3.31", "windows-sys 0.48.0", ] @@ -3508,18 +3450,19 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa", + "js-sys", "libc", "num_threads", "powerfmt", "serde", "time-core", - "time-macros 0.2.15", + "time-macros 0.2.16", ] [[package]] @@ -3540,9 +3483,9 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -4265,15 +4208,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.33.0" diff --git a/Cargo.toml b/Cargo.toml index 4c73bef021..2cb9823d46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,6 @@ boa_temporal = { version = "~0.17.0", path = "core/temporal" } # Shared deps arbitrary = "1" bitflags = "2.4.2" -chrono = { version = "0.4.31", default-features = false } clap = "4.4.18" colored = "2.1.0" fast-float = "0.2.0" @@ -67,6 +66,7 @@ serde = "1.0.195" static_assertions = "1.1.0" textwrap = "0.16.0" thin-vec = "0.2.13" +time = {version = "0.3.31", no-default-features = true, features = ["local-offset", "large-dates", "wasm-bindgen", "parsing", "formatting", "macros"]} # ICU4X diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index 13b264b72f..82a12b497f 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -74,7 +74,6 @@ num-integer = "0.1.45" bitflags.workspace = true indexmap = { workspace = true, features = ["std"] } ryu-js = "1.0.0" -chrono = { workspace = true, default-features = false, features = ["clock", "std"] } fast-float.workspace = true once_cell = { workspace = true, features = ["std"] } tap = "1.0.1" @@ -93,6 +92,7 @@ bytemuck = { version = "1.14.0", features = ["derive"] } arrayvec = "0.7.4" intrusive-collections = "0.9.6" cfg-if = "1.0.0" +time.workspace = true # intl deps boa_icu_provider = {workspace = true, features = ["std"], optional = true } diff --git a/core/engine/src/builtins/date/mod.rs b/core/engine/src/builtins/date/mod.rs index 7ce242b0de..4987fe7827 100644 --- a/core/engine/src/builtins/date/mod.rs +++ b/core/engine/src/builtins/date/mod.rs @@ -8,7 +8,16 @@ //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date use crate::{ - builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + date::utils::{ + date_from_time, date_string, day, hour_from_time, local_time, make_date, make_day, + make_full_year, make_time, min_from_time, month_from_time, ms_from_time, pad_five, + pad_four, pad_six, pad_three, pad_two, parse_date, sec_from_time, time_clip, + time_string, time_within_day, time_zone_string, to_date_string_t, utc_t, week_day, + year_from_time, MS_PER_MINUTE, + }, + BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, context::{ intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, HostHooks, @@ -20,73 +29,31 @@ use crate::{ realm::Realm, string::{common::StaticJsStrings, utf16}, symbol::JsSymbol, - value::{IntegerOrNan, JsValue, PreferredType}, + value::{JsValue, PreferredType}, Context, JsArgs, JsData, JsError, JsResult, JsString, }; use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; -use chrono::{Datelike, NaiveDateTime, TimeZone, Timelike, Utc}; -use utils::{ - make_date, make_day, make_time, parse_date, replace_params, time_clip, DateParameters, -}; pub(crate) mod utils; #[cfg(test)] mod tests; -/// Extracts `Some` from an `Option` or returns `NaN` if the object contains `None`. -macro_rules! some_or_nan { - ($v:expr) => { - match $v { - Some(dt) => dt, - None => return Ok(JsValue::from(f64::NAN)), - } - }; -} - -/// Gets a mutable reference to the inner `Date` object of `val`, or returns -/// a `TypeError` if `val` is not a `Date` object. -macro_rules! get_mut_date { - ($val:expr) => { - $val.as_object() - .and_then(JsObject::downcast_mut::) - .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? - }; -} - -/// Abstract operation [`thisTimeValue`][spec]. -/// -/// [spec]: https://tc39.es/ecma262/#sec-thistimevalue -pub(super) fn this_time_value(value: &JsValue) -> JsResult> { - Ok(value - .as_object() - .and_then(|obj| obj.downcast_ref::().as_deref().copied()) - .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? - .0) -} - /// The internal representation of a `Date` object. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Trace, Finalize, JsData)] +#[derive(Debug, Copy, Clone, Trace, Finalize, JsData)] #[boa_gc(empty_trace)] -pub struct Date(Option); +pub struct Date(f64); impl Date { /// Creates a new `Date`. - pub(crate) const fn new(dt: Option) -> Self { + pub(crate) const fn new(dt: f64) -> Self { Self(dt) } /// Creates a new `Date` from the current UTC time of the host. pub(crate) fn utc_now(hooks: &dyn HostHooks) -> Self { - let dt = hooks.utc_now(); - Self(Some(dt.timestamp_millis())) - } - - /// Converts the `Date` into a `JsValue`, mapping `None` to `NaN` and `Some(datetime)` to - /// `JsValue::from(datetime.timestamp_millis())`. - fn as_value(&self) -> JsValue { - self.0.map_or_else(|| f64::NAN.into(), Into::into) + Self(hooks.utc_now() as f64) } } @@ -238,13 +205,15 @@ impl BuiltInConstructor for Date { // 1. If NewTarget is undefined, then if new_target.is_undefined() { // a. Let now be the time value (UTC) identifying the current time. + let now = context.host_hooks().utc_now(); + // b. Return ToDateString(now). - return Ok(JsValue::new(js_string!(context - .host_hooks() - .local_from_utc(context.host_hooks().utc_now()) - .format("%a %b %d %Y %H:%M:%S GMT%:z") - .to_string()))); + return Ok(JsValue::from(to_date_string_t( + now as f64, + context.host_hooks(), + ))); } + // 2. Let numberOfArgs be the number of elements in values. let dv = match args { // 3. If numberOfArgs = 0, then @@ -254,49 +223,77 @@ impl BuiltInConstructor for Date { } // 4. Else if numberOfArgs = 1, then // a. Let value be values[0]. - [value] => match value - .as_object() - .and_then(|obj| obj.downcast_ref::().as_deref().copied()) - { + [value] => { // b. If value is an Object and value has a [[DateValue]] internal slot, then - Some(dt) => { - // i. Let tv be ! thisTimeValue(value). - dt + let tv = if let Some(date) = + value.as_object().and_then(JsObject::downcast_ref::) + { + // i. Let tv be value.[[DateValue]]. + date.0 } // c. Else, - None => { + else { // i. Let v be ? ToPrimitive(value). - match value.to_primitive(context, PreferredType::Default)? { - // ii. If v is a String, then - JsValue::String(ref str) => { - // 1. Assert: The next step never returns an abrupt completion because v is a String. - // 2. Let tv be the result of parsing v as a date, in exactly the same manner as for the - // parse method (21.4.3.2). - Self::new(parse_date(str, context.host_hooks())) - } - // iii. Else, - v => { - // Directly convert to integer - // 1. Let tv be ? ToNumber(v). - - let dt = v - .to_integer_or_nan(context)? - .as_integer() - // d. Let dv be TimeClip(tv). - .and_then(time_clip); - Self(dt) + let v = value.to_primitive(context, PreferredType::Default)?; + + // ii. If v is a String, then + if let Some(v) = v.as_string() { + // 1. Assert: The next step never returns an abrupt completion because v is a String. + // 2. Let tv be the result of parsing v as a date, in exactly the same manner as for the parse method (21.4.3.2). + let tv = parse_date(v, context.host_hooks()); + if let Some(tv) = tv { + tv as f64 + } else { + f64::NAN } } - } - }, + // iii. Else, + else { + // 1. Let tv be ? ToNumber(v). + v.to_number(context)? + } + }; + + // d. Let dv be TimeClip(tv). + Self(time_clip(tv)) + } // 5. Else, _ => { // Separating this into its own function to simplify the logic. + //let dt = Self::construct_date(args, context)? + // .and_then(|dt| context.host_hooks().local_from_naive_local(dt).earliest()); + //Self(dt.map(|dt| dt.timestamp_millis())) + + // a. Assert: numberOfArgs ≥ 2. + // b. Let y be ? ToNumber(values[0]). + let y = args.get_or_undefined(0).to_number(context)?; + + // c. Let m be ? ToNumber(values[1]). + let m = args.get_or_undefined(1).to_number(context)?; + + // d. If numberOfArgs > 2, let dt be ? ToNumber(values[2]); else let dt be 1𝔽. + let dt = args.get(2).map_or(Ok(1.0), |n| n.to_number(context))?; + + // e. If numberOfArgs > 3, let h be ? ToNumber(values[3]); else let h be +0𝔽. + let h = args.get(3).map_or(Ok(0.0), |n| n.to_number(context))?; + + // f. If numberOfArgs > 4, let min be ? ToNumber(values[4]); else let min be +0𝔽. + let min = args.get(4).map_or(Ok(0.0), |n| n.to_number(context))?; + + // g. If numberOfArgs > 5, let s be ? ToNumber(values[5]); else let s be +0𝔽. + let s = args.get(5).map_or(Ok(0.0), |n| n.to_number(context))?; + + // h. If numberOfArgs > 6, let milli be ? ToNumber(values[6]); else let milli be +0𝔽. + let milli = args.get(6).map_or(Ok(0.0), |n| n.to_number(context))?; - let dt = Self::construct_date(args, context)? - .and_then(|dt| context.host_hooks().local_from_naive_local(dt).earliest()); + // i. Let yr be MakeFullYear(y). + let yr = make_full_year(y); - Self(dt.map(|dt| dt.timestamp_millis())) + // j. Let finalDate be MakeDate(MakeDay(yr, m, dt), MakeTime(h, min, s, milli)). + let final_date = make_date(make_day(yr, m, dt), make_time(h, min, s, milli)); + + // k. Let dv be TimeClip(UTC(finalDate)). + Self(time_clip(utc_t(final_date, context.host_hooks()))) } }; @@ -314,101 +311,6 @@ impl BuiltInConstructor for Date { } impl Date { - /// Gets the timestamp from a list of component values. - fn construct_date( - values: &[JsValue], - context: &mut Context, - ) -> JsResult> { - // 1. Let y be ? ToNumber(year). - let Some(mut year) = values - .get_or_undefined(0) - .to_integer_or_nan(context)? - .as_integer() - else { - return Ok(None); - }; - - // 2. If month is present, let m be ? ToNumber(month); else let m be +0𝔽. - let Some(month) = values.get(1).map_or(Ok(Some(0)), |value| { - value - .to_integer_or_nan(context) - .map(IntegerOrNan::as_integer) - })? - else { - return Ok(None); - }; - - // 3. If date is present, let dt be ? ToNumber(date); else let dt be 1𝔽. - let Some(date) = values.get(2).map_or(Ok(Some(1)), |value| { - value - .to_integer_or_nan(context) - .map(IntegerOrNan::as_integer) - })? - else { - return Ok(None); - }; - - // 4. If hours is present, let h be ? ToNumber(hours); else let h be +0𝔽. - let Some(hour) = values.get(3).map_or(Ok(Some(0)), |value| { - value - .to_integer_or_nan(context) - .map(IntegerOrNan::as_integer) - })? - else { - return Ok(None); - }; - - // 5. If minutes is present, let min be ? ToNumber(minutes); else let min be +0𝔽. - let Some(min) = values.get(4).map_or(Ok(Some(0)), |value| { - value - .to_integer_or_nan(context) - .map(IntegerOrNan::as_integer) - })? - else { - return Ok(None); - }; - - // 6. If seconds is present, let s be ? ToNumber(seconds); else let s be +0𝔽. - let Some(sec) = values.get(5).map_or(Ok(Some(0)), |value| { - value - .to_integer_or_nan(context) - .map(IntegerOrNan::as_integer) - })? - else { - return Ok(None); - }; - - // 7. If ms is present, let milli be ? ToNumber(ms); else let milli be +0𝔽. - let Some(ms) = values.get(6).map_or(Ok(Some(0)), |value| { - value - .to_integer_or_nan(context) - .map(IntegerOrNan::as_integer) - })? - else { - return Ok(None); - }; - - // 8. If y is NaN, let yr be NaN. - // 9. Else, - // a. Let yi be ! ToIntegerOrInfinity(y). - // b. If 0 ≤ yi ≤ 99, let yr be 1900𝔽 + 𝔽(yi); otherwise, let yr be y. - if (0..=99).contains(&year) { - year += 1900; - } - - // 10. Return TimeClip(MakeDate(MakeDay(yr, m, dt), MakeTime(h, min, s, milli))). - // PLEASE RUST TEAM GIVE US TRY BLOCKS ;-; - let timestamp = (move || { - let day = make_day(year, month, date)?; - let time = make_time(hour, min, sec, ms)?; - make_date(day, time) - })(); - - Ok(timestamp - .and_then(time_clip) - .and_then(NaiveDateTime::from_timestamp_millis)) - } - /// `Date.now()` /// /// The static `Date.now()` method returns the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC. @@ -421,9 +323,7 @@ impl Date { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now #[allow(clippy::unnecessary_wraps)] pub(crate) fn now(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - Ok(JsValue::new( - context.host_hooks().utc_now().timestamp_millis(), - )) + Ok(JsValue::new(context.host_hooks().utc_now())) } /// `Date.parse()` @@ -454,9 +354,47 @@ impl Date { /// [spec]: https://tc39.es/ecma262/#sec-date.utc /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC pub(crate) fn utc(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - let t = some_or_nan!(Self::construct_date(args, context)?); + // 1. Let y be ? ToNumber(year). + let y = args.get_or_undefined(0).to_number(context)?; + + // 2. If month is present, let m be ? ToNumber(month); else let m be +0𝔽. + let m = args + .get(1) + .map_or(Ok(0f64), |value| value.to_number(context))?; + + // 3. If date is present, let dt be ? ToNumber(date); else let dt be 1𝔽. + let dt = args + .get(2) + .map_or(Ok(1f64), |value| value.to_number(context))?; + + // 4. If hours is present, let h be ? ToNumber(hours); else let h be +0𝔽. + let h = args + .get(3) + .map_or(Ok(0f64), |value| value.to_number(context))?; + + // 5. If minutes is present, let min be ? ToNumber(minutes); else let min be +0𝔽. + let min = args + .get(4) + .map_or(Ok(0f64), |value| value.to_number(context))?; + + // 6. If seconds is present, let s be ? ToNumber(seconds); else let s be +0𝔽. + let s = args + .get(5) + .map_or(Ok(0f64), |value| value.to_number(context))?; + + // 7. If ms is present, let milli be ? ToNumber(ms); else let milli be +0𝔽. + let milli = args + .get(6) + .map_or(Ok(0f64), |value| value.to_number(context))?; + + // 8. Let yr be MakeFullYear(y). + let yr = make_full_year(y); - Ok(JsValue::from(t.timestamp_millis())) + // 9. Return TimeClip(MakeDate(MakeDay(yr, m, dt), MakeTime(h, min, s, milli))). + Ok(JsValue::from(time_clip(make_date( + make_day(yr, m, dt), + make_time(h, min, s, milli), + )))) } /// [`Date.prototype.getDate ( )`][local] and @@ -471,20 +409,30 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; - // 3. Return DateFromTime(LocalTime(t)). - Ok(JsValue::new(t.day())) + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + if LOCAL { + // 5. Return DateFromTime(LocalTime(t)). + Ok(JsValue::from(date_from_time(local_time( + t, + context.host_hooks(), + )))) + } else { + // 5. Return DateFromTime(t). + Ok(JsValue::from(date_from_time(t))) + } } /// [`Date.prototype.getDay ( )`][local] and @@ -500,20 +448,27 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; - // 3. Return WeekDay(LocalTime(t)). - Ok(JsValue::new(t.weekday().num_days_from_sunday())) + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + if LOCAL { + // 5. Return WeekDay(LocalTime(t)). + Ok(JsValue::from(week_day(local_time(t, context.host_hooks())))) + } else { + // 5. Return WeekDay(t). + Ok(JsValue::from(week_day(t))) + } } /// [`Date.prototype.getYear()`][spec]. @@ -532,15 +487,24 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t.and_then(NaiveDateTime::from_timestamp_millis)); + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 3. Return YearFromTime(LocalTime(t)) - 1900𝔽. - let local = context.host_hooks().local_from_utc(t); - Ok(JsValue::from(local.year() - 1900)) + // 5. Return YearFromTime(LocalTime(t)) - 1900𝔽. + Ok(JsValue::from( + year_from_time(local_time(t, context.host_hooks())) - 1900, + )) } /// [`Date.prototype.getFullYear ( )`][local] and @@ -555,20 +519,30 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 3. Return YearFromTime(LocalTime(t)). - Ok(JsValue::new(t.year())) + if LOCAL { + // 5. Return YearFromTime(LocalTime(t)). + Ok(JsValue::from(year_from_time(local_time( + t, + context.host_hooks(), + )))) + } else { + // 5. Return YearFromTime(t). + Ok(JsValue::from(year_from_time(t))) + } } /// [`Date.prototype.getHours ( )`][local] and @@ -583,20 +557,30 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 3. Return HourFromTime(LocalTime(t)). - Ok(JsValue::new(t.hour())) + if LOCAL { + // 5. Return HourFromTime(LocalTime(t)). + Ok(JsValue::from(hour_from_time(local_time( + t, + context.host_hooks(), + )))) + } else { + // 5. Return HourFromTime(t). + Ok(JsValue::from(hour_from_time(t))) + } } /// [`Date.prototype.getMilliseconds ( )`][local] and @@ -611,20 +595,30 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; - // 3. Return msFromTime(LocalTime(t)). - Ok(JsValue::new(t.timestamp_subsec_millis())) + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + if LOCAL { + // 5. Return msFromTime(LocalTime(t)). + Ok(JsValue::from(ms_from_time(local_time( + t, + context.host_hooks(), + )))) + } else { + // 5. Return msFromTime(t). + Ok(JsValue::from(ms_from_time(t))) + } } /// [`Date.prototype.getMinutes ( )`][local] and @@ -639,20 +633,30 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 3. Return MinFromTime(LocalTime(t)). - Ok(JsValue::new(t.minute())) + if LOCAL { + // 5. Return MinFromTime(LocalTime(t)). + Ok(JsValue::from(min_from_time(local_time( + t, + context.host_hooks(), + )))) + } else { + // 5. Return MinFromTime(t). + Ok(JsValue::from(min_from_time(t))) + } } /// [`Date.prototype.getMonth ( )`][local] and @@ -668,20 +672,30 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 3. Return MonthFromTime(LocalTime(t)). - Ok(JsValue::new(t.month0())) + if LOCAL { + // 5. Return MonthFromTime(LocalTime(t)). + Ok(JsValue::from(month_from_time(local_time( + t, + context.host_hooks(), + )))) + } else { + // 5. Return MonthFromTime(t). + Ok(JsValue::from(month_from_time(t))) + } } /// [`Date.prototype.getSeconds ( )`][local] and @@ -696,20 +710,30 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let t = this_time_value(this)?; - - // 2. If t is NaN, return NaN. - let t = some_or_nan!(t - .and_then(NaiveDateTime::from_timestamp_millis) - .map(|dt| if LOCAL { - context.host_hooks().local_from_utc(dt).naive_local() - } else { - dt - })); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; - // 3. Return SecFromTime(LocalTime(t)) - Ok(JsValue::new(t.second())) + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + if LOCAL { + // 5. Return SecFromTime(LocalTime(t)). + Ok(JsValue::from(sec_from_time(local_time( + t, + context.host_hooks(), + )))) + } else { + // 5. Return SecFromTime(t). + Ok(JsValue::from(sec_from_time(t))) + } } /// `Date.prototype.getTime()`. @@ -727,8 +751,15 @@ impl Date { _args: &[JsValue], _context: &mut Context, ) -> JsResult { - // 1. Return ? thisTimeValue(this value). - Ok(this_time_value(this)?.map_or(JsValue::nan(), JsValue::from)) + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Return dateObject.[[DateValue]]. + Ok(this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0 + .into()) } /// `Date.prototype.getTimeZoneOffset()`. @@ -747,18 +778,23 @@ impl Date { _: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - // 2. If t is NaN, return NaN. - let t = some_or_nan!(this_time_value(this)?.and_then(NaiveDateTime::from_timestamp_millis)); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let t be dateObject.[[DateValue]]. + let t = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 3. Return (t - LocalTime(t)) / msPerMinute. + // 5. Return (t - LocalTime(t)) / msPerMinute. Ok(JsValue::from( - -context - .host_hooks() - .local_from_utc(t) - .offset() - .local_minus_utc() - / 60, + (t - local_time(t, context.host_hooks())) / MS_PER_MINUTE, )) } @@ -775,32 +811,48 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be LocalTime(? thisTimeValue(this value)). - let mut t = get_mut_date!(this); - - // 2. Let dt be ? ToNumber(date). - let date = args.get_or_undefined(0).to_integer_or_nan(context)?; - - // 3. If t is NaN, return NaN. - let datetime = some_or_nan!(t.0); - - // 4. Set t to LocalTime(t). - // 5. Let newDate be MakeDate(MakeDay(YearFromTime(t), MonthFromTime(t), dt), TimeWithinDay(t)). - // 6. Let u be TimeClip(UTC(newDate)). - let datetime = replace_params::( - datetime, - DateParameters { - date: Some(date), - ..Default::default() - }, - context.host_hooks(), + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; + + // 3. Let t be dateObject.[[DateValue]]. + let mut t = date_object.0; + + // 4. Let dt be ? ToNumber(date). + let dt = args.get_or_undefined(0).to_number(context)?; + + // 5. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + if LOCAL { + // 6. Set t to LocalTime(t). + t = local_time(t, context.host_hooks()); + } + + // 7. Let newDate be MakeDate(MakeDay(YearFromTime(t), MonthFromTime(t), dt), TimeWithinDay(t)). + let new_date = make_date( + make_day(year_from_time(t).into(), month_from_time(t).into(), dt), + time_within_day(t), ); - // 7. Set the [[DateValue]] internal slot of this Date object to u. - *t = Self::new(datetime); + let u = if LOCAL { + // 8. Let u be TimeClip(UTC(newDate)). + time_clip(utc_t(new_date, context.host_hooks())) + } else { + // 8. Let v be TimeClip(newDate). + time_clip(new_date) + }; + + // 9. Set dateObject.[[DateValue]] to u. + date_object.0 = u; - // 8. Return u. - Ok(t.as_value()) + // 10. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setFullYear ( year [ , month [ , date ] ] )`][local] and @@ -816,58 +868,65 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let mut t = get_mut_date!(this); - - // 2. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). - let datetime = - t.0.and_then(NaiveDateTime::from_timestamp_millis) - .or_else(|| { - if LOCAL { - context - .host_hooks() - .local_from_naive_local(NaiveDateTime::default()) - .earliest() - .map(|dt| dt.naive_utc()) - } else { - Some(NaiveDateTime::default()) - } - }); - let datetime = some_or_nan!(datetime); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; - // 3. Let y be ? ToNumber(year). - let year = args.get_or_undefined(0).to_integer_or_nan(context)?; + // 3. Let t be dateObject.[[DateValue]]. + let t = date_object.0; - // 4. If month is not present, let m be MonthFromTime(t); otherwise, let m be ? ToNumber(month). - let month = args - .get(1) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + let t = if LOCAL { + // 5. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). + if t.is_nan() { + 0.0 + } else { + local_time(t, context.host_hooks()) + } + } else { + // 4. If t is NaN, set t to +0𝔽. + if t.is_nan() { + 0.0 + } else { + t + } + }; - // 5. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). - let date = args - .get(2) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; - - // 6. Let newDate be MakeDate(MakeDay(y, m, dt), TimeWithinDay(t)). - // 7. Let u be TimeClip(UTC(newDate)). - let datetime = replace_params::( - datetime.timestamp_millis(), - DateParameters { - year: Some(year), - month, - date, - ..Default::default() - }, - context.host_hooks(), - ); + // 4. Let y be ? ToNumber(year). + let y = args.get_or_undefined(0).to_number(context)?; + + // 6. If month is not present, let m be MonthFromTime(t); otherwise, let m be ? ToNumber(month). + let m = if let Some(month) = args.get(1) { + month.to_number(context)? + } else { + month_from_time(t).into() + }; + + // 7. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). + let dt = if let Some(date) = args.get(2) { + date.to_number(context)? + } else { + date_from_time(t).into() + }; - // 8. Set the [[DateValue]] internal slot of this Date object to u. - *t = Self::new(datetime); + // 8. Let newDate be MakeDate(MakeDay(y, m, dt), TimeWithinDay(t)). + let new_date = make_date(make_day(y, m, dt), time_within_day(t)); - // 9. Return u. - Ok(t.as_value()) + let u = if LOCAL { + // 9. Let u be TimeClip(UTC(newDate)). + time_clip(utc_t(new_date, context.host_hooks())) + } else { + // 9. Let u be TimeClip(newDate). + time_clip(new_date) + }; + + // 10. Set dateObject.[[DateValue]] to u. + date_object.0 = u; + + // 11. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setHours ( hour [ , min [ , sec [ , ms ] ] ] )`][local] and @@ -879,61 +938,69 @@ impl Date { /// /// [local]: https://tc39.es/ecma262/#sec-date.prototype.sethours /// [utc]: https://tc39.es/ecma262/#sec-date.prototype.setutchours + #[allow(clippy::many_single_char_names)] pub(crate) fn set_hours( this: &JsValue, args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let mut t = get_mut_date!(this); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; - // 2. Let h be ? ToNumber(hour). - let hour = args.get_or_undefined(0).to_integer_or_nan(context)?; + // 3. Let t be dateObject.[[DateValue]]. + let mut t = date_object.0; - // 3. If min is present, let m be ? ToNumber(min). - let minute = args - .get(1) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + // 4. Let h be ? ToNumber(hour). + let h = args.get_or_undefined(0).to_number(context)?; - // 4. If sec is present, let s be ? ToNumber(sec). - let second = args - .get(2) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + // 5. If min is present, let m be ? ToNumber(min). + let m = args.get(1).map(|v| v.to_number(context)).transpose()?; - // 5. If ms is present, let milli be ? ToNumber(ms). - let millisecond = args - .get(3) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + // 6. If sec is present, let s be ? ToNumber(sec). + let s = args.get(2).map(|v| v.to_number(context)).transpose()?; - // 6. If t is NaN, return NaN. - let datetime = some_or_nan!(t.0); + // 7. If ms is present, let milli be ? ToNumber(ms). + let milli = args.get(3).map(|v| v.to_number(context)).transpose()?; - // 7. Set t to LocalTime(t). - // 8. If min is not present, let m be MinFromTime(t). - // 9. If sec is not present, let s be SecFromTime(t). - // 10. If ms is not present, let milli be msFromTime(t). - // 11. Let date be MakeDate(Day(t), MakeTime(h, m, s, milli)). - // 12. Let u be TimeClip(UTC(date)). - let datetime = replace_params::( - datetime, - DateParameters { - hour: Some(hour), - minute, - second, - millisecond, - ..Default::default() - }, - context.host_hooks(), - ); + // 8. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 13. Set the [[DateValue]] internal slot of this Date object to u. - *t = Self::new(datetime); + if LOCAL { + // 9. Set t to LocalTime(t). + t = local_time(t, context.host_hooks()); + } - // 14. Return u. - Ok(t.as_value()) + // 10. If min is not present, let m be MinFromTime(t). + let m: f64 = m.unwrap_or_else(|| min_from_time(t).into()); + + // 11. If sec is not present, let s be SecFromTime(t). + let s = s.unwrap_or_else(|| sec_from_time(t).into()); + + // 12. If ms is not present, let milli be msFromTime(t). + let milli = milli.unwrap_or_else(|| ms_from_time(t).into()); + + // 13. Let date be MakeDate(Day(t), MakeTime(h, m, s, milli)). + let date = make_date(day(t), make_time(h, m, s, milli)); + + let u = if LOCAL { + // 14. Let u be TimeClip(UTC(date)). + time_clip(utc_t(date, context.host_hooks())) + } else { + // 14. Let u be TimeClip(date). + time_clip(date) + }; + + // 15. Set dateObject.[[DateValue]] to u. + date_object.0 = u; + + // 16. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setMilliseconds ( ms )`[local] and @@ -948,33 +1015,50 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - // 1. Let t be LocalTime(? thisTimeValue(this value)). - let mut t = get_mut_date!(this); - - // 2. Set ms to ? ToNumber(ms). - let ms = args.get_or_undefined(0).to_integer_or_nan(context)?; - - // 3. If t is NaN, return NaN. - let datetime = some_or_nan!(t.0); - - // 4. Set t to LocalTime(t). - // 5. Let time be MakeTime(HourFromTime(t), MinFromTime(t), SecFromTime(t), ms). - // 6. Let u be TimeClip(UTC(MakeDate(Day(t), time))). - let datetime = replace_params::( - datetime, - DateParameters { - millisecond: Some(ms), - ..Default::default() - }, - context.host_hooks(), + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; + + // 3. Let t be dateObject.[[DateValue]]. + let mut t = date_object.0; + + // 4. Set ms to ? ToNumber(ms). + let ms = args.get_or_undefined(0).to_number(context)?; + + // 5. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + if LOCAL { + // 6. Set t to LocalTime(t). + t = local_time(t, context.host_hooks()); + } + + // 7. Let time be MakeTime(HourFromTime(t), MinFromTime(t), SecFromTime(t), ms). + let time = make_time( + hour_from_time(t).into(), + min_from_time(t).into(), + sec_from_time(t).into(), + ms, ); - // 7. Set the [[DateValue]] internal slot of this Date object to u. - *t = Self::new(datetime); + let u = if LOCAL { + // 8. Let u be TimeClip(UTC(MakeDate(Day(t), time))). + time_clip(utc_t(make_date(day(t), time), context.host_hooks())) + } else { + // 8. Let u be TimeClip(MakeDate(Day(t), time)). + time_clip(make_date(day(t), time)) + }; + + // 9. Set dateObject.[[DateValue]] to u. + date_object.0 = u; - // 8. Return u. - Ok(t.as_value()) + // 10. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setMinutes ( min [ , sec [ , ms ] ] )`][local] and @@ -989,48 +1073,57 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let mut t = get_mut_date!(this); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; - // 2. Let m be ? ToNumber(min). - let minute = args.get_or_undefined(0).to_integer_or_nan(context)?; + // 3. Let t be dateObject.[[DateValue]]. + let mut t = date_object.0; - // 3. If sec is present, let s be ? ToNumber(sec). - let second = args - .get(1) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + // 4. Let m be ? ToNumber(min). + let m = args.get_or_undefined(0).to_number(context)?; - // 4. If ms is present, let milli be ? ToNumber(ms). - let millisecond = args - .get(2) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + // 5. If sec is present, let s be ? ToNumber(sec). + let s = args.get(1).map(|v| v.to_number(context)).transpose()?; - // 5. If t is NaN, return NaN. - let datetime = some_or_nan!(t.0); + // 6. If ms is present, let milli be ? ToNumber(ms). + let milli = args.get(2).map(|v| v.to_number(context)).transpose()?; - // 6. Set t to LocalTime(t). - // 7. If sec is not present, let s be SecFromTime(t). - // 8. If ms is not present, let milli be msFromTime(t). - // 9. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), m, s, milli)). - // 10. Let u be TimeClip(UTC(date)). - let datetime = replace_params::( - datetime, - DateParameters { - minute: Some(minute), - second, - millisecond, - ..Default::default() - }, - context.host_hooks(), - ); + // 7. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; - // 11. Set the [[DateValue]] internal slot of this Date object to u. - *t = Self::new(datetime); + if LOCAL { + // 8. Set t to LocalTime(t). + t = local_time(t, context.host_hooks()); + } - // 12. Return u. - Ok(t.as_value()) + // 9. If sec is not present, let s be SecFromTime(t). + let s = s.unwrap_or_else(|| sec_from_time(t).into()); + + // 10. If ms is not present, let milli be msFromTime(t). + let milli = milli.unwrap_or_else(|| ms_from_time(t).into()); + + // 11. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), m, s, milli)). + let date = make_date(day(t), make_time(hour_from_time(t).into(), m, s, milli)); + + let u = if LOCAL { + // 12. Let u be TimeClip(UTC(date)). + time_clip(utc_t(date, context.host_hooks())) + } else { + // 12. Let u be TimeClip(date). + time_clip(date) + }; + + // 13. Set dateObject.[[DateValue]] to u. + date_object.0 = u; + + // 14. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setMonth ( month [ , date ] )`][local] and @@ -1046,40 +1139,54 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let mut t = get_mut_date!(this); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; - // 2. Let m be ? ToNumber(month). - let month = args.get_or_undefined(0).to_integer_or_nan(context)?; + // 3. Let t be dateObject.[[DateValue]]. + let mut t = date_object.0; - // 3. If date is present, let dt be ? ToNumber(date). - let date = args - .get(1) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + // 4. Let m be ? ToNumber(month). + let m = args.get_or_undefined(0).to_number(context)?; - // 4. If t is NaN, return NaN. - let datetime = some_or_nan!(t.0); - - // 5. Set t to LocalTime(t). - // 6. If date is not present, let dt be DateFromTime(t). - // 7. Let newDate be MakeDate(MakeDay(YearFromTime(t), m, dt), TimeWithinDay(t)). - // 8. Let u be TimeClip(UTC(newDate)). - let datetime = replace_params::( - datetime, - DateParameters { - month: Some(month), - date, - ..Default::default() - }, - context.host_hooks(), + // 5. If date is present, let dt be ? ToNumber(date). + let dt = args.get(1).map(|v| v.to_number(context)).transpose()?; + + // 6. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + // 7. Set t to LocalTime(t). + if LOCAL { + t = local_time(t, context.host_hooks()); + } + + // 8. If date is not present, let dt be DateFromTime(t). + let dt = dt.unwrap_or_else(|| date_from_time(t).into()); + + // 9. Let newDate be MakeDate(MakeDay(YearFromTime(t), m, dt), TimeWithinDay(t)). + let new_date = make_date( + make_day(year_from_time(t).into(), m, dt), + time_within_day(t), ); - // 9. Set the [[DateValue]] internal slot of this Date object to u. - *t = Self::new(datetime); + let u = if LOCAL { + // 10. Let u be TimeClip(UTC(newDate)). + time_clip(utc_t(new_date, context.host_hooks())) + } else { + // 10. Let u be TimeClip(newDate). + time_clip(new_date) + }; - // 10. Return u. - Ok(t.as_value()) + // 11. Set dateObject.[[DateValue]] to u. + date_object.0 = u; + + // 12. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setSeconds ( sec [ , ms ] )`[local] and @@ -1094,40 +1201,54 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let mut t = get_mut_date!(this); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; - // 2. Let s be ? ToNumber(sec). - let second = args.get_or_undefined(0).to_integer_or_nan(context)?; + // 3. Let t be dateObject.[[DateValue]]. + let mut t = date_object.0; - // 3. If ms is present, let milli be ? ToNumber(ms). - let millisecond = args - .get(1) - .map(|v| v.to_integer_or_nan(context)) - .transpose()?; + // 4. Let s be ? ToNumber(sec). + let s = args.get_or_undefined(0).to_number(context)?; - // 4. If t is NaN, return NaN. - let datetime = some_or_nan!(t.0); - - // 5. Set t to LocalTime(t). - // 6. If ms is not present, let milli be msFromTime(t). - // 7. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), MinFromTime(t), s, milli)). - // 8. Let u be TimeClip(UTC(date)). - let datetime = replace_params::( - datetime, - DateParameters { - second: Some(second), - millisecond, - ..Default::default() - }, - context.host_hooks(), + // 5. If ms is present, let milli be ? ToNumber(ms). + let milli = args.get(1).map(|v| v.to_number(context)).transpose()?; + + // 6. If t is NaN, return NaN. + if t.is_nan() { + return Ok(JsValue::from(f64::NAN)); + }; + + // 7. Set t to LocalTime(t). + if LOCAL { + t = local_time(t, context.host_hooks()); + } + + // 8. If ms is not present, let milli be msFromTime(t). + let milli = milli.unwrap_or_else(|| ms_from_time(t).into()); + + // 9. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), MinFromTime(t), s, milli)). + let date = make_date( + day(t), + make_time(hour_from_time(t).into(), min_from_time(t).into(), s, milli), ); - // 9. Set the [[DateValue]] internal slot of this Date object to u. - *t = Self::new(datetime); + let u = if LOCAL { + // 10. Let u be TimeClip(UTC(date)). + time_clip(utc_t(date, context.host_hooks())) + } else { + // 10. Let u be TimeClip(date). + time_clip(date) + }; + + // 11. Set dateObject.[[DateValue]] to u. + date_object.0 = u; - // 10. Return u. - Ok(t.as_value()) + // 12. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setYear()`][spec]. @@ -1149,56 +1270,43 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let t be ? thisTimeValue(this value). - let mut t = get_mut_date!(this); - - // 2. Let y be ? ToNumber(year). - // 5. Let yi be ! ToIntegerOrInfinity(y). - let year = args.get_or_undefined(0).to_integer_or_nan(context)?; - - // 3. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). - let datetime = - t.0.and_then(NaiveDateTime::from_timestamp_millis) - .or_else(|| { - context - .host_hooks() - .local_from_naive_local(NaiveDateTime::default()) - .earliest() - .map(|dt| dt.naive_utc()) - }); - let datetime = some_or_nan!(datetime); - - // 4. If y is NaN, then - let Some(mut year) = year.as_integer() else { - // a. Set the [[DateValue]] internal slot of this Date object to NaN. - *t = Self::new(None); - - // b. Return NaN. - return Ok(t.as_value()); + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; + + // 3. Let t be dateObject.[[DateValue]]. + let t = date_object.0; + + // 4. Let y be ? ToNumber(year). + let y = args.get_or_undefined(0).to_number(context)?; + + // 5. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). + let t = if t.is_nan() { + 0.0 + } else { + local_time(t, context.host_hooks()) }; - // 6. If 0 ≤ yi ≤ 99, let yyyy be 1900𝔽 + 𝔽(yi). - // 7. Else, let yyyy be y. - if (0..=99).contains(&year) { - year += 1900; - } + // 6. Let yyyy be MakeFullYear(y). + let yyyy = make_full_year(y); - // 8. Let d be MakeDay(yyyy, MonthFromTime(t), DateFromTime(t)). - // 9. Let date be UTC(MakeDate(d, TimeWithinDay(t))). - let datetime = replace_params::( - datetime.timestamp_millis(), - DateParameters { - year: Some(IntegerOrNan::Integer(year)), - ..Default::default() - }, - context.host_hooks(), - ); + // 7. Let d be MakeDay(yyyy, MonthFromTime(t), DateFromTime(t)). + let d = make_day(yyyy, month_from_time(t).into(), date_from_time(t).into()); + + // 8. Let date be MakeDate(d, TimeWithinDay(t)). + let date = make_date(d, time_within_day(t)); + + // 9. Let u be TimeClip(UTC(date)). + let u = time_clip(utc_t(date, context.host_hooks())); - // 10. Set the [[DateValue]] internal slot of this Date object to TimeClip(date). - *t = Self::new(datetime); + // 10. Set dateObject.[[DateValue]] to u. + date_object.0 = u; - // 11. Return the value of the [[DateValue]] internal slot of this Date object. - Ok(t.as_value()) + // 11. Return u. + Ok(JsValue::from(u)) } /// [`Date.prototype.setTime()`][spec]. @@ -1216,22 +1324,24 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Perform ? thisTimeValue(this value). - let mut t = get_mut_date!(this); - - // 2. Let t be ? ToNumber(time). - // 3. Let v be TimeClip(t). - let timestamp = args - .get_or_undefined(0) - .to_integer_or_nan(context)? - .as_integer() - .and_then(time_clip); - - // 4. Set the [[DateValue]] internal slot of this Date object to v. - *t = Self::new(timestamp); - - // 5. Return v. - Ok(t.as_value()) + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + let mut date_object = this + .as_object() + .and_then(JsObject::downcast_mut::) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))?; + + // 3. Let t be ? ToNumber(time). + let t = args.get_or_undefined(0).to_number(context)?; + + // 4. Let v be TimeClip(t). + let v = time_clip(t); + + // 5. Set dateObject.[[DateValue]] to v. + date_object.0 = v; + + // 6. Return v. + Ok(JsValue::from(v)) } /// [`Date.prototype.toDateString()`][spec]. @@ -1248,21 +1358,25 @@ impl Date { _: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let O be this Date object. - // 2. Let tv be ? thisTimeValue(O). - let Some(tv) = this_time_value(this)?.and_then(NaiveDateTime::from_timestamp_millis) else { - // 3. If tv is NaN, return "Invalid Date". + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let tv be dateObject.[[DateValue]]. + let tv = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If tv is NaN, return "Invalid Date". + if tv.is_nan() { return Ok(js_string!("Invalid Date").into()); }; - // 4. Let t be LocalTime(tv). - // 5. Return DateString(t). - Ok(js_string!(context - .host_hooks() - .local_from_utc(tv) - .format("%a %b %d %Y") - .to_string()) - .into()) + // 5. Let t be LocalTime(tv). + let t = local_time(tv, context.host_hooks()); + + // 6. Return DateString(t). + Ok(JsValue::from(date_string(t))) } /// [`Date.prototype.toISOString()`][spec]. @@ -1281,14 +1395,56 @@ impl Date { _: &[JsValue], _: &mut Context, ) -> JsResult { - let t = this_time_value(this)? - .and_then(NaiveDateTime::from_timestamp_millis) - .ok_or_else(|| JsNativeError::range().with_message("Invalid time value"))?; - Ok(js_string!(Utc - .from_utc_datetime(&t) - .format("%Y-%m-%dT%H:%M:%S.%3fZ") - .to_string()) - .into()) + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let tv be dateObject.[[DateValue]]. + let tv = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If tv is not finite, throw a RangeError exception. + if !tv.is_finite() { + return Err(JsNativeError::range() + .with_message("Invalid time value") + .into()); + } + + // 5. If tv corresponds with a year that cannot be represented in the Date Time String Format, throw a RangeError exception. + // 6. Return a String representation of tv in the Date Time String Format on the UTC time scale, + // including all format elements and the UTC offset representation "Z". + let year = year_from_time(tv); + let year = if year.is_positive() && year >= 10000 { + js_string!(utf16!("+"), &pad_six(year.unsigned_abs())) + } else if year.is_positive() { + JsString::from(&pad_four(year.unsigned_abs())) + } else { + js_string!(utf16!("-"), &pad_six(year.unsigned_abs())) + }; + let month = pad_two(month_from_time(tv) + 1); + let day = pad_two(date_from_time(tv)); + let hour = pad_two(hour_from_time(tv)); + let minute = pad_two(min_from_time(tv)); + let second = pad_two(sec_from_time(tv)); + let millisecond = pad_three(ms_from_time(tv)); + + Ok(JsValue::from(js_string!( + &year, + utf16!("-"), + &month, + utf16!("-"), + &day, + utf16!("T"), + &hour, + utf16!(":"), + &minute, + utf16!(":"), + &second, + utf16!("."), + &millisecond, + utf16!("Z") + ))) } /// [`Date.prototype.toJSON()`][spec]. @@ -1312,10 +1468,8 @@ impl Date { let tv = this.to_primitive(context, PreferredType::Number)?; // 3. If Type(tv) is Number and tv is not finite, return null. - if let Some(number) = tv.as_number() { - if !number.is_finite() { - return Ok(JsValue::null()); - } + if tv.as_number().map(f64::is_finite) == Some(false) { + return Ok(JsValue::null()); } // 4. Return ? Invoke(O, "toISOString"). @@ -1396,17 +1550,17 @@ impl Date { _: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let tv be ? thisTimeValue(this value). - // 2. Return ToDateString(tv). - let Some(tv) = this_time_value(this)?.and_then(NaiveDateTime::from_timestamp_millis) else { - return Ok(js_string!("Invalid Date").into()); - }; - Ok(js_string!(context - .host_hooks() - .local_from_utc(tv) - .format("%a %b %d %Y %H:%M:%S GMT%z") - .to_string()) - .into()) + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let tv be dateObject.[[DateValue]]. + let tv = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. Return ToDateString(tv). + Ok(JsValue::from(to_date_string_t(tv, context.host_hooks()))) } /// [`Date.prototype.toTimeString()`][spec]. @@ -1424,21 +1578,28 @@ impl Date { _: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let O be this Date object. - // 2. Let tv be ? thisTimeValue(O). - let Some(tv) = this_time_value(this)?.and_then(NaiveDateTime::from_timestamp_millis) else { - // 3. If tv is NaN, return "Invalid Date". + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let tv be dateObject.[[DateValue]]. + let tv = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If tv is NaN, return "Invalid Date". + if tv.is_nan() { return Ok(js_string!("Invalid Date").into()); - }; + } + + // 5. Let t be LocalTime(tv). + let t = local_time(tv, context.host_hooks()); - // 4. Let t be LocalTime(tv). - // 5. Return the string-concatenation of TimeString(t) and TimeZoneString(tv). - Ok(js_string!(context - .host_hooks() - .local_from_utc(tv) - .format("%H:%M:%S GMT%z") - .to_string()) - .into()) + // 6. Return the string-concatenation of TimeString(t) and TimeZoneString(tv). + Ok(JsValue::from(js_string!( + &time_string(t), + &time_zone_string(t, context.host_hooks()) + ))) } /// [`Date.prototype.toUTCString()`][spec]. @@ -1455,24 +1616,93 @@ impl Date { _args: &[JsValue], _context: &mut Context, ) -> JsResult { - // 1. Let O be this Date object. - let Some(t) = this_time_value(this)?.and_then(NaiveDateTime::from_timestamp_millis) else { - // 3. If tv is NaN, return "Invalid Date". + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Let tv be dateObject.[[DateValue]]. + let tv = this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0; + + // 4. If tv is NaN, return "Invalid Date". + if tv.is_nan() { return Ok(js_string!("Invalid Date").into()); + } + + // 5. Let weekday be the Name of the entry in Table 63 with the Number WeekDay(tv). + let weekday = match week_day(tv) { + 0 => utf16!("Sun"), + 1 => utf16!("Mon"), + 2 => utf16!("Tue"), + 3 => utf16!("Wed"), + 4 => utf16!("Thu"), + 5 => utf16!("Fri"), + 6 => utf16!("Sat"), + _ => unreachable!(), }; - // 2. Let tv be ? thisTimeValue(O). - // 4. Let weekday be the Name of the entry in Table 60 with the Number WeekDay(tv). - // 5. Let month be the Name of the entry in Table 61 with the Number MonthFromTime(tv). - // 6. Let day be ToZeroPaddedDecimalString(ℝ(DateFromTime(tv)), 2). - // 7. Let yv be YearFromTime(tv). - // 8. If yv is +0𝔽 or yv > +0𝔽, let yearSign be the empty String; otherwise, let yearSign be "-". - // 9. Let paddedYear be ToZeroPaddedDecimalString(abs(ℝ(yv)), 4). - // 10. Return the string-concatenation of weekday, ",", the code unit 0x0020 (SPACE), day, the - // code unit 0x0020 (SPACE), month, the code unit 0x0020 (SPACE), yearSign, paddedYear, the code - // unit 0x0020 (SPACE), and TimeString(tv) - let utc_string = t.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); - Ok(JsValue::new(js_string!(utc_string))) + // 6. Let month be the Name of the entry in Table 64 with the Number MonthFromTime(tv). + let month = match month_from_time(tv) { + 0 => utf16!("Jan"), + 1 => utf16!("Feb"), + 2 => utf16!("Mar"), + 3 => utf16!("Apr"), + 4 => utf16!("May"), + 5 => utf16!("Jun"), + 6 => utf16!("Jul"), + 7 => utf16!("Aug"), + 8 => utf16!("Sep"), + 9 => utf16!("Oct"), + 10 => utf16!("Nov"), + 11 => utf16!("Dec"), + _ => unreachable!(), + }; + + // 7. Let day be ToZeroPaddedDecimalString(ℝ(DateFromTime(tv)), 2). + let day = pad_two(date_from_time(tv)); + + // 8. Let yv be YearFromTime(tv). + let yv = year_from_time(tv); + + // 9. If yv is +0𝔽 or yv > +0𝔽, let yearSign be the empty String; otherwise, let yearSign be "-". + let year_sign = if yv >= 0 { utf16!("") } else { utf16!("-") }; + + // 10. Let paddedYear be ToZeroPaddedDecimalString(abs(ℝ(yv)), 4). + let yv = yv.unsigned_abs(); + let padded_year = if yv >= 100_000 { + js_string!(&pad_six(yv)) + } else if yv >= 10000 { + js_string!(&pad_five(yv)) + } else { + js_string!(&pad_four(yv)) + }; + + // 11. Return the string-concatenation of + // weekday, + // ",", + // the code unit 0x0020 (SPACE), + // day, + // the code unit 0x0020 (SPACE), + // month, + // the code unit 0x0020 (SPACE), + // yearSign, + // paddedYear, + // the code unit 0x0020 (SPACE), + // and TimeString(tv). + Ok(JsValue::from(js_string!( + weekday, + utf16!(","), + utf16!(" "), + &day, + utf16!(" "), + month, + utf16!(" "), + year_sign, + &padded_year, + utf16!(" "), + &time_string(tv) + ))) } /// [`Date.prototype.valueOf()`][spec]. @@ -1489,8 +1719,15 @@ impl Date { _args: &[JsValue], _context: &mut Context, ) -> JsResult { - // 1. Return ? thisTimeValue(this value). - Ok(Self::new(this_time_value(this)?).as_value()) + // 1. Let dateObject be the this value. + // 2. Perform ? RequireInternalSlot(dateObject, [[DateValue]]). + // 3. Return dateObject.[[DateValue]]. + Ok(this + .as_object() + .and_then(|obj| obj.downcast_ref::().as_deref().copied()) + .ok_or_else(|| JsNativeError::typ().with_message("'this' is not a Date"))? + .0 + .into()) } /// [`Date.prototype [ @@toPrimitive ] ( hint )`][spec]. diff --git a/core/engine/src/builtins/date/tests.rs b/core/engine/src/builtins/date/tests.rs index d6845daf17..cf7497119d 100644 --- a/core/engine/src/builtins/date/tests.rs +++ b/core/engine/src/builtins/date/tests.rs @@ -1,46 +1,81 @@ use crate::{js_string, run_test_actions, JsNativeErrorKind, TestAction}; -use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; use indoc::indoc; - -// NOTE: Javascript Uses 0-based months, where chrono uses 1-based months. Many of the assertions look wrong because of -// this. +use time::{macros::format_description, util::local_offset, OffsetDateTime}; + +// NOTE: Javascript Uses 0-based months, where time uses 1-based months. +// Many of the assertions look wrong because of this. + +fn month_from_u8(month: u8) -> time::Month { + match month { + 1 => time::Month::January, + 2 => time::Month::February, + 3 => time::Month::March, + 4 => time::Month::April, + 5 => time::Month::May, + 6 => time::Month::June, + 7 => time::Month::July, + 8 => time::Month::August, + 9 => time::Month::September, + 10 => time::Month::October, + 11 => time::Month::November, + 12 => time::Month::December, + _ => unreachable!(), + } +} + +fn from_local( + year: i32, + month: u8, + date: u8, + hour: u8, + minute: u8, + second: u8, + millisecond: u16, +) -> OffsetDateTime { + // Safety: This is needed during tests because cargo is running tests in multiple threads. + // It is safe because tests do not modify the environment. + #[cfg(test)] + unsafe { + local_offset::set_soundness(local_offset::Soundness::Unsound); + } + + let t = time::Date::from_calendar_date(year, month_from_u8(month), date) + .unwrap() + .with_hms_milli(hour, minute, second, millisecond) + .unwrap() + .assume_utc(); + let offset = time::UtcOffset::local_offset_at(t).unwrap(); + t.replace_offset(offset) +} fn timestamp_from_local( year: i32, - month: u32, - date: u32, - hour: u32, - minute: u32, - second: u32, - millisecond: u32, + month: u8, + date: u8, + hour: u8, + minute: u8, + second: u8, + millisecond: u16, ) -> i64 { - Local - .from_local_datetime( - &NaiveDate::from_ymd_opt(year, month, date) - .unwrap() - .and_hms_milli_opt(hour, minute, second, millisecond) - .unwrap(), - ) - .earliest() - .unwrap() - .naive_utc() - .timestamp_millis() + let t = from_local(year, month, date, hour, minute, second, millisecond); + t.unix_timestamp() * 1000 + i64::from(t.millisecond()) } fn timestamp_from_utc( year: i32, - month: u32, - date: u32, - hour: u32, - minute: u32, - second: u32, - millisecond: u32, + month: u8, + date: u8, + hour: u8, + minute: u8, + second: u8, + millisecond: u16, ) -> i64 { - NaiveDate::from_ymd_opt(year, month, date) + let t = time::Date::from_calendar_date(year, month_from_u8(month), date) .unwrap() - .and_hms_milli_opt(hour, minute, second, millisecond) + .with_hms_milli(hour, minute, second, millisecond) .unwrap() - .timestamp_millis() + .assume_utc(); + t.unix_timestamp() * 1000 + i64::from(t.millisecond()) } #[test] @@ -257,18 +292,8 @@ fn date_proto_get_timezone_offset() { TestAction::assert_eq( "new Date('1975-08-19T23:15:30+07:00').getTimezoneOffset()", { - // The value of now().offset() depends on the host machine, so we have to replicate the method code here. - let dt = Local - .from_local_datetime( - &NaiveDate::from_ymd_opt(1975, 8, 19) - .unwrap() - .and_hms_opt(23, 15, 30) - .unwrap(), - ) - .earliest() - .unwrap(); - let offset_seconds = dt.offset().local_minus_utc(); - -offset_seconds / 60 + let t = from_local(1975, 8, 19, 23, 15, 30, 0); + -t.offset().whole_seconds() / 60 }, ), ]); @@ -792,33 +817,31 @@ fn date_proto_to_json() { #[test] fn date_proto_to_string() { + let to_string_format = format_description!( + "[weekday repr:short] [month repr:short] [day] [year] [hour]:[minute]:[second] GMT[offset_hour sign:mandatory][offset_minute][end]" + ); + let t = from_local(2020, 7, 8, 9, 16, 15, 779) + .format(to_string_format) + .unwrap(); + run_test_actions([TestAction::assert_eq( "new Date(2020, 6, 8, 9, 16, 15, 779).toString()", - js_string!(Local - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 7, 8).unwrap(), - NaiveTime::from_hms_milli_opt(9, 16, 15, 779).unwrap(), - )) - .earliest() - .unwrap() - .format("Wed Jul 08 2020 09:16:15 GMT%z") - .to_string()), + js_string!(t), )]); } #[test] fn date_proto_to_time_string() { + let to_time_string_format = format_description!( + "[hour]:[minute]:[second] GMT[offset_hour sign:mandatory][offset_minute][end]" + ); + let t = from_local(2020, 7, 8, 9, 16, 15, 779) + .format(to_time_string_format) + .unwrap(); + run_test_actions([TestAction::assert_eq( "new Date(2020, 6, 8, 9, 16, 15, 779).toTimeString()", - js_string!(Local - .from_local_datetime(&NaiveDateTime::new( - NaiveDate::from_ymd_opt(2020, 7, 8).unwrap(), - NaiveTime::from_hms_milli_opt(9, 16, 15, 779).unwrap(), - )) - .earliest() - .unwrap() - .format("09:16:15 GMT%z") - .to_string()), + js_string!(t), )]); } diff --git a/core/engine/src/builtins/date/utils.rs b/core/engine/src/builtins/date/utils.rs index c6bc036fed..fdabb5d0d1 100644 --- a/core/engine/src/builtins/date/utils.rs +++ b/core/engine/src/builtins/date/utils.rs @@ -1,202 +1,721 @@ -use crate::{context::HostHooks, value::IntegerOrNan, JsString}; -use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, Timelike}; +use crate::{context::HostHooks, js_string, value::IntegerOrInfinity, JsString}; +use boa_macros::utf16; use std::{iter::Peekable, str::Chars}; +use time::{macros::format_description, OffsetDateTime, PrimitiveDateTime}; -/// The absolute maximum value of a timestamp -pub(super) const MAX_TIMESTAMP: i64 = 864 * 10i64.pow(13); -/// The number of milliseconds in a second. -pub(super) const MILLIS_PER_SECOND: i64 = 1000; -/// The number of milliseconds in a minute. -pub(super) const MILLIS_PER_MINUTE: i64 = MILLIS_PER_SECOND * 60; -/// The number of milliseconds in an hour. -pub(super) const MILLIS_PER_HOUR: i64 = MILLIS_PER_MINUTE * 60; -/// The number of milliseconds in a day. -pub(super) const MILLIS_PER_DAY: i64 = MILLIS_PER_HOUR * 24; - -// https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-time-values-and-time-range +// Time-related Constants // -// The smaller range supported by a time value as specified in this section is approximately -273,790 to 273,790 -// years relative to 1970. -pub(super) const MIN_YEAR: i64 = -300_000; -pub(super) const MAX_YEAR: i64 = -MIN_YEAR; -pub(super) const MIN_MONTH: i64 = MIN_YEAR * 12; -pub(super) const MAX_MONTH: i64 = MAX_YEAR * 12; - -/// Calculates the absolute day number from the year number. -pub(super) const fn day_from_year(year: i64) -> i64 { - // Taken from https://chromium.googlesource.com/v8/v8/+/refs/heads/main/src/date/date.cc#496 - // Useful to avoid negative divisions and overflows on 32-bit platforms (if we plan to support them). - const YEAR_DELTA: i64 = 399_999; - const fn day(year: i64) -> i64 { - let year = year + YEAR_DELTA; - 365 * year + year / 4 - year / 100 + year / 400 +// More info: +// - [ECMAScript reference][spec] +// +// https://tc39.es/ecma262/#sec-time-related-constants + +// HoursPerDay = 24 +const HOURS_PER_DAY: f64 = 24.0; + +// MinutesPerHour = 60 +const MINUTES_PER_HOUR: f64 = 60.0; + +// SecondsPerMinute = 60 +const SECONDS_PER_MINUTE: f64 = 60.0; + +// msPerSecond = 1000𝔽 +const MS_PER_SECOND: f64 = 1000.0; + +// msPerMinute = 60000𝔽 = msPerSecond × 𝔽(SecondsPerMinute) +pub(super) const MS_PER_MINUTE: f64 = MS_PER_SECOND * SECONDS_PER_MINUTE; + +// msPerHour = 3600000𝔽 = msPerMinute × 𝔽(MinutesPerHour) +const MS_PER_HOUR: f64 = MS_PER_MINUTE * MINUTES_PER_HOUR; + +// msPerDay = 86400000𝔽 = msPerHour × 𝔽(HoursPerDay) +const MS_PER_DAY: f64 = MS_PER_HOUR * HOURS_PER_DAY; + +/// Abstract operation `Day ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-day +pub(super) fn day(t: f64) -> f64 { + // 1. Return 𝔽(floor(ℝ(t / msPerDay))). + (t / MS_PER_DAY).floor() +} + +/// Abstract operation `TimeWithinDay ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-timewithinday +pub(super) fn time_within_day(t: f64) -> f64 { + // 1. Return 𝔽(ℝ(t) modulo ℝ(msPerDay)). + t.rem_euclid(MS_PER_DAY) +} + +/// Abstract operation `DaysInYear ( y )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-daysinyear +fn days_in_year(y: f64) -> u16 { + // 1. Let ry be ℝ(y). + let ry = y; + + // 2. If (ry modulo 400) = 0, return 366𝔽. + if ry.rem_euclid(400.0) == 0.0 { + return 366; + } + + // 3. If (ry modulo 100) = 0, return 365𝔽. + if ry.rem_euclid(100.0) == 0.0 { + return 365; } - assert!(MIN_YEAR <= year && year <= MAX_YEAR); - day(year) - day(1970) + // 4. If (ry modulo 4) = 0, return 366𝔽. + if ry.rem_euclid(4.0) == 0.0 { + return 366; + } + + // 5. Return 365𝔽. + 365 } -/// Abstract operation [`MakeTime`][spec]. +/// Abstract operation `DayFromYear ( y )` +/// +/// More info: +/// - [ECMAScript reference][spec] /// -/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-maketime -pub(crate) fn make_time(hour: i64, min: i64, sec: i64, ms: i64) -> Option { - // 1. If hour is not finite or min is not finite or sec is not finite or ms is not finite, return NaN. +/// [spec]: https://tc39.es/ecma262/#sec-dayfromyear +fn day_from_year(y: f64) -> f64 { + // 1. Let ry be ℝ(y). + // 2. NOTE: In the following steps, each _numYearsN_ is the number of years divisible by N + // that occur between the epoch and the start of year y. + // (The number is negative if y is before the epoch.) + + // 3. Let numYears1 be (ry - 1970). + let num_years_1 = y - 1970.0; + + // 4. Let numYears4 be floor((ry - 1969) / 4). + let num_years_4 = ((y - 1969.0) / 4.0).floor(); + + // 5. Let numYears100 be floor((ry - 1901) / 100). + let num_years_100 = ((y - 1901.0) / 100.0).floor(); + + // 6. Let numYears400 be floor((ry - 1601) / 400). + let num_years_400 = ((y - 1601.0) / 400.0).floor(); + + // 7. Return 𝔽(365 × numYears1 + numYears4 - numYears100 + numYears400). + 365.0 * num_years_1 + num_years_4 - num_years_100 + num_years_400 +} + +/// Abstract operation `TimeFromYear ( y )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-timefromyear +fn time_from_year(y: f64) -> f64 { + // 1. Return msPerDay × DayFromYear(y). + MS_PER_DAY * day_from_year(y) +} + +/// Abstract operation `YearFromTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-yearfromtime +pub(super) fn year_from_time(t: f64) -> i32 { + const MS_PER_AVERAGE_YEAR: f64 = 12.0 * 30.436_875 * MS_PER_DAY; + + // 1. Return the largest integral Number y (closest to +∞) such that TimeFromYear(y) ≤ t. + let mut year = (((t + MS_PER_AVERAGE_YEAR / 2.0) / MS_PER_AVERAGE_YEAR).floor()) as i32 + 1970; + if time_from_year(year.into()) > t { + year -= 1; + } + year +} + +/// Abstract operation `DayWithinYear ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-daywithinyear +fn day_within_year(t: f64) -> u16 { + // 1. Return Day(t) - DayFromYear(YearFromTime(t)). + (day(t) - day_from_year(year_from_time(t).into())) as u16 +} + +/// Abstract operation `InLeapYear ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-inleapyear +fn in_leap_year(t: f64) -> u16 { + // 1. If DaysInYear(YearFromTime(t)) is 366𝔽, return 1𝔽; else return +0𝔽. + (days_in_year(year_from_time(t).into()) == 366).into() +} + +/// Abstract operation `MonthFromTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-monthfromtime +pub(super) fn month_from_time(t: f64) -> u8 { + // 1. Let inLeapYear be InLeapYear(t). + let in_leap_year = in_leap_year(t); + + // 2. Let dayWithinYear be DayWithinYear(t). + let day_within_year = day_within_year(t); + + match day_within_year { + // 3. If dayWithinYear < 31𝔽, return +0𝔽. + t if t < 31 => 0, + // 4. If dayWithinYear < 59𝔽 + inLeapYear, return 1𝔽. + t if t < 59 + in_leap_year => 1, + // 5. If dayWithinYear < 90𝔽 + inLeapYear, return 2𝔽. + t if t < 90 + in_leap_year => 2, + // 6. If dayWithinYear < 120𝔽 + inLeapYear, return 3𝔽. + t if t < 120 + in_leap_year => 3, + // 7. If dayWithinYear < 151𝔽 + inLeapYear, return 4𝔽. + t if t < 151 + in_leap_year => 4, + // 8. If dayWithinYear < 181𝔽 + inLeapYear, return 5𝔽. + t if t < 181 + in_leap_year => 5, + // 9. If dayWithinYear < 212𝔽 + inLeapYear, return 6𝔽. + t if t < 212 + in_leap_year => 6, + // 10. If dayWithinYear < 243𝔽 + inLeapYear, return 7𝔽. + t if t < 243 + in_leap_year => 7, + // 11. If dayWithinYear < 273𝔽 + inLeapYear, return 8𝔽. + t if t < 273 + in_leap_year => 8, + // 12. If dayWithinYear < 304𝔽 + inLeapYear, return 9𝔽. + t if t < 304 + in_leap_year => 9, + // 13. If dayWithinYear < 334𝔽 + inLeapYear, return 10𝔽. + t if t < 334 + in_leap_year => 10, + // 14. Assert: dayWithinYear < 365𝔽 + inLeapYear. + // 15. Return 11𝔽. + _ => 11, + } +} + +/// Abstract operation `DateFromTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-datefromtime +pub(super) fn date_from_time(t: f64) -> u8 { + // 1. Let inLeapYear be InLeapYear(t). + let in_leap_year = in_leap_year(t); + + // 2. Let dayWithinYear be DayWithinYear(t). + let day_within_year = day_within_year(t); + + // 3. Let month be MonthFromTime(t). + let month = month_from_time(t); + + let date = match month { + // 4. If month is +0𝔽, return dayWithinYear + 1𝔽. + 0 => day_within_year + 1, + // 5. If month is 1𝔽, return dayWithinYear - 30𝔽. + 1 => day_within_year - 30, + // 6. If month is 2𝔽, return dayWithinYear - 58𝔽 - inLeapYear. + 2 => day_within_year - 58 - in_leap_year, + // 7. If month is 3𝔽, return dayWithinYear - 89𝔽 - inLeapYear. + 3 => day_within_year - 89 - in_leap_year, + // 8. If month is 4𝔽, return dayWithinYear - 119𝔽 - inLeapYear. + 4 => day_within_year - 119 - in_leap_year, + // 9. If month is 5𝔽, return dayWithinYear - 150𝔽 - inLeapYear. + 5 => day_within_year - 150 - in_leap_year, + // 10. If month is 6𝔽, return dayWithinYear - 180𝔽 - inLeapYear. + 6 => day_within_year - 180 - in_leap_year, + // 11. If month is 7𝔽, return dayWithinYear - 211𝔽 - inLeapYear. + 7 => day_within_year - 211 - in_leap_year, + // 12. If month is 8𝔽, return dayWithinYear - 242𝔽 - inLeapYear. + 8 => day_within_year - 242 - in_leap_year, + // 13. If month is 9𝔽, return dayWithinYear - 272𝔽 - inLeapYear. + 9 => day_within_year - 272 - in_leap_year, + // 14. If month is 10𝔽, return dayWithinYear - 303𝔽 - inLeapYear. + 10 => day_within_year - 303 - in_leap_year, + // 15. Assert: month is 11𝔽. + // 16. Return dayWithinYear - 333𝔽 - inLeapYear. + _ => day_within_year - 333 - in_leap_year, + }; + date as u8 +} + +/// Abstract operation `WeekDay ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-weekday +pub(super) fn week_day(t: f64) -> u8 { + // 1. Return 𝔽(ℝ(Day(t) + 4𝔽) modulo 7). + (day(t) + 4.0).rem_euclid(7.0) as u8 +} + +/// Abstract operation `HourFromTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-hourfromtime +pub(super) fn hour_from_time(t: f64) -> u8 { + // 1. Return 𝔽(floor(ℝ(t / msPerHour)) modulo HoursPerDay). + ((t / MS_PER_HOUR).floor()).rem_euclid(HOURS_PER_DAY) as u8 +} + +/// Abstract operation `MinFromTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-minfromtime +pub(super) fn min_from_time(t: f64) -> u8 { + // 1. Return 𝔽(floor(ℝ(t / msPerMinute)) modulo MinutesPerHour). + ((t / MS_PER_MINUTE).floor()).rem_euclid(MINUTES_PER_HOUR) as u8 +} + +/// Abstract operation `SecFromTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-secfromtime +pub(super) fn sec_from_time(t: f64) -> u8 { + // 1. Return 𝔽(floor(ℝ(t / msPerSecond)) modulo SecondsPerMinute). + ((t / MS_PER_SECOND).floor()).rem_euclid(SECONDS_PER_MINUTE) as u8 +} + +/// Abstract operation `msFromTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-msfromtime +pub(super) fn ms_from_time(t: f64) -> u16 { + // 1. Return 𝔽(ℝ(t) modulo ℝ(msPerSecond)). + t.rem_euclid(MS_PER_SECOND) as u16 +} + +/// Abstract operation `LocalTime ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-localtime +pub(super) fn local_time(t: f64, hooks: &dyn HostHooks) -> f64 { + t + f64::from(local_timezone_offset_seconds(t, hooks)) * MS_PER_SECOND +} + +/// Abstract operation `UTC ( t )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-utc-t +pub(super) fn utc_t(t: f64, hooks: &dyn HostHooks) -> f64 { + // 1. If t is not finite, return NaN. + if !t.is_finite() { + return f64::NAN; + } + + t - f64::from(local_timezone_offset_seconds(t, hooks)) * MS_PER_SECOND +} + +/// Abstract operation `MakeTime ( hour, min, sec, ms )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-maketime +pub(super) fn make_time(hour: f64, min: f64, sec: f64, ms: f64) -> f64 { + // 1. If hour is not finite, min is not finite, sec is not finite, or ms is not finite, return NaN. + if !hour.is_finite() || !min.is_finite() || !sec.is_finite() || !ms.is_finite() { + return f64::NAN; + } + // 2. Let h be 𝔽(! ToIntegerOrInfinity(hour)). + let h = hour.abs().floor().copysign(hour); + // 3. Let m be 𝔽(! ToIntegerOrInfinity(min)). - // 4. Let s be 𝔽(! ToIntegerOrInfinity(sec)). - // 5. Let milli be 𝔽(! ToIntegerOrInfinity(ms)). + let m = min.abs().floor().copysign(min); - // 6. Let t be ((h * msPerHour + m * msPerMinute) + s * msPerSecond) + milli, performing the arithmetic according to IEEE 754-2019 rules (that is, as if using the ECMAScript operators * and +). - // 7. Return t. + // 4. Let s be 𝔽(! ToIntegerOrInfinity(sec)). + let s = sec.abs().floor().copysign(sec); - let h_ms = hour.checked_mul(MILLIS_PER_HOUR)?; - let m_ms = min.checked_mul(MILLIS_PER_MINUTE)?; - let s_ms = sec.checked_mul(MILLIS_PER_SECOND)?; + // 5. Let milli be 𝔽(! ToIntegerOrInfinity(ms)). + let milli = ms.abs().floor().copysign(ms); - h_ms.checked_add(m_ms)?.checked_add(s_ms)?.checked_add(ms) + // 6. Return ((h × msPerHour + m × msPerMinute) + s × msPerSecond) + milli. + ((h * MS_PER_HOUR + m * MS_PER_MINUTE) + s * MS_PER_SECOND) + milli } -/// Abstract operation [`MakeDay`][spec]. +/// Abstract operation `MakeDay ( year, month, date )` /// -/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makeday -pub(crate) fn make_day(mut year: i64, mut month: i64, date: i64) -> Option { - // 1. If year is not finite or month is not finite or date is not finite, return NaN. +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-makeday +pub(super) fn make_day(year: f64, month: f64, date: f64) -> f64 { + // 1. If year is not finite, month is not finite, or date is not finite, return NaN. + if !year.is_finite() || !month.is_finite() || !date.is_finite() { + return f64::NAN; + } + // 2. Let y be 𝔽(! ToIntegerOrInfinity(year)). + let y = year.abs().floor().copysign(year); + // 3. Let m be 𝔽(! ToIntegerOrInfinity(month)). - // 4. Let dt be 𝔽(! ToIntegerOrInfinity(date)). - if !(MIN_YEAR..=MAX_YEAR).contains(&year) || !(MIN_MONTH..=MAX_MONTH).contains(&month) { - return None; - } + let m = month.abs().floor().copysign(month); - // At this point, we've already asserted that year and month are much less than its theoretical - // maximum and minimum values (i64::MAX/MIN), so we don't need to do checked operations. + // 4. Let dt be 𝔽(! ToIntegerOrInfinity(date)). + let dt = date.abs().floor().copysign(date); // 5. Let ym be y + 𝔽(floor(ℝ(m) / 12)). + let ym = y + (m / 12.0).floor(); + // 6. If ym is not finite, return NaN. - year += month / 12; - // 7. Let mn be 𝔽(ℝ(m) modulo 12). - month %= 12; - if month < 0 { - month += 12; - year -= 1; + if !ym.is_finite() { + return f64::NAN; } - // 8. Find a finite time value t such that YearFromTime(t) is ym and MonthFromTime(t) is mn and DateFromTime(t) is - // 1𝔽; but if this is not possible (because some argument is out of range), return NaN. - let month = usize::try_from(month).expect("month must be between 0 and 11 at this point"); - - let mut day = day_from_year(year); - - // Consider leap years when calculating the cumulative days added to the year from the input month - if (year % 4 != 0) || (year % 100 == 0 && year % 400 != 0) { - day += [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][month]; - } else { - day += [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335][month]; - } + // 7. Let mn be 𝔽(ℝ(m) modulo 12). + let mn = m.rem_euclid(12.0) as u8; + + // 8. Find a finite time value t such that YearFromTime(t) is ym, MonthFromTime(t) is mn, + // and DateFromTime(t) is 1𝔽; + // but if this is not possible (because some argument is out of range), return NaN. + let rest = if mn > 1 { 1.0 } else { 0.0 }; + let days_within_year_to_end_of_month = match mn { + 0 => 0.0, + 1 => 31.0, + 2 => 59.0, + 3 => 90.0, + 4 => 120.0, + 5 => 151.0, + 6 => 181.0, + 7 => 212.0, + 8 => 243.0, + 9 => 273.0, + 10 => 304.0, + 11 => 334.0, + 12 => 365.0, + _ => unreachable!(), + }; + let t = + (day_from_year(ym + rest) - 365.0 * rest + days_within_year_to_end_of_month) * MS_PER_DAY; // 9. Return Day(t) + dt - 1𝔽. - (day - 1).checked_add(date) + day(t) + dt - 1.0 } -/// Abstract operation [`MakeDate`][spec]. +/// Abstract operation `MakeDate ( day, time )` +/// +/// More info: +/// - [ECMAScript reference][spec] /// -/// [spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makedate -pub(crate) fn make_date(day: i64, time: i64) -> Option { +/// [spec]: https://tc39.es/ecma262/#sec-makedate +pub(super) fn make_date(day: f64, time: f64) -> f64 { // 1. If day is not finite or time is not finite, return NaN. + if !day.is_finite() || !time.is_finite() { + return f64::NAN; + } + // 2. Let tv be day × msPerDay + time. + let tv = day * MS_PER_DAY + time; + // 3. If tv is not finite, return NaN. + if !tv.is_finite() { + return f64::NAN; + } + // 4. Return tv. - day.checked_mul(MILLIS_PER_DAY)?.checked_add(time) + tv } -/// Abstract operation [`TimeClip`][spec] -/// Returns the timestamp (number of milliseconds) if it is in the expected range. -/// Otherwise, returns `None`. +/// Abstract operation `MakeFullYear ( year )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-makefullyear +pub(super) fn make_full_year(year: f64) -> f64 { + // 1. If year is NaN, return NaN. + if year.is_nan() { + return f64::NAN; + } + + // 2. Let truncated be ! ToIntegerOrInfinity(year). + let truncated = IntegerOrInfinity::from(year); + + // 3. If truncated is in the inclusive interval from 0 to 99, return 1900𝔽 + 𝔽(truncated). + // 4. Return 𝔽(truncated). + match truncated { + IntegerOrInfinity::Integer(i) if (0..=99).contains(&i) => 1900.0 + i as f64, + IntegerOrInfinity::Integer(i) => i as f64, + IntegerOrInfinity::PositiveInfinity => f64::INFINITY, + IntegerOrInfinity::NegativeInfinity => f64::NEG_INFINITY, + } +} + +/// Abstract operation `TimeClip ( time )` +/// +/// More info: +/// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-timeclip -pub(super) fn time_clip(time: i64) -> Option { +pub(crate) fn time_clip(time: f64) -> f64 { // 1. If time is not finite, return NaN. - // 2. If abs(ℝ(time)) > 8.64 × 10^15, return NaN. + if !time.is_finite() { + return f64::NAN; + } + + // 2. If abs(ℝ(time)) > 8.64 × 10**15, return NaN. + if time.abs() > 8.64e15 { + return f64::NAN; + } + // 3. Return 𝔽(! ToIntegerOrInfinity(time)). - (time.checked_abs()? <= MAX_TIMESTAMP).then_some(time) -} - -#[derive(Default, Debug, Clone, Copy)] -pub(super) struct DateParameters { - pub(super) year: Option, - pub(super) month: Option, - pub(super) date: Option, - pub(super) hour: Option, - pub(super) minute: Option, - pub(super) second: Option, - pub(super) millisecond: Option, -} - -/// Replaces some (or all) parameters of `date` with the specified parameters -pub(super) fn replace_params( - datetime: i64, - params: DateParameters, - hooks: &dyn HostHooks, -) -> Option { - let datetime = NaiveDateTime::from_timestamp_millis(datetime)?; - let DateParameters { - year, - month, - date, - hour, - minute, - second, - millisecond, - } = params; - - let datetime = if LOCAL { - hooks.local_from_utc(datetime).naive_local() - } else { - datetime - }; + let time = time.trunc(); + if time.abs() == 0.0 { + return 0.0; + } - let year = match year { - Some(i) => i.as_integer()?, - None => i64::from(datetime.year()), - }; - let month = match month { - Some(i) => i.as_integer()?, - None => i64::from(datetime.month() - 1), - }; - let date = match date { - Some(i) => i.as_integer()?, - None => i64::from(datetime.day()), - }; - let hour = match hour { - Some(i) => i.as_integer()?, - None => i64::from(datetime.hour()), + time +} + +/// Abstract operation `TimeString ( tv )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-timestring +pub(super) fn time_string(tv: f64) -> JsString { + // 1. Let hour be ToZeroPaddedDecimalString(ℝ(HourFromTime(tv)), 2). + let hour = pad_two(hour_from_time(tv)); + + // 2. Let minute be ToZeroPaddedDecimalString(ℝ(MinFromTime(tv)), 2). + let minute = pad_two(min_from_time(tv)); + + // 3. Let second be ToZeroPaddedDecimalString(ℝ(SecFromTime(tv)), 2). + let second = pad_two(sec_from_time(tv)); + + // 4. Return the string-concatenation of + // hour, + // ":", + // minute, + // ":", + // second, + // the code unit 0x0020 (SPACE), + // and "GMT". + js_string!( + &hour, + utf16!(":"), + &minute, + utf16!(":"), + &second, + utf16!(" GMT") + ) +} + +/// Abstract operation `DateString ( tv )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-datestring +pub(super) fn date_string(tv: f64) -> JsString { + // 1. Let weekday be the Name of the entry in Table 63 with the Number WeekDay(tv). + let weekday = match week_day(tv) { + 0 => utf16!("Sun"), + 1 => utf16!("Mon"), + 2 => utf16!("Tue"), + 3 => utf16!("Wed"), + 4 => utf16!("Thu"), + 5 => utf16!("Fri"), + 6 => utf16!("Sat"), + _ => unreachable!(), }; - let minute = match minute { - Some(i) => i.as_integer()?, - None => i64::from(datetime.minute()), + + // 2. Let month be the Name of the entry in Table 64 with the Number MonthFromTime(tv). + let month = match month_from_time(tv) { + 0 => utf16!("Jan"), + 1 => utf16!("Feb"), + 2 => utf16!("Mar"), + 3 => utf16!("Apr"), + 4 => utf16!("May"), + 5 => utf16!("Jun"), + 6 => utf16!("Jul"), + 7 => utf16!("Aug"), + 8 => utf16!("Sep"), + 9 => utf16!("Oct"), + 10 => utf16!("Nov"), + 11 => utf16!("Dec"), + _ => unreachable!(), }; - let second = match second { - Some(i) => i.as_integer()?, - None => i64::from(datetime.second()), + + // 3. Let day be ToZeroPaddedDecimalString(ℝ(DateFromTime(tv)), 2). + let day = pad_two(date_from_time(tv)); + + // 4. Let yv be YearFromTime(tv). + let yv = year_from_time(tv); + + // 5. If yv is +0𝔽 or yv > +0𝔽, let yearSign be the empty String; otherwise, let yearSign be "-". + let year_sign = if yv >= 0 { utf16!("") } else { utf16!("-") }; + + // 6. Let paddedYear be ToZeroPaddedDecimalString(abs(ℝ(yv)), 4). + let yv = yv.unsigned_abs(); + let padded_year = if yv >= 100_000 { + js_string!(&pad_six(yv)) + } else if yv >= 10000 { + js_string!(&pad_five(yv)) + } else { + js_string!(&pad_four(yv)) }; - let millisecond = match millisecond { - Some(i) => i.as_integer()?, - None => i64::from(datetime.timestamp_subsec_millis()), + + // 7. Return the string-concatenation of + // weekday, + // the code unit 0x0020 (SPACE), + // month, + // the code unit 0x0020 (SPACE), + // day, + // the code unit 0x0020 (SPACE), + // yearSign, + // and paddedYear. + js_string!( + weekday, + utf16!(" "), + month, + utf16!(" "), + &day, + utf16!(" "), + year_sign, + &padded_year + ) +} + +/// Abstract operation `TimeZoneString ( tv )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-timezoneestring +pub(super) fn time_zone_string(t: f64, hooks: &dyn HostHooks) -> JsString { + // 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier(). + // 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then + // a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier). + // 3. Else, + // a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ℤ(ℝ(tv) × 10**6)). + // 4. Let offset be 𝔽(truncate(offsetNs / 10**6)). + let offset = f64::from(local_timezone_offset_seconds(t, hooks)) * MS_PER_SECOND; + //let offset = hooks.local_timezone_offset_seconds((t / MS_PER_SECOND).floor() as i64); + + // 5. If offset is +0𝔽 or offset > +0𝔽, then + let (offset_sign, abs_offset) = if offset >= 0.0 { + // a. Let offsetSign be "+". + // b. Let absOffset be offset. + (utf16!("+"), offset) + } + // 6. Else, + else { + // a. Let offsetSign be "-". + // b. Let absOffset be -offset. + (utf16!("-"), -offset) }; - let new_day = make_day(year, month, date)?; - let new_time = make_time(hour, minute, second, millisecond)?; - let mut ts = make_date(new_day, new_time)?; + // 7. Let offsetMin be ToZeroPaddedDecimalString(ℝ(MinFromTime(absOffset)), 2). + let offset_min = pad_two(min_from_time(abs_offset)); + + // 8. Let offsetHour be ToZeroPaddedDecimalString(ℝ(HourFromTime(absOffset)), 2). + let offset_hour = pad_two(hour_from_time(abs_offset)); + + // 9. Let tzName be an implementation-defined string that is either the empty String or the + // string-concatenation of the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), + // an implementation-defined timezone name, and the code unit 0x0029 (RIGHT PARENTHESIS). + // 10. Return the string-concatenation of offsetSign, offsetHour, offsetMin, and tzName. + js_string!(offset_sign, &offset_hour, &offset_min) +} + +/// Abstract operation `ToDateString ( tv )` +/// +/// More info: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-todatestring +pub(super) fn to_date_string_t(tv: f64, hooks: &dyn HostHooks) -> JsString { + // 1. If tv is NaN, return "Invalid Date". + if tv.is_nan() { + return JsString::from("Invalid Date"); + } + + // 2. Let t be LocalTime(tv). + let t = local_time(tv, hooks); + + // 3. Return the string-concatenation of + // DateString(t), + // the code unit 0x0020 (SPACE), + // TimeString(t), + // and TimeZoneString(tv). + js_string!( + &date_string(t), + utf16!(" "), + &time_string(t), + &time_zone_string(t, hooks) + ) +} + +fn local_timezone_offset_seconds(t: f64, hooks: &dyn HostHooks) -> i32 { + let millis = t.rem_euclid(MS_PER_SECOND); + let seconds = ((t - millis) / MS_PER_SECOND) as i64; + hooks.local_timezone_offset_seconds(seconds) +} - if LOCAL { - ts = hooks - .local_from_naive_local(NaiveDateTime::from_timestamp_millis(ts)?) - .earliest()? - .naive_utc() - .timestamp_millis(); +pub(super) fn pad_two(t: u8) -> [u16; 2] { + if t < 10 { + [0x30, 0x30 + u16::from(t)] + } else { + [0x30 + (u16::from(t) / 10), 0x30 + (u16::from(t) % 10)] } +} - time_clip(ts) +pub(super) fn pad_three(t: u16) -> [u16; 3] { + [0x30 + t / 100, 0x30 + ((t / 10) % 10), 0x30 + (t % 10)] +} + +pub(super) fn pad_four(t: u32) -> [u16; 4] { + [ + 0x30 + (t / 1000) as u16, + 0x30 + ((t / 100) % 10) as u16, + 0x30 + ((t / 10) % 10) as u16, + 0x30 + (t % 10) as u16, + ] +} + +pub(super) fn pad_five(t: u32) -> [u16; 5] { + [ + 0x30 + (t / 10_000) as u16, + 0x30 + ((t / 1000) % 10) as u16, + 0x30 + ((t / 100) % 10) as u16, + 0x30 + ((t / 10) % 10) as u16, + 0x30 + (t % 10) as u16, + ] +} + +pub(super) fn pad_six(t: u32) -> [u16; 6] { + [ + 0x30 + (t / 100_000) as u16, + 0x30 + ((t / 10_000) % 10) as u16, + 0x30 + ((t / 1000) % 10) as u16, + 0x30 + ((t / 100) % 10) as u16, + 0x30 + ((t / 10) % 10) as u16, + 0x30 + (t % 10) as u16, + ] } /// Parse a date string according to the steps specified in [`Date.parse`][spec]. @@ -216,17 +735,18 @@ pub(super) fn parse_date(date: &JsString, hooks: &dyn HostHooks) -> Option // Date Time String Format: 'YYYY-MM-DDTHH:mm:ss.sssZ' if let Some(dt) = DateParser::new(&date, hooks).parse() { - return Some(dt.timestamp_millis()); + return Some(dt); } // `toString` format: `Thu Jan 01 1970 00:00:00 GMT+0000` - if let Ok(dt) = DateTime::parse_from_str(&date, "%a %b %d %Y %H:%M:%S GMT%z") { - return Some(dt.naive_utc().timestamp_millis()); + if let Ok(t) = OffsetDateTime::parse(&date, &format_description!("[weekday repr:short] [month repr:short] [day] [year] [hour]:[minute]:[second] GMT[offset_hour sign:mandatory][offset_minute][end]")) { + return Some(t.unix_timestamp() * 1000 + i64::from(t.millisecond())); } // `toUTCString` format: `Thu, 01 Jan 1970 00:00:00 GMT` - if let Ok(dt) = NaiveDateTime::parse_from_str(&date, "%a, %d %b %Y %H:%M:%S GMT") { - return Some(dt.timestamp_millis()); + if let Ok(t) = PrimitiveDateTime::parse(&date, &format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT[end]")) { + let t = t.assume_utc(); + return Some(t.unix_timestamp() * 1000 + i64::from(t.millisecond())); } None @@ -245,7 +765,7 @@ struct DateParser<'a> { minute: u32, second: u32, millisecond: u32, - offset: Duration, + offset: i64, } impl<'a> DateParser<'a> { @@ -260,7 +780,7 @@ impl<'a> DateParser<'a> { minute: 0, second: 0, millisecond: 0, - offset: Duration::minutes(0), + offset: 0, } } @@ -280,28 +800,55 @@ impl<'a> DateParser<'a> { }) } - fn finish(&mut self) -> Option { + fn finish(&mut self) -> Option { if self.input.peek().is_some() { return None; } - NaiveDate::from_ymd_opt(self.year, self.month, self.day) - .and_then(|date| { - date.and_hms_milli_opt(self.hour, self.minute, self.second, self.millisecond) - }) - .map(|dt| dt + self.offset) + let date = make_date( + make_day(self.year.into(), (self.month - 1).into(), self.day.into()), + make_time( + self.hour.into(), + self.minute.into(), + self.second.into(), + self.millisecond.into(), + ), + ); + + let date = date + (self.offset as f64) * MS_PER_MINUTE; + + let t = time_clip(date); + if t.is_finite() { + Some(t as i64) + } else { + None + } } - fn finish_local(&mut self) -> Option { - self.finish().and_then(|dt| { - self.hooks - .local_from_naive_local(dt) - .earliest() - .map(|dt| dt.naive_utc()) - }) + fn finish_local(&mut self) -> Option { + if self.input.peek().is_some() { + return None; + } + + let date = make_date( + make_day(self.year.into(), (self.month - 1).into(), self.day.into()), + make_time( + self.hour.into(), + self.minute.into(), + self.second.into(), + self.millisecond.into(), + ), + ); + + let t = time_clip(utc_t(date, self.hooks)); + if t.is_finite() { + Some(t as i64) + } else { + None + } } - fn parse(&mut self) -> Option { + fn parse(&mut self) -> Option { self.parse_year()?; match self.input.peek() { Some('T') => return self.parse_time(), @@ -310,6 +857,9 @@ impl<'a> DateParser<'a> { } self.next_expect('-')?; self.month = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?); + if self.month < 1 || self.month > 12 { + return None; + } match self.input.peek() { Some('T') => return self.parse_time(), None => return self.finish(), @@ -317,6 +867,9 @@ impl<'a> DateParser<'a> { } self.next_expect('-')?; self.day = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?); + if self.day < 1 || self.day > 31 { + return None; + } match self.input.peek() { Some('T') => self.parse_time(), _ => self.finish(), @@ -358,11 +911,17 @@ impl<'a> DateParser<'a> { } } - fn parse_time(&mut self) -> Option { + fn parse_time(&mut self) -> Option { self.next_expect('T')?; self.hour = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?); + if self.hour > 24 { + return None; + } self.next_expect(':')?; self.minute = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?); + if self.minute > 59 { + return None; + } match self.input.peek() { Some(':') => {} None => return self.finish_local(), @@ -373,6 +932,9 @@ impl<'a> DateParser<'a> { } self.next_expect(':')?; self.second = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?); + if self.second > 59 { + return None; + } match self.input.peek() { Some('.') => {} None => return self.finish_local(), @@ -397,30 +959,40 @@ impl<'a> DateParser<'a> { match self.input.next() { Some('Z') => return Some(()), Some('+') => { - self.offset = -Duration::hours( - i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?), - ); + let offset_hour = + i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?); + if offset_hour > 23 { + return None; + } + self.offset = -offset_hour * 60; if self.input.peek().is_none() { return Some(()); } self.next_expect(':')?; - self.offset = self.offset - + -Duration::minutes( - i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?), - ); + let offset_minute = + i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?); + if offset_minute > 59 { + return None; + } + self.offset += -offset_minute; } Some('-') => { - self.offset = Duration::hours( - i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?), - ); + let offset_hour = + i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?); + if offset_hour > 23 { + return None; + } + self.offset = offset_hour * 60; if self.input.peek().is_none() { return Some(()); } self.next_expect(':')?; - self.offset = self.offset - + Duration::minutes( - i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?), - ); + let offset_minute = + i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?); + if offset_minute > 59 { + return None; + } + self.offset += offset_minute; } _ => return None, } diff --git a/core/engine/src/context/hooks.rs b/core/engine/src/context/hooks.rs index 0fec62c967..3912a6f86f 100644 --- a/core/engine/src/context/hooks.rs +++ b/core/engine/src/context/hooks.rs @@ -1,13 +1,15 @@ use crate::{ builtins::promise::OperationType, + context::intrinsics::Intrinsics, job::JobCallback, object::{JsFunction, JsObject}, realm::Realm, Context, JsResult, JsValue, }; -use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone, Utc}; +use time::{OffsetDateTime, UtcOffset}; -use super::intrinsics::Intrinsics; +#[cfg(test)] +use time::util::local_offset; /// [`Host Hooks`] customizable by the host code or engine. /// @@ -170,46 +172,28 @@ pub trait HostHooks { /// Gets the current UTC time of the host. /// - /// Defaults to using [`Utc::now`] on all targets, which can cause panics if the target - /// doesn't support [`SystemTime::now`][time]. + /// Defaults to using [`OffsetDateTime::now_utc`] on all targets, + /// which can cause panics if the target doesn't support [`SystemTime::now`][time]. /// /// [time]: std::time::SystemTime::now - fn utc_now(&self) -> NaiveDateTime { - Utc::now().naive_utc() + fn utc_now(&self) -> i64 { + let now = OffsetDateTime::now_utc(); + now.unix_timestamp() * 1000 + i64::from(now.millisecond()) } - /// Converts the naive datetime `utc` to the corresponding local datetime. - /// - /// Defaults to using [`Local`] on all targets, which can cause panics if the taget - /// doesn't support [`SystemTime::now`][time]. - /// - /// [time]: std::time::SystemTime::now - fn local_from_utc(&self, utc: NaiveDateTime) -> DateTime { - let offset = Local.offset_from_utc_datetime(&utc); - offset.from_utc_datetime(&utc) - } - - /// Converts the naive local datetime `local` to a local timezone datetime. - /// - /// Defaults to using [`Local`] on all targets, which can cause panics if the target - /// doesn't support [`SystemTime::now`][time]. - /// - /// [time]: std::time::SystemTime::now - fn local_from_naive_local(&self, local: NaiveDateTime) -> LocalResult> { - match Local.offset_from_local_datetime(&local) { - LocalResult::None => LocalResult::None, - LocalResult::Single(offset) => offset.from_local_datetime(&local), - LocalResult::Ambiguous(earliest, latest) => { - match ( - earliest.from_local_datetime(&local).earliest(), - latest.from_local_datetime(&local).latest(), - ) { - (Some(earliest), Some(latest)) => LocalResult::Ambiguous(earliest, latest), - (Some(dt), None) | (None, Some(dt)) => LocalResult::Single(dt), - (None, None) => LocalResult::None, - } - } + /// Returns the offset of the local timezone to the `utc` timezone in seconds. + fn local_timezone_offset_seconds(&self, unix_time_seconds: i64) -> i32 { + // Safety: This is needed during tests because cargo is running tests in multiple threads. + // It is safe because tests do not modify the environment. + #[cfg(test)] + unsafe { + local_offset::set_soundness(local_offset::Soundness::Unsound); } + + OffsetDateTime::from_unix_timestamp(unix_time_seconds) + .ok() + .and_then(|t| UtcOffset::local_offset_at(t).ok()) + .map_or(0, UtcOffset::whole_seconds) } /// Gets the maximum size in bits that can be allocated for an `ArrayBuffer` or a diff --git a/core/engine/src/object/builtins/jsdate.rs b/core/engine/src/object/builtins/jsdate.rs index ae06af33c9..6f6ad4b52f 100644 --- a/core/engine/src/object/builtins/jsdate.rs +++ b/core/engine/src/object/builtins/jsdate.rs @@ -1,8 +1,4 @@ //! A Rust API wrapper for Boa's `Date` ECMAScript Builtin Object. -use std::ops::Deref; - -use boa_gc::{Finalize, Trace}; -use chrono::DateTime; use crate::{ builtins::Date, @@ -10,6 +6,9 @@ use crate::{ value::TryFromJs, Context, JsNativeError, JsResult, JsValue, }; +use boa_gc::{Finalize, Trace}; +use std::ops::Deref; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; /// `JsDate` is a wrapper for JavaScript `JsDate` builtin object /// @@ -551,9 +550,9 @@ impl JsDate { .to_string(context)? .to_std_string() .map_err(|_| JsNativeError::typ().with_message("unpaired surrogate on date string"))?; - let date_time = DateTime::parse_from_rfc3339(&string) + let t = OffsetDateTime::parse(&string, &Rfc3339) .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; - let date_time = Date::new(Some(date_time.naive_local().timestamp_millis())); + let date_time = Date::new((t.unix_timestamp() * 1000 + i64::from(t.millisecond())) as f64); Ok(Self { inner: JsObject::from_proto_and_data_with_shared_shape( diff --git a/core/engine/src/value/integer.rs b/core/engine/src/value/integer.rs index 6212ae7e5e..a81b63c058 100644 --- a/core/engine/src/value/integer.rs +++ b/core/engine/src/value/integer.rs @@ -99,26 +99,3 @@ impl PartialOrd for i64 { } } } - -/// Represents the result of the `to_integer_or_nan` method. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum IntegerOrNan { - Integer(i64), - Nan, -} - -impl IntegerOrNan { - /// Gets the wrapped `i64` if the variant is an `Integer`. - pub(crate) const fn as_integer(self) -> Option { - match self { - Self::Integer(i) => Some(i), - Self::Nan => None, - } - } -} - -impl From for IntegerOrNan { - fn from(ior: IntegerOrInfinity) -> Self { - ior.as_integer().map_or(Self::Nan, IntegerOrNan::Integer) - } -} diff --git a/core/engine/src/value/mod.rs b/core/engine/src/value/mod.rs index 8f7caf566a..07ae303563 100644 --- a/core/engine/src/value/mod.rs +++ b/core/engine/src/value/mod.rs @@ -45,7 +45,7 @@ pub use self::{ #[doc(inline)] pub use boa_macros::TryFromJs; -pub(crate) use self::{conversions::IntoOrUndefined, integer::IntegerOrNan}; +pub(crate) use self::conversions::IntoOrUndefined; static TWO_E_64: Lazy = Lazy::new(|| { const TWO_E_64: u128 = 2u128.pow(64); @@ -855,22 +855,6 @@ impl JsValue { Ok(IntegerOrInfinity::from(number)) } - /// Modified abstract operation `ToIntegerOrInfinity ( argument )`. - /// - /// This function is almost the same as [`Self::to_integer_or_infinity`], but with the exception - /// that this will return `Nan` if [`Self::to_number`] returns a non-finite number. - pub(crate) fn to_integer_or_nan(&self, context: &mut Context) -> JsResult { - // 1. Let number be ? ToNumber(argument). - let number = self.to_number(context)?; - - if number.is_nan() { - return Ok(IntegerOrNan::Nan); - } - - // Continues on `IntegerOrInfinity::from::` - Ok(IntegerOrInfinity::from(number).into()) - } - /// Converts a value to a double precision floating point. /// /// This function is equivalent to the unary `+` operator (`+value`) in JavaScript diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 0694bc0ffa..78ba86f61a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -16,7 +16,7 @@ boa_interner.workspace = true boa_gc.workspace = true boa_parser.workspace = true boa_runtime.workspace = true -chrono.workspace = true +time.workspace = true smol = "2.0.0" futures-util = "0.3.30" diff --git a/examples/src/bin/jsdate.rs b/examples/src/bin/jsdate.rs index 04df85acda..89848e6094 100644 --- a/examples/src/bin/jsdate.rs +++ b/examples/src/bin/jsdate.rs @@ -1,22 +1,15 @@ use boa_engine::{ context::HostHooks, js_string, object::builtins::JsDate, Context, JsResult, JsValue, }; -use chrono::{DateTime, FixedOffset, LocalResult, NaiveDateTime, TimeZone}; struct CustomTimezone; // This pins the local timezone to a system-agnostic value; in this case, UTC+3 impl HostHooks for CustomTimezone { - fn local_from_utc(&self, utc: NaiveDateTime) -> DateTime { - FixedOffset::east_opt(3 * 3600) - .unwrap() - .from_utc_datetime(&utc) - } - - fn local_from_naive_local(&self, local: NaiveDateTime) -> LocalResult> { - FixedOffset::east_opt(3 * 3600) - .unwrap() - .from_local_datetime(&local) + fn local_timezone_offset_seconds(&self, _: i64) -> i32 { + time::UtcOffset::from_hms(3, 0, 0) + .expect("must be valid offset") + .whole_seconds() } } diff --git a/ffi/wasm/Cargo.toml b/ffi/wasm/Cargo.toml index e00edd2556..03ea5a2794 100644 --- a/ffi/wasm/Cargo.toml +++ b/ffi/wasm/Cargo.toml @@ -15,7 +15,6 @@ rust-version.workspace = true boa_engine = { workspace = true, features = ["js"] } wasm-bindgen = { version = "0.2.90", default-features = false } getrandom = { version = "0.2.12", features = ["js"] } -chrono = { workspace = true, default-features = false, features = ["clock", "std", "wasmbind"] } console_error_panic_hook = "0.1.7" [features] diff --git a/ffi/wasm/src/lib.rs b/ffi/wasm/src/lib.rs index 57e1837955..5f95bc7f74 100644 --- a/ffi/wasm/src/lib.rs +++ b/ffi/wasm/src/lib.rs @@ -7,7 +7,6 @@ #![cfg_attr(not(test), forbid(clippy::unwrap_used))] use boa_engine::{Context, Source}; -use chrono as _; use getrandom as _; use wasm_bindgen::prelude::*; diff --git a/tests/tester/Cargo.toml b/tests/tester/Cargo.toml index 3f8784adb5..624b4e6d5e 100644 --- a/tests/tester/Cargo.toml +++ b/tests/tester/Cargo.toml @@ -31,6 +31,7 @@ phf = { workspace = true, features = ["macros"] } comfy-table = "7.1.0" serde_repr = "0.1.18" bus = "2.4.1" +time.workspace = true [features] default = ["boa_engine/intl", "boa_engine/experimental", "boa_engine/annex-b"] diff --git a/tests/tester/src/main.rs b/tests/tester/src/main.rs index 0305d2f4b7..792f26d471 100644 --- a/tests/tester/src/main.rs +++ b/tests/tester/src/main.rs @@ -192,6 +192,12 @@ const DEFAULT_TEST262_DIRECTORY: &str = "test262"; /// Program entry point. fn main() -> Result<()> { + // Safety: This is needed because we run tests in multiple threads. + // It is safe because tests do not modify the environment. + unsafe { + time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound); + } + // initializes the monotonic clock. Lazy::force(&START); color_eyre::install()?;