1use crate::host::{with_host, JsObj};
14use fusevm::Value;
15use indexmap::IndexMap;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18pub const STATIC_METHODS: &[&str] = &["now", "parse", "UTC"];
19
20pub const INSTANCE_METHODS: &[&str] = &[
26 "getTime",
27 "valueOf",
28 "toISOString",
29 "toJSON",
30 "toUTCString",
31 "toGMTString",
32 "toString",
33 "toDateString",
34 "toLocaleString",
35 "toLocaleDateString",
36 "toLocaleTimeString",
37 "getFullYear",
38 "getUTCFullYear",
39 "getMonth",
40 "getUTCMonth",
41 "getDate",
42 "getUTCDate",
43 "getDay",
44 "getUTCDay",
45 "getHours",
46 "getUTCHours",
47 "getMinutes",
48 "getUTCMinutes",
49 "getSeconds",
50 "getUTCSeconds",
51 "getMilliseconds",
52 "getUTCMilliseconds",
53 "getTimezoneOffset",
54 "toTimeString",
55 "setTime",
56 "setFullYear",
57 "setUTCFullYear",
58 "setMonth",
59 "setUTCMonth",
60 "setDate",
61 "setUTCDate",
62 "setHours",
63 "setUTCHours",
64 "setMinutes",
65 "setUTCMinutes",
66 "setSeconds",
67 "setUTCSeconds",
68 "setMilliseconds",
69 "setUTCMilliseconds",
70 "getYear",
71 "setYear",
72 "@@toPrimitive",
77];
78
79const MS_PER_DAY: f64 = 86_400_000.0;
80const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
81const MONTHS: [&str; 12] = [
82 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
83];
84
85fn now_ms() -> f64 {
87 SystemTime::now()
88 .duration_since(UNIX_EPOCH)
89 .map(|d| d.as_millis() as f64)
90 .unwrap_or(0.0)
91}
92
93fn from_ms(ms: f64) -> Value {
95 with_host(|h| {
96 let mut m = IndexMap::new();
97 m.insert("@@native".into(), h.new_str("Date"));
98 m.insert("@@ms".into(), Value::Float(ms));
99 h.new_object(m)
100 })
101}
102
103fn ms_of(recv: &Value) -> f64 {
105 with_host(|h| match h.get(recv) {
106 Some(JsObj::Object(p)) => p.get("@@ms").map(|v| h.to_number(v)).unwrap_or(f64::NAN),
107 _ => f64::NAN,
108 })
109}
110
111pub fn construct(args: &[Value]) -> Result<Value, String> {
113 let ms = match args.len() {
114 0 => now_ms(),
115 1 => {
116 let a = &args[0];
117 if let Value::Str(_) = a {
120 parse_str(&with_host(|h| h.str_of(a)))
121 } else if with_host(|h| matches!(h.get(a), Some(JsObj::Str(_)))) {
122 parse_str(&with_host(|h| h.str_of(a)))
123 } else if super::native_tag(a).as_deref() == Some("Date") {
124 ms_of(a)
125 } else {
126 if with_host(|h| matches!(h.get(a), Some(crate::host::JsObj::Symbol { .. }))) {
130 return Err(crate::host::type_error(
131 "Cannot convert a Symbol value to a number",
132 ));
133 }
134 with_host(|h| h.to_number(a))
135 }
136 }
137 _ => {
142 let n = |i: usize, dflt: f64| {
143 args.get(i)
144 .map(|v| with_host(|h| h.to_number(v)))
145 .unwrap_or(dflt)
146 };
147 let mut year = n(0, f64::NAN);
148 if (0.0..=99.0).contains(&year.trunc()) {
150 year = year.trunc() + 1900.0;
151 }
152 utc_from_local(utc_from_fields(
153 year,
154 n(1, 0.0),
155 n(2, 1.0),
156 n(3, 0.0),
157 n(4, 0.0),
158 n(5, 0.0),
159 n(6, 0.0),
160 ))
161 }
162 };
163 Ok(from_ms(time_clip(ms)))
167}
168
169pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
171 Some(match method {
172 "now" => Ok(Value::Float(now_ms())),
173 "parse" => Ok(Value::Float(parse_str(&super::arg_str(args, 0)))),
174 "UTC" => {
175 let n = |i: usize, dflt: f64| {
176 args.get(i)
177 .map(|v| with_host(|h| h.to_number(v)))
178 .unwrap_or(dflt)
179 };
180 let mut year = n(0, f64::NAN);
181 if (0.0..=99.0).contains(&year.trunc()) {
182 year = year.trunc() + 1900.0;
183 }
184 Ok(Value::Float(utc_from_fields(
185 year,
186 n(1, 0.0),
187 n(2, 1.0),
188 n(3, 0.0),
189 n(4, 0.0),
190 n(5, 0.0),
191 n(6, 0.0),
192 )))
193 }
194 _ => return None,
195 })
196}
197
198pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
200 let coerced: Vec<Value>;
205 let _args: &[Value] = if method.starts_with("set") {
206 let mut out = Vec::with_capacity(_args.len());
207 for a in _args {
208 let p = crate::host::to_primitive(a, "number")?;
209 out.push(Value::Float(with_host(|h| h.to_number(&p))));
210 }
211 coerced = out;
212 &coerced
213 } else {
214 _args
215 };
216 let ms = ms_of(recv);
217 let f = |ms: f64| ms; Ok(match method {
219 "getTime" | "valueOf" => Value::Float(f(ms)),
220 "@@toPrimitive" => {
224 let hint = with_host(|h| h.str_of(&_args.first().cloned().unwrap_or(Value::Undef)));
225 if hint == "number" {
226 Value::Float(f(ms))
227 } else {
228 return instance_call(recv, "toString", _args);
229 }
230 }
231 "toISOString" | "toJSON" => {
232 if ms.is_nan() {
233 if method == "toJSON" {
234 with_host(|h| h.null())
235 } else {
236 return Err(crate::host::range_error("Invalid time value"));
237 }
238 } else {
239 with_host(|h| h.new_str(iso_string(ms)))
240 }
241 }
242 "toUTCString" | "toGMTString" => with_host(|h| h.new_str(utc_string(ms))),
243 "toString" => with_host(|h| {
250 h.new_str(if ms.is_nan() {
251 "Invalid Date".into()
252 } else {
253 format!("{} {}", date_string(local_ms(ms)), time_string(ms))
254 })
255 }),
256 "toDateString" => with_host(|h| h.new_str(date_string(local_ms(ms)))),
257 "toLocaleString" => with_host(|h| {
265 h.new_str(if ms.is_nan() {
266 "Invalid Date".into()
267 } else {
268 format!(
269 "{}, {}",
270 locale_date(local_ms(ms)),
271 locale_time(local_ms(ms))
272 )
273 })
274 }),
275 "toLocaleDateString" => with_host(|h| {
276 h.new_str(if ms.is_nan() {
277 "Invalid Date".into()
278 } else {
279 locale_date(local_ms(ms))
280 })
281 }),
282 "toLocaleTimeString" => with_host(|h| {
283 h.new_str(if ms.is_nan() {
284 "Invalid Date".into()
285 } else {
286 locale_time(local_ms(ms))
287 })
288 }),
289 "getFullYear" => Value::Float(field(local_ms(ms), Field::Year)),
290 "getUTCFullYear" => Value::Float(field(ms, Field::Year)),
291 "getMonth" => Value::Float(field(local_ms(ms), Field::Month)),
292 "getUTCMonth" => Value::Float(field(ms, Field::Month)),
293 "getDate" => Value::Float(field(local_ms(ms), Field::Day)),
294 "getUTCDate" => Value::Float(field(ms, Field::Day)),
295 "getDay" => Value::Float(field(local_ms(ms), Field::Weekday)),
296 "getUTCDay" => Value::Float(field(ms, Field::Weekday)),
297 "getHours" => Value::Float(field(local_ms(ms), Field::Hours)),
298 "getUTCHours" => Value::Float(field(ms, Field::Hours)),
299 "getMinutes" => Value::Float(field(local_ms(ms), Field::Minutes)),
300 "getUTCMinutes" => Value::Float(field(ms, Field::Minutes)),
301 "getSeconds" => Value::Float(field(local_ms(ms), Field::Seconds)),
302 "getUTCSeconds" => Value::Float(field(ms, Field::Seconds)),
303 "getMilliseconds" => Value::Float(field(local_ms(ms), Field::Millis)),
304 "getUTCMilliseconds" => Value::Float(field(ms, Field::Millis)),
305 "getTimezoneOffset" => Value::Float(if ms.is_nan() {
310 f64::NAN
311 } else {
312 (ms - local_ms(ms)) / 60_000.0
313 }),
314 "toTimeString" => with_host(|h| h.new_str(time_string(ms))),
315 "setTime" => Value::Float(store_ms(recv, time_clip(super::arg_num(_args, 0)))),
316 "setFullYear" => Value::Float(set_fields_local(recv, ms, 0, _args, false)),
323 "setUTCFullYear" => Value::Float(set_fields(recv, ms, 0, _args, false)),
324 "setMonth" => Value::Float(set_fields_local(recv, ms, 1, _args, false)),
325 "setUTCMonth" => Value::Float(set_fields(recv, ms, 1, _args, false)),
326 "setDate" => Value::Float(set_fields_local(recv, ms, 2, _args, false)),
327 "setUTCDate" => Value::Float(set_fields(recv, ms, 2, _args, false)),
328 "setHours" => Value::Float(set_fields_local(recv, ms, 3, _args, false)),
329 "setUTCHours" => Value::Float(set_fields(recv, ms, 3, _args, false)),
330 "setMinutes" => Value::Float(set_fields_local(recv, ms, 4, _args, false)),
331 "setUTCMinutes" => Value::Float(set_fields(recv, ms, 4, _args, false)),
332 "setSeconds" => Value::Float(set_fields_local(recv, ms, 5, _args, false)),
333 "setUTCSeconds" => Value::Float(set_fields(recv, ms, 5, _args, false)),
334 "setMilliseconds" => Value::Float(set_fields_local(recv, ms, 6, _args, false)),
335 "setUTCMilliseconds" => Value::Float(set_fields(recv, ms, 6, _args, false)),
336 "getYear" => Value::Float(if ms.is_nan() {
341 f64::NAN
342 } else {
343 field(local_ms(ms), Field::Year) - 1900.0
344 }),
345 "setYear" => Value::Float(set_fields_local(recv, ms, 0, _args, true)),
346 _ => {
347 return Err(crate::host::type_error(&format!(
348 "date.{method} is not a function"
349 )))
350 }
351 })
352}
353
354enum Field {
357 Year,
358 Month,
359 Day,
360 Weekday,
361 Hours,
362 Minutes,
363 Seconds,
364 Millis,
365}
366
367fn split_day(ms: f64) -> (i64, i64) {
370 let day = (ms / MS_PER_DAY).floor();
371 let rem = ms - day * MS_PER_DAY;
372 (day as i64, rem as i64)
373}
374
375fn civil_from_days(z: i64) -> (i64, i64, i64) {
378 let z = z + 719_468;
379 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
380 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
383 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)
388}
389
390fn days_from_civil(y: i64, m0: i64, d: i64) -> i64 {
392 let m = m0 + 1;
393 let y = if m <= 2 { y - 1 } else { y };
394 let era = if y >= 0 { y } else { y - 399 } / 400;
395 let yoe = y - era * 400;
396 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
397 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
398 era * 146_097 + doe - 719_468
399}
400
401fn field(ms: f64, which: Field) -> f64 {
402 if ms.is_nan() {
403 return f64::NAN;
404 }
405 let (day, rem) = split_day(ms);
406 let (y, mo, d) = civil_from_days(day);
407 match which {
408 Field::Year => y as f64,
409 Field::Month => mo as f64,
410 Field::Day => d as f64,
411 Field::Weekday => (((day % 7) + 4 + 7) % 7) as f64,
413 Field::Hours => (rem / 3_600_000) as f64,
414 Field::Minutes => (rem / 60_000 % 60) as f64,
415 Field::Seconds => (rem / 1000 % 60) as f64,
416 Field::Millis => (rem % 1000) as f64,
417 }
418}
419
420fn utc_from_fields(y: f64, mo: f64, d: f64, h: f64, mi: f64, s: f64, ms: f64) -> f64 {
423 if [y, mo, d, h, mi, s, ms].iter().any(|v| !v.is_finite()) {
427 return f64::NAN;
428 }
429 let [y, mo, d, h, mi, s, ms] = [y, mo, d, h, mi, s, ms].map(f64::trunc);
430 let total_months = y as i64 * 12 + mo as i64;
432 let year = total_months.div_euclid(12);
433 let month = total_months.rem_euclid(12);
434 let days = days_from_civil(year, month, d as i64);
435 days as f64 * MS_PER_DAY + h * 3_600_000.0 + mi * 60_000.0 + s * 1000.0 + ms
436}
437
438#[cfg(unix)]
449fn zone_offset_ms(ms: f64) -> f64 {
450 if !ms.is_finite() {
451 return 0.0;
452 }
453 let secs = (ms / 1000.0).floor() as i64;
456 let t = secs as libc::time_t;
457 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
458 let ok = unsafe { !libc::localtime_r(&t, &mut tm).is_null() };
460 if !ok {
461 return 0.0;
462 }
463 tm.tm_gmtoff as f64 * 1000.0
464}
465
466#[cfg(not(unix))]
467fn zone_offset_ms(_ms: f64) -> f64 {
468 0.0
469}
470
471fn local_ms(ms: f64) -> f64 {
474 ms + zone_offset_ms(ms)
475}
476
477fn utc_from_local(local: f64) -> f64 {
487 if !local.is_finite() {
488 return local;
489 }
490 let off_naive = zone_offset_ms(local);
491 let cand_a = local - off_naive;
492 let off_corrected = zone_offset_ms(cand_a);
493 if off_corrected == off_naive {
494 return cand_a;
495 }
496 let cand_b = local - off_corrected;
497 if local_ms(cand_b) == local {
498 return cand_b;
499 }
500 if local_ms(cand_a) == local {
501 return cand_a;
502 }
503 local - off_naive.min(off_corrected)
504}
505
506fn utc_string(ms: f64) -> String {
507 if ms.is_nan() {
508 return "Invalid Date".into();
509 }
510 let (day, _) = split_day(ms);
511 let (y, mo, d) = civil_from_days(day);
512 let wd = (((day % 7) + 4 + 7) % 7) as usize;
513 format!(
514 "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
515 DAYS[wd],
516 d,
517 MONTHS[mo as usize],
518 y,
519 field(ms, Field::Hours) as i64,
520 field(ms, Field::Minutes) as i64,
521 field(ms, Field::Seconds) as i64,
522 )
523}
524
525fn time_string(ms: f64) -> String {
535 if ms.is_nan() {
536 return "Invalid Date".into();
537 }
538 let local = local_ms(ms);
539 let off_min = (zone_offset_ms(ms) / 60_000.0) as i64;
540 let sign = if off_min < 0 { '-' } else { '+' };
541 let abs = off_min.abs();
542 format!(
543 "{:02}:{:02}:{:02} GMT{}{:02}{:02} ({})",
544 field(local, Field::Hours) as i64,
545 field(local, Field::Minutes) as i64,
546 field(local, Field::Seconds) as i64,
547 sign,
548 abs / 60,
549 abs % 60,
550 zone_name(ms),
551 )
552}
553
554#[cfg(unix)]
557fn zone_name(ms: f64) -> String {
558 if zone_offset_ms(ms) == 0.0 {
559 return "Coordinated Universal Time".into();
560 }
561 let secs = (ms / 1000.0).floor() as i64;
562 let t = secs as libc::time_t;
563 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
564 if unsafe { libc::localtime_r(&t, &mut tm).is_null() } || tm.tm_zone.is_null() {
566 return "Coordinated Universal Time".into();
567 }
568 let z = unsafe { std::ffi::CStr::from_ptr(tm.tm_zone) };
570 z.to_string_lossy().into_owned()
571}
572
573#[cfg(not(unix))]
574fn zone_name(_ms: f64) -> String {
575 "Coordinated Universal Time".into()
576}
577
578fn time_clip(ms: f64) -> f64 {
585 if !ms.is_finite() || ms.abs() > 8.64e15 {
586 return f64::NAN;
587 }
588 ms.trunc()
589}
590
591fn store_ms(recv: &Value, ms: f64) -> f64 {
594 with_host(|h| {
595 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
596 p.insert("@@ms".into(), Value::Float(ms));
597 }
598 });
599 ms
600}
601
602fn set_fields_local(recv: &Value, ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
617 if ms.is_nan() && start != 0 {
618 return store_ms(recv, f64::NAN);
619 }
620 let local = set_fields_value(local_ms(ms), start, args, legacy_year);
621 store_ms(recv, time_clip(utc_from_local(local)))
622}
623
624fn set_fields(recv: &Value, ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
625 if ms.is_nan() && start != 0 {
626 return store_ms(recv, f64::NAN);
627 }
628 store_ms(
629 recv,
630 time_clip(set_fields_value(ms, start, args, legacy_year)),
631 )
632}
633
634fn set_fields_value(ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
639 let base = if ms.is_nan() {
640 0.0 } else {
642 ms
643 };
644 let mut f = [
645 field(base, Field::Year),
646 field(base, Field::Month),
647 field(base, Field::Day),
648 field(base, Field::Hours),
649 field(base, Field::Minutes),
650 field(base, Field::Seconds),
651 field(base, Field::Millis),
652 ];
653 let end = if start < 3 { 3 } else { 7 };
655 for (i, slot) in f.iter_mut().enumerate().take(end).skip(start) {
656 match args.get(i - start) {
657 Some(v) => *slot = with_host(|h| h.to_number(v)).trunc(),
658 None => break,
659 }
660 }
661 if legacy_year && (0.0..=99.0).contains(&f[0]) {
662 f[0] += 1900.0;
663 }
664 utc_from_fields(f[0], f[1], f[2], f[3], f[4], f[5], f[6])
665}
666
667fn date_string(ms: f64) -> String {
669 if ms.is_nan() {
670 return "Invalid Date".into();
671 }
672 let (day, _) = split_day(ms);
673 let (y, mo, d) = civil_from_days(day);
674 let wd = (((day % 7) + 4 + 7) % 7) as usize;
675 format!("{} {} {:02} {:04}", DAYS[wd], MONTHS[mo as usize], d, y)
676}
677
678fn locale_date(ms: f64) -> String {
680 let (day, _) = split_day(ms);
681 let (y, mo, d) = civil_from_days(day);
682 format!("{}/{}/{:04}", mo + 1, d, y)
683}
684
685fn locale_time(ms: f64) -> String {
688 let h24 = field(ms, Field::Hours) as i64;
689 let (h12, meridiem) = match h24 {
690 0 => (12, "AM"),
691 1..=11 => (h24, "AM"),
692 12 => (12, "PM"),
693 _ => (h24 - 12, "PM"),
694 };
695 format!(
696 "{}:{:02}:{:02} {}",
697 h12,
698 field(ms, Field::Minutes) as i64,
699 field(ms, Field::Seconds) as i64,
700 meridiem
701 )
702}
703
704fn iso_year(y: i64) -> String {
711 if (0..=9999).contains(&y) {
712 return format!("{y:04}");
713 }
714 let sign = if y < 0 { '-' } else { '+' };
715 format!("{sign}{:06}", y.abs())
716}
717
718fn iso_string(ms: f64) -> String {
720 let (day, _) = split_day(ms);
721 let (y, mo, d) = civil_from_days(day);
722 format!(
723 "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
724 iso_year(y),
725 mo + 1,
726 d,
727 field(ms, Field::Hours) as i64,
728 field(ms, Field::Minutes) as i64,
729 field(ms, Field::Seconds) as i64,
730 field(ms, Field::Millis) as i64,
731 )
732}
733
734fn parse_str(s: &str) -> f64 {
741 time_clip(parse_iso(s).or_else(|| parse_legacy(s)).unwrap_or(f64::NAN))
742}
743
744fn parse_iso(s: &str) -> Option<f64> {
756 let b = s.as_bytes();
757 let mut i = 0;
758 let digits = |i: &mut usize, n: usize| -> Option<i64> {
760 let end = *i + n;
761 let part = b.get(*i..end)?;
762 if !part.iter().all(u8::is_ascii_digit) {
763 return None;
764 }
765 *i = end;
766 std::str::from_utf8(part).ok()?.parse().ok()
767 };
768 let year = match b.first()? {
769 sign @ (b'+' | b'-') => {
770 i = 1;
771 let y = digits(&mut i, 6)?;
772 if *sign == b'-' && y == 0 {
774 return None;
775 }
776 if *sign == b'-' {
777 -y
778 } else {
779 y
780 }
781 }
782 _ => digits(&mut i, 4)?,
783 };
784 let (mut month, mut day) = (1, 1);
785 if b.get(i) == Some(&b'-') {
786 i += 1;
787 month = digits(&mut i, 2)?;
788 if b.get(i) == Some(&b'-') {
789 i += 1;
790 day = digits(&mut i, 2)?;
791 }
792 }
793 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
794 return None;
795 }
796 let date = days_from_civil(year, month - 1, day) as f64 * MS_PER_DAY;
797 match b.get(i) {
799 None => return Some(date),
800 Some(b'Z' | b'z') if i + 1 == b.len() => return Some(date),
801 Some(b'T' | b't' | b' ') => i += 1,
802 Some(_) => return None,
803 }
804 let h = digits(&mut i, 2)?;
805 if b.get(i) != Some(&b':') {
806 return None;
807 }
808 i += 1;
809 let mi = digits(&mut i, 2)?;
810 let (mut sec, mut milli) = (0, 0);
811 if b.get(i) == Some(&b':') {
812 i += 1;
813 sec = digits(&mut i, 2)?;
814 if b.get(i) == Some(&b'.') {
815 i += 1;
816 let start = i;
817 while b.get(i).is_some_and(u8::is_ascii_digit) {
818 i += 1;
819 }
820 if i == start {
821 return None;
822 }
823 let frac = &s[start..i.min(start + 3)];
825 milli = format!("{frac:0<3}").parse().ok()?;
826 }
827 }
828 if h > 24 || mi > 59 || sec > 59 || (h == 24 && (mi, sec, milli) != (0, 0, 0)) {
830 return None;
831 }
832 let wall =
833 date + h as f64 * 3_600_000.0 + mi as f64 * 60_000.0 + sec as f64 * 1000.0 + milli as f64;
834 let offset = match b.get(i) {
835 None => return Some(utc_from_local(wall)),
836 Some(b'Z' | b'z') if i + 1 == b.len() => 0.0,
837 Some(sign @ (b'+' | b'-')) => {
838 let sign = if *sign == b'-' { -1.0 } else { 1.0 };
839 i += 1;
840 let oh = digits(&mut i, 2)?;
841 if b.get(i) == Some(&b':') {
842 i += 1;
843 }
844 let om = digits(&mut i, 2)?;
845 if i != b.len() || oh > 23 || om > 59 {
846 return None;
847 }
848 sign * (oh as f64 * 3_600_000.0 + om as f64 * 60_000.0)
849 }
850 Some(_) => return None,
851 };
852 Some(wall - offset)
853}
854
855#[derive(Clone, Copy, PartialEq)]
860enum Tok<'a> {
861 Num(i64, usize),
862 Word(&'a str),
863 Sym(u8),
864 Space,
865 End,
866}
867
868struct Toks<'a> {
869 s: &'a str,
870 i: usize,
871}
872
873impl<'a> Toks<'a> {
874 fn next(&mut self) -> Tok<'a> {
875 let b = self.s.as_bytes();
876 let Some(&c) = b.get(self.i) else {
877 return Tok::End;
878 };
879 let start = self.i;
880 let run = |i: &mut usize, f: fn(&u8) -> bool| {
881 while b.get(*i).is_some_and(f) {
882 *i += 1;
883 }
884 };
885 if c.is_ascii_digit() {
886 run(&mut self.i, u8::is_ascii_digit);
887 let text = &self.s[start..self.i];
888 return Tok::Num(text.parse().unwrap_or(i64::MAX), text.len());
891 }
892 if c.is_ascii_alphabetic() {
893 run(&mut self.i, u8::is_ascii_alphabetic);
894 return Tok::Word(&self.s[start..self.i]);
895 }
896 if c.is_ascii_whitespace() {
897 run(&mut self.i, u8::is_ascii_whitespace);
898 return Tok::Space;
899 }
900 if c == b'(' {
901 let mut depth = 0;
902 while let Some(&c) = b.get(self.i) {
903 self.i += 1;
904 match c {
905 b'(' => depth += 1,
906 b')' => {
907 depth -= 1;
908 if depth == 0 {
909 break;
910 }
911 }
912 _ => {}
913 }
914 }
915 return Tok::Space;
916 }
917 let len = self.s[start..].chars().next().map_or(1, char::len_utf8);
919 self.i += len;
920 Tok::Sym(if len == 1 { c } else { 0 })
921 }
922
923 fn peek(&self) -> Tok<'a> {
924 Toks {
925 s: self.s,
926 i: self.i,
927 }
928 .next()
929 }
930
931 fn skip(&mut self, sym: u8) -> bool {
932 let taken = self.peek() == Tok::Sym(sym);
933 if taken {
934 self.next();
935 }
936 taken
937 }
938}
939
940enum Keyword {
944 Month(i64),
945 AmPm(i64),
946 Zone(i64),
948 Other,
949}
950
951fn keyword(word: &str) -> Keyword {
952 let w = word.to_ascii_lowercase();
953 let prefix = &w[..w.len().min(3)];
954 if w.len() >= 3 {
955 const MONTHS: [&str; 12] = [
956 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
957 ];
958 if let Some(m) = MONTHS.iter().position(|m| *m == prefix) {
959 return Keyword::Month(m as i64 + 1);
960 }
961 }
962 match prefix {
963 "am" if w.len() == 2 => Keyword::AmPm(0),
964 "pm" if w.len() == 2 => Keyword::AmPm(12),
965 "ut" if w.len() == 2 => Keyword::Zone(0),
966 "z" => Keyword::Zone(0),
967 "utc" | "gmt" if w.len() >= 3 => Keyword::Zone(0),
968 "cdt" => Keyword::Zone(-5),
969 "cst" => Keyword::Zone(-6),
970 "edt" => Keyword::Zone(-4),
971 "est" => Keyword::Zone(-5),
972 "mdt" => Keyword::Zone(-6),
973 "mst" => Keyword::Zone(-7),
974 "pdt" => Keyword::Zone(-7),
975 "pst" => Keyword::Zone(-8),
976 _ => Keyword::Other,
977 }
978}
979
980fn parse_legacy(s: &str) -> Option<f64> {
990 const NONE: i64 = i64::MIN;
991 let mut t = Toks { s, i: 0 };
992 let (mut day, mut named_month) = (Vec::with_capacity(3), NONE);
993 let (mut time, mut hour_offset) = (Vec::with_capacity(4), NONE);
994 let (mut sign, mut tz_hour, mut tz_min) = (0i64, NONE, NONE);
995 let mut read_number = false;
996 let expecting = |time: &Vec<i64>, n: i64| match time.len() {
998 1 | 2 => (0..60).contains(&n),
999 3 => (0..1000).contains(&n),
1000 _ => false,
1001 };
1002 loop {
1003 match t.next() {
1004 Tok::End => break,
1005 Tok::Num(n, _) => {
1006 read_number = true;
1007 if t.skip(b':') {
1008 if t.skip(b':') {
1009 if !time.is_empty() {
1010 return None;
1011 }
1012 time.extend([n, 0]);
1013 } else {
1014 if time.len() >= 4 {
1015 return None;
1016 }
1017 time.push(n);
1018 t.skip(b'.');
1019 }
1020 } else if t.peek() == Tok::Sym(b'.') && expecting(&time, n) {
1021 t.next();
1022 time.push(n);
1023 let Tok::Num(ms, len) = t.next() else {
1024 return None;
1025 };
1026 let ms = match len {
1028 1 => ms * 100,
1029 2 => ms * 10,
1030 3 => ms,
1031 _ => t.s[t.i - len..t.i - len + 3].parse().ok()?,
1032 };
1033 time.push(ms);
1034 time.resize(4, 0);
1035 } else if tz_hour != NONE && tz_min == NONE && (0..60).contains(&n) {
1036 tz_min = n;
1037 } else if expecting(&time, n) {
1038 time.push(n);
1039 time.resize(4, 0);
1040 match t.peek() {
1042 Tok::End | Tok::Space | Tok::Sym(b'+' | b'-') => {}
1043 Tok::Word(w) if w.eq_ignore_ascii_case("z") => {}
1044 _ => return None,
1045 }
1046 } else {
1047 if day.len() >= 3 {
1048 return None;
1049 }
1050 day.push(n);
1051 t.skip(b'-');
1052 }
1053 }
1054 Tok::Word(w) => match keyword(w) {
1055 Keyword::AmPm(off) if !time.is_empty() => hour_offset = off,
1056 Keyword::Month(m) => {
1057 named_month = m;
1058 t.skip(b'-');
1059 }
1060 Keyword::Zone(h) if read_number => {
1061 sign = if h < 0 { -1 } else { 1 };
1062 tz_hour = h.abs();
1063 tz_min = 0;
1064 }
1065 _ => {
1066 if read_number || matches!(t.peek(), Tok::Num(..)) {
1069 return None;
1070 }
1071 }
1072 },
1073 Tok::Sym(c @ (b'+' | b'-'))
1074 if (sign != 0 && tz_hour == 0 && tz_min == 0) || !time.is_empty() =>
1075 {
1076 sign = if c == b'-' { -1 } else { 1 };
1079 let (n, len) = match t.peek() {
1080 Tok::Num(n, len) => {
1081 t.next();
1082 (n, len)
1083 }
1084 _ => (0, 0),
1085 };
1086 read_number = true;
1087 if t.peek() == Tok::Sym(b':') {
1088 tz_hour = n;
1089 tz_min = NONE;
1090 } else if len <= 2 {
1091 tz_hour = n;
1092 tz_min = 0;
1093 } else if len <= 4 {
1094 tz_hour = n / 100;
1095 tz_min = n % 100;
1096 } else {
1097 return None;
1098 }
1099 }
1100 Tok::Sym(b'+' | b'-' | b')') if read_number => return None,
1101 Tok::Sym(_) | Tok::Space => {}
1102 }
1103 }
1104
1105 if day.is_empty() {
1108 return None;
1109 }
1110 day.resize(3, 1);
1111 let is_day = |n: i64| (1..=31).contains(&n);
1112 let (mut year, month, dd) = if named_month == NONE {
1113 if is_day(day[0]) {
1114 (day[2], day[0], day[1])
1115 } else {
1116 (day[0], day[1], day[2])
1117 }
1118 } else if !is_day(day[0]) {
1119 (day[0], named_month, day[1])
1120 } else {
1121 (day[1], named_month, day[0])
1122 };
1123 if (0..=49).contains(&year) {
1124 year += 2000;
1125 } else if (50..=99).contains(&year) {
1126 year += 1900;
1127 }
1128 if !(1..=12).contains(&month) || !is_day(dd) {
1129 return None;
1130 }
1131
1132 time.resize(4, 0);
1135 let (mut h, mi, sec, ms) = (time[0], time[1], time[2], time[3]);
1136 if hour_offset != NONE {
1137 if !(0..=12).contains(&h) {
1138 return None;
1139 }
1140 h = h % 12 + hour_offset;
1141 }
1142 let in_range = (0..24).contains(&h)
1143 && (0..60).contains(&mi)
1144 && (0..60).contains(&sec)
1145 && (0..1000).contains(&ms);
1146 if !in_range && (h, mi, sec, ms) != (24, 0, 0, 0) {
1147 return None;
1148 }
1149
1150 let wall = utc_from_fields(
1151 year as f64,
1152 (month - 1) as f64,
1153 dd as f64,
1154 h as f64,
1155 mi as f64,
1156 sec as f64,
1157 ms as f64,
1158 );
1159 if sign == 0 {
1160 return Some(utc_from_local(wall));
1161 }
1162 let tz_min = if tz_min == NONE { 0 } else { tz_min };
1163 let tz_hour = if tz_hour == NONE { 0 } else { tz_hour };
1164 Some(wall - sign as f64 * (tz_hour * 3_600_000 + tz_min * 60_000) as f64)
1165}
1166
1167pub(crate) fn inspect_with_host(h: &crate::host::JsHost, v: &Value) -> String {
1179 let ms = match h.get(v) {
1180 Some(JsObj::Object(p)) => p.get("@@ms").map(|x| h.to_number(x)).unwrap_or(f64::NAN),
1181 _ => f64::NAN,
1182 };
1183 if ms.is_nan() {
1184 "Invalid Date".into()
1185 } else {
1186 iso_string(ms)
1187 }
1188}