use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
use std::time::{SystemTime, UNIX_EPOCH};
pub const STATIC_METHODS: &[&str] = &["now", "parse", "UTC"];
pub const INSTANCE_METHODS: &[&str] = &[
"getTime",
"valueOf",
"toISOString",
"toJSON",
"toUTCString",
"toGMTString",
"toString",
"toDateString",
"toLocaleString",
"toLocaleDateString",
"toLocaleTimeString",
"getFullYear",
"getUTCFullYear",
"getMonth",
"getUTCMonth",
"getDate",
"getUTCDate",
"getDay",
"getUTCDay",
"getHours",
"getUTCHours",
"getMinutes",
"getUTCMinutes",
"getSeconds",
"getUTCSeconds",
"getMilliseconds",
"getUTCMilliseconds",
"getTimezoneOffset",
"toTimeString",
"setTime",
"setFullYear",
"setUTCFullYear",
"setMonth",
"setUTCMonth",
"setDate",
"setUTCDate",
"setHours",
"setUTCHours",
"setMinutes",
"setUTCMinutes",
"setSeconds",
"setUTCSeconds",
"setMilliseconds",
"setUTCMilliseconds",
"getYear",
"setYear",
"@@toPrimitive",
];
const MS_PER_DAY: f64 = 86_400_000.0;
const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
fn now_ms() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as f64)
.unwrap_or(0.0)
}
fn from_ms(ms: f64) -> Value {
with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("Date"));
m.insert("@@ms".into(), Value::Float(ms));
h.new_object(m)
})
}
fn ms_of(recv: &Value) -> f64 {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@ms").map(|v| h.to_number(v)).unwrap_or(f64::NAN),
_ => f64::NAN,
})
}
pub fn construct(args: &[Value]) -> Result<Value, String> {
let ms = match args.len() {
0 => now_ms(),
1 => {
let a = &args[0];
if let Value::Str(_) = a {
parse_str(&with_host(|h| h.str_of(a)))
} else if with_host(|h| matches!(h.get(a), Some(JsObj::Str(_)))) {
parse_str(&with_host(|h| h.str_of(a)))
} else if super::native_tag(a).as_deref() == Some("Date") {
ms_of(a)
} else {
if with_host(|h| matches!(h.get(a), Some(crate::host::JsObj::Symbol { .. }))) {
return Err(crate::host::type_error(
"Cannot convert a Symbol value to a number",
));
}
with_host(|h| h.to_number(a))
}
}
_ => {
let n = |i: usize, dflt: f64| {
args.get(i)
.map(|v| with_host(|h| h.to_number(v)))
.unwrap_or(dflt)
};
let mut year = n(0, f64::NAN);
if (0.0..=99.0).contains(&year.trunc()) {
year = year.trunc() + 1900.0;
}
utc_from_local(utc_from_fields(
year,
n(1, 0.0),
n(2, 1.0),
n(3, 0.0),
n(4, 0.0),
n(5, 0.0),
n(6, 0.0),
))
}
};
Ok(from_ms(time_clip(ms)))
}
pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"now" => Ok(Value::Float(now_ms())),
"parse" => Ok(Value::Float(parse_str(&super::arg_str(args, 0)))),
"UTC" => {
let n = |i: usize, dflt: f64| {
args.get(i)
.map(|v| with_host(|h| h.to_number(v)))
.unwrap_or(dflt)
};
let mut year = n(0, f64::NAN);
if (0.0..=99.0).contains(&year.trunc()) {
year = year.trunc() + 1900.0;
}
Ok(Value::Float(utc_from_fields(
year,
n(1, 0.0),
n(2, 1.0),
n(3, 0.0),
n(4, 0.0),
n(5, 0.0),
n(6, 0.0),
)))
}
_ => return None,
})
}
pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
let coerced: Vec<Value>;
let _args: &[Value] = if method.starts_with("set") {
let mut out = Vec::with_capacity(_args.len());
for a in _args {
let p = crate::host::to_primitive(a, "number")?;
out.push(Value::Float(with_host(|h| h.to_number(&p))));
}
coerced = out;
&coerced
} else {
_args
};
let ms = ms_of(recv);
let f = |ms: f64| ms; Ok(match method {
"getTime" | "valueOf" => Value::Float(f(ms)),
"@@toPrimitive" => {
let hint = with_host(|h| h.str_of(&_args.first().cloned().unwrap_or(Value::Undef)));
if hint == "number" {
Value::Float(f(ms))
} else {
return instance_call(recv, "toString", _args);
}
}
"toISOString" | "toJSON" => {
if ms.is_nan() {
if method == "toJSON" {
with_host(|h| h.null())
} else {
return Err(crate::host::range_error("Invalid time value"));
}
} else {
with_host(|h| h.new_str(iso_string(ms)))
}
}
"toUTCString" | "toGMTString" => with_host(|h| h.new_str(utc_string(ms))),
"toString" => with_host(|h| {
h.new_str(if ms.is_nan() {
"Invalid Date".into()
} else {
format!("{} {}", date_string(local_ms(ms)), time_string(ms))
})
}),
"toDateString" => with_host(|h| h.new_str(date_string(local_ms(ms)))),
"toLocaleString" => with_host(|h| {
h.new_str(if ms.is_nan() {
"Invalid Date".into()
} else {
format!(
"{}, {}",
locale_date(local_ms(ms)),
locale_time(local_ms(ms))
)
})
}),
"toLocaleDateString" => with_host(|h| {
h.new_str(if ms.is_nan() {
"Invalid Date".into()
} else {
locale_date(local_ms(ms))
})
}),
"toLocaleTimeString" => with_host(|h| {
h.new_str(if ms.is_nan() {
"Invalid Date".into()
} else {
locale_time(local_ms(ms))
})
}),
"getFullYear" => Value::Float(field(local_ms(ms), Field::Year)),
"getUTCFullYear" => Value::Float(field(ms, Field::Year)),
"getMonth" => Value::Float(field(local_ms(ms), Field::Month)),
"getUTCMonth" => Value::Float(field(ms, Field::Month)),
"getDate" => Value::Float(field(local_ms(ms), Field::Day)),
"getUTCDate" => Value::Float(field(ms, Field::Day)),
"getDay" => Value::Float(field(local_ms(ms), Field::Weekday)),
"getUTCDay" => Value::Float(field(ms, Field::Weekday)),
"getHours" => Value::Float(field(local_ms(ms), Field::Hours)),
"getUTCHours" => Value::Float(field(ms, Field::Hours)),
"getMinutes" => Value::Float(field(local_ms(ms), Field::Minutes)),
"getUTCMinutes" => Value::Float(field(ms, Field::Minutes)),
"getSeconds" => Value::Float(field(local_ms(ms), Field::Seconds)),
"getUTCSeconds" => Value::Float(field(ms, Field::Seconds)),
"getMilliseconds" => Value::Float(field(local_ms(ms), Field::Millis)),
"getUTCMilliseconds" => Value::Float(field(ms, Field::Millis)),
"getTimezoneOffset" => Value::Float(if ms.is_nan() {
f64::NAN
} else {
(ms - local_ms(ms)) / 60_000.0
}),
"toTimeString" => with_host(|h| h.new_str(time_string(ms))),
"setTime" => Value::Float(store_ms(recv, time_clip(super::arg_num(_args, 0)))),
"setFullYear" => Value::Float(set_fields_local(recv, ms, 0, _args, false)),
"setUTCFullYear" => Value::Float(set_fields(recv, ms, 0, _args, false)),
"setMonth" => Value::Float(set_fields_local(recv, ms, 1, _args, false)),
"setUTCMonth" => Value::Float(set_fields(recv, ms, 1, _args, false)),
"setDate" => Value::Float(set_fields_local(recv, ms, 2, _args, false)),
"setUTCDate" => Value::Float(set_fields(recv, ms, 2, _args, false)),
"setHours" => Value::Float(set_fields_local(recv, ms, 3, _args, false)),
"setUTCHours" => Value::Float(set_fields(recv, ms, 3, _args, false)),
"setMinutes" => Value::Float(set_fields_local(recv, ms, 4, _args, false)),
"setUTCMinutes" => Value::Float(set_fields(recv, ms, 4, _args, false)),
"setSeconds" => Value::Float(set_fields_local(recv, ms, 5, _args, false)),
"setUTCSeconds" => Value::Float(set_fields(recv, ms, 5, _args, false)),
"setMilliseconds" => Value::Float(set_fields_local(recv, ms, 6, _args, false)),
"setUTCMilliseconds" => Value::Float(set_fields(recv, ms, 6, _args, false)),
"getYear" => Value::Float(if ms.is_nan() {
f64::NAN
} else {
field(local_ms(ms), Field::Year) - 1900.0
}),
"setYear" => Value::Float(set_fields_local(recv, ms, 0, _args, true)),
_ => {
return Err(crate::host::type_error(&format!(
"date.{method} is not a function"
)))
}
})
}
enum Field {
Year,
Month,
Day,
Weekday,
Hours,
Minutes,
Seconds,
Millis,
}
fn split_day(ms: f64) -> (i64, i64) {
let day = (ms / MS_PER_DAY).floor();
let rem = ms - day * MS_PER_DAY;
(day as i64, rem as i64)
}
fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; (if m <= 2 { y + 1 } else { y }, m - 1, d)
}
fn days_from_civil(y: i64, m0: i64, d: i64) -> i64 {
let m = m0 + 1;
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn field(ms: f64, which: Field) -> f64 {
if ms.is_nan() {
return f64::NAN;
}
let (day, rem) = split_day(ms);
let (y, mo, d) = civil_from_days(day);
match which {
Field::Year => y as f64,
Field::Month => mo as f64,
Field::Day => d as f64,
Field::Weekday => (((day % 7) + 4 + 7) % 7) as f64,
Field::Hours => (rem / 3_600_000) as f64,
Field::Minutes => (rem / 60_000 % 60) as f64,
Field::Seconds => (rem / 1000 % 60) as f64,
Field::Millis => (rem % 1000) as f64,
}
}
fn utc_from_fields(y: f64, mo: f64, d: f64, h: f64, mi: f64, s: f64, ms: f64) -> f64 {
if [y, mo, d, h, mi, s, ms].iter().any(|v| !v.is_finite()) {
return f64::NAN;
}
let [y, mo, d, h, mi, s, ms] = [y, mo, d, h, mi, s, ms].map(f64::trunc);
let total_months = y as i64 * 12 + mo as i64;
let year = total_months.div_euclid(12);
let month = total_months.rem_euclid(12);
let days = days_from_civil(year, month, d as i64);
days as f64 * MS_PER_DAY + h * 3_600_000.0 + mi * 60_000.0 + s * 1000.0 + ms
}
#[cfg(unix)]
fn zone_offset_ms(ms: f64) -> f64 {
if !ms.is_finite() {
return 0.0;
}
let secs = (ms / 1000.0).floor() as i64;
let t = secs as libc::time_t;
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
let ok = unsafe { !libc::localtime_r(&t, &mut tm).is_null() };
if !ok {
return 0.0;
}
tm.tm_gmtoff as f64 * 1000.0
}
#[cfg(not(unix))]
fn zone_offset_ms(_ms: f64) -> f64 {
0.0
}
fn local_ms(ms: f64) -> f64 {
ms + zone_offset_ms(ms)
}
fn utc_from_local(local: f64) -> f64 {
if !local.is_finite() {
return local;
}
let off_naive = zone_offset_ms(local);
let cand_a = local - off_naive;
let off_corrected = zone_offset_ms(cand_a);
if off_corrected == off_naive {
return cand_a;
}
let cand_b = local - off_corrected;
if local_ms(cand_b) == local {
return cand_b;
}
if local_ms(cand_a) == local {
return cand_a;
}
local - off_naive.min(off_corrected)
}
fn utc_string(ms: f64) -> String {
if ms.is_nan() {
return "Invalid Date".into();
}
let (day, _) = split_day(ms);
let (y, mo, d) = civil_from_days(day);
let wd = (((day % 7) + 4 + 7) % 7) as usize;
format!(
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
DAYS[wd],
d,
MONTHS[mo as usize],
y,
field(ms, Field::Hours) as i64,
field(ms, Field::Minutes) as i64,
field(ms, Field::Seconds) as i64,
)
}
fn time_string(ms: f64) -> String {
if ms.is_nan() {
return "Invalid Date".into();
}
let local = local_ms(ms);
let off_min = (zone_offset_ms(ms) / 60_000.0) as i64;
let sign = if off_min < 0 { '-' } else { '+' };
let abs = off_min.abs();
format!(
"{:02}:{:02}:{:02} GMT{}{:02}{:02} ({})",
field(local, Field::Hours) as i64,
field(local, Field::Minutes) as i64,
field(local, Field::Seconds) as i64,
sign,
abs / 60,
abs % 60,
zone_name(ms),
)
}
#[cfg(unix)]
fn zone_name(ms: f64) -> String {
if zone_offset_ms(ms) == 0.0 {
return "Coordinated Universal Time".into();
}
let secs = (ms / 1000.0).floor() as i64;
let t = secs as libc::time_t;
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
if unsafe { libc::localtime_r(&t, &mut tm).is_null() } || tm.tm_zone.is_null() {
return "Coordinated Universal Time".into();
}
let z = unsafe { std::ffi::CStr::from_ptr(tm.tm_zone) };
z.to_string_lossy().into_owned()
}
#[cfg(not(unix))]
fn zone_name(_ms: f64) -> String {
"Coordinated Universal Time".into()
}
fn time_clip(ms: f64) -> f64 {
if !ms.is_finite() || ms.abs() > 8.64e15 {
return f64::NAN;
}
ms.trunc()
}
fn store_ms(recv: &Value, ms: f64) -> f64 {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert("@@ms".into(), Value::Float(ms));
}
});
ms
}
fn set_fields_local(recv: &Value, ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
if ms.is_nan() && start != 0 {
return store_ms(recv, f64::NAN);
}
let local = set_fields_value(local_ms(ms), start, args, legacy_year);
store_ms(recv, time_clip(utc_from_local(local)))
}
fn set_fields(recv: &Value, ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
if ms.is_nan() && start != 0 {
return store_ms(recv, f64::NAN);
}
store_ms(
recv,
time_clip(set_fields_value(ms, start, args, legacy_year)),
)
}
fn set_fields_value(ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
let base = if ms.is_nan() {
0.0 } else {
ms
};
let mut f = [
field(base, Field::Year),
field(base, Field::Month),
field(base, Field::Day),
field(base, Field::Hours),
field(base, Field::Minutes),
field(base, Field::Seconds),
field(base, Field::Millis),
];
let end = if start < 3 { 3 } else { 7 };
for (i, slot) in f.iter_mut().enumerate().take(end).skip(start) {
match args.get(i - start) {
Some(v) => *slot = with_host(|h| h.to_number(v)).trunc(),
None => break,
}
}
if legacy_year && (0.0..=99.0).contains(&f[0]) {
f[0] += 1900.0;
}
utc_from_fields(f[0], f[1], f[2], f[3], f[4], f[5], f[6])
}
fn date_string(ms: f64) -> String {
if ms.is_nan() {
return "Invalid Date".into();
}
let (day, _) = split_day(ms);
let (y, mo, d) = civil_from_days(day);
let wd = (((day % 7) + 4 + 7) % 7) as usize;
format!("{} {} {:02} {:04}", DAYS[wd], MONTHS[mo as usize], d, y)
}
fn locale_date(ms: f64) -> String {
let (day, _) = split_day(ms);
let (y, mo, d) = civil_from_days(day);
format!("{}/{}/{:04}", mo + 1, d, y)
}
fn locale_time(ms: f64) -> String {
let h24 = field(ms, Field::Hours) as i64;
let (h12, meridiem) = match h24 {
0 => (12, "AM"),
1..=11 => (h24, "AM"),
12 => (12, "PM"),
_ => (h24 - 12, "PM"),
};
format!(
"{}:{:02}:{:02} {}",
h12,
field(ms, Field::Minutes) as i64,
field(ms, Field::Seconds) as i64,
meridiem
)
}
fn iso_year(y: i64) -> String {
if (0..=9999).contains(&y) {
return format!("{y:04}");
}
let sign = if y < 0 { '-' } else { '+' };
format!("{sign}{:06}", y.abs())
}
fn iso_string(ms: f64) -> String {
let (day, _) = split_day(ms);
let (y, mo, d) = civil_from_days(day);
format!(
"{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
iso_year(y),
mo + 1,
d,
field(ms, Field::Hours) as i64,
field(ms, Field::Minutes) as i64,
field(ms, Field::Seconds) as i64,
field(ms, Field::Millis) as i64,
)
}
fn parse_str(s: &str) -> f64 {
time_clip(parse_iso(s).or_else(|| parse_legacy(s)).unwrap_or(f64::NAN))
}
fn parse_iso(s: &str) -> Option<f64> {
let b = s.as_bytes();
let mut i = 0;
let digits = |i: &mut usize, n: usize| -> Option<i64> {
let end = *i + n;
let part = b.get(*i..end)?;
if !part.iter().all(u8::is_ascii_digit) {
return None;
}
*i = end;
std::str::from_utf8(part).ok()?.parse().ok()
};
let year = match b.first()? {
sign @ (b'+' | b'-') => {
i = 1;
let y = digits(&mut i, 6)?;
if *sign == b'-' && y == 0 {
return None;
}
if *sign == b'-' {
-y
} else {
y
}
}
_ => digits(&mut i, 4)?,
};
let (mut month, mut day) = (1, 1);
if b.get(i) == Some(&b'-') {
i += 1;
month = digits(&mut i, 2)?;
if b.get(i) == Some(&b'-') {
i += 1;
day = digits(&mut i, 2)?;
}
}
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let date = days_from_civil(year, month - 1, day) as f64 * MS_PER_DAY;
match b.get(i) {
None => return Some(date),
Some(b'Z' | b'z') if i + 1 == b.len() => return Some(date),
Some(b'T' | b't' | b' ') => i += 1,
Some(_) => return None,
}
let h = digits(&mut i, 2)?;
if b.get(i) != Some(&b':') {
return None;
}
i += 1;
let mi = digits(&mut i, 2)?;
let (mut sec, mut milli) = (0, 0);
if b.get(i) == Some(&b':') {
i += 1;
sec = digits(&mut i, 2)?;
if b.get(i) == Some(&b'.') {
i += 1;
let start = i;
while b.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == start {
return None;
}
let frac = &s[start..i.min(start + 3)];
milli = format!("{frac:0<3}").parse().ok()?;
}
}
if h > 24 || mi > 59 || sec > 59 || (h == 24 && (mi, sec, milli) != (0, 0, 0)) {
return None;
}
let wall =
date + h as f64 * 3_600_000.0 + mi as f64 * 60_000.0 + sec as f64 * 1000.0 + milli as f64;
let offset = match b.get(i) {
None => return Some(utc_from_local(wall)),
Some(b'Z' | b'z') if i + 1 == b.len() => 0.0,
Some(sign @ (b'+' | b'-')) => {
let sign = if *sign == b'-' { -1.0 } else { 1.0 };
i += 1;
let oh = digits(&mut i, 2)?;
if b.get(i) == Some(&b':') {
i += 1;
}
let om = digits(&mut i, 2)?;
if i != b.len() || oh > 23 || om > 59 {
return None;
}
sign * (oh as f64 * 3_600_000.0 + om as f64 * 60_000.0)
}
Some(_) => return None,
};
Some(wall - offset)
}
#[derive(Clone, Copy, PartialEq)]
enum Tok<'a> {
Num(i64, usize),
Word(&'a str),
Sym(u8),
Space,
End,
}
struct Toks<'a> {
s: &'a str,
i: usize,
}
impl<'a> Toks<'a> {
fn next(&mut self) -> Tok<'a> {
let b = self.s.as_bytes();
let Some(&c) = b.get(self.i) else {
return Tok::End;
};
let start = self.i;
let run = |i: &mut usize, f: fn(&u8) -> bool| {
while b.get(*i).is_some_and(f) {
*i += 1;
}
};
if c.is_ascii_digit() {
run(&mut self.i, u8::is_ascii_digit);
let text = &self.s[start..self.i];
return Tok::Num(text.parse().unwrap_or(i64::MAX), text.len());
}
if c.is_ascii_alphabetic() {
run(&mut self.i, u8::is_ascii_alphabetic);
return Tok::Word(&self.s[start..self.i]);
}
if c.is_ascii_whitespace() {
run(&mut self.i, u8::is_ascii_whitespace);
return Tok::Space;
}
if c == b'(' {
let mut depth = 0;
while let Some(&c) = b.get(self.i) {
self.i += 1;
match c {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
}
return Tok::Space;
}
let len = self.s[start..].chars().next().map_or(1, char::len_utf8);
self.i += len;
Tok::Sym(if len == 1 { c } else { 0 })
}
fn peek(&self) -> Tok<'a> {
Toks {
s: self.s,
i: self.i,
}
.next()
}
fn skip(&mut self, sym: u8) -> bool {
let taken = self.peek() == Tok::Sym(sym);
if taken {
self.next();
}
taken
}
}
enum Keyword {
Month(i64),
AmPm(i64),
Zone(i64),
Other,
}
fn keyword(word: &str) -> Keyword {
let w = word.to_ascii_lowercase();
let prefix = &w[..w.len().min(3)];
if w.len() >= 3 {
const MONTHS: [&str; 12] = [
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
];
if let Some(m) = MONTHS.iter().position(|m| *m == prefix) {
return Keyword::Month(m as i64 + 1);
}
}
match prefix {
"am" if w.len() == 2 => Keyword::AmPm(0),
"pm" if w.len() == 2 => Keyword::AmPm(12),
"ut" if w.len() == 2 => Keyword::Zone(0),
"z" => Keyword::Zone(0),
"utc" | "gmt" if w.len() >= 3 => Keyword::Zone(0),
"cdt" => Keyword::Zone(-5),
"cst" => Keyword::Zone(-6),
"edt" => Keyword::Zone(-4),
"est" => Keyword::Zone(-5),
"mdt" => Keyword::Zone(-6),
"mst" => Keyword::Zone(-7),
"pdt" => Keyword::Zone(-7),
"pst" => Keyword::Zone(-8),
_ => Keyword::Other,
}
}
fn parse_legacy(s: &str) -> Option<f64> {
const NONE: i64 = i64::MIN;
let mut t = Toks { s, i: 0 };
let (mut day, mut named_month) = (Vec::with_capacity(3), NONE);
let (mut time, mut hour_offset) = (Vec::with_capacity(4), NONE);
let (mut sign, mut tz_hour, mut tz_min) = (0i64, NONE, NONE);
let mut read_number = false;
let expecting = |time: &Vec<i64>, n: i64| match time.len() {
1 | 2 => (0..60).contains(&n),
3 => (0..1000).contains(&n),
_ => false,
};
loop {
match t.next() {
Tok::End => break,
Tok::Num(n, _) => {
read_number = true;
if t.skip(b':') {
if t.skip(b':') {
if !time.is_empty() {
return None;
}
time.extend([n, 0]);
} else {
if time.len() >= 4 {
return None;
}
time.push(n);
t.skip(b'.');
}
} else if t.peek() == Tok::Sym(b'.') && expecting(&time, n) {
t.next();
time.push(n);
let Tok::Num(ms, len) = t.next() else {
return None;
};
let ms = match len {
1 => ms * 100,
2 => ms * 10,
3 => ms,
_ => t.s[t.i - len..t.i - len + 3].parse().ok()?,
};
time.push(ms);
time.resize(4, 0);
} else if tz_hour != NONE && tz_min == NONE && (0..60).contains(&n) {
tz_min = n;
} else if expecting(&time, n) {
time.push(n);
time.resize(4, 0);
match t.peek() {
Tok::End | Tok::Space | Tok::Sym(b'+' | b'-') => {}
Tok::Word(w) if w.eq_ignore_ascii_case("z") => {}
_ => return None,
}
} else {
if day.len() >= 3 {
return None;
}
day.push(n);
t.skip(b'-');
}
}
Tok::Word(w) => match keyword(w) {
Keyword::AmPm(off) if !time.is_empty() => hour_offset = off,
Keyword::Month(m) => {
named_month = m;
t.skip(b'-');
}
Keyword::Zone(h) if read_number => {
sign = if h < 0 { -1 } else { 1 };
tz_hour = h.abs();
tz_min = 0;
}
_ => {
if read_number || matches!(t.peek(), Tok::Num(..)) {
return None;
}
}
},
Tok::Sym(c @ (b'+' | b'-'))
if (sign != 0 && tz_hour == 0 && tz_min == 0) || !time.is_empty() =>
{
sign = if c == b'-' { -1 } else { 1 };
let (n, len) = match t.peek() {
Tok::Num(n, len) => {
t.next();
(n, len)
}
_ => (0, 0),
};
read_number = true;
if t.peek() == Tok::Sym(b':') {
tz_hour = n;
tz_min = NONE;
} else if len <= 2 {
tz_hour = n;
tz_min = 0;
} else if len <= 4 {
tz_hour = n / 100;
tz_min = n % 100;
} else {
return None;
}
}
Tok::Sym(b'+' | b'-' | b')') if read_number => return None,
Tok::Sym(_) | Tok::Space => {}
}
}
if day.is_empty() {
return None;
}
day.resize(3, 1);
let is_day = |n: i64| (1..=31).contains(&n);
let (mut year, month, dd) = if named_month == NONE {
if is_day(day[0]) {
(day[2], day[0], day[1])
} else {
(day[0], day[1], day[2])
}
} else if !is_day(day[0]) {
(day[0], named_month, day[1])
} else {
(day[1], named_month, day[0])
};
if (0..=49).contains(&year) {
year += 2000;
} else if (50..=99).contains(&year) {
year += 1900;
}
if !(1..=12).contains(&month) || !is_day(dd) {
return None;
}
time.resize(4, 0);
let (mut h, mi, sec, ms) = (time[0], time[1], time[2], time[3]);
if hour_offset != NONE {
if !(0..=12).contains(&h) {
return None;
}
h = h % 12 + hour_offset;
}
let in_range = (0..24).contains(&h)
&& (0..60).contains(&mi)
&& (0..60).contains(&sec)
&& (0..1000).contains(&ms);
if !in_range && (h, mi, sec, ms) != (24, 0, 0, 0) {
return None;
}
let wall = utc_from_fields(
year as f64,
(month - 1) as f64,
dd as f64,
h as f64,
mi as f64,
sec as f64,
ms as f64,
);
if sign == 0 {
return Some(utc_from_local(wall));
}
let tz_min = if tz_min == NONE { 0 } else { tz_min };
let tz_hour = if tz_hour == NONE { 0 } else { tz_hour };
Some(wall - sign as f64 * (tz_hour * 3_600_000 + tz_min * 60_000) as f64)
}
pub(crate) fn inspect_with_host(h: &crate::host::JsHost, v: &Value) -> String {
let ms = match h.get(v) {
Some(JsObj::Object(p)) => p.get("@@ms").map(|x| h.to_number(x)).unwrap_or(f64::NAN),
_ => f64::NAN,
};
if ms.is_nan() {
"Invalid Date".into()
} else {
iso_string(ms)
}
}