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 _ => {
139 let n = |i: usize, dflt: f64| {
140 args.get(i)
141 .map(|v| with_host(|h| h.to_number(v)))
142 .unwrap_or(dflt)
143 };
144 let mut year = n(0, f64::NAN);
145 if (0.0..=99.0).contains(&year) {
147 year += 1900.0;
148 }
149 utc_from_fields(
150 year,
151 n(1, 0.0),
152 n(2, 1.0),
153 n(3, 0.0),
154 n(4, 0.0),
155 n(5, 0.0),
156 n(6, 0.0),
157 )
158 }
159 };
160 Ok(from_ms(time_clip(ms)))
164}
165
166pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
168 Some(match method {
169 "now" => Ok(Value::Float(now_ms())),
170 "parse" => Ok(Value::Float(parse_str(&super::arg_str(args, 0)))),
171 "UTC" => {
172 let n = |i: usize, dflt: f64| {
173 args.get(i)
174 .map(|v| with_host(|h| h.to_number(v)))
175 .unwrap_or(dflt)
176 };
177 let mut year = n(0, f64::NAN);
178 if (0.0..=99.0).contains(&year) {
179 year += 1900.0;
180 }
181 Ok(Value::Float(utc_from_fields(
182 year,
183 n(1, 0.0),
184 n(2, 1.0),
185 n(3, 0.0),
186 n(4, 0.0),
187 n(5, 0.0),
188 n(6, 0.0),
189 )))
190 }
191 _ => return None,
192 })
193}
194
195pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
197 let coerced: Vec<Value>;
202 let _args: &[Value] = if method.starts_with("set") {
203 let mut out = Vec::with_capacity(_args.len());
204 for a in _args {
205 let p = crate::host::to_primitive(a, "number")?;
206 out.push(Value::Float(with_host(|h| h.to_number(&p))));
207 }
208 coerced = out;
209 &coerced
210 } else {
211 _args
212 };
213 let ms = ms_of(recv);
214 let f = |ms: f64| ms; Ok(match method {
216 "getTime" | "valueOf" => Value::Float(f(ms)),
217 "@@toPrimitive" => {
221 let hint = with_host(|h| h.str_of(&_args.first().cloned().unwrap_or(Value::Undef)));
222 if hint == "number" {
223 Value::Float(f(ms))
224 } else {
225 return instance_call(recv, "toString", _args);
226 }
227 }
228 "toISOString" | "toJSON" => {
229 if ms.is_nan() {
230 if method == "toJSON" {
231 with_host(|h| h.null())
232 } else {
233 return Err(crate::host::range_error("Invalid time value"));
234 }
235 } else {
236 with_host(|h| h.new_str(iso_string(ms)))
237 }
238 }
239 "toUTCString" | "toGMTString" => with_host(|h| h.new_str(utc_string(ms))),
240 "toString" => with_host(|h| {
247 h.new_str(if ms.is_nan() {
248 "Invalid Date".into()
249 } else {
250 format!("{} {}", date_string(local_ms(ms)), time_string(ms))
251 })
252 }),
253 "toDateString" => with_host(|h| h.new_str(date_string(local_ms(ms)))),
254 "toLocaleString" => with_host(|h| {
262 h.new_str(if ms.is_nan() {
263 "Invalid Date".into()
264 } else {
265 format!(
266 "{}, {}",
267 locale_date(local_ms(ms)),
268 locale_time(local_ms(ms))
269 )
270 })
271 }),
272 "toLocaleDateString" => with_host(|h| {
273 h.new_str(if ms.is_nan() {
274 "Invalid Date".into()
275 } else {
276 locale_date(local_ms(ms))
277 })
278 }),
279 "toLocaleTimeString" => with_host(|h| {
280 h.new_str(if ms.is_nan() {
281 "Invalid Date".into()
282 } else {
283 locale_time(local_ms(ms))
284 })
285 }),
286 "getFullYear" => Value::Float(field(local_ms(ms), Field::Year)),
287 "getUTCFullYear" => Value::Float(field(ms, Field::Year)),
288 "getMonth" => Value::Float(field(local_ms(ms), Field::Month)),
289 "getUTCMonth" => Value::Float(field(ms, Field::Month)),
290 "getDate" => Value::Float(field(local_ms(ms), Field::Day)),
291 "getUTCDate" => Value::Float(field(ms, Field::Day)),
292 "getDay" => Value::Float(field(local_ms(ms), Field::Weekday)),
293 "getUTCDay" => Value::Float(field(ms, Field::Weekday)),
294 "getHours" => Value::Float(field(local_ms(ms), Field::Hours)),
295 "getUTCHours" => Value::Float(field(ms, Field::Hours)),
296 "getMinutes" => Value::Float(field(local_ms(ms), Field::Minutes)),
297 "getUTCMinutes" => Value::Float(field(ms, Field::Minutes)),
298 "getSeconds" => Value::Float(field(local_ms(ms), Field::Seconds)),
299 "getUTCSeconds" => Value::Float(field(ms, Field::Seconds)),
300 "getMilliseconds" => Value::Float(field(local_ms(ms), Field::Millis)),
301 "getUTCMilliseconds" => Value::Float(field(ms, Field::Millis)),
302 "getTimezoneOffset" => Value::Float(if ms.is_nan() {
305 f64::NAN
306 } else {
307 -zone_offset_ms(ms) / 60_000.0
308 }),
309 "toTimeString" => with_host(|h| h.new_str(time_string(ms))),
310 "setTime" => Value::Float(store_ms(recv, time_clip(super::arg_num(_args, 0)))),
311 "setFullYear" => Value::Float(set_fields_local(recv, ms, 0, _args, false)),
318 "setUTCFullYear" => Value::Float(set_fields(recv, ms, 0, _args, false)),
319 "setMonth" => Value::Float(set_fields_local(recv, ms, 1, _args, false)),
320 "setUTCMonth" => Value::Float(set_fields(recv, ms, 1, _args, false)),
321 "setDate" => Value::Float(set_fields_local(recv, ms, 2, _args, false)),
322 "setUTCDate" => Value::Float(set_fields(recv, ms, 2, _args, false)),
323 "setHours" => Value::Float(set_fields_local(recv, ms, 3, _args, false)),
324 "setUTCHours" => Value::Float(set_fields(recv, ms, 3, _args, false)),
325 "setMinutes" => Value::Float(set_fields_local(recv, ms, 4, _args, false)),
326 "setUTCMinutes" => Value::Float(set_fields(recv, ms, 4, _args, false)),
327 "setSeconds" => Value::Float(set_fields_local(recv, ms, 5, _args, false)),
328 "setUTCSeconds" => Value::Float(set_fields(recv, ms, 5, _args, false)),
329 "setMilliseconds" => Value::Float(set_fields_local(recv, ms, 6, _args, false)),
330 "setUTCMilliseconds" => Value::Float(set_fields(recv, ms, 6, _args, false)),
331 "getYear" => Value::Float(if ms.is_nan() {
336 f64::NAN
337 } else {
338 field(local_ms(ms), Field::Year) - 1900.0
339 }),
340 "setYear" => Value::Float(set_fields_local(recv, ms, 0, _args, true)),
341 _ => {
342 return Err(crate::host::type_error(&format!(
343 "date.{method} is not a function"
344 )))
345 }
346 })
347}
348
349enum Field {
352 Year,
353 Month,
354 Day,
355 Weekday,
356 Hours,
357 Minutes,
358 Seconds,
359 Millis,
360}
361
362fn split_day(ms: f64) -> (i64, i64) {
365 let day = (ms / MS_PER_DAY).floor();
366 let rem = ms - day * MS_PER_DAY;
367 (day as i64, rem as i64)
368}
369
370fn civil_from_days(z: i64) -> (i64, i64, i64) {
373 let z = z + 719_468;
374 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
375 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
378 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)
383}
384
385fn days_from_civil(y: i64, m0: i64, d: i64) -> i64 {
387 let m = m0 + 1;
388 let y = if m <= 2 { y - 1 } else { y };
389 let era = if y >= 0 { y } else { y - 399 } / 400;
390 let yoe = y - era * 400;
391 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
392 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
393 era * 146_097 + doe - 719_468
394}
395
396fn field(ms: f64, which: Field) -> f64 {
397 if ms.is_nan() {
398 return f64::NAN;
399 }
400 let (day, rem) = split_day(ms);
401 let (y, mo, d) = civil_from_days(day);
402 match which {
403 Field::Year => y as f64,
404 Field::Month => mo as f64,
405 Field::Day => d as f64,
406 Field::Weekday => (((day % 7) + 4 + 7) % 7) as f64,
408 Field::Hours => (rem / 3_600_000) as f64,
409 Field::Minutes => (rem / 60_000 % 60) as f64,
410 Field::Seconds => (rem / 1000 % 60) as f64,
411 Field::Millis => (rem % 1000) as f64,
412 }
413}
414
415fn utc_from_fields(y: f64, mo: f64, d: f64, h: f64, mi: f64, s: f64, ms: f64) -> f64 {
418 if [y, mo, d, h, mi, s, ms].iter().any(|v| v.is_nan()) {
419 return f64::NAN;
420 }
421 let total_months = y as i64 * 12 + mo as i64;
423 let year = total_months.div_euclid(12);
424 let month = total_months.rem_euclid(12);
425 let days = days_from_civil(year, month, d as i64);
426 days as f64 * MS_PER_DAY + h * 3_600_000.0 + mi * 60_000.0 + s * 1000.0 + ms
427}
428
429#[cfg(unix)]
440fn zone_offset_ms(ms: f64) -> f64 {
441 if !ms.is_finite() {
442 return 0.0;
443 }
444 let secs = (ms / 1000.0).floor() as i64;
447 let t = secs as libc::time_t;
448 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
449 let ok = unsafe { !libc::localtime_r(&t, &mut tm).is_null() };
451 if !ok {
452 return 0.0;
453 }
454 tm.tm_gmtoff as f64 * 1000.0
455}
456
457#[cfg(not(unix))]
458fn zone_offset_ms(_ms: f64) -> f64 {
459 0.0
460}
461
462fn local_ms(ms: f64) -> f64 {
465 ms + zone_offset_ms(ms)
466}
467
468fn utc_from_local(local: f64) -> f64 {
478 if !local.is_finite() {
479 return local;
480 }
481 let off_naive = zone_offset_ms(local);
482 let cand_a = local - off_naive;
483 let off_corrected = zone_offset_ms(cand_a);
484 if off_corrected == off_naive {
485 return cand_a;
486 }
487 let cand_b = local - off_corrected;
488 if local_ms(cand_b) == local {
489 return cand_b;
490 }
491 if local_ms(cand_a) == local {
492 return cand_a;
493 }
494 local - off_naive.min(off_corrected)
495}
496
497fn utc_string(ms: f64) -> String {
498 if ms.is_nan() {
499 return "Invalid Date".into();
500 }
501 let (day, _) = split_day(ms);
502 let (y, mo, d) = civil_from_days(day);
503 let wd = (((day % 7) + 4 + 7) % 7) as usize;
504 format!(
505 "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
506 DAYS[wd],
507 d,
508 MONTHS[mo as usize],
509 y,
510 field(ms, Field::Hours) as i64,
511 field(ms, Field::Minutes) as i64,
512 field(ms, Field::Seconds) as i64,
513 )
514}
515
516fn time_string(ms: f64) -> String {
526 if ms.is_nan() {
527 return "Invalid Date".into();
528 }
529 let local = local_ms(ms);
530 let off_min = (zone_offset_ms(ms) / 60_000.0) as i64;
531 let sign = if off_min < 0 { '-' } else { '+' };
532 let abs = off_min.abs();
533 format!(
534 "{:02}:{:02}:{:02} GMT{}{:02}{:02} ({})",
535 field(local, Field::Hours) as i64,
536 field(local, Field::Minutes) as i64,
537 field(local, Field::Seconds) as i64,
538 sign,
539 abs / 60,
540 abs % 60,
541 zone_name(ms),
542 )
543}
544
545#[cfg(unix)]
548fn zone_name(ms: f64) -> String {
549 if zone_offset_ms(ms) == 0.0 {
550 return "Coordinated Universal Time".into();
551 }
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 "Coordinated Universal Time".into();
558 }
559 let z = unsafe { std::ffi::CStr::from_ptr(tm.tm_zone) };
561 z.to_string_lossy().into_owned()
562}
563
564#[cfg(not(unix))]
565fn zone_name(_ms: f64) -> String {
566 "Coordinated Universal Time".into()
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 locale_date(ms: f64) -> String {
671 let (day, _) = split_day(ms);
672 let (y, mo, d) = civil_from_days(day);
673 format!("{}/{}/{:04}", mo + 1, d, y)
674}
675
676fn locale_time(ms: f64) -> String {
679 let h24 = field(ms, Field::Hours) as i64;
680 let (h12, meridiem) = match h24 {
681 0 => (12, "AM"),
682 1..=11 => (h24, "AM"),
683 12 => (12, "PM"),
684 _ => (h24 - 12, "PM"),
685 };
686 format!(
687 "{}:{:02}:{:02} {}",
688 h12,
689 field(ms, Field::Minutes) as i64,
690 field(ms, Field::Seconds) as i64,
691 meridiem
692 )
693}
694
695fn iso_year(y: i64) -> String {
702 if (0..=9999).contains(&y) {
703 return format!("{y:04}");
704 }
705 let sign = if y < 0 { '-' } else { '+' };
706 format!("{sign}{:06}", y.abs())
707}
708
709fn iso_string(ms: f64) -> String {
711 let (day, _) = split_day(ms);
712 let (y, mo, d) = civil_from_days(day);
713 format!(
714 "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
715 iso_year(y),
716 mo + 1,
717 d,
718 field(ms, Field::Hours) as i64,
719 field(ms, Field::Minutes) as i64,
720 field(ms, Field::Seconds) as i64,
721 field(ms, Field::Millis) as i64,
722 )
723}
724
725fn parse_str(s: &str) -> f64 {
730 let s = s.trim();
731 if let Some(ms) = parse_iso(s) {
732 return ms;
733 }
734 if let Some(ms) = parse_rfc1123(s) {
735 return ms;
736 }
737 f64::NAN
738}
739
740fn parse_iso(s: &str) -> Option<f64> {
743 let (date, time) = match s.split_once(['T', ' ']) {
744 Some((d, t)) => (d, Some(t)),
745 None => (s, None),
746 };
747 let dp: Vec<&str> = date.split('-').collect();
748 if dp.len() != 3 {
749 return None;
750 }
751 let y: i64 = dp[0].parse().ok()?;
752 let mo: i64 = dp[1].parse().ok()?;
753 let d: i64 = dp[2].parse().ok()?;
754 if !(1..=12).contains(&mo) || !(1..=31).contains(&d) {
755 return None;
756 }
757 let (mut h, mut mi, mut sec, mut milli) = (0i64, 0i64, 0i64, 0i64);
758 if let Some(t) = time {
759 let t = t.trim_end_matches('Z');
760 let (hms, frac) = match t.split_once('.') {
761 Some((a, b)) => (a, Some(b)),
762 None => (t, None),
763 };
764 let tp: Vec<&str> = hms.split(':').collect();
765 if tp.is_empty() {
766 return None;
767 }
768 h = tp[0].parse().ok()?;
769 mi = tp.get(1).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
770 sec = tp.get(2).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
771 if let Some(fr) = frac {
772 let fr: String = fr.chars().take(3).collect();
773 let padded = format!("{fr:0<3}");
774 milli = padded.parse().ok()?;
775 }
776 }
777 let days = days_from_civil(y, mo - 1, d);
778 Some(
779 days as f64 * MS_PER_DAY
780 + h as f64 * 3_600_000.0
781 + mi as f64 * 60_000.0
782 + sec as f64 * 1000.0
783 + milli as f64,
784 )
785}
786
787fn parse_rfc1123(s: &str) -> Option<f64> {
789 let s = match s.split_once(", ") {
791 Some((_, rest)) => rest,
792 None => s,
793 };
794 let parts: Vec<&str> = s.split_whitespace().collect();
795 if parts.len() < 5 {
796 return None;
797 }
798 let d: i64 = parts[0].parse().ok()?;
799 let mo = MONTHS.iter().position(|m| *m == parts[1])? as i64;
800 let y: i64 = parts[2].parse().ok()?;
801 let tp: Vec<&str> = parts[3].split(':').collect();
802 if tp.len() < 2 {
803 return None;
804 }
805 let h: i64 = tp[0].parse().ok()?;
806 let mi: i64 = tp[1].parse().ok()?;
807 let sec: i64 = tp.get(2).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
808 let days = days_from_civil(y, mo, d);
809 Some(
810 days as f64 * MS_PER_DAY
811 + h as f64 * 3_600_000.0
812 + mi as f64 * 60_000.0
813 + sec as f64 * 1000.0,
814 )
815}
816
817pub(crate) fn inspect_with_host(h: &crate::host::JsHost, v: &Value) -> String {
829 let ms = match h.get(v) {
830 Some(JsObj::Object(p)) => p.get("@@ms").map(|x| h.to_number(x)).unwrap_or(f64::NAN),
831 _ => f64::NAN,
832 };
833 if ms.is_nan() {
834 "Invalid Date".into()
835 } else {
836 iso_string(ms)
837 }
838}