Skip to main content

nodejs/stdlib/
date.rs

1//! JavaScript `Date` (global constructor). A Date is a plain object tagged
2//! `@@native = "Date"` whose time value (milliseconds since the Unix epoch, or
3//! NaN for an invalid date) lives in a hidden `@@ms` field.
4//!
5//! The UTC-based surface is implemented in full: `getTime`/`valueOf`, the
6//! `toISOString`/`toUTCString`/`toString`/`toDateString`/`toTimeString`/
7//! `toLocale*` renderings, the field getters, the component SETTERS, the Annex-B
8//! `getYear`/`setYear`, and the statics `Date.now`/`Date.parse`/`Date.UTC`.
9//! Local-timezone getters and setters alias the UTC ones (node-js runs as if
10//! `TZ=UTC`), which is the correct answer for the machine-readable date headers
11//! express/send/fresh produce.
12
13use 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
20/// Every method `instance_call` below answers. `valueOf` and `toString` are on
21/// this list because `Object.prototype` has methods by those names too: without
22/// the entry, `host::call_method` hands a Date to `object_builtin_method` and
23/// `date.valueOf()` returns the Date itself instead of its time value (so `+d`
24/// and `d - 0` were `NaN`).
25pub 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
80/// Milliseconds since the Unix epoch, right now.
81fn 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
88/// Build a Date value carrying `ms` (NaN → an "Invalid Date").
89fn 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
98/// The stored time value of a Date instance (NaN if not a Date).
99fn 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
106/// `new Date(...)`.
107pub 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            // A string argument is parsed; anything else is coerced to a number
113            // (milliseconds). Another Date coerces via its time value.
114            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        // (year, month[, day, hours, minutes, seconds, ms]) — interpreted as UTC.
125        _ => {
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            // Years 0..99 map to 1900..1999 per the spec.
133            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    // TimeClip (21.4.1.31): a value beyond ±8.64e15 ms is not a representable
148    // date and becomes NaN. `new Date(8.64e15 + 1)` used to keep the raw number
149    // and print a real date where node prints `Invalid Date`.
150    Ok(from_ms(time_clip(ms)))
151}
152
153/// `Date.now()` / `Date.parse(str)` / `Date.UTC(...)`.
154pub 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
182/// Date instance methods (all treated as UTC — see the module note).
183pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
184    let ms = ms_of(recv);
185    let f = |ms: f64| ms; // readability alias for numeric returns
186    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` (21.4.4.41) is NOT the RFC-7231 header form — that is
201        // `toUTCString`. It is `ToDateString`: the `toDateString` half, a space,
202        // then the `toTimeString` half. This used to answer `toUTCString`, so
203        // `String(date)` and `` `${date}` `` printed
204        // `Thu, 01 Jan 1970 00:00:00 GMT` where node prints
205        // `Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time)`.
206        "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        // The three `toLocale*` forms threw `is not a function` — absent
215        // entirely, so `new Date(0).toLocaleString()` failed where node prints
216        // `1/1/1970, 12:00:00 AM`. Rendered in node's default en-US shape
217        // (`M/D/YYYY` and 12-hour `h:mm:ss AM/PM`) at UTC, consistent with the
218        // rest of this module running as if `TZ=UTC`. The `locales`/`options`
219        // arguments are accepted and ignored: without ICU there is nothing to
220        // vary, and answering the default form beats throwing.
221        "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), // node-js runs as UTC
251        "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        // The component setters (21.4.4.20-21.4.4.28). Each takes its own field
254        // plus every LOWER-order one it can reach, defaulting the rest from the
255        // current time value, then rebuilds and TimeClips. `setUTCFullYear` and
256        // friends were absent entirely, so `d.setUTCFullYear(2000)` threw
257        // `is not a function` — a Date could be read but never modified except
258        // wholesale through `setTime`.
259        "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        // Annex B B.2.3.3 / B.2.3.4 — offset-from-1900 year accessors kept for
269        // legacy code. `setYear` maps 0..99 onto 1900..1999, which is the only
270        // way it differs from `setFullYear`.
271        "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
285// ── civil-calendar conversions (days-from-epoch ⇄ Y/M/D), UTC only ────────────
286
287enum Field {
288    Year,
289    Month,
290    Day,
291    Weekday,
292    Hours,
293    Minutes,
294    Seconds,
295    Millis,
296}
297
298/// Split a time value into (days-from-epoch, ms-within-day), flooring toward -∞
299/// so negative (pre-1970) times decompose correctly.
300fn 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
306/// Convert a days-from-epoch count to (year, month 0-11, day 1-31) using
307/// Howard Hinnant's civil_from_days algorithm.
308fn 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; // [0, 146096]
312    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; // [0, 399]
313    let y = yoe + era * 400;
314    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
315    let mp = (5 * doy + 2) / 153; // [0, 11]
316    let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
317    let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
318    (if m <= 2 { y + 1 } else { y }, m - 1, d)
319}
320
321/// Inverse: (year, month 0-11, day) → days from epoch.
322fn 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        // Weekday: 1970-01-01 (day 0) was a Thursday (4).
343        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
351/// Assemble a UTC time value from broken-down fields (with month/day overflow
352/// normalized the way JS does, e.g. month 12 rolls into the next year).
353fn 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    // Normalize month into 0..11, carrying into the year.
358    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
365/// `Wed, 21 Oct 2015 07:28:00 GMT` — the RFC-7231 IMF-fixdate HTTP header form.
366fn 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
385/// `00:00:00 GMT+0000 (Coordinated Universal Time)` — the `toTimeString` form
386/// (21.4.4.42 TimeString + TimeZoneString). The offset is always `+0000`
387/// because this module runs as if `TZ=UTC`.
388fn 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
400/// TimeClip (21.4.1.31): a time value more than 8.64e15 ms from the epoch is not
401/// representable and becomes NaN; anything else truncates toward zero.
402///
403/// Without this a `new Date(8.64e15 + 1)` kept the out-of-range value and
404/// printed a real date (`Sat, 13 Sep 275760 …`) where node prints
405/// `Invalid Date`, so the boundary every date-range check relies on was absent.
406fn time_clip(ms: f64) -> f64 {
407    if !ms.is_finite() || ms.abs() > 8.64e15 {
408        return f64::NAN;
409    }
410    ms.trunc()
411}
412
413/// Write a time value into the receiver's hidden `@@ms` slot, returning it (as
414/// every mutator does).
415fn 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
424/// The shared body of every component setter.
425///
426/// `start` indexes the field the setter names within
427/// `[year, month, date, hours, minutes, seconds, ms]`; the setter consumes that
428/// field and every LOWER-order one in its own group (date fields 0..2, time
429/// fields 3..6), defaulting anything not supplied from the current time value.
430///
431/// `legacy_year` applies Annex B `setYear`'s 0..99 → 1900..1999 mapping.
432///
433/// NaN handling follows the spec's split: `setFullYear` on an invalid date
434/// treats the time value as +0 and so can REVIVE it (21.4.4.21 step 2), while
435/// every other setter leaves an invalid date invalid.
436fn 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 // setFullYear/setYear on an Invalid Date starts from the epoch.
442    } 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    // A date setter reaches at most field 2; a time setter at most field 6.
455    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
469/// `Wed Oct 21 2015` — the `toDateString` form.
470fn 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
480/// `1/2/2020` — the `toLocaleDateString` default (en-US `M/D/YYYY`, no padding).
481fn 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
487/// `3:04:05 PM` — the `toLocaleTimeString` default (en-US 12-hour). Hour 0 and
488/// hour 12 both render as `12`, which is why this is not `h % 12`.
489fn 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
506/// The year field of an ISO-8601 date (21.4.4.36 `Date.prototype.toISOString`).
507///
508/// Years 0..=9999 are four digits; anything outside that range uses the EXPANDED
509/// form — an explicit sign and exactly six digits, `+275760` / `-000001`. A bare
510/// `{:04}` gets both wrong, since Rust counts the sign inside the width (`-1`
511/// formats as `-001`) and never emits `+`.
512fn 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
520/// `2015-10-21T07:28:00.000Z` — the ISO-8601 / `toISOString` form.
521fn 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
536/// Parse a date string. Supports the two forms HTTP code produces: ISO-8601
537/// (`2015-10-21T07:28:00.000Z` / date-only `2015-10-21`) and the RFC-1123 /
538/// IMF-fixdate header form (`Wed, 21 Oct 2015 07:28:00 GMT`). Returns NaN on any
539/// input that does not match — the JS "Invalid Date" contract.
540fn 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
551/// ISO-8601: `YYYY-MM-DD[THH:MM:SS[.sss]][Z]` (a bare date is treated as UTC
552/// midnight, matching modern V8).
553fn 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
598/// RFC-1123 / IMF-fixdate: `Wed, 21 Oct 2015 07:28:00 GMT`.
599fn parse_rfc1123(s: &str) -> Option<f64> {
600    // Drop an optional leading weekday token (`Wed,`).
601    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}