1use crate::host::{with_host, JsObj};
13use fusevm::Value;
14use indexmap::IndexMap;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17pub const STATIC_METHODS: &[&str] = &["now", "parse", "UTC"];
18
19pub const INSTANCE_METHODS: &[&str] = &[
25 "getTime",
26 "valueOf",
27 "toISOString",
28 "toJSON",
29 "toUTCString",
30 "toGMTString",
31 "toString",
32 "toDateString",
33 "toLocaleString",
34 "toLocaleDateString",
35 "toLocaleTimeString",
36 "getFullYear",
37 "getUTCFullYear",
38 "getMonth",
39 "getUTCMonth",
40 "getDate",
41 "getUTCDate",
42 "getDay",
43 "getUTCDay",
44 "getHours",
45 "getUTCHours",
46 "getMinutes",
47 "getUTCMinutes",
48 "getSeconds",
49 "getUTCSeconds",
50 "getMilliseconds",
51 "getUTCMilliseconds",
52 "getTimezoneOffset",
53 "toTimeString",
54 "setTime",
55 "setFullYear",
56 "setUTCFullYear",
57 "setMonth",
58 "setUTCMonth",
59 "setDate",
60 "setUTCDate",
61 "setHours",
62 "setUTCHours",
63 "setMinutes",
64 "setUTCMinutes",
65 "setSeconds",
66 "setUTCSeconds",
67 "setMilliseconds",
68 "setUTCMilliseconds",
69 "getYear",
70 "setYear",
71 "@@toPrimitive",
76];
77
78const MS_PER_DAY: f64 = 86_400_000.0;
79const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
80const MONTHS: [&str; 12] = [
81 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
82];
83
84fn now_ms() -> f64 {
86 SystemTime::now()
87 .duration_since(UNIX_EPOCH)
88 .map(|d| d.as_millis() as f64)
89 .unwrap_or(0.0)
90}
91
92fn from_ms(ms: f64) -> Value {
94 with_host(|h| {
95 let mut m = IndexMap::new();
96 m.insert("@@native".into(), h.new_str("Date"));
97 m.insert("@@ms".into(), Value::Float(ms));
98 h.new_object(m)
99 })
100}
101
102fn ms_of(recv: &Value) -> f64 {
104 with_host(|h| match h.get(recv) {
105 Some(JsObj::Object(p)) => p.get("@@ms").map(|v| h.to_number(v)).unwrap_or(f64::NAN),
106 _ => f64::NAN,
107 })
108}
109
110pub fn construct(args: &[Value]) -> Result<Value, String> {
112 let ms = match args.len() {
113 0 => now_ms(),
114 1 => {
115 let a = &args[0];
116 if let Value::Str(_) = a {
119 parse_str(&with_host(|h| h.str_of(a)))
120 } else if with_host(|h| matches!(h.get(a), Some(JsObj::Str(_)))) {
121 parse_str(&with_host(|h| h.str_of(a)))
122 } else if super::native_tag(a).as_deref() == Some("Date") {
123 ms_of(a)
124 } else {
125 if with_host(|h| matches!(h.get(a), Some(crate::host::JsObj::Symbol { .. }))) {
129 return Err(crate::host::type_error(
130 "Cannot convert a Symbol value to a number",
131 ));
132 }
133 with_host(|h| h.to_number(a))
134 }
135 }
136 _ => {
141 let n = |i: usize, dflt: f64| {
142 args.get(i)
143 .map(|v| with_host(|h| h.to_number(v)))
144 .unwrap_or(dflt)
145 };
146 let mut year = n(0, f64::NAN);
147 if (0.0..=99.0).contains(&year.trunc()) {
149 year = year.trunc() + 1900.0;
150 }
151 utc_from_local(utc_from_fields(
152 year,
153 n(1, 0.0),
154 n(2, 1.0),
155 n(3, 0.0),
156 n(4, 0.0),
157 n(5, 0.0),
158 n(6, 0.0),
159 ))
160 }
161 };
162 Ok(from_ms(time_clip(ms)))
166}
167
168pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
170 Some(match method {
171 "now" => Ok(Value::Float(now_ms())),
172 "parse" => Ok(Value::Float(parse_str(&super::arg_str(args, 0)))),
173 "UTC" => {
174 let n = |i: usize, dflt: f64| {
175 args.get(i)
176 .map(|v| with_host(|h| h.to_number(v)))
177 .unwrap_or(dflt)
178 };
179 let mut year = n(0, f64::NAN);
180 if (0.0..=99.0).contains(&year.trunc()) {
181 year = year.trunc() + 1900.0;
182 }
183 Ok(Value::Float(utc_from_fields(
184 year,
185 n(1, 0.0),
186 n(2, 1.0),
187 n(3, 0.0),
188 n(4, 0.0),
189 n(5, 0.0),
190 n(6, 0.0),
191 )))
192 }
193 _ => return None,
194 })
195}
196
197pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
199 let coerced: Vec<Value>;
204 let _args: &[Value] = if method.starts_with("set") {
205 let mut out = Vec::with_capacity(_args.len());
206 for a in _args {
207 let p = crate::host::to_primitive(a, "number")?;
208 out.push(Value::Float(with_host(|h| h.to_number(&p))));
209 }
210 coerced = out;
211 &coerced
212 } else {
213 _args
214 };
215 let ms = ms_of(recv);
216 let f = |ms: f64| ms; Ok(match method {
218 "getTime" | "valueOf" => Value::Float(f(ms)),
219 "@@toPrimitive" => {
223 let hint = with_host(|h| h.str_of(&_args.first().cloned().unwrap_or(Value::Undef)));
224 if hint == "number" {
225 Value::Float(f(ms))
226 } else {
227 return instance_call(recv, "toString", _args);
228 }
229 }
230 "toISOString" | "toJSON" => {
231 if ms.is_nan() {
232 if method == "toJSON" {
233 with_host(|h| h.null())
234 } else {
235 return Err(crate::host::range_error("Invalid time value"));
236 }
237 } else {
238 with_host(|h| h.new_str(iso_string(ms)))
239 }
240 }
241 "toUTCString" | "toGMTString" => with_host(|h| h.new_str(utc_string(ms))),
242 "toString" => with_host(|h| {
249 h.new_str(if ms.is_nan() {
250 "Invalid Date".into()
251 } else {
252 format!("{} {}", date_string(local_ms(ms)), time_string(ms))
253 })
254 }),
255 "toDateString" => with_host(|h| h.new_str(date_string(local_ms(ms)))),
256 "toLocaleString" | "toLocaleDateString" | "toLocaleTimeString" => {
260 let required = match method {
261 "toLocaleDateString" => crate::datefmt::Required::Date,
262 "toLocaleTimeString" => crate::datefmt::Required::Time,
263 _ => crate::datefmt::Required::Any,
264 };
265 let opts = _args.get(1).cloned().unwrap_or(Value::Undef);
266 let s = crate::datefmt::date_to_locale(
267 ms,
268 &opts,
269 required,
270 &format!("Date.prototype.{method}"),
271 )?;
272 with_host(|h| h.new_str(s))
273 }
274 "getFullYear" => Value::Float(field(local_ms(ms), Field::Year)),
275 "getUTCFullYear" => Value::Float(field(ms, Field::Year)),
276 "getMonth" => Value::Float(field(local_ms(ms), Field::Month)),
277 "getUTCMonth" => Value::Float(field(ms, Field::Month)),
278 "getDate" => Value::Float(field(local_ms(ms), Field::Day)),
279 "getUTCDate" => Value::Float(field(ms, Field::Day)),
280 "getDay" => Value::Float(field(local_ms(ms), Field::Weekday)),
281 "getUTCDay" => Value::Float(field(ms, Field::Weekday)),
282 "getHours" => Value::Float(field(local_ms(ms), Field::Hours)),
283 "getUTCHours" => Value::Float(field(ms, Field::Hours)),
284 "getMinutes" => Value::Float(field(local_ms(ms), Field::Minutes)),
285 "getUTCMinutes" => Value::Float(field(ms, Field::Minutes)),
286 "getSeconds" => Value::Float(field(local_ms(ms), Field::Seconds)),
287 "getUTCSeconds" => Value::Float(field(ms, Field::Seconds)),
288 "getMilliseconds" => Value::Float(field(local_ms(ms), Field::Millis)),
289 "getUTCMilliseconds" => Value::Float(field(ms, Field::Millis)),
290 "getTimezoneOffset" => Value::Float(if ms.is_nan() {
295 f64::NAN
296 } else {
297 (ms - local_ms(ms)) / 60_000.0
298 }),
299 "toTimeString" => with_host(|h| h.new_str(time_string(ms))),
300 "setTime" => Value::Float(store_ms(recv, time_clip(super::arg_num(_args, 0)))),
301 "setFullYear" => Value::Float(set_fields_local(recv, ms, 0, _args, false)),
308 "setUTCFullYear" => Value::Float(set_fields(recv, ms, 0, _args, false)),
309 "setMonth" => Value::Float(set_fields_local(recv, ms, 1, _args, false)),
310 "setUTCMonth" => Value::Float(set_fields(recv, ms, 1, _args, false)),
311 "setDate" => Value::Float(set_fields_local(recv, ms, 2, _args, false)),
312 "setUTCDate" => Value::Float(set_fields(recv, ms, 2, _args, false)),
313 "setHours" => Value::Float(set_fields_local(recv, ms, 3, _args, false)),
314 "setUTCHours" => Value::Float(set_fields(recv, ms, 3, _args, false)),
315 "setMinutes" => Value::Float(set_fields_local(recv, ms, 4, _args, false)),
316 "setUTCMinutes" => Value::Float(set_fields(recv, ms, 4, _args, false)),
317 "setSeconds" => Value::Float(set_fields_local(recv, ms, 5, _args, false)),
318 "setUTCSeconds" => Value::Float(set_fields(recv, ms, 5, _args, false)),
319 "setMilliseconds" => Value::Float(set_fields_local(recv, ms, 6, _args, false)),
320 "setUTCMilliseconds" => Value::Float(set_fields(recv, ms, 6, _args, false)),
321 "getYear" => Value::Float(if ms.is_nan() {
326 f64::NAN
327 } else {
328 field(local_ms(ms), Field::Year) - 1900.0
329 }),
330 "setYear" => Value::Float(set_fields_local(recv, ms, 0, _args, true)),
331 _ => {
332 return Err(crate::host::type_error(&format!(
333 "date.{method} is not a function"
334 )))
335 }
336 })
337}
338
339enum Field {
342 Year,
343 Month,
344 Day,
345 Weekday,
346 Hours,
347 Minutes,
348 Seconds,
349 Millis,
350}
351
352pub(crate) fn split_day(ms: f64) -> (i64, i64) {
355 let day = (ms / MS_PER_DAY).floor();
356 let rem = ms - day * MS_PER_DAY;
357 (day as i64, rem as i64)
358}
359
360pub(crate) fn civil_from_days(z: i64) -> (i64, i64, i64) {
363 let z = z + 719_468;
364 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
365 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
368 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)
373}
374
375pub(crate) fn days_from_civil(y: i64, m0: i64, d: i64) -> i64 {
377 let m = m0 + 1;
378 let y = if m <= 2 { y - 1 } else { y };
379 let era = if y >= 0 { y } else { y - 399 } / 400;
380 let yoe = y - era * 400;
381 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
382 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
383 era * 146_097 + doe - 719_468
384}
385
386fn field(ms: f64, which: Field) -> f64 {
387 if ms.is_nan() {
388 return f64::NAN;
389 }
390 let (day, rem) = split_day(ms);
391 let (y, mo, d) = civil_from_days(day);
392 match which {
393 Field::Year => y as f64,
394 Field::Month => mo as f64,
395 Field::Day => d as f64,
396 Field::Weekday => (((day % 7) + 4 + 7) % 7) as f64,
398 Field::Hours => (rem / 3_600_000) as f64,
399 Field::Minutes => (rem / 60_000 % 60) as f64,
400 Field::Seconds => (rem / 1000 % 60) as f64,
401 Field::Millis => (rem % 1000) as f64,
402 }
403}
404
405fn utc_from_fields(y: f64, mo: f64, d: f64, h: f64, mi: f64, s: f64, ms: f64) -> f64 {
408 if [y, mo, d, h, mi, s, ms].iter().any(|v| !v.is_finite()) {
412 return f64::NAN;
413 }
414 let [y, mo, d, h, mi, s, ms] = [y, mo, d, h, mi, s, ms].map(f64::trunc);
415 let total_months = y as i64 * 12 + mo as i64;
417 let year = total_months.div_euclid(12);
418 let month = total_months.rem_euclid(12);
419 let days = days_from_civil(year, month, d as i64);
420 days as f64 * MS_PER_DAY + h * 3_600_000.0 + mi * 60_000.0 + s * 1000.0 + ms
421}
422
423#[cfg(unix)]
434pub(crate) fn zone_offset_ms(ms: f64) -> f64 {
435 if !ms.is_finite() {
436 return 0.0;
437 }
438 let secs = (ms / 1000.0).floor() as i64;
441 let t = secs as libc::time_t;
442 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
443 let ok = unsafe { !libc::localtime_r(&t, &mut tm).is_null() };
445 if !ok {
446 return 0.0;
447 }
448 tm.tm_gmtoff as f64 * 1000.0
449}
450
451#[cfg(not(unix))]
452pub(crate) fn zone_offset_ms(_ms: f64) -> f64 {
453 0.0
454}
455
456fn local_ms(ms: f64) -> f64 {
459 ms + zone_offset_ms(ms)
460}
461
462fn utc_from_local(local: f64) -> f64 {
472 if !local.is_finite() {
473 return local;
474 }
475 let off_naive = zone_offset_ms(local);
476 let cand_a = local - off_naive;
477 let off_corrected = zone_offset_ms(cand_a);
478 if off_corrected == off_naive {
479 return cand_a;
480 }
481 let cand_b = local - off_corrected;
482 if local_ms(cand_b) == local {
483 return cand_b;
484 }
485 if local_ms(cand_a) == local {
486 return cand_a;
487 }
488 local - off_naive.min(off_corrected)
489}
490
491fn utc_string(ms: f64) -> String {
492 if ms.is_nan() {
493 return "Invalid Date".into();
494 }
495 let (day, _) = split_day(ms);
496 let (y, mo, d) = civil_from_days(day);
497 let wd = (((day % 7) + 4 + 7) % 7) as usize;
498 format!(
499 "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
500 DAYS[wd],
501 d,
502 MONTHS[mo as usize],
503 y,
504 field(ms, Field::Hours) as i64,
505 field(ms, Field::Minutes) as i64,
506 field(ms, Field::Seconds) as i64,
507 )
508}
509
510fn time_string(ms: f64) -> String {
520 if ms.is_nan() {
521 return "Invalid Date".into();
522 }
523 let local = local_ms(ms);
524 let off_min = (zone_offset_ms(ms) / 60_000.0) as i64;
525 let sign = if off_min < 0 { '-' } else { '+' };
526 let abs = off_min.abs();
527 format!(
528 "{:02}:{:02}:{:02} GMT{}{:02}{:02} ({})",
529 field(local, Field::Hours) as i64,
530 field(local, Field::Minutes) as i64,
531 field(local, Field::Seconds) as i64,
532 sign,
533 abs / 60,
534 abs % 60,
535 zone_name(ms),
536 )
537}
538
539fn zone_name(ms: f64) -> String {
542 if zone_offset_ms(ms) == 0.0 {
543 return "Coordinated Universal Time".into();
544 }
545 zone_abbrev(ms).unwrap_or_else(|| "Coordinated Universal Time".into())
546}
547
548#[cfg(unix)]
551pub(crate) fn zone_abbrev(ms: f64) -> Option<String> {
552 let secs = (ms / 1000.0).floor() as i64;
553 let t = secs as libc::time_t;
554 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
555 if unsafe { libc::localtime_r(&t, &mut tm).is_null() } || tm.tm_zone.is_null() {
557 return None;
558 }
559 let z = unsafe { std::ffi::CStr::from_ptr(tm.tm_zone) };
561 Some(z.to_string_lossy().into_owned())
562}
563
564#[cfg(not(unix))]
565pub(crate) fn zone_abbrev(_ms: f64) -> Option<String> {
566 None
567}
568
569fn time_clip(ms: f64) -> f64 {
576 if !ms.is_finite() || ms.abs() > 8.64e15 {
577 return f64::NAN;
578 }
579 ms.trunc()
580}
581
582fn store_ms(recv: &Value, ms: f64) -> f64 {
585 with_host(|h| {
586 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
587 p.insert("@@ms".into(), Value::Float(ms));
588 }
589 });
590 ms
591}
592
593fn set_fields_local(recv: &Value, ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
608 if ms.is_nan() && start != 0 {
609 return store_ms(recv, f64::NAN);
610 }
611 let local = set_fields_value(local_ms(ms), start, args, legacy_year);
612 store_ms(recv, time_clip(utc_from_local(local)))
613}
614
615fn set_fields(recv: &Value, ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
616 if ms.is_nan() && start != 0 {
617 return store_ms(recv, f64::NAN);
618 }
619 store_ms(
620 recv,
621 time_clip(set_fields_value(ms, start, args, legacy_year)),
622 )
623}
624
625fn set_fields_value(ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
630 let base = if ms.is_nan() {
631 0.0 } else {
633 ms
634 };
635 let mut f = [
636 field(base, Field::Year),
637 field(base, Field::Month),
638 field(base, Field::Day),
639 field(base, Field::Hours),
640 field(base, Field::Minutes),
641 field(base, Field::Seconds),
642 field(base, Field::Millis),
643 ];
644 let end = if start < 3 { 3 } else { 7 };
646 for (i, slot) in f.iter_mut().enumerate().take(end).skip(start) {
647 match args.get(i - start) {
648 Some(v) => *slot = with_host(|h| h.to_number(v)).trunc(),
649 None => break,
650 }
651 }
652 if legacy_year && (0.0..=99.0).contains(&f[0]) {
653 f[0] += 1900.0;
654 }
655 utc_from_fields(f[0], f[1], f[2], f[3], f[4], f[5], f[6])
656}
657
658fn date_string(ms: f64) -> String {
660 if ms.is_nan() {
661 return "Invalid Date".into();
662 }
663 let (day, _) = split_day(ms);
664 let (y, mo, d) = civil_from_days(day);
665 let wd = (((day % 7) + 4 + 7) % 7) as usize;
666 format!("{} {} {:02} {:04}", DAYS[wd], MONTHS[mo as usize], d, y)
667}
668
669fn iso_year(y: i64) -> String {
676 if (0..=9999).contains(&y) {
677 return format!("{y:04}");
678 }
679 let sign = if y < 0 { '-' } else { '+' };
680 format!("{sign}{:06}", y.abs())
681}
682
683fn iso_string(ms: f64) -> String {
685 let (day, _) = split_day(ms);
686 let (y, mo, d) = civil_from_days(day);
687 format!(
688 "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
689 iso_year(y),
690 mo + 1,
691 d,
692 field(ms, Field::Hours) as i64,
693 field(ms, Field::Minutes) as i64,
694 field(ms, Field::Seconds) as i64,
695 field(ms, Field::Millis) as i64,
696 )
697}
698
699fn parse_str(s: &str) -> f64 {
706 time_clip(parse_iso(s).or_else(|| parse_legacy(s)).unwrap_or(f64::NAN))
707}
708
709fn parse_iso(s: &str) -> Option<f64> {
721 let b = s.as_bytes();
722 let mut i = 0;
723 let digits = |i: &mut usize, n: usize| -> Option<i64> {
725 let end = *i + n;
726 let part = b.get(*i..end)?;
727 if !part.iter().all(u8::is_ascii_digit) {
728 return None;
729 }
730 *i = end;
731 std::str::from_utf8(part).ok()?.parse().ok()
732 };
733 let year = match b.first()? {
734 sign @ (b'+' | b'-') => {
735 i = 1;
736 let y = digits(&mut i, 6)?;
737 if *sign == b'-' && y == 0 {
739 return None;
740 }
741 if *sign == b'-' {
742 -y
743 } else {
744 y
745 }
746 }
747 _ => digits(&mut i, 4)?,
748 };
749 let (mut month, mut day) = (1, 1);
750 if b.get(i) == Some(&b'-') {
751 i += 1;
752 month = digits(&mut i, 2)?;
753 if b.get(i) == Some(&b'-') {
754 i += 1;
755 day = digits(&mut i, 2)?;
756 }
757 }
758 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
759 return None;
760 }
761 let date = days_from_civil(year, month - 1, day) as f64 * MS_PER_DAY;
762 match b.get(i) {
764 None => return Some(date),
765 Some(b'Z' | b'z') if i + 1 == b.len() => return Some(date),
766 Some(b'T' | b't' | b' ') => i += 1,
767 Some(_) => return None,
768 }
769 let h = digits(&mut i, 2)?;
770 if b.get(i) != Some(&b':') {
771 return None;
772 }
773 i += 1;
774 let mi = digits(&mut i, 2)?;
775 let (mut sec, mut milli) = (0, 0);
776 if b.get(i) == Some(&b':') {
777 i += 1;
778 sec = digits(&mut i, 2)?;
779 if b.get(i) == Some(&b'.') {
780 i += 1;
781 let start = i;
782 while b.get(i).is_some_and(u8::is_ascii_digit) {
783 i += 1;
784 }
785 if i == start {
786 return None;
787 }
788 let frac = &s[start..i.min(start + 3)];
790 milli = format!("{frac:0<3}").parse().ok()?;
791 }
792 }
793 if h > 24 || mi > 59 || sec > 59 || (h == 24 && (mi, sec, milli) != (0, 0, 0)) {
795 return None;
796 }
797 let wall =
798 date + h as f64 * 3_600_000.0 + mi as f64 * 60_000.0 + sec as f64 * 1000.0 + milli as f64;
799 let offset = match b.get(i) {
800 None => return Some(utc_from_local(wall)),
801 Some(b'Z' | b'z') if i + 1 == b.len() => 0.0,
802 Some(sign @ (b'+' | b'-')) => {
803 let sign = if *sign == b'-' { -1.0 } else { 1.0 };
804 i += 1;
805 let oh = digits(&mut i, 2)?;
806 if b.get(i) == Some(&b':') {
807 i += 1;
808 }
809 let om = digits(&mut i, 2)?;
810 if i != b.len() || oh > 23 || om > 59 {
811 return None;
812 }
813 sign * (oh as f64 * 3_600_000.0 + om as f64 * 60_000.0)
814 }
815 Some(_) => return None,
816 };
817 Some(wall - offset)
818}
819
820#[derive(Clone, Copy, PartialEq)]
825enum Tok<'a> {
826 Num(i64, usize),
827 Word(&'a str),
828 Sym(u8),
829 Space,
830 End,
831}
832
833struct Toks<'a> {
834 s: &'a str,
835 i: usize,
836}
837
838impl<'a> Toks<'a> {
839 fn next(&mut self) -> Tok<'a> {
840 let b = self.s.as_bytes();
841 let Some(&c) = b.get(self.i) else {
842 return Tok::End;
843 };
844 let start = self.i;
845 let run = |i: &mut usize, f: fn(&u8) -> bool| {
846 while b.get(*i).is_some_and(f) {
847 *i += 1;
848 }
849 };
850 if c.is_ascii_digit() {
851 run(&mut self.i, u8::is_ascii_digit);
852 let text = &self.s[start..self.i];
853 return Tok::Num(text.parse().unwrap_or(i64::MAX), text.len());
856 }
857 if c.is_ascii_alphabetic() {
858 run(&mut self.i, u8::is_ascii_alphabetic);
859 return Tok::Word(&self.s[start..self.i]);
860 }
861 if c.is_ascii_whitespace() {
862 run(&mut self.i, u8::is_ascii_whitespace);
863 return Tok::Space;
864 }
865 if c == b'(' {
866 let mut depth = 0;
867 while let Some(&c) = b.get(self.i) {
868 self.i += 1;
869 match c {
870 b'(' => depth += 1,
871 b')' => {
872 depth -= 1;
873 if depth == 0 {
874 break;
875 }
876 }
877 _ => {}
878 }
879 }
880 return Tok::Space;
881 }
882 let len = self.s[start..].chars().next().map_or(1, char::len_utf8);
884 self.i += len;
885 Tok::Sym(if len == 1 { c } else { 0 })
886 }
887
888 fn peek(&self) -> Tok<'a> {
889 Toks {
890 s: self.s,
891 i: self.i,
892 }
893 .next()
894 }
895
896 fn skip(&mut self, sym: u8) -> bool {
897 let taken = self.peek() == Tok::Sym(sym);
898 if taken {
899 self.next();
900 }
901 taken
902 }
903}
904
905enum Keyword {
909 Month(i64),
910 AmPm(i64),
911 Zone(i64),
913 Other,
914}
915
916fn keyword(word: &str) -> Keyword {
917 let w = word.to_ascii_lowercase();
918 let prefix = &w[..w.len().min(3)];
919 if w.len() >= 3 {
920 const MONTHS: [&str; 12] = [
921 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
922 ];
923 if let Some(m) = MONTHS.iter().position(|m| *m == prefix) {
924 return Keyword::Month(m as i64 + 1);
925 }
926 }
927 match prefix {
928 "am" if w.len() == 2 => Keyword::AmPm(0),
929 "pm" if w.len() == 2 => Keyword::AmPm(12),
930 "ut" if w.len() == 2 => Keyword::Zone(0),
931 "z" => Keyword::Zone(0),
932 "utc" | "gmt" if w.len() >= 3 => Keyword::Zone(0),
933 "cdt" => Keyword::Zone(-5),
934 "cst" => Keyword::Zone(-6),
935 "edt" => Keyword::Zone(-4),
936 "est" => Keyword::Zone(-5),
937 "mdt" => Keyword::Zone(-6),
938 "mst" => Keyword::Zone(-7),
939 "pdt" => Keyword::Zone(-7),
940 "pst" => Keyword::Zone(-8),
941 _ => Keyword::Other,
942 }
943}
944
945fn parse_legacy(s: &str) -> Option<f64> {
955 const NONE: i64 = i64::MIN;
956 let mut t = Toks { s, i: 0 };
957 let (mut day, mut named_month) = (Vec::with_capacity(3), NONE);
958 let (mut time, mut hour_offset) = (Vec::with_capacity(4), NONE);
959 let (mut sign, mut tz_hour, mut tz_min) = (0i64, NONE, NONE);
960 let mut read_number = false;
961 let expecting = |time: &Vec<i64>, n: i64| match time.len() {
963 1 | 2 => (0..60).contains(&n),
964 3 => (0..1000).contains(&n),
965 _ => false,
966 };
967 loop {
968 match t.next() {
969 Tok::End => break,
970 Tok::Num(n, _) => {
971 read_number = true;
972 if t.skip(b':') {
973 if t.skip(b':') {
974 if !time.is_empty() {
975 return None;
976 }
977 time.extend([n, 0]);
978 } else {
979 if time.len() >= 4 {
980 return None;
981 }
982 time.push(n);
983 t.skip(b'.');
984 }
985 } else if t.peek() == Tok::Sym(b'.') && expecting(&time, n) {
986 t.next();
987 time.push(n);
988 let Tok::Num(ms, len) = t.next() else {
989 return None;
990 };
991 let ms = match len {
993 1 => ms * 100,
994 2 => ms * 10,
995 3 => ms,
996 _ => t.s[t.i - len..t.i - len + 3].parse().ok()?,
997 };
998 time.push(ms);
999 time.resize(4, 0);
1000 } else if tz_hour != NONE && tz_min == NONE && (0..60).contains(&n) {
1001 tz_min = n;
1002 } else if expecting(&time, n) {
1003 time.push(n);
1004 time.resize(4, 0);
1005 match t.peek() {
1007 Tok::End | Tok::Space | Tok::Sym(b'+' | b'-') => {}
1008 Tok::Word(w) if w.eq_ignore_ascii_case("z") => {}
1009 _ => return None,
1010 }
1011 } else {
1012 if day.len() >= 3 {
1013 return None;
1014 }
1015 day.push(n);
1016 t.skip(b'-');
1017 }
1018 }
1019 Tok::Word(w) => match keyword(w) {
1020 Keyword::AmPm(off) if !time.is_empty() => hour_offset = off,
1021 Keyword::Month(m) => {
1022 named_month = m;
1023 t.skip(b'-');
1024 }
1025 Keyword::Zone(h) if read_number => {
1026 sign = if h < 0 { -1 } else { 1 };
1027 tz_hour = h.abs();
1028 tz_min = 0;
1029 }
1030 _ => {
1031 if read_number || matches!(t.peek(), Tok::Num(..)) {
1034 return None;
1035 }
1036 }
1037 },
1038 Tok::Sym(c @ (b'+' | b'-'))
1039 if (sign != 0 && tz_hour == 0 && tz_min == 0) || !time.is_empty() =>
1040 {
1041 sign = if c == b'-' { -1 } else { 1 };
1044 let (n, len) = match t.peek() {
1045 Tok::Num(n, len) => {
1046 t.next();
1047 (n, len)
1048 }
1049 _ => (0, 0),
1050 };
1051 read_number = true;
1052 if t.peek() == Tok::Sym(b':') {
1053 tz_hour = n;
1054 tz_min = NONE;
1055 } else if len <= 2 {
1056 tz_hour = n;
1057 tz_min = 0;
1058 } else if len <= 4 {
1059 tz_hour = n / 100;
1060 tz_min = n % 100;
1061 } else {
1062 return None;
1063 }
1064 }
1065 Tok::Sym(b'+' | b'-' | b')') if read_number => return None,
1066 Tok::Sym(_) | Tok::Space => {}
1067 }
1068 }
1069
1070 if day.is_empty() {
1073 return None;
1074 }
1075 day.resize(3, 1);
1076 let is_day = |n: i64| (1..=31).contains(&n);
1077 let (mut year, month, dd) = if named_month == NONE {
1078 if is_day(day[0]) {
1079 (day[2], day[0], day[1])
1080 } else {
1081 (day[0], day[1], day[2])
1082 }
1083 } else if !is_day(day[0]) {
1084 (day[0], named_month, day[1])
1085 } else {
1086 (day[1], named_month, day[0])
1087 };
1088 if (0..=49).contains(&year) {
1089 year += 2000;
1090 } else if (50..=99).contains(&year) {
1091 year += 1900;
1092 }
1093 if !(1..=12).contains(&month) || !is_day(dd) {
1094 return None;
1095 }
1096
1097 time.resize(4, 0);
1100 let (mut h, mi, sec, ms) = (time[0], time[1], time[2], time[3]);
1101 if hour_offset != NONE {
1102 if !(0..=12).contains(&h) {
1103 return None;
1104 }
1105 h = h % 12 + hour_offset;
1106 }
1107 let in_range = (0..24).contains(&h)
1108 && (0..60).contains(&mi)
1109 && (0..60).contains(&sec)
1110 && (0..1000).contains(&ms);
1111 if !in_range && (h, mi, sec, ms) != (24, 0, 0, 0) {
1112 return None;
1113 }
1114
1115 let wall = utc_from_fields(
1116 year as f64,
1117 (month - 1) as f64,
1118 dd as f64,
1119 h as f64,
1120 mi as f64,
1121 sec as f64,
1122 ms as f64,
1123 );
1124 if sign == 0 {
1125 return Some(utc_from_local(wall));
1126 }
1127 let tz_min = if tz_min == NONE { 0 } else { tz_min };
1128 let tz_hour = if tz_hour == NONE { 0 } else { tz_hour };
1129 Some(wall - sign as f64 * (tz_hour * 3_600_000 + tz_min * 60_000) as f64)
1130}
1131
1132pub(crate) fn inspect_with_host(h: &crate::host::JsHost, v: &Value) -> String {
1144 let ms = match h.get(v) {
1145 Some(JsObj::Object(p)) => p.get("@@ms").map(|x| h.to_number(x)).unwrap_or(f64::NAN),
1146 _ => f64::NAN,
1147 };
1148 if ms.is_nan() {
1149 "Invalid Date".into()
1150 } else {
1151 iso_string(ms)
1152 }
1153}