Browse Source

Implement Date parsing according to the spec (#3564)

pull/3577/head
raskad 11 months ago committed by GitHub
parent
commit
a592883088
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 43
      core/engine/src/builtins/date/mod.rs
  2. 235
      core/engine/src/builtins/date/utils.rs

43
core/engine/src/builtins/date/mod.rs

@ -7,15 +7,8 @@
//! [spec]: https://tc39.es/ecma262/#sec-date-objects //! [spec]: https://tc39.es/ecma262/#sec-date-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
pub(crate) mod utils;
use boa_gc::{Finalize, Trace};
use utils::{make_date, make_day, make_time, replace_params, time_clip, DateParameters};
#[cfg(test)]
mod tests;
use crate::{ use crate::{
builtins::BuiltInObject, builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::{ context::{
intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
HostHooks, HostHooks,
@ -30,10 +23,17 @@ use crate::{
value::{IntegerOrNan, JsValue, PreferredType}, value::{IntegerOrNan, JsValue, PreferredType},
Context, JsArgs, JsData, JsError, JsResult, JsString, Context, JsArgs, JsData, JsError, JsResult, JsString,
}; };
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use chrono::prelude::*; use chrono::{Datelike, NaiveDateTime, TimeZone, Timelike, Utc};
use utils::{
make_date, make_day, make_time, parse_date, replace_params, time_clip, DateParameters,
};
use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject}; pub(crate) mod utils;
#[cfg(test)]
mod tests;
/// Extracts `Some` from an `Option<T>` or returns `NaN` if the object contains `None`. /// Extracts `Some` from an `Option<T>` or returns `NaN` if the object contains `None`.
macro_rules! some_or_nan { macro_rules! some_or_nan {
@ -272,13 +272,7 @@ impl BuiltInConstructor for Date {
// 1. Assert: The next step never returns an abrupt completion because v is a 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 // 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). // parse method (21.4.3.2).
Self::new(parse_date(str, context.host_hooks()))
let dt = str
.to_std_string()
.ok()
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s.as_str()).ok())
.map(|dt| dt.naive_utc());
Self::new(dt.map(|dt| dt.timestamp_millis()))
} }
// iii. Else, // iii. Else,
v => { v => {
@ -445,19 +439,8 @@ impl Date {
/// [spec]: https://tc39.es/ecma262/#sec-date.parse /// [spec]: https://tc39.es/ecma262/#sec-date.parse
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> { pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// This method is implementation-defined and discouraged, so we just require the same format as the string let date = args.get_or_undefined(0).to_string(context)?;
// constructor. Ok(parse_date(&date, context.host_hooks()).map_or(JsValue::from(f64::NAN), JsValue::from))
let date = some_or_nan!(args.first());
let date = date.to_string(context)?;
Ok(date
.to_std_string()
.ok()
.and_then(|s| DateTime::parse_from_rfc3339(s.as_str()).ok())
.and_then(|date| time_clip(date.naive_utc().timestamp_millis()))
.map_or_else(|| JsValue::from(f64::NAN), JsValue::from))
} }
/// `Date.UTC()` /// `Date.UTC()`

235
core/engine/src/builtins/date/utils.rs

@ -1,6 +1,6 @@
use chrono::{Datelike, NaiveDateTime, Timelike}; use crate::{context::HostHooks, value::IntegerOrNan, JsString};
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, Timelike};
use crate::{context::HostHooks, value::IntegerOrNan}; use std::{iter::Peekable, str::Chars};
/// The absolute maximum value of a timestamp /// The absolute maximum value of a timestamp
pub(super) const MAX_TIMESTAMP: i64 = 864 * 10i64.pow(13); pub(super) const MAX_TIMESTAMP: i64 = 864 * 10i64.pow(13);
@ -198,3 +198,232 @@ pub(super) fn replace_params<const LOCAL: bool>(
time_clip(ts) time_clip(ts)
} }
/// Parse a date string according to the steps specified in [`Date.parse`][spec].
///
/// We parse three different formats:
/// - The [`Date Time String Format`][spec-format] specified in the spec: `YYYY-MM-DDTHH:mm:ss.sssZ`
/// - The `toString` format: `Thu Jan 01 1970 00:00:00 GMT+0000`
/// - The `toUTCString` format: `Thu, 01 Jan 1970 00:00:00 GMT`
///
/// [spec]: https://tc39.es/ecma262/#sec-date.parse
/// [spec-format]: https://tc39.es/ecma262/#sec-date-time-string-format
pub(super) fn parse_date(date: &JsString, hooks: &dyn HostHooks) -> Option<i64> {
// All characters must be ASCII so we can return early if we find a non-ASCII character.
let Ok(date) = date.to_std_string() else {
return None;
};
// Date Time String Format: 'YYYY-MM-DDTHH:mm:ss.sssZ'
if let Some(dt) = DateParser::new(&date, hooks).parse() {
return Some(dt.timestamp_millis());
}
// `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());
}
// `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());
}
None
}
/// Parses a date string according to the [`Date Time String Format`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-date-time-string-format
struct DateParser<'a> {
hooks: &'a dyn HostHooks,
input: Peekable<Chars<'a>>,
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
millisecond: u32,
offset: Duration,
}
impl<'a> DateParser<'a> {
fn new(s: &'a str, hooks: &'a dyn HostHooks) -> Self {
Self {
hooks,
input: s.chars().peekable(),
year: 0,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
offset: Duration::minutes(0),
}
}
fn next_expect(&mut self, expect: char) -> Option<()> {
self.input
.next()
.and_then(|c| if c == expect { Some(()) } else { None })
}
fn next_digit(&mut self) -> Option<u8> {
self.input.next().and_then(|c| {
if c.is_ascii_digit() {
Some((u32::from(c) - u32::from('0')) as u8)
} else {
None
}
})
}
fn finish(&mut self) -> Option<NaiveDateTime> {
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)
}
fn finish_local(&mut self) -> Option<NaiveDateTime> {
self.finish().and_then(|dt| {
self.hooks
.local_from_naive_local(dt)
.earliest()
.map(|dt| dt.naive_utc())
})
}
fn parse(&mut self) -> Option<NaiveDateTime> {
self.parse_year()?;
match self.input.peek() {
Some('T') => return self.parse_time(),
None => return self.finish(),
_ => {}
}
self.next_expect('-')?;
self.month = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
match self.input.peek() {
Some('T') => return self.parse_time(),
None => return self.finish(),
_ => {}
}
self.next_expect('-')?;
self.day = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
match self.input.peek() {
Some('T') => self.parse_time(),
_ => self.finish(),
}
}
fn parse_year(&mut self) -> Option<()> {
match self.input.next()? {
'+' => {
self.year = i32::from(self.next_digit()?) * 100_000
+ i32::from(self.next_digit()?) * 10000
+ i32::from(self.next_digit()?) * 1000
+ i32::from(self.next_digit()?) * 100
+ i32::from(self.next_digit()?) * 10
+ i32::from(self.next_digit()?);
Some(())
}
'-' => {
let year = i32::from(self.next_digit()?) * 100_000
+ i32::from(self.next_digit()?) * 10000
+ i32::from(self.next_digit()?) * 1000
+ i32::from(self.next_digit()?) * 100
+ i32::from(self.next_digit()?) * 10
+ i32::from(self.next_digit()?);
if year == 0 {
return None;
}
self.year = -year;
Some(())
}
c if c.is_ascii_digit() => {
self.year = i32::from((u32::from(c) - u32::from('0')) as u8) * 1000
+ i32::from(self.next_digit()?) * 100
+ i32::from(self.next_digit()?) * 10
+ i32::from(self.next_digit()?);
Some(())
}
_ => None,
}
}
fn parse_time(&mut self) -> Option<NaiveDateTime> {
self.next_expect('T')?;
self.hour = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
self.next_expect(':')?;
self.minute = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
match self.input.peek() {
Some(':') => {}
None => return self.finish_local(),
_ => {
self.parse_timezone()?;
return self.finish();
}
}
self.next_expect(':')?;
self.second = u32::from(self.next_digit()?) * 10 + u32::from(self.next_digit()?);
match self.input.peek() {
Some('.') => {}
None => return self.finish_local(),
_ => {
self.parse_timezone()?;
return self.finish();
}
}
self.next_expect('.')?;
self.millisecond = u32::from(self.next_digit()?) * 100
+ u32::from(self.next_digit()?) * 10
+ u32::from(self.next_digit()?);
if self.input.peek().is_some() {
self.parse_timezone()?;
self.finish()
} else {
self.finish_local()
}
}
fn parse_timezone(&mut self) -> Option<()> {
match self.input.next() {
Some('Z') => return Some(()),
Some('+') => {
self.offset = -Duration::hours(
i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?),
);
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()?),
);
}
Some('-') => {
self.offset = Duration::hours(
i64::from(self.next_digit()?) * 10 + i64::from(self.next_digit()?),
);
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()?),
);
}
_ => return None,
}
Some(())
}
}

Loading…
Cancel
Save