diff --git a/boa/src/builtins/date/mod.rs b/boa/src/builtins/date/mod.rs index cd6487506d..ec6958d156 100644 --- a/boa/src/builtins/date/mod.rs +++ b/boa/src/builtins/date/mod.rs @@ -6,8 +6,9 @@ use crate::{ gc::{empty_trace, Finalize, Trace}, object::{ConstructorBuilder, ObjectData, PROTOTYPE}, property::Attribute, + symbol::WellKnownSymbols, value::{JsValue, PreferredType}, - BoaProfiler, Context, Result, + BoaProfiler, Context, JsString, Result, }; use chrono::{prelude::*, Duration, LocalResult}; use std::fmt::Display; @@ -58,36 +59,6 @@ macro_rules! getter_method { }}; } -macro_rules! setter_method { - ($name:ident($($e:expr),* $(,)?)) => {{ - fn set_value(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { - let mut result = this_time_value(this, context)?; - result.$name( - $( - args - .get($e) - .and_then(|value| { - value.to_numeric_number(context).map_or_else( - |_| None, - |value| { - if value == 0f64 || value.is_normal() { - Some(value) - } else { - None - } - }, - ) - }) - ),* - ); - - this.set_data(ObjectData::Date(result)); - Ok(JsValue::new(result.get_time())) - } - set_value - }}; -} - #[derive(Debug, Finalize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Date(Option); @@ -135,11 +106,7 @@ impl BuiltIn for Date { .method(getter_method!(get_seconds), "getSeconds", 0) .method(getter_method!(get_time), "getTime", 0) .method(getter_method!(get_year), "getYear", 0) - .method( - getter_method!(Self::get_timezone_offset), - "getTimezoneOffset", - 0, - ) + .method(Self::get_timezone_offset, "getTimezoneOffset", 0) .method(getter_method!(get_utc_date), "getUTCDate", 0) .method(getter_method!(get_utc_day), "getUTCDay", 0) .method(getter_method!(get_utc_full_year), "getUTCFullYear", 0) @@ -152,39 +119,36 @@ impl BuiltIn for Date { .method(getter_method!(get_utc_minutes), "getUTCMinutes", 0) .method(getter_method!(get_utc_month), "getUTCMonth", 0) .method(getter_method!(get_utc_seconds), "getUTCSeconds", 0) - .method(setter_method!(set_date(0)), "setDate", 1) - .method(setter_method!(set_full_year(0, 1, 2)), "setFullYear", 1) - .method(setter_method!(set_hours(0, 1, 2, 3)), "setHours", 1) - .method(setter_method!(set_milliseconds(0)), "setMilliseconds", 1) - .method(setter_method!(set_minutes(0, 1, 2)), "setMinutes", 1) - .method(setter_method!(set_month(0, 1)), "setMonth", 1) - .method(setter_method!(set_seconds(0, 1)), "setSeconds", 1) - .method(setter_method!(set_year(0, 1, 2)), "setYear", 1) - .method(setter_method!(set_time(0)), "setTime", 1) - .method(setter_method!(set_utc_date(0)), "setUTCDate", 1) - .method( - setter_method!(set_utc_full_year(0, 1, 2)), - "setUTCFullYear", - 1, - ) - .method(setter_method!(set_utc_hours(0, 1, 2, 3)), "setUTCHours", 1) - .method( - setter_method!(set_utc_milliseconds(0)), - "setUTCMilliseconds", - 1, - ) - .method(setter_method!(set_utc_minutes(0, 1, 2)), "setUTCMinutes", 1) - .method(setter_method!(set_utc_month(0, 1)), "setUTCMonth", 1) - .method(setter_method!(set_utc_seconds(0, 1)), "setUTCSeconds", 1) - .method(getter_method!(to_date_string), "toDateString", 0) + .method(Self::set_date, "setDate", 1) + .method(Self::set_full_year, "setFullYear", 3) + .method(Self::set_hours, "setHours", 4) + .method(Self::set_milliseconds, "setMilliseconds", 1) + .method(Self::set_minutes, "setMinutes", 3) + .method(Self::set_month, "setMonth", 2) + .method(Self::set_seconds, "setSeconds", 2) + .method(Self::set_year, "setYear", 1) + .method(Self::set_time, "setTime", 1) + .method(Self::set_utc_date, "setUTCDate", 1) + .method(Self::set_utc_full_year, "setUTCFullYear", 3) + .method(Self::set_utc_hours, "setUTCHours", 4) + .method(Self::set_utc_milliseconds, "setUTCMilliseconds", 1) + .method(Self::set_utc_minutes, "setUTCMinutes", 3) + .method(Self::set_utc_month, "setUTCMonth", 2) + .method(Self::set_utc_seconds, "setUTCSeconds", 2) + .method(Self::to_date_string, "toDateString", 0) .method(getter_method!(to_gmt_string), "toGMTString", 0) - .method(getter_method!(to_iso_string), "toISOString", 0) - .method(getter_method!(to_json), "toJSON", 0) + .method(Self::to_iso_string, "toISOString", 0) + .method(Self::to_json, "toJSON", 1) // Locale strings - .method(getter_method!(to_string), "toString", 0) - .method(getter_method!(to_time_string), "toTimeString", 0) + .method(Self::to_string, "toString", 0) + .method(Self::to_time_string, "toTimeString", 0) .method(getter_method!(to_utc_string), "toUTCString", 0) .method(getter_method!(value_of), "valueOf", 0) + .method( + Self::to_primitive, + (WellKnownSymbols::to_primitive(), "[Symbol.toPrimitive]"), + 1, + ) .static_method(Self::now, "now", 0) .static_method(Self::parse, "parse", 1) .static_method(Self::utc, "UTC", 7) @@ -198,7 +162,7 @@ impl Date { /// The amount of arguments this function object takes. pub(crate) const LENGTH: usize = 7; - /// Check if the time (number of miliseconds) is in the expected range. + /// Check if the time (number of milliseconds) is in the expected range. /// Returns None if the time is not in the range, otherwise returns the time itself in option. /// /// More information: @@ -217,6 +181,7 @@ impl Date { /// Converts the `Date` to a local `DateTime`. /// /// If the `Date` is invalid (i.e. NAN), this function will return `None`. + #[inline] pub fn to_local(self) -> Option> { self.0 .map(|utc| Local::now().timezone().from_utc_datetime(&utc)) @@ -482,7 +447,7 @@ impl Date { args: &[JsValue], context: &mut Context, ) -> Result { - let year = args[0].to_number(context)?; + let mut year = args[0].to_number(context)?; let month = args[1].to_number(context)?; let day = args .get(2) @@ -507,29 +472,75 @@ impl Date { return Ok(this.clone()); } - let year = year as i32; - let month = month as u32; - let day = day as u32; - let hour = hour as u32; - let min = min as u32; - let sec = sec as u32; - let milli = milli as u32; + if (0.0..=99.0).contains(&year) { + year += 1900.0 + } - let year = if (0..=99).contains(&year) { - 1900 + year + let mut date = Self( + NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()), + ); + + date.set_components( + false, + Some(year), + Some(month), + Some(day), + Some(hour), + Some(min), + Some(sec), + Some(milli), + ); + + this.set_data(ObjectData::Date(date)); + + Ok(this.clone()) + } + + /// `Date.prototype[@@toPrimitive]` + /// + /// The [@@toPrimitive]() method converts a Date object to a primitive value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype-@@toprimitive + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/@@toPrimitive + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_primitive( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let O be the this value. + // 2. If Type(O) is not Object, throw a TypeError exception. + let o = if let Some(o) = this.as_object() { + o } else { - year + return context.throw_type_error("Date.prototype[@@toPrimitive] called on non object"); }; - let final_date = NaiveDate::from_ymd_opt(year, month + 1, day) - .and_then(|naive_date| naive_date.and_hms_milli_opt(hour, min, sec, milli)) - .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) - .map(|local| local.naive_utc()) - .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); + let hint = args.get(0).cloned().unwrap_or_default(); + + let try_first = match hint.as_string().map(|s| s.as_str()) { + // 3. If hint is "string" or "default", then + // a. Let tryFirst be string. + Some("string") | Some("default") => PreferredType::String, + // 4. Else if hint is "number", then + // a. Let tryFirst be number. + Some("number") => PreferredType::Number, + // 5. Else, throw a TypeError exception. + _ => { + return context + .throw_type_error("Date.prototype[@@toPrimitive] called with invalid hint") + } + }; - let date = Date(final_date); - this.set_data(ObjectData::Date(date)); - Ok(this.clone()) + // 6. Return ? OrdinaryToPrimitive(O, tryFirst). + o.ordinary_to_primitive(context, try_first) } /// `Date.prototype.getDate()` @@ -694,9 +705,23 @@ impl Date { /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.gettimezoneoffset /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset #[inline] - pub fn get_timezone_offset() -> f64 { - let offset_seconds = chrono::Local::now().offset().local_minus_utc() as f64; - offset_seconds / 60f64 + pub fn get_timezone_offset( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let t = this_time_value(this, context)?; + + // 2. If t is NaN, return NaN. + if t.0.is_none() { + return Ok(JsValue::nan()); + } + + // 3. Return (t - LocalTime(t)) / msPerMinute. + Ok(JsValue::new( + -Local::now().offset().local_minus_utc() as f64 / 60f64, + )) } /// `Date.prototype.getUTCDate()` @@ -829,12 +854,28 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setdate /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate - pub fn set_date(&mut self, day: Option) { - if let Some(day) = day { - self.set_components(false, None, None, Some(day), None, None, None, None) - } else { - self.0 = None - } + pub fn set_date(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let dt be ? ToNumber(date). + let dt = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let newDate be MakeDate(MakeDay(YearFromTime(t), MonthFromTime(t), dt), TimeWithinDay(t)). + t.set_components(false, None, None, Some(dt), None, None, None, None); + + // 4. Let u be TimeClip(UTC(newDate)). + let u = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::Date(t)); + + // 6. Return u. + Ok(u.into()) } /// `Date.prototype.setFullYear()` @@ -848,19 +889,54 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setfullyear /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setFullYear - pub fn set_full_year(&mut self, year: Option, month: Option, day: Option) { - if let Some(year) = year { - if self.0.is_none() { - self.0 = NaiveDateTime::from_timestamp_opt(0, 0) - .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) - .map(|local| local.naive_utc()) - .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); - } + pub fn set_full_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). + if t.0.is_none() { + t.0 = NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); + } + + // 3. Let y be ? ToNumber(year). + let y = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; - self.set_components(false, Some(year), month, day, None, None, None, None) + // 4. If month is not present, let m be MonthFromTime(t); otherwise, let m be ? ToNumber(month). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 5. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). + let dt = if let Some(dt) = args.get(2) { + Some(dt.to_number(context)?) + } else { + None + }; + + // 6. Let newDate be MakeDate(MakeDay(y, m, dt), TimeWithinDay(t)). + t.set_components(false, Some(y), m, dt, None, None, None, None); + + // 7. Let u be TimeClip(UTC(newDate)). + let u = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::Date(t)); + + // 9. Return u. + Ok(u.into()) } /// `Date.prototype.setHours()` @@ -875,27 +951,49 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.sethours /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setHours - pub fn set_hours( - &mut self, - hour: Option, - minute: Option, - second: Option, - millisecond: Option, - ) { - if let Some(hour) = hour { - self.set_components( - false, - None, - None, - None, - Some(hour), - minute, - second, - millisecond, - ) + pub fn set_hours(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let h be ? ToNumber(hour). + let h = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If min is not present, let m be MinFromTime(t); otherwise, let m be ? ToNumber(min). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 4. If sec is not present, let s be SecFromTime(t); otherwise, let s be ? ToNumber(sec). + let sec = if let Some(sec) = args.get(2) { + Some(sec.to_number(context)?) + } else { + None + }; + + // 5. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let milli = if let Some(milli) = args.get(3) { + Some(milli.to_number(context)?) + } else { + None + }; + + // 6. Let date be MakeDate(Day(t), MakeTime(h, m, s, milli)). + t.set_components(false, None, None, None, Some(h), m, sec, milli); + + // 7. Let u be TimeClip(UTC(date)). + let u = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::Date(t)); + + // 9. Return u. + Ok(u.into()) } /// `Date.prototype.setMilliseconds()` @@ -908,12 +1006,32 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setmilliseconds /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMilliseconds - pub fn set_milliseconds(&mut self, millisecond: Option) { - if let Some(millisecond) = millisecond { - self.set_components(false, None, None, None, None, None, None, Some(millisecond)) - } else { - self.0 = None - } + pub fn set_milliseconds( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Set ms to ? ToNumber(ms). + let ms = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let time be MakeTime(HourFromTime(t), MinFromTime(t), SecFromTime(t), ms). + t.set_components(false, None, None, None, None, None, None, Some(ms)); + + // 4. Let u be TimeClip(UTC(MakeDate(Day(t), time))). + let u = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::Date(t)); + + // 6. Return u. + Ok(u.into()) } /// `Date.prototype.setMinutes()` @@ -926,26 +1044,42 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setminutes /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMinutes - pub fn set_minutes( - &mut self, - minute: Option, - second: Option, - millisecond: Option, - ) { - if let Some(minute) = minute { - self.set_components( - false, - None, - None, - None, - None, - Some(minute), - second, - millisecond, - ) + pub fn set_minutes(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(min). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If sec is not present, let s be SecFromTime(t); otherwise, let s be ? ToNumber(sec). + let s = if let Some(s) = args.get(1) { + Some(s.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 4. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let milli = if let Some(milli) = args.get(2) { + Some(milli.to_number(context)?) + } else { + None + }; + + // 5. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), m, s, milli)). + t.set_components(false, None, None, None, None, Some(m), s, milli); + + // 6. Let u be TimeClip(UTC(date)). + let u = t.get_time(); + + // 7. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::Date(t)); + + // 8. Return u. + Ok(u.into()) } /// `Date.prototype.setMonth()` @@ -958,12 +1092,35 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setmonth /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMonth - pub fn set_month(&mut self, month: Option, day: Option) { - if let Some(month) = month { - self.set_components(false, None, Some(month), day, None, None, None, None) + pub fn set_month(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(month). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). + let dt = if let Some(date) = args.get(1) { + Some(date.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 4. Let newDate be MakeDate(MakeDay(YearFromTime(t), m, dt), TimeWithinDay(t)). + t.set_components(false, None, Some(m), dt, None, None, None, None); + + // 5. Let u be TimeClip(UTC(newDate)). + let u = t.get_time(); + + // 6. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::Date(t)); + + // 7. Return u. + Ok(u.into()) } /// `Date.prototype.setSeconds()` @@ -976,21 +1133,35 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setseconds /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setSeconds - pub fn set_seconds(&mut self, second: Option, millisecond: Option) { - if let Some(second) = second { - self.set_components( - false, - None, - None, - None, - None, - None, - Some(second), - millisecond, - ) + pub fn set_seconds(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let s be ? ToNumber(sec). + let s = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let milli = if let Some(milli) = args.get(1) { + Some(milli.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 4. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), MinFromTime(t), s, milli)). + t.set_components(false, None, None, None, None, None, Some(s), milli); + + // 5. Let u be TimeClip(UTC(date)). + let u = t.get_time(); + + // 6. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::Date(t)); + + // 7. Return u. + Ok(u.into()) } /// `Date.prototype.setYear()` @@ -1003,17 +1174,50 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setyear /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setYear - pub fn set_year(&mut self, year: Option, month: Option, day: Option) { - if let Some(mut year) = year { - year += if (0f64..100f64).contains(&year) { - 1900f64 - } else { - 0f64 - }; - self.set_components(false, Some(year), month, day, None, None, None, None) - } else { - self.0 = None + pub fn set_year(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). + if t.0.is_none() { + t.0 = NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); + } + + // 3. Let y be ? ToNumber(year). + let mut y = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 4. If y is NaN, then + if y.is_nan() { + // a. Set the [[DateValue]] internal slot of this Date object to NaN. + this.set_data(ObjectData::Date(Date(None))); + + // b. Return NaN. + return Ok(JsValue::nan()); } + + // 5. Let yi be ! ToIntegerOrInfinity(y). + // 6. If 0 ≤ yi ≤ 99, let yyyy be 1900𝔽 + 𝔽(yi). + // 7. Else, let yyyy be y. + if (0f64..=99f64).contains(&y) { + y += 1900f64; + } + + // 8. Let d be MakeDay(yyyy, MonthFromTime(t), DateFromTime(t)). + // 9. Let date be UTC(MakeDate(d, TimeWithinDay(t))). + t.set_components(false, Some(y), None, None, None, None, None, None); + + // 10. Set the [[DateValue]] internal slot of this Date object to TimeClip(date). + this.set_data(ObjectData::Date(t)); + + // 11. Return the value of the [[DateValue]] internal slot of this Date object. + Ok(t.get_time().into()) } /// `Date.prototype.setTime()` @@ -1027,14 +1231,31 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.settime /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setTime - pub fn set_time(&mut self, time: Option) { - if let Some(time) = time { - let secs = (time / 1_000f64) as i64; - let nsecs = ((time % 1_000f64) * 1_000_000f64) as u32; - self.0 = ignore_ambiguity(Local.timestamp_opt(secs, nsecs)).map(|dt| dt.naive_utc()); + pub fn set_time(this: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Perform ? thisTimeValue(this value). + this_time_value(this, context)?; + + // 2. Let t be ? ToNumber(time). + let t = if let Some(t) = args.get(0) { + let t = t.to_number(context)?; + let seconds = (t / 1_000f64) as i64; + let nanoseconds = ((t % 1_000f64) * 1_000_000f64) as u32; + Date( + ignore_ambiguity(Local.timestamp_opt(seconds, nanoseconds)) + .map(|dt| dt.naive_utc()), + ) } else { - self.0 = None - } + Date(None) + }; + + // 3. Let v be TimeClip(t). + let v = t.get_time(); + + // 4. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 5. Return v. + Ok(v.into()) } /// `Date.prototype.setUTCDate()` @@ -1047,12 +1268,32 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcdate /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCDate - pub fn set_utc_date(&mut self, day: Option) { - if let Some(day) = day { - self.set_components(true, None, None, Some(day), None, None, None, None) - } else { - self.0 = None - } + pub fn set_utc_date( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let dt be ? ToNumber(date). + let dt = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let newDate be MakeDate(MakeDay(YearFromTime(t), MonthFromTime(t), dt), TimeWithinDay(t)). + t.set_components(true, None, None, Some(dt), None, None, None, None); + + // 4. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 6. Return v. + Ok(v.into()) } /// `Date.prototype.setFullYear()` @@ -1066,12 +1307,54 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcfullyear /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCFullYear - pub fn set_utc_full_year(&mut self, year: Option, month: Option, day: Option) { - if let Some(year) = year { - self.set_components(true, Some(year), month, day, None, None, None, None) - } else { - self.0 = None + pub fn set_utc_full_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. If t is NaN, set t to +0𝔽. + if t.0.is_none() { + t.0 = NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); } + + // 3. Let y be ? ToNumber(year). + let y = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 4. If month is not present, let m be MonthFromTime(t); otherwise, let m be ? ToNumber(month). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) + } else { + None + }; + + // 5. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). + let dt = if let Some(dt) = args.get(2) { + Some(dt.to_number(context)?) + } else { + None + }; + + // 6. Let newDate be MakeDate(MakeDay(y, m, dt), TimeWithinDay(t)). + t.set_components(true, Some(y), m, dt, None, None, None, None); + + // 7. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 9. Return v. + Ok(v.into()) } /// `Date.prototype.setUTCHours()` @@ -1087,26 +1370,52 @@ impl Date { /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutchours /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCHours pub fn set_utc_hours( - &mut self, - hour: Option, - minute: Option, - second: Option, - millisecond: Option, - ) { - if let Some(hour) = hour { - self.set_components( - true, - None, - None, - None, - Some(hour), - minute, - second, - millisecond, - ) + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let h be ? ToNumber(hour). + let h = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If min is not present, let m be MinFromTime(t); otherwise, let m be ? ToNumber(min). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 4. If sec is not present, let s be SecFromTime(t); otherwise, let s be ? ToNumber(sec). + let sec = if let Some(s) = args.get(2) { + Some(s.to_number(context)?) + } else { + None + }; + + // 5. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let ms = if let Some(ms) = args.get(3) { + Some(ms.to_number(context)?) + } else { + None + }; + + // 6. Let newDate be MakeDate(Day(t), MakeTime(h, m, s, milli)). + t.set_components(true, None, None, None, Some(h), m, sec, ms); + + // 7. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 9. Return v. + Ok(v.into()) } /// `Date.prototype.setUTCMilliseconds()` @@ -1119,12 +1428,32 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcmilliseconds /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMilliseconds - pub fn set_utc_milliseconds(&mut self, millisecond: Option) { - if let Some(millisecond) = millisecond { - self.set_components(true, None, None, None, None, None, None, Some(millisecond)) - } else { - self.0 = None - } + pub fn set_utc_milliseconds( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let milli be ? ToNumber(ms). + let ms = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let time be MakeTime(HourFromTime(t), MinFromTime(t), SecFromTime(t), milli). + t.set_components(true, None, None, None, None, None, None, Some(ms)); + + // 4. Let v be TimeClip(MakeDate(Day(t), time)). + let v = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 6. Return v. + Ok(v.into()) } /// `Date.prototype.setUTCMinutes()` @@ -1138,25 +1467,49 @@ impl Date { /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcminutes /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMinutes pub fn set_utc_minutes( - &mut self, - minute: Option, - second: Option, - millisecond: Option, - ) { - if let Some(minute) = minute { - self.set_components( - true, - None, - None, - None, - None, - Some(minute), - second, - millisecond, - ) + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(min). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If sec is not present, let s be SecFromTime(t). + // 4. Else, + let s = if let Some(s) = args.get(1) { + // a. Let s be ? ToNumber(sec). + Some(s.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 5. If ms is not present, let milli be msFromTime(t). + // 6. Else, + let milli = if let Some(ms) = args.get(2) { + // a. Let milli be ? ToNumber(ms). + Some(ms.to_number(context)?) + } else { + None + }; + + // 7. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), m, s, milli)). + t.set_components(true, None, None, None, None, Some(m), s, milli); + + // 8. Let v be TimeClip(date). + let v = t.get_time(); + + // 9. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 10. Return v. + Ok(v.into()) } /// `Date.prototype.setUTCMonth()` @@ -1169,12 +1522,41 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcmonth /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMonth - pub fn set_utc_month(&mut self, month: Option, day: Option) { - if let Some(month) = month { - self.set_components(true, None, Some(month), day, None, None, None, None) + pub fn set_utc_month( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(month). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If date is not present, let dt be DateFromTime(t). + // 4. Else, + let dt = if let Some(dt) = args.get(1) { + // a. Let dt be ? ToNumber(date). + Some(dt.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 5. Let newDate be MakeDate(MakeDay(YearFromTime(t), m, dt), TimeWithinDay(t)). + t.set_components(true, None, Some(m), dt, None, None, None, None); + + // 6. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 7. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 8. Return v. + Ok(v.into()) } /// `Date.prototype.setUTCSeconds()` @@ -1187,21 +1569,41 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcseconds /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCSeconds - pub fn set_utc_seconds(&mut self, second: Option, millisecond: Option) { - if let Some(second) = second { - self.set_components( - true, - None, - None, - None, - None, - None, - Some(second), - millisecond, - ) + pub fn set_utc_seconds( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> Result { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let s be ? ToNumber(sec). + let s = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If ms is not present, let milli be msFromTime(t). + // 4. Else, + let milli = if let Some(milli) = args.get(1) { + // a. Let milli be ? ToNumber(ms). + Some(milli.to_number(context)?) } else { - self.0 = None - } + None + }; + + // 5. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), MinFromTime(t), s, milli)). + t.set_components(true, None, None, None, None, None, Some(s), milli); + + // 6. Let v be TimeClip(date). + let v = t.get_time(); + + // 7. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::Date(t)); + + // 8. Return v. + Ok(v.into()) } /// `Date.prototype.toDateString()` @@ -1214,10 +1616,20 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.todatestring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toDateString - pub fn to_date_string(self) -> String { - self.to_local() - .map(|date_time| date_time.format("%a %b %d %Y").to_string()) - .unwrap_or_else(|| "Invalid Date".to_string()) + #[allow(clippy::wrong_self_convention)] + pub fn to_date_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> Result { + // 1. Let O be this Date object. + // 2. Let tv be ? thisTimeValue(O). + let tv = this_time_value(this, context)?; + + // 3. If tv is NaN, return "Invalid Date". + // 4. Let t be LocalTime(tv). + // 5. Return DateString(t). + if let Some(t) = tv.0 { + Ok(t.format("%a %b %d %Y").to_string().into()) + } else { + Ok(JsString::from("Invalid Date").into()) + } } /// `Date.prototype.toGMTString()` @@ -1246,11 +1658,18 @@ impl Date { /// [iso8601]: http://en.wikipedia.org/wiki/ISO_8601 /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.toisostring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString - pub fn to_iso_string(self) -> String { - self.to_utc() - // RFC 3389 uses +0.00 for UTC, where JS expects Z, so we can't use the built-in chrono function. - .map(|f| f.format("%Y-%m-%dT%H:%M:%S.%3fZ").to_string()) - .unwrap_or_else(|| "Invalid Date".to_string()) + #[allow(clippy::wrong_self_convention)] + pub fn to_iso_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> Result { + if let Some(t) = this_time_value(this, context)?.0 { + Ok(Utc::now() + .timezone() + .from_utc_datetime(&t) + .format("%Y-%m-%dT%H:%M:%S.%3fZ") + .to_string() + .into()) + } else { + context.throw_range_error("Invalid time value") + } } /// `Date.prototype.toJSON()` @@ -1263,8 +1682,55 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tojson /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON - pub fn to_json(self) -> String { - self.to_iso_string() + #[allow(clippy::wrong_self_convention)] + pub fn to_json(this: &JsValue, _: &[JsValue], context: &mut Context) -> Result { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let tv be ? ToPrimitive(O, number). + 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()); + } + } + + // 4. Return ? Invoke(O, "toISOString"). + if let Some(to_iso_string) = o.get_method(context, "toISOString")? { + to_iso_string.call(this, &[], context) + } else { + context.throw_type_error("toISOString in undefined") + } + } + + /// `Date.prototype.toString()` + /// + /// The toString() method returns a string representing the specified Date object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toString + #[allow(clippy::wrong_self_convention)] + pub fn to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> Result { + // 1. Let tv be ? thisTimeValue(this value). + let tv = this_time_value(this, context)?; + + // 2. Return ToDateString(tv). + if let Some(t) = tv.0 { + Ok(Local::now() + .timezone() + .from_utc_datetime(&t) + .format("%a %b %d %Y %H:%M:%S GMT%z") + .to_string() + .into()) + } else { + Ok(JsString::from("Invalid Date").into()) + } } /// `Date.prototype.toTimeString()` @@ -1278,10 +1744,25 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.totimestring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toTimeString - pub fn to_time_string(self) -> String { - self.to_local() - .map(|date_time| date_time.format("%H:%M:%S GMT%:z").to_string()) - .unwrap_or_else(|| "Invalid Date".to_string()) + #[allow(clippy::wrong_self_convention)] + pub fn to_time_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> Result { + // 1. Let O be this Date object. + // 2. Let tv be ? thisTimeValue(O). + let tv = this_time_value(this, context)?; + + // 3. If tv is NaN, return "Invalid Date". + // 4. Let t be LocalTime(tv). + // 5. Return the string-concatenation of TimeString(t) and TimeZoneString(tv). + if let Some(t) = tv.0 { + Ok(Local::now() + .timezone() + .from_utc_datetime(&t) + .format("%H:%M:%S GMT%z") + .to_string() + .into()) + } else { + Ok(JsString::from("Invalid Date").into()) + } } /// `Date.prototype.toUTCString()` @@ -1370,7 +1851,7 @@ impl Date { .map_or(Ok(f64::NAN), |value| value.to_number(context))?; let month = args .get(1) - .map_or(Ok(1f64), |value| value.to_number(context))?; + .map_or(Ok(0f64), |value| value.to_number(context))?; let day = args .get(2) .map_or(Ok(1f64), |value| value.to_number(context))?; diff --git a/boa/src/builtins/date/tests.rs b/boa/src/builtins/date/tests.rs index 1f48d22e96..7e63002db3 100644 --- a/boa/src/builtins/date/tests.rs +++ b/boa/src/builtins/date/tests.rs @@ -408,7 +408,7 @@ fn date_proto_get_timezone_offset() -> Result<(), Box> { let actual = forward_val( &mut context, - "new Date('August 19, 1975 23:15:30 GMT+07:00').getTimezoneOffset() === new Date('August 19, 1975 23:15:30 GMT-02:00').getTimezoneOffset()", + "new Date('1975-08-19T23:15:30+07:00').getTimezoneOffset() === new Date('1975-08-19T23:15:30-02:00').getTimezoneOffset()", ); // NB: Host Settings, not TZ specified in the DateTime. @@ -416,17 +416,17 @@ fn date_proto_get_timezone_offset() -> Result<(), Box> { let actual = forward_val( &mut context, - "new Date('August 19, 1975 23:15:30 GMT+07:00').getTimezoneOffset()", + "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 offset_seconds = chrono::Local::now().offset().local_minus_utc() as f64; - let offset_minutes = offset_seconds / 60f64; + let offset_minutes = -offset_seconds / 60f64; assert_eq!(Ok(JsValue::new(offset_minutes)), actual); let actual = forward_val( &mut context, - "new Date(1/0, 06, 08, 09, 16, 15, 779).getTimezoneOffset()", + "new Date('1975-08-19T23:15:30+07:00').getTimezoneOffset()", ); assert_eq!(Ok(JsValue::new(offset_minutes)), actual); Ok(()) @@ -1282,7 +1282,7 @@ fn date_proto_to_string() -> Result<(), Box> { )) .earliest() .unwrap() - .format("Wed Jul 08 2020 09:16:15 GMT%:z") + .format("Wed Jul 08 2020 09:16:15 GMT%z") .to_string() )), actual @@ -1310,7 +1310,7 @@ fn date_proto_to_time_string() -> Result<(), Box> { )) .earliest() .unwrap() - .format("09:16:15 GMT%:z") + .format("09:16:15 GMT%z") .to_string() )), actual