Skip to content

Commit

Permalink
Implement Date parsing according to the spec (#3564)
Browse files Browse the repository at this point in the history
  • Loading branch information
raskad authored Jan 9, 2024
1 parent 9975773 commit a592883
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 33 deletions.
43 changes: 13 additions & 30 deletions core/engine/src/builtins/date/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,8 @@
//! [spec]: https://tc39.es/ecma262/#sec-date-objects
//! [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::{
builtins::BuiltInObject,
builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject},
context::{
intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
HostHooks,
Expand All @@ -30,10 +23,17 @@ use crate::{
value::{IntegerOrNan, JsValue, PreferredType},
Context, JsArgs, JsData, JsError, JsResult, JsString,
};
use boa_gc::{Finalize, Trace};
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`.
macro_rules! some_or_nan {
Expand Down Expand Up @@ -272,13 +272,7 @@ impl BuiltInConstructor for Date {
// 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 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()))
Self::new(parse_date(str, context.host_hooks()))
}
// iii. Else,
v => {
Expand Down Expand Up @@ -445,19 +439,8 @@ impl Date {
/// [spec]: https://tc39.es/ecma262/#sec-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> {
// This method is implementation-defined and discouraged, so we just require the same format as the string
// constructor.

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))
let date = args.get_or_undefined(0).to_string(context)?;
Ok(parse_date(&date, context.host_hooks()).map_or(JsValue::from(f64::NAN), JsValue::from))
}

/// `Date.UTC()`
Expand Down
235 changes: 232 additions & 3 deletions core/engine/src/builtins/date/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use chrono::{Datelike, NaiveDateTime, Timelike};

use crate::{context::HostHooks, value::IntegerOrNan};
use crate::{context::HostHooks, value::IntegerOrNan, JsString};
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, Timelike};
use std::{iter::Peekable, str::Chars};

/// The absolute maximum value of a timestamp
pub(super) const MAX_TIMESTAMP: i64 = 864 * 10i64.pow(13);
Expand Down Expand Up @@ -198,3 +198,232 @@ pub(super) fn replace_params<const LOCAL: bool>(

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(())
}
}

0 comments on commit a592883

Please sign in to comment.