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];
73
74const MS_PER_DAY: f64 = 86_400_000.0;
75const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
76const MONTHS: [&str; 12] = [
77 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
78];
79
80fn now_ms() -> f64 {
82 SystemTime::now()
83 .duration_since(UNIX_EPOCH)
84 .map(|d| d.as_millis() as f64)
85 .unwrap_or(0.0)
86}
87
88fn from_ms(ms: f64) -> Value {
90 with_host(|h| {
91 let mut m = IndexMap::new();
92 m.insert("@@native".into(), h.new_str("Date"));
93 m.insert("@@ms".into(), Value::Float(ms));
94 h.new_object(m)
95 })
96}
97
98fn ms_of(recv: &Value) -> f64 {
100 with_host(|h| match h.get(recv) {
101 Some(JsObj::Object(p)) => p.get("@@ms").map(|v| h.to_number(v)).unwrap_or(f64::NAN),
102 _ => f64::NAN,
103 })
104}
105
106pub fn construct(args: &[Value]) -> Result<Value, String> {
108 let ms = match args.len() {
109 0 => now_ms(),
110 1 => {
111 let a = &args[0];
112 if let Value::Str(_) = a {
115 parse_str(&with_host(|h| h.str_of(a)))
116 } else if with_host(|h| matches!(h.get(a), Some(JsObj::Str(_)))) {
117 parse_str(&with_host(|h| h.str_of(a)))
118 } else if super::native_tag(a).as_deref() == Some("Date") {
119 ms_of(a)
120 } else {
121 with_host(|h| h.to_number(a))
122 }
123 }
124 _ => {
126 let n = |i: usize, dflt: f64| {
127 args.get(i)
128 .map(|v| with_host(|h| h.to_number(v)))
129 .unwrap_or(dflt)
130 };
131 let mut year = n(0, f64::NAN);
132 if (0.0..=99.0).contains(&year) {
134 year += 1900.0;
135 }
136 utc_from_fields(
137 year,
138 n(1, 0.0),
139 n(2, 1.0),
140 n(3, 0.0),
141 n(4, 0.0),
142 n(5, 0.0),
143 n(6, 0.0),
144 )
145 }
146 };
147 Ok(from_ms(time_clip(ms)))
151}
152
153pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
155 Some(match method {
156 "now" => Ok(Value::Float(now_ms())),
157 "parse" => Ok(Value::Float(parse_str(&super::arg_str(args, 0)))),
158 "UTC" => {
159 let n = |i: usize, dflt: f64| {
160 args.get(i)
161 .map(|v| with_host(|h| h.to_number(v)))
162 .unwrap_or(dflt)
163 };
164 let mut year = n(0, f64::NAN);
165 if (0.0..=99.0).contains(&year) {
166 year += 1900.0;
167 }
168 Ok(Value::Float(utc_from_fields(
169 year,
170 n(1, 0.0),
171 n(2, 1.0),
172 n(3, 0.0),
173 n(4, 0.0),
174 n(5, 0.0),
175 n(6, 0.0),
176 )))
177 }
178 _ => return None,
179 })
180}
181
182pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
184 let ms = ms_of(recv);
185 let f = |ms: f64| ms; Ok(match method {
187 "getTime" | "valueOf" => Value::Float(f(ms)),
188 "toISOString" | "toJSON" => {
189 if ms.is_nan() {
190 if method == "toJSON" {
191 with_host(|h| h.null())
192 } else {
193 return Err(crate::host::range_error("Invalid time value"));
194 }
195 } else {
196 with_host(|h| h.new_str(iso_string(ms)))
197 }
198 }
199 "toUTCString" | "toGMTString" => with_host(|h| h.new_str(utc_string(ms))),
200 "toString" => with_host(|h| {
207 h.new_str(if ms.is_nan() {
208 "Invalid Date".into()
209 } else {
210 format!("{} {}", date_string(ms), time_string(ms))
211 })
212 }),
213 "toDateString" => with_host(|h| h.new_str(date_string(ms))),
214 "toLocaleString" => with_host(|h| {
222 h.new_str(if ms.is_nan() {
223 "Invalid Date".into()
224 } else {
225 format!("{}, {}", locale_date(ms), locale_time(ms))
226 })
227 }),
228 "toLocaleDateString" => with_host(|h| {
229 h.new_str(if ms.is_nan() {
230 "Invalid Date".into()
231 } else {
232 locale_date(ms)
233 })
234 }),
235 "toLocaleTimeString" => with_host(|h| {
236 h.new_str(if ms.is_nan() {
237 "Invalid Date".into()
238 } else {
239 locale_time(ms)
240 })
241 }),
242 "getFullYear" | "getUTCFullYear" => Value::Float(field(ms, Field::Year)),
243 "getMonth" | "getUTCMonth" => Value::Float(field(ms, Field::Month)),
244 "getDate" | "getUTCDate" => Value::Float(field(ms, Field::Day)),
245 "getDay" | "getUTCDay" => Value::Float(field(ms, Field::Weekday)),
246 "getHours" | "getUTCHours" => Value::Float(field(ms, Field::Hours)),
247 "getMinutes" | "getUTCMinutes" => Value::Float(field(ms, Field::Minutes)),
248 "getSeconds" | "getUTCSeconds" => Value::Float(field(ms, Field::Seconds)),
249 "getMilliseconds" | "getUTCMilliseconds" => Value::Float(field(ms, Field::Millis)),
250 "getTimezoneOffset" => Value::Float(0.0), "toTimeString" => with_host(|h| h.new_str(time_string(ms))),
252 "setTime" => Value::Float(store_ms(recv, time_clip(super::arg_num(_args, 0)))),
253 "setFullYear" | "setUTCFullYear" => Value::Float(set_fields(recv, ms, 0, _args, false)),
260 "setMonth" | "setUTCMonth" => Value::Float(set_fields(recv, ms, 1, _args, false)),
261 "setDate" | "setUTCDate" => Value::Float(set_fields(recv, ms, 2, _args, false)),
262 "setHours" | "setUTCHours" => Value::Float(set_fields(recv, ms, 3, _args, false)),
263 "setMinutes" | "setUTCMinutes" => Value::Float(set_fields(recv, ms, 4, _args, false)),
264 "setSeconds" | "setUTCSeconds" => Value::Float(set_fields(recv, ms, 5, _args, false)),
265 "setMilliseconds" | "setUTCMilliseconds" => {
266 Value::Float(set_fields(recv, ms, 6, _args, false))
267 }
268 "getYear" => Value::Float(if ms.is_nan() {
272 f64::NAN
273 } else {
274 field(ms, Field::Year) - 1900.0
275 }),
276 "setYear" => Value::Float(set_fields(recv, ms, 0, _args, true)),
277 _ => {
278 return Err(crate::host::type_error(&format!(
279 "date.{method} is not a function"
280 )))
281 }
282 })
283}
284
285enum Field {
288 Year,
289 Month,
290 Day,
291 Weekday,
292 Hours,
293 Minutes,
294 Seconds,
295 Millis,
296}
297
298fn split_day(ms: f64) -> (i64, i64) {
301 let day = (ms / MS_PER_DAY).floor();
302 let rem = ms - day * MS_PER_DAY;
303 (day as i64, rem as i64)
304}
305
306fn civil_from_days(z: i64) -> (i64, i64, i64) {
309 let z = z + 719_468;
310 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
311 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
314 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)
319}
320
321fn days_from_civil(y: i64, m0: i64, d: i64) -> i64 {
323 let m = m0 + 1;
324 let y = if m <= 2 { y - 1 } else { y };
325 let era = if y >= 0 { y } else { y - 399 } / 400;
326 let yoe = y - era * 400;
327 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
328 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
329 era * 146_097 + doe - 719_468
330}
331
332fn field(ms: f64, which: Field) -> f64 {
333 if ms.is_nan() {
334 return f64::NAN;
335 }
336 let (day, rem) = split_day(ms);
337 let (y, mo, d) = civil_from_days(day);
338 match which {
339 Field::Year => y as f64,
340 Field::Month => mo as f64,
341 Field::Day => d as f64,
342 Field::Weekday => (((day % 7) + 4 + 7) % 7) as f64,
344 Field::Hours => (rem / 3_600_000) as f64,
345 Field::Minutes => (rem / 60_000 % 60) as f64,
346 Field::Seconds => (rem / 1000 % 60) as f64,
347 Field::Millis => (rem % 1000) as f64,
348 }
349}
350
351fn utc_from_fields(y: f64, mo: f64, d: f64, h: f64, mi: f64, s: f64, ms: f64) -> f64 {
354 if [y, mo, d, h, mi, s, ms].iter().any(|v| v.is_nan()) {
355 return f64::NAN;
356 }
357 let total_months = y as i64 * 12 + mo as i64;
359 let year = total_months.div_euclid(12);
360 let month = total_months.rem_euclid(12);
361 let days = days_from_civil(year, month, d as i64);
362 days as f64 * MS_PER_DAY + h * 3_600_000.0 + mi * 60_000.0 + s * 1000.0 + ms
363}
364
365fn utc_string(ms: f64) -> String {
367 if ms.is_nan() {
368 return "Invalid Date".into();
369 }
370 let (day, _) = split_day(ms);
371 let (y, mo, d) = civil_from_days(day);
372 let wd = (((day % 7) + 4 + 7) % 7) as usize;
373 format!(
374 "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
375 DAYS[wd],
376 d,
377 MONTHS[mo as usize],
378 y,
379 field(ms, Field::Hours) as i64,
380 field(ms, Field::Minutes) as i64,
381 field(ms, Field::Seconds) as i64,
382 )
383}
384
385fn time_string(ms: f64) -> String {
389 if ms.is_nan() {
390 return "Invalid Date".into();
391 }
392 format!(
393 "{:02}:{:02}:{:02} GMT+0000 (Coordinated Universal Time)",
394 field(ms, Field::Hours) as i64,
395 field(ms, Field::Minutes) as i64,
396 field(ms, Field::Seconds) as i64,
397 )
398}
399
400fn time_clip(ms: f64) -> f64 {
407 if !ms.is_finite() || ms.abs() > 8.64e15 {
408 return f64::NAN;
409 }
410 ms.trunc()
411}
412
413fn store_ms(recv: &Value, ms: f64) -> f64 {
416 with_host(|h| {
417 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
418 p.insert("@@ms".into(), Value::Float(ms));
419 }
420 });
421 ms
422}
423
424fn set_fields(recv: &Value, ms: f64, start: usize, args: &[Value], legacy_year: bool) -> f64 {
437 let base = if ms.is_nan() {
438 if start != 0 {
439 return store_ms(recv, f64::NAN);
440 }
441 0.0 } else {
443 ms
444 };
445 let mut f = [
446 field(base, Field::Year),
447 field(base, Field::Month),
448 field(base, Field::Day),
449 field(base, Field::Hours),
450 field(base, Field::Minutes),
451 field(base, Field::Seconds),
452 field(base, Field::Millis),
453 ];
454 let end = if start < 3 { 3 } else { 7 };
456 for (i, slot) in f.iter_mut().enumerate().take(end).skip(start) {
457 match args.get(i - start) {
458 Some(v) => *slot = with_host(|h| h.to_number(v)).trunc(),
459 None => break,
460 }
461 }
462 if legacy_year && (0.0..=99.0).contains(&f[0]) {
463 f[0] += 1900.0;
464 }
465 let t = utc_from_fields(f[0], f[1], f[2], f[3], f[4], f[5], f[6]);
466 store_ms(recv, time_clip(t))
467}
468
469fn date_string(ms: f64) -> String {
471 if ms.is_nan() {
472 return "Invalid Date".into();
473 }
474 let (day, _) = split_day(ms);
475 let (y, mo, d) = civil_from_days(day);
476 let wd = (((day % 7) + 4 + 7) % 7) as usize;
477 format!("{} {} {:02} {:04}", DAYS[wd], MONTHS[mo as usize], d, y)
478}
479
480fn locale_date(ms: f64) -> String {
482 let (day, _) = split_day(ms);
483 let (y, mo, d) = civil_from_days(day);
484 format!("{}/{}/{:04}", mo + 1, d, y)
485}
486
487fn locale_time(ms: f64) -> String {
490 let h24 = field(ms, Field::Hours) as i64;
491 let (h12, meridiem) = match h24 {
492 0 => (12, "AM"),
493 1..=11 => (h24, "AM"),
494 12 => (12, "PM"),
495 _ => (h24 - 12, "PM"),
496 };
497 format!(
498 "{}:{:02}:{:02} {}",
499 h12,
500 field(ms, Field::Minutes) as i64,
501 field(ms, Field::Seconds) as i64,
502 meridiem
503 )
504}
505
506fn iso_year(y: i64) -> String {
513 if (0..=9999).contains(&y) {
514 return format!("{y:04}");
515 }
516 let sign = if y < 0 { '-' } else { '+' };
517 format!("{sign}{:06}", y.abs())
518}
519
520fn iso_string(ms: f64) -> String {
522 let (day, _) = split_day(ms);
523 let (y, mo, d) = civil_from_days(day);
524 format!(
525 "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
526 iso_year(y),
527 mo + 1,
528 d,
529 field(ms, Field::Hours) as i64,
530 field(ms, Field::Minutes) as i64,
531 field(ms, Field::Seconds) as i64,
532 field(ms, Field::Millis) as i64,
533 )
534}
535
536fn parse_str(s: &str) -> f64 {
541 let s = s.trim();
542 if let Some(ms) = parse_iso(s) {
543 return ms;
544 }
545 if let Some(ms) = parse_rfc1123(s) {
546 return ms;
547 }
548 f64::NAN
549}
550
551fn parse_iso(s: &str) -> Option<f64> {
554 let (date, time) = match s.split_once(['T', ' ']) {
555 Some((d, t)) => (d, Some(t)),
556 None => (s, None),
557 };
558 let dp: Vec<&str> = date.split('-').collect();
559 if dp.len() != 3 {
560 return None;
561 }
562 let y: i64 = dp[0].parse().ok()?;
563 let mo: i64 = dp[1].parse().ok()?;
564 let d: i64 = dp[2].parse().ok()?;
565 if !(1..=12).contains(&mo) || !(1..=31).contains(&d) {
566 return None;
567 }
568 let (mut h, mut mi, mut sec, mut milli) = (0i64, 0i64, 0i64, 0i64);
569 if let Some(t) = time {
570 let t = t.trim_end_matches('Z');
571 let (hms, frac) = match t.split_once('.') {
572 Some((a, b)) => (a, Some(b)),
573 None => (t, None),
574 };
575 let tp: Vec<&str> = hms.split(':').collect();
576 if tp.is_empty() {
577 return None;
578 }
579 h = tp[0].parse().ok()?;
580 mi = tp.get(1).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
581 sec = tp.get(2).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
582 if let Some(fr) = frac {
583 let fr: String = fr.chars().take(3).collect();
584 let padded = format!("{fr:0<3}");
585 milli = padded.parse().ok()?;
586 }
587 }
588 let days = days_from_civil(y, mo - 1, d);
589 Some(
590 days as f64 * MS_PER_DAY
591 + h as f64 * 3_600_000.0
592 + mi as f64 * 60_000.0
593 + sec as f64 * 1000.0
594 + milli as f64,
595 )
596}
597
598fn parse_rfc1123(s: &str) -> Option<f64> {
600 let s = match s.split_once(", ") {
602 Some((_, rest)) => rest,
603 None => s,
604 };
605 let parts: Vec<&str> = s.split_whitespace().collect();
606 if parts.len() < 5 {
607 return None;
608 }
609 let d: i64 = parts[0].parse().ok()?;
610 let mo = MONTHS.iter().position(|m| *m == parts[1])? as i64;
611 let y: i64 = parts[2].parse().ok()?;
612 let tp: Vec<&str> = parts[3].split(':').collect();
613 if tp.len() < 2 {
614 return None;
615 }
616 let h: i64 = tp[0].parse().ok()?;
617 let mi: i64 = tp[1].parse().ok()?;
618 let sec: i64 = tp.get(2).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
619 let days = days_from_civil(y, mo, d);
620 Some(
621 days as f64 * MS_PER_DAY
622 + h as f64 * 3_600_000.0
623 + mi as f64 * 60_000.0
624 + sec as f64 * 1000.0,
625 )
626}