1use crate::host::{with_host, JsObj};
12use fusevm::Value;
13use indexmap::IndexMap;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16pub const STATIC_METHODS: &[&str] = &["now", "parse", "UTC"];
17
18const MS_PER_DAY: f64 = 86_400_000.0;
19const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
20const MONTHS: [&str; 12] = [
21 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
22];
23
24fn now_ms() -> f64 {
26 SystemTime::now()
27 .duration_since(UNIX_EPOCH)
28 .map(|d| d.as_millis() as f64)
29 .unwrap_or(0.0)
30}
31
32fn from_ms(ms: f64) -> Value {
34 with_host(|h| {
35 let mut m = IndexMap::new();
36 m.insert("@@native".into(), h.new_str("Date"));
37 m.insert("@@ms".into(), Value::Float(ms));
38 h.new_object(m)
39 })
40}
41
42fn ms_of(recv: &Value) -> f64 {
44 with_host(|h| match h.get(recv) {
45 Some(JsObj::Object(p)) => p.get("@@ms").map(|v| h.to_number(v)).unwrap_or(f64::NAN),
46 _ => f64::NAN,
47 })
48}
49
50pub fn construct(args: &[Value]) -> Result<Value, String> {
52 let ms = match args.len() {
53 0 => now_ms(),
54 1 => {
55 let a = &args[0];
56 if let Value::Str(_) = a {
59 parse_str(&with_host(|h| h.str_of(a)))
60 } else if with_host(|h| matches!(h.get(a), Some(JsObj::Str(_)))) {
61 parse_str(&with_host(|h| h.str_of(a)))
62 } else if super::native_tag(a).as_deref() == Some("Date") {
63 ms_of(a)
64 } else {
65 with_host(|h| h.to_number(a))
66 }
67 }
68 _ => {
70 let n = |i: usize, dflt: f64| {
71 args.get(i)
72 .map(|v| with_host(|h| h.to_number(v)))
73 .unwrap_or(dflt)
74 };
75 let mut year = n(0, f64::NAN);
76 if (0.0..=99.0).contains(&year) {
78 year += 1900.0;
79 }
80 utc_from_fields(
81 year,
82 n(1, 0.0),
83 n(2, 1.0),
84 n(3, 0.0),
85 n(4, 0.0),
86 n(5, 0.0),
87 n(6, 0.0),
88 )
89 }
90 };
91 Ok(from_ms(ms))
92}
93
94pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
96 Some(match method {
97 "now" => Ok(Value::Float(now_ms())),
98 "parse" => Ok(Value::Float(parse_str(&super::arg_str(args, 0)))),
99 "UTC" => {
100 let n = |i: usize, dflt: f64| {
101 args.get(i)
102 .map(|v| with_host(|h| h.to_number(v)))
103 .unwrap_or(dflt)
104 };
105 let mut year = n(0, f64::NAN);
106 if (0.0..=99.0).contains(&year) {
107 year += 1900.0;
108 }
109 Ok(Value::Float(utc_from_fields(
110 year,
111 n(1, 0.0),
112 n(2, 1.0),
113 n(3, 0.0),
114 n(4, 0.0),
115 n(5, 0.0),
116 n(6, 0.0),
117 )))
118 }
119 _ => return None,
120 })
121}
122
123pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
125 let ms = ms_of(recv);
126 let f = |ms: f64| ms; Ok(match method {
128 "getTime" | "valueOf" => Value::Float(f(ms)),
129 "toISOString" | "toJSON" => {
130 if ms.is_nan() {
131 if method == "toJSON" {
132 with_host(|h| h.null())
133 } else {
134 return Err(crate::host::range_error("Invalid time value"));
135 }
136 } else {
137 with_host(|h| h.new_str(iso_string(ms)))
138 }
139 }
140 "toUTCString" | "toGMTString" => with_host(|h| h.new_str(utc_string(ms))),
141 "toString" => with_host(|h| {
142 h.new_str(if ms.is_nan() {
143 "Invalid Date".into()
144 } else {
145 utc_string(ms)
146 })
147 }),
148 "toDateString" => with_host(|h| h.new_str(date_string(ms))),
149 "getFullYear" | "getUTCFullYear" => Value::Float(field(ms, Field::Year)),
150 "getMonth" | "getUTCMonth" => Value::Float(field(ms, Field::Month)),
151 "getDate" | "getUTCDate" => Value::Float(field(ms, Field::Day)),
152 "getDay" | "getUTCDay" => Value::Float(field(ms, Field::Weekday)),
153 "getHours" | "getUTCHours" => Value::Float(field(ms, Field::Hours)),
154 "getMinutes" | "getUTCMinutes" => Value::Float(field(ms, Field::Minutes)),
155 "getSeconds" | "getUTCSeconds" => Value::Float(field(ms, Field::Seconds)),
156 "getMilliseconds" | "getUTCMilliseconds" => Value::Float(field(ms, Field::Millis)),
157 "getTimezoneOffset" => Value::Float(0.0), "setTime" => {
159 let new_ms = super::arg_num(_args, 0);
160 with_host(|h| {
161 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
162 p.insert("@@ms".into(), Value::Float(new_ms));
163 }
164 });
165 Value::Float(new_ms)
166 }
167 _ => {
168 return Err(crate::host::type_error(&format!(
169 "date.{method} is not a function"
170 )))
171 }
172 })
173}
174
175enum Field {
178 Year,
179 Month,
180 Day,
181 Weekday,
182 Hours,
183 Minutes,
184 Seconds,
185 Millis,
186}
187
188fn split_day(ms: f64) -> (i64, i64) {
191 let day = (ms / MS_PER_DAY).floor();
192 let rem = ms - day * MS_PER_DAY;
193 (day as i64, rem as i64)
194}
195
196fn civil_from_days(z: i64) -> (i64, i64, i64) {
199 let z = z + 719_468;
200 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
201 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
204 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)
209}
210
211fn days_from_civil(y: i64, m0: i64, d: i64) -> i64 {
213 let m = m0 + 1;
214 let y = if m <= 2 { y - 1 } else { y };
215 let era = if y >= 0 { y } else { y - 399 } / 400;
216 let yoe = y - era * 400;
217 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
218 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
219 era * 146_097 + doe - 719_468
220}
221
222fn field(ms: f64, which: Field) -> f64 {
223 if ms.is_nan() {
224 return f64::NAN;
225 }
226 let (day, rem) = split_day(ms);
227 let (y, mo, d) = civil_from_days(day);
228 match which {
229 Field::Year => y as f64,
230 Field::Month => mo as f64,
231 Field::Day => d as f64,
232 Field::Weekday => (((day % 7) + 4 + 7) % 7) as f64,
234 Field::Hours => (rem / 3_600_000) as f64,
235 Field::Minutes => (rem / 60_000 % 60) as f64,
236 Field::Seconds => (rem / 1000 % 60) as f64,
237 Field::Millis => (rem % 1000) as f64,
238 }
239}
240
241fn utc_from_fields(y: f64, mo: f64, d: f64, h: f64, mi: f64, s: f64, ms: f64) -> f64 {
244 if [y, mo, d, h, mi, s, ms].iter().any(|v| v.is_nan()) {
245 return f64::NAN;
246 }
247 let total_months = y as i64 * 12 + mo as i64;
249 let year = total_months.div_euclid(12);
250 let month = total_months.rem_euclid(12);
251 let days = days_from_civil(year, month, d as i64);
252 days as f64 * MS_PER_DAY + h * 3_600_000.0 + mi * 60_000.0 + s * 1000.0 + ms
253}
254
255fn utc_string(ms: f64) -> String {
257 if ms.is_nan() {
258 return "Invalid Date".into();
259 }
260 let (day, _) = split_day(ms);
261 let (y, mo, d) = civil_from_days(day);
262 let wd = (((day % 7) + 4 + 7) % 7) as usize;
263 format!(
264 "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
265 DAYS[wd],
266 d,
267 MONTHS[mo as usize],
268 y,
269 field(ms, Field::Hours) as i64,
270 field(ms, Field::Minutes) as i64,
271 field(ms, Field::Seconds) as i64,
272 )
273}
274
275fn date_string(ms: f64) -> String {
277 if ms.is_nan() {
278 return "Invalid Date".into();
279 }
280 let (day, _) = split_day(ms);
281 let (y, mo, d) = civil_from_days(day);
282 let wd = (((day % 7) + 4 + 7) % 7) as usize;
283 format!("{} {} {:02} {:04}", DAYS[wd], MONTHS[mo as usize], d, y)
284}
285
286fn iso_string(ms: f64) -> String {
288 let (day, _) = split_day(ms);
289 let (y, mo, d) = civil_from_days(day);
290 format!(
291 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
292 y,
293 mo + 1,
294 d,
295 field(ms, Field::Hours) as i64,
296 field(ms, Field::Minutes) as i64,
297 field(ms, Field::Seconds) as i64,
298 field(ms, Field::Millis) as i64,
299 )
300}
301
302fn parse_str(s: &str) -> f64 {
307 let s = s.trim();
308 if let Some(ms) = parse_iso(s) {
309 return ms;
310 }
311 if let Some(ms) = parse_rfc1123(s) {
312 return ms;
313 }
314 f64::NAN
315}
316
317fn parse_iso(s: &str) -> Option<f64> {
320 let (date, time) = match s.split_once(['T', ' ']) {
321 Some((d, t)) => (d, Some(t)),
322 None => (s, None),
323 };
324 let dp: Vec<&str> = date.split('-').collect();
325 if dp.len() != 3 {
326 return None;
327 }
328 let y: i64 = dp[0].parse().ok()?;
329 let mo: i64 = dp[1].parse().ok()?;
330 let d: i64 = dp[2].parse().ok()?;
331 if !(1..=12).contains(&mo) || !(1..=31).contains(&d) {
332 return None;
333 }
334 let (mut h, mut mi, mut sec, mut milli) = (0i64, 0i64, 0i64, 0i64);
335 if let Some(t) = time {
336 let t = t.trim_end_matches('Z');
337 let (hms, frac) = match t.split_once('.') {
338 Some((a, b)) => (a, Some(b)),
339 None => (t, None),
340 };
341 let tp: Vec<&str> = hms.split(':').collect();
342 if tp.is_empty() {
343 return None;
344 }
345 h = tp[0].parse().ok()?;
346 mi = tp.get(1).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
347 sec = tp.get(2).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
348 if let Some(fr) = frac {
349 let fr: String = fr.chars().take(3).collect();
350 let padded = format!("{fr:0<3}");
351 milli = padded.parse().ok()?;
352 }
353 }
354 let days = days_from_civil(y, mo - 1, d);
355 Some(
356 days as f64 * MS_PER_DAY
357 + h as f64 * 3_600_000.0
358 + mi as f64 * 60_000.0
359 + sec as f64 * 1000.0
360 + milli as f64,
361 )
362}
363
364fn parse_rfc1123(s: &str) -> Option<f64> {
366 let s = match s.split_once(", ") {
368 Some((_, rest)) => rest,
369 None => s,
370 };
371 let parts: Vec<&str> = s.split_whitespace().collect();
372 if parts.len() < 5 {
373 return None;
374 }
375 let d: i64 = parts[0].parse().ok()?;
376 let mo = MONTHS.iter().position(|m| *m == parts[1])? as i64;
377 let y: i64 = parts[2].parse().ok()?;
378 let tp: Vec<&str> = parts[3].split(':').collect();
379 if tp.len() < 2 {
380 return None;
381 }
382 let h: i64 = tp[0].parse().ok()?;
383 let mi: i64 = tp[1].parse().ok()?;
384 let sec: i64 = tp.get(2).map(|v| v.parse().ok()).unwrap_or(Some(0))?;
385 let days = days_from_civil(y, mo, d);
386 Some(
387 days as f64 * MS_PER_DAY
388 + h as f64 * 3_600_000.0
389 + mi as f64 * 60_000.0
390 + sec as f64 * 1000.0,
391 )
392}