Skip to main content

atmos/os_lib/js/builtins/
date.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ===== Date =====
6// 1970-01-01 からの経過日数 → (年,月,日)。Howard Hinnant の civil_from_days アルゴリズム
7// (`kernel::timer` に同一ロジックの private 実装があるが、モジュールを跨いだ公開のための
8// 配線コストに見合わないほど小さい純粋関数なので、ここでも独立に定義する)。
9pub(crate) fn civil_from_days(z: i64) -> (i64, u32, u32) {
10    let z = z + 719468;
11    let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
12    let doe = z - era * 146097;
13    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
14    let y = yoe + era * 400;
15    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
16    let mp = (5 * doy + 2) / 153;
17    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
18    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
19    (if m <= 2 { y + 1 } else { y }, m, d)
20}
21/// (年,月,日) → 1970-01-01 からの経過日数。上記の逆変換(同じ Howard Hinnant アルゴリズム)。
22pub(crate) fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
23    let y = if m <= 2 { y - 1 } else { y };
24    let era = (if y >= 0 { y } else { y - 399 }) / 400;
25    let yoe = y - era * 400;
26    let mp = (m as i64 + 9) % 12;
27    let doy = (153 * mp + 2) / 5 + d as i64 - 1;
28    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
29    era * 146097 + doe - 719468
30}
31/// epoch ミリ秒 → (年,月,日,時,分,秒,ミリ秒,曜日[0=日曜]) の UTC 分解。
32pub(crate) fn decompose_ms(ms: f64) -> (i64, u32, u32, u32, u32, u32, u32, u32) {
33    let total_ms = libm::floor(ms) as i64;
34    let days = total_ms.div_euclid(86_400_000);
35    let ms_of_day = total_ms.rem_euclid(86_400_000);
36    let (y, mo, d) = civil_from_days(days);
37    let hh = (ms_of_day / 3_600_000) as u32;
38    let mm = ((ms_of_day / 60_000) % 60) as u32;
39    let ss = ((ms_of_day / 1000) % 60) as u32;
40    let mms = (ms_of_day % 1000) as u32;
41    // 1970-01-01 は木曜日(曜日番号4)。
42    let dow = ((days.rem_euclid(7)) + 4).rem_euclid(7) as u32;
43    (y, mo, d, hh, mm, ss, mms, dow)
44}
45pub(crate) fn epoch_ms_now() -> f64 {
46    match crate::kernel::timer::get_unix_time() {
47        Some(secs) => (secs as f64) * 1000.0,
48        // NTP 未同期時のフォールバック: Unix epoch(1970-01-01)。実時刻としては不正確だが、
49        // Date 演算自体(差分計算等)は一貫して動作する。
50        None => 0.0,
51    }
52}
53/// ISO 8601 のサブセット(`YYYY-MM-DD`/`YYYY-MM-DDTHH:mm:ss[.sss][Z|±HH:mm]` 等)を解析して
54/// epoch ミリ秒を返す。`Date` コンストラクタ/`Date.parse` の「日時文字列パースは非対応」
55/// だった大きな既知の限界を、最も一般的な ISO 8601 形式に限定して解消する
56/// (RFC 2822 形式は `parse_rfc2822_date`/`parse_date_string` が別途対応。それ以外の
57/// 実装依存フォーマット(`"Jan 1, 2024"` 等)は非対応のまま)。
58pub(crate) fn parse_hh_mm(s: &str) -> Option<(i64, i64)> {
59    let mut it = s.splitn(2, ':');
60    let h: i64 = it.next()?.parse().ok()?;
61    let m: i64 = it.next().unwrap_or("0").parse().ok()?;
62    Some((h, m))
63}
64/// タイムゾーンオフセットの `±HH:mm`/`±HHmm`/`±HH` いずれの表記も解釈する。
65/// 以前は `parse_hh_mm`(コロン必須)をそのまま流用しており、コロン無し4桁形式
66/// (`+0900` 等。ISO 8601/ECMA-262 Date Time String 双方で正当な表記)を
67/// 「時=900・分=0」と誤読し、実際のオフセットの60倍という壊れた値になる
68/// 静かなバグだった(`"2024-01-01T00:00:00+0900"` が丸ごと不正な日時にずれる)。
69pub(crate) fn parse_tz_offset_hhmm(s: &str) -> Option<(i64, i64)> {
70    if s.contains(':') {
71        return parse_hh_mm(s);
72    }
73    match s.len() {
74        4 => {
75            let h: i64 = s.get(0..2)?.parse().ok()?;
76            let m: i64 = s.get(2..4)?.parse().ok()?;
77            Some((h, m))
78        }
79        2 => {
80            let h: i64 = s.parse().ok()?;
81            Some((h, 0))
82        }
83        _ => parse_hh_mm(s),
84    }
85}
86pub(crate) fn parse_iso_date(s: &str) -> Option<f64> {
87    let s = s.trim();
88    if s.is_empty() {
89        return None;
90    }
91    // タイムゾーン部分を切り出す。'Z' か、'T' 以降に現れる '+'/'-' オフセット。
92    // (日付部分先頭の '-' は西暦紀元前を表すため、時刻部分より前は見ない)
93    let (main, tz_offset_min): (&str, i64) = if let Some(rest) = s.strip_suffix('Z') {
94        (rest, 0)
95    } else if let Some(t_pos) = s.find('T') {
96        let time_part = s.get(t_pos..).unwrap_or("");
97        if let Some(sign_pos_rel) = time_part.rfind(['+', '-']) {
98            let sign_pos = t_pos + sign_pos_rel;
99            let sign = if s.as_bytes()[sign_pos] == b'-' { -1i64 } else { 1i64 };
100            let (th, tm) = parse_tz_offset_hhmm(s.get(sign_pos + 1..).unwrap_or(""))?;
101            (s.get(..sign_pos).unwrap_or(""), sign * (th * 60 + tm))
102        } else {
103            (s, 0)
104        }
105    } else {
106        (s, 0)
107    };
108    let (date_part, time_part) = match main.find('T') {
109        Some(p) => (
110            main.get(..p).unwrap_or(""),
111            Some(main.get(p + 1..).unwrap_or("")),
112        ),
113        None => (main, None),
114    };
115    let mut date_it = date_part.split('-');
116    let y: i64 = date_it.next()?.parse().ok()?;
117    let mo: u32 = match date_it.next() {
118        Some(x) => x.parse().ok()?,
119        None => 1,
120    };
121    let d: u32 = match date_it.next() {
122        Some(x) => x.parse().ok()?,
123        None => 1,
124    };
125    if !(1..=12).contains(&mo) || !(1..=31).contains(&d) {
126        return None;
127    }
128    let days = days_from_civil(y, mo, d);
129    let mut ms_of_day: i64 = 0;
130    if let Some(tp) = time_part {
131        let mut parts = tp.splitn(2, ':');
132        let hh: i64 = parts.next()?.parse().ok()?;
133        let rest = parts.next()?;
134        let mut parts2 = rest.splitn(2, ':');
135        let mi: i64 = parts2.next()?.parse().ok()?;
136        let (ss, mms) = match parts2.next() {
137            Some(sec_part) => {
138                if let Some((s_, f_)) = sec_part.split_once('.') {
139                    let ss: i64 = s_.parse().ok()?;
140                    let mut f = String::from(f_);
141                    while f.len() < 3 {
142                        f.push('0');
143                    }
144                    f.truncate(3);
145                    let mms: i64 = f.parse().ok()?;
146                    (ss, mms)
147                } else {
148                    (sec_part.parse().ok()?, 0)
149                }
150            }
151            None => (0, 0),
152        };
153        ms_of_day = hh * 3_600_000 + mi * 60_000 + ss * 1000 + mms;
154    }
155    let ms = days * 86_400_000 + ms_of_day - tz_offset_min * 60_000;
156    Some(ms as f64)
157}
158pub(crate) const MONTH_ABBREVS: [&str; 12] = [
159    "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
160];
161
162/// RFC 2822(HTTP日付ヘッダ等でよく使われる `"Mon, 25 Dec 2023 00:00:00 GMT"` 形式)の
163/// 日時文字列を解析する。`Date`/`Date.parse` は ISO 8601 サブセットのみ対応で、この形式は
164/// 丸ごと非対応のままだった(`parse_iso_date` の doc コメントにも既知の制約として
165/// 明記されていた)。曜日名は任意(あれば読み飛ばすだけ)。タイムゾーンは
166/// `GMT`/`UT`/`Z`(0オフセット扱い)または `+HHMM`/`-HHMM` のみ対応
167/// (`EST`/`PST` 等の略称は非対応で0オフセット扱いにフォールバック)。
168pub(crate) fn parse_rfc2822_date(s: &str) -> Option<f64> {
169    let s = s.trim();
170    // 曜日名 + カンマ(例: "Mon, ")があれば読み飛ばす。
171    let s = match s.find(',') {
172        Some(pos) => s.get(pos + 1..)?.trim(),
173        None => s,
174    };
175    let mut tokens = s.split_whitespace();
176    let day: u32 = tokens.next()?.parse().ok()?;
177    let mon_str = tokens.next()?.to_lowercase();
178    let mon = MONTH_ABBREVS
179        .iter()
180        .position(|m| mon_str.starts_with(m))? as u32
181        + 1;
182    let year: i64 = tokens.next()?.parse().ok()?;
183    let time_tok = tokens.next()?;
184    let mut tp = time_tok.splitn(3, ':');
185    let hh: i64 = tp.next()?.parse().ok()?;
186    let mi: i64 = tp.next()?.parse().ok()?;
187    let ss: i64 = tp.next().unwrap_or("0").parse().unwrap_or(0);
188    let tz_offset_min: i64 = match tokens.next() {
189        Some(tz) if tz.eq_ignore_ascii_case("gmt") || tz.eq_ignore_ascii_case("ut") || tz == "Z" => {
190            0
191        }
192        Some(tz) if tz.len() == 5 && (tz.starts_with('+') || tz.starts_with('-')) => {
193            let sign = if tz.starts_with('-') { -1 } else { 1 };
194            let hh_off: i64 = tz.get(1..3)?.parse().ok()?;
195            let mm_off: i64 = tz.get(3..5)?.parse().ok()?;
196            sign * (hh_off * 60 + mm_off)
197        }
198        _ => 0,
199    };
200    if !(1..=12).contains(&mon) || !(1..=31).contains(&day) {
201        return None;
202    }
203    let days = days_from_civil(year, mon, day);
204    let ms_of_day = hh * 3_600_000 + mi * 60_000 + ss * 1000;
205    let ms = days * 86_400_000 + ms_of_day - tz_offset_min * 60_000;
206    Some(ms as f64)
207}
208
209/// ISO 8601(`parse_iso_date`)を先に試し、失敗すれば RFC 2822 形式
210/// (`parse_rfc2822_date`)にフォールバックする。`Date` コンストラクタ/`Date.parse`
211/// 双方の単一文字列引数はこれを使う。
212pub(crate) fn parse_date_string(s: &str) -> Option<f64> {
213    parse_iso_date(s).or_else(|| parse_rfc2822_date(s))
214}
215
216pub(crate) fn date_parse_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
217    Ok(Value::Number(
218        parse_date_string(&arg(a, 0).to_js_string()).unwrap_or(f64::NAN),
219    ))
220}
221/// `new Date(...)`。`construct_object` が用意する `this` は使わず、`ObjKind::DateObj` を
222/// 積んだオブジェクトを直接返す(`RegExp`/`Map`/`Set` と同じ設計)。
223/// 対応する引数形式: 0引数(現在時刻)/ 1引数(epoch ミリ秒 or ISO 8601 日時文字列)/
224/// 2引数以上(年, 月[0始まり], 日?, 時?, 分?, 秒?, ミリ秒?)。
225pub(crate) fn date_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
226    let ms = if a.is_empty() {
227        epoch_ms_now()
228    } else if a.len() == 1 {
229        let v0 = arg(a, 0);
230        match &v0 {
231            Value::Str(s) => parse_date_string(s).unwrap_or(f64::NAN),
232            _ => v0.to_number(),
233        }
234    } else {
235        let y = arg(a, 0).to_number() as i64;
236        let mo0 = arg(a, 1).to_number() as i64; // 0始まり
237        let d = if a.len() > 2 { arg(a, 2).to_number() as i64 } else { 1 };
238        let hh = if a.len() > 3 { arg(a, 3).to_number() as i64 } else { 0 };
239        let mi = if a.len() > 4 { arg(a, 4).to_number() as i64 } else { 0 };
240        let ss = if a.len() > 5 { arg(a, 5).to_number() as i64 } else { 0 };
241        let mms = if a.len() > 6 { arg(a, 6).to_number() as i64 } else { 0 };
242        // 月は 0-11 の範囲外でも繰り上げ/繰り下げが仕様どおり起きるよう div_euclid で正規化。
243        let y_adj = y + mo0.div_euclid(12);
244        let m_adj = (mo0.rem_euclid(12) + 1) as u32;
245        let days = days_from_civil(y_adj, m_adj, 1) + (d - 1);
246        (days * 86_400_000 + hh * 3_600_000 + mi * 60_000 + ss * 1000 + mms) as f64
247    };
248    Ok(Value::Object(Obj::date_obj(ms)))
249}
250pub(crate) fn date_now_static(_: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
251    Ok(Value::Number(epoch_ms_now()))
252}
253/// `Date.UTC(year, month?, day?, hours?, minutes?, seconds?, ms?)`。`new Date(...)`
254/// の多引数コンストラクタと同じ成分だが、Date インスタンスではなく epoch ミリ秒の
255/// 数値を直接返す静的ファクトリ(`Date.now`/`Date.parse` と3つ揃いのはずが1つ
256/// 欠けていた)。省略された引数の既定値は仕様どおり `year`/`month` 以外は0、
257/// `day` のみ1。
258pub(crate) fn date_utc_static(_: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
259    let y = arg(a, 0).to_number() as i64;
260    let mo0 = if a.len() > 1 { arg(a, 1).to_number() as i64 } else { 0 };
261    let d = if a.len() > 2 { arg(a, 2).to_number() as i64 } else { 1 };
262    let hh = if a.len() > 3 { arg(a, 3).to_number() as i64 } else { 0 };
263    let mi = if a.len() > 4 { arg(a, 4).to_number() as i64 } else { 0 };
264    let ss = if a.len() > 5 { arg(a, 5).to_number() as i64 } else { 0 };
265    let mms = if a.len() > 6 { arg(a, 6).to_number() as i64 } else { 0 };
266    Ok(Value::Number(compose_civil_ms(y, mo0, d, hh, mi, ss, mms)))
267}
268pub(crate) fn date_ms(t: &Value) -> f64 {
269    match t {
270        Value::Object(o) => match &o.borrow().kind {
271            ObjKind::DateObj(ms) => *ms,
272            _ => f64::NAN,
273        },
274        _ => f64::NAN,
275    }
276}
277/// `input.valueAsDate`(HTML5。`type="date"`/`"month"` の `.value` 文字列を
278/// `Date` オブジェクトとして読み書きする IDL 属性。丸ごと未対応だった。仕様上は
279/// `week` にも適用されるが、ISO 週番号⇄暦日の変換は本キャンペーンの規模を超える
280/// ため対象外とする簡略方針(`date`/`month` 以外は常に `null`。`valueAsNumber`
281/// が対象外の `type` で `NaN` を返すのと異なり、`valueAsDate` は仕様どおり
282/// `null` を返す)。`interp.rs` の DOM プロパティ get/set から呼ぶ。
283pub(crate) fn input_value_as_date_ms(type_attr: &str, value: &str) -> Option<f64> {
284    // `type="datetime-local"`(`YYYY-MM-DDTHH:MM`形式)は既存の`parse_iso_date`が
285    // 日付部分+`T`+時刻部分の完全なISO 8601形式を既にサポートしているため、
286    // 新規パーサ不要でこの一覧に加えるだけで対応できる。`week`/`time`は
287    // `parse_iso_date`が日付部分(年月日)を必須としているため未対応のまま。
288    if !matches!(type_attr, "date" | "month" | "datetime-local") {
289        return None;
290    }
291    parse_iso_date(value)
292}
293/// 上記の逆変換: `Date` オブジェクトの epoch ミリ秒を `type` に応じた
294/// `value` 属性文字列(`date` → `YYYY-MM-DD`、`month` → `YYYY-MM`)へ整形する。
295pub(crate) fn input_date_ms_to_value(type_attr: &str, ms: f64) -> Option<String> {
296    if (type_attr != "date" && type_attr != "month") || ms.is_nan() {
297        return None;
298    }
299    let (y, mo, d, ..) = decompose_ms(ms);
300    if type_attr == "month" {
301        Some(alloc::format!("{y:04}-{mo:02}"))
302    } else {
303        Some(alloc::format!("{y:04}-{mo:02}-{d:02}"))
304    }
305}
306/// `Value`(`Date` インスタンス想定)から epoch ミリ秒を取り出す。`date_ms` は
307/// このファイル内 private のため、`interp.rs` から呼べる薄いラッパー。
308pub(crate) fn date_value_ms(v: &Value) -> f64 {
309    date_ms(v)
310}
311pub(crate) fn date_get_time(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
312    Ok(Value::Number(date_ms(&t)))
313}
314pub(crate) fn date_get_full_year(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
315    Ok(Value::Number(decompose_ms(date_ms(&t)).0 as f64))
316}
317pub(crate) fn date_get_month(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
318    Ok(Value::Number((decompose_ms(date_ms(&t)).1 - 1) as f64))
319}
320pub(crate) fn date_get_date(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
321    Ok(Value::Number(decompose_ms(date_ms(&t)).2 as f64))
322}
323pub(crate) fn date_get_hours(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
324    Ok(Value::Number(decompose_ms(date_ms(&t)).3 as f64))
325}
326pub(crate) fn date_get_minutes(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
327    Ok(Value::Number(decompose_ms(date_ms(&t)).4 as f64))
328}
329pub(crate) fn date_get_seconds(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
330    Ok(Value::Number(decompose_ms(date_ms(&t)).5 as f64))
331}
332pub(crate) fn date_get_milliseconds(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
333    Ok(Value::Number(decompose_ms(date_ms(&t)).6 as f64))
334}
335pub(crate) fn date_get_day(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
336    Ok(Value::Number(decompose_ms(date_ms(&t)).7 as f64))
337}
338pub(crate) fn format_iso(ms: f64) -> String {
339    let (y, mo, d, hh, mm, ss, mms, _) = decompose_ms(ms);
340    format!(
341        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
342        y, mo, d, hh, mm, ss, mms
343    )
344}
345pub(crate) fn date_to_iso_string(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
346    Ok(Value::str(format_iso(date_ms(&t))))
347}
348pub(crate) fn date_to_string(it: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
349    // 簡略実装: タイムゾーン名等は付けず ISO 形式のまま返す
350    // (`toString`/`toLocaleString` のロケール依存フォーマットは非対応)。
351    date_to_iso_string(it, t, a)
352}
353/// `toDateString`/`toLocaleDateString`(日付部分のみ)。以前は `toString` と同じ
354/// フル ISO 日時文字列を返しており、`new Date(0).toDateString()` に時刻部分が
355/// 混入していた(`toTimeString` 側は逆に日付部分が混入していた)。
356pub(crate) fn date_to_date_string(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
357    let (y, mo, d, ..) = decompose_ms(date_ms(&t));
358    Ok(Value::str(format!("{:04}-{:02}-{:02}", y, mo, d)))
359}
360/// `toTimeString`/`toLocaleTimeString`(時刻部分のみ)。
361pub(crate) fn date_to_time_string(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
362    let (_, _, _, hh, mm, ss, ..) = decompose_ms(date_ms(&t));
363    Ok(Value::str(format!("{:02}:{:02}:{:02}", hh, mm, ss)))
364}
365/// `Date.prototype.set*` 系メソッドが `setTime` も含めて1つも実装されておらず、
366/// `date_get` に getter しか登録されていない丸ごと未対応バグだった
367/// (`d.setFullYear(2025)`/`d.setDate(d.getDate()+1)` 等、Date を書き換える
368/// 定番イディオムが軒並み `undefined is not a function` になっていた)。
369/// `date_ctor` の多引数分岐と同じ正規化ロジック(月は0始まり・範囲外は繰り上げ/
370/// 繰り下げ)を共有ヘルパへ切り出し、getter と対称な `set*` 一式を追加する。
371pub(crate) fn compose_civil_ms(y: i64, mo0: i64, d: i64, hh: i64, mi: i64, ss: i64, mms: i64) -> f64 {
372    let y_adj = y + mo0.div_euclid(12);
373    let m_adj = (mo0.rem_euclid(12) + 1) as u32;
374    let days = days_from_civil(y_adj, m_adj, 1) + (d - 1);
375    (days * 86_400_000 + hh * 3_600_000 + mi * 60_000 + ss * 1000 + mms) as f64
376}
377pub(crate) fn date_write_ms(t: &Value, ms: f64) {
378    if let Value::Object(o) = t {
379        if let ObjKind::DateObj(slot) = &mut o.borrow_mut().kind {
380            *slot = ms;
381        }
382    }
383}
384pub(crate) fn date_set_time(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
385    let ms = arg(a, 0).to_number();
386    date_write_ms(&t, ms);
387    Ok(Value::Number(ms))
388}
389/// `setFullYear(y, mo?, d?)`。省略された引数は現在値を引き継ぐ(仕様どおり)。
390/// `Date.prototype.getYear()`(Annex B.2.4。`getFullYear() - 1900` を返す
391/// 2桁年時代の遺産)が丸ごと未対応だった。
392pub(crate) fn date_get_year(_: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
393    Ok(Value::Number(decompose_ms(date_ms(&t)).0 as f64 - 1900.0))
394}
395/// `Date.prototype.setYear(y)`(Annex B.2.5)。`0 <= y <= 99` なら `1900+y`
396/// をフルイヤーとして設定し、それ以外は `y` をそのままフルイヤーとして使う
397/// (仕様どおりの奇妙な後方互換ロジック)。
398pub(crate) fn date_set_year(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
399    let (_, mo1, d0, hh, mi, ss, mms, _) = decompose_ms(date_ms(&t));
400    let y_arg = arg(a, 0).to_number();
401    let y = if (0.0..=99.0).contains(&y_arg) { 1900.0 + y_arg } else { y_arg } as i64;
402    let ms = compose_civil_ms(y, mo1 as i64 - 1, d0 as i64, hh as i64, mi as i64, ss as i64, mms as i64);
403    date_write_ms(&t, ms);
404    Ok(Value::Number(ms))
405}
406pub(crate) fn date_set_full_year(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
407    let (_, mo1, d0, hh, mi, ss, mms, _) = decompose_ms(date_ms(&t));
408    let y = arg(a, 0).to_number() as i64;
409    let mo0 = if a.len() > 1 { arg(a, 1).to_number() as i64 } else { mo1 as i64 - 1 };
410    let d = if a.len() > 2 { arg(a, 2).to_number() as i64 } else { d0 as i64 };
411    let ms = compose_civil_ms(y, mo0, d, hh as i64, mi as i64, ss as i64, mms as i64);
412    date_write_ms(&t, ms);
413    Ok(Value::Number(ms))
414}
415pub(crate) fn date_set_month(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
416    let (y, _, d0, hh, mi, ss, mms, _) = decompose_ms(date_ms(&t));
417    let mo0 = arg(a, 0).to_number() as i64;
418    let d = if a.len() > 1 { arg(a, 1).to_number() as i64 } else { d0 as i64 };
419    let ms = compose_civil_ms(y, mo0, d, hh as i64, mi as i64, ss as i64, mms as i64);
420    date_write_ms(&t, ms);
421    Ok(Value::Number(ms))
422}
423pub(crate) fn date_set_date(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
424    let (y, mo1, _, hh, mi, ss, mms, _) = decompose_ms(date_ms(&t));
425    let d = arg(a, 0).to_number() as i64;
426    let ms = compose_civil_ms(y, mo1 as i64 - 1, d, hh as i64, mi as i64, ss as i64, mms as i64);
427    date_write_ms(&t, ms);
428    Ok(Value::Number(ms))
429}
430pub(crate) fn date_set_hours(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
431    let (y, mo1, d, _, mi0, ss0, mms0, _) = decompose_ms(date_ms(&t));
432    let hh = arg(a, 0).to_number() as i64;
433    let mi = if a.len() > 1 { arg(a, 1).to_number() as i64 } else { mi0 as i64 };
434    let ss = if a.len() > 2 { arg(a, 2).to_number() as i64 } else { ss0 as i64 };
435    let mms = if a.len() > 3 { arg(a, 3).to_number() as i64 } else { mms0 as i64 };
436    let ms = compose_civil_ms(y, mo1 as i64 - 1, d as i64, hh, mi, ss, mms);
437    date_write_ms(&t, ms);
438    Ok(Value::Number(ms))
439}
440pub(crate) fn date_set_minutes(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
441    let (y, mo1, d, hh, _, ss0, mms0, _) = decompose_ms(date_ms(&t));
442    let mi = arg(a, 0).to_number() as i64;
443    let ss = if a.len() > 1 { arg(a, 1).to_number() as i64 } else { ss0 as i64 };
444    let mms = if a.len() > 2 { arg(a, 2).to_number() as i64 } else { mms0 as i64 };
445    let ms = compose_civil_ms(y, mo1 as i64 - 1, d as i64, hh as i64, mi, ss, mms);
446    date_write_ms(&t, ms);
447    Ok(Value::Number(ms))
448}
449pub(crate) fn date_set_seconds(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
450    let (y, mo1, d, hh, mi, _, mms0, _) = decompose_ms(date_ms(&t));
451    let ss = arg(a, 0).to_number() as i64;
452    let mms = if a.len() > 1 { arg(a, 1).to_number() as i64 } else { mms0 as i64 };
453    let ms = compose_civil_ms(y, mo1 as i64 - 1, d as i64, hh as i64, mi as i64, ss, mms);
454    date_write_ms(&t, ms);
455    Ok(Value::Number(ms))
456}
457pub(crate) fn date_set_milliseconds(_: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
458    let (y, mo1, d, hh, mi, ss, _, _) = decompose_ms(date_ms(&t));
459    let mms = arg(a, 0).to_number() as i64;
460    let ms = compose_civil_ms(y, mo1 as i64 - 1, d as i64, hh as i64, mi as i64, ss as i64, mms);
461    date_write_ms(&t, ms);
462    Ok(Value::Number(ms))
463}
464pub fn date_get(key: &str) -> Value {
465    match key {
466        "getTime" | "valueOf" => nv("getTime", date_get_time),
467        "getFullYear" | "getUTCFullYear" => nv("getFullYear", date_get_full_year),
468        "getMonth" | "getUTCMonth" => nv("getMonth", date_get_month),
469        "getDate" | "getUTCDate" => nv("getDate", date_get_date),
470        "getHours" | "getUTCHours" => nv("getHours", date_get_hours),
471        "getMinutes" | "getUTCMinutes" => nv("getMinutes", date_get_minutes),
472        "getSeconds" | "getUTCSeconds" => nv("getSeconds", date_get_seconds),
473        "getMilliseconds" | "getUTCMilliseconds" => nv("getMilliseconds", date_get_milliseconds),
474        "getDay" | "getUTCDay" => nv("getDay", date_get_day),
475        // `getYear`/`setYear`(Annex B.2.4/B.2.5。2桁年時代の遺産だが仕様に
476        // なお残る)が丸ごと未対応だった。
477        "getYear" => nv("getYear", date_get_year),
478        "setYear" => nv("setYear", date_set_year),
479        "setTime" => nv("setTime", date_set_time),
480        "setFullYear" | "setUTCFullYear" => nv("setFullYear", date_set_full_year),
481        "setMonth" | "setUTCMonth" => nv("setMonth", date_set_month),
482        "setDate" | "setUTCDate" => nv("setDate", date_set_date),
483        "setHours" | "setUTCHours" => nv("setHours", date_set_hours),
484        "setMinutes" | "setUTCMinutes" => nv("setMinutes", date_set_minutes),
485        "setSeconds" | "setUTCSeconds" => nv("setSeconds", date_set_seconds),
486        "setMilliseconds" | "setUTCMilliseconds" => nv("setMilliseconds", date_set_milliseconds),
487        "toISOString" | "toJSON" => nv("toISOString", date_to_iso_string),
488        "toString" => nv("toString", date_to_string),
489        "toLocaleString" => nv("toLocaleString", date_to_locale_string),
490        "toDateString" => nv("toDateString", date_to_date_string),
491        "toLocaleDateString" => nv("toLocaleDateString", date_to_locale_date_string),
492        "toTimeString" => nv("toTimeString", date_to_time_string),
493        "toLocaleTimeString" => nv("toLocaleTimeString", date_to_locale_time_string),
494        // `Date.prototype.getTimezoneOffset()`(ES1。常に存在するはずの最基礎メソッドだが
495        // 丸ごと未登録だった)。このエンジンは「ローカル時刻」を常に UTC と同一視する簡略
496        // 実装(`get*`/`set*` に UTC 版とローカル版の区別が無いのと同じ方針)のため、
497        // 実際の TZ データベースを持たずとも仕様上正しく常に `0` を返せる。
498        "getTimezoneOffset" => nv("getTimezoneOffset", date_get_timezone_offset),
499        _ => Value::Undefined,
500    }
501}
502pub(crate) fn date_get_timezone_offset(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
503    Ok(Value::Number(0.0))
504}
505
506/// `toLocaleDateString`/`.toLocaleTimeString`/`.toLocaleString`が丸ごと
507/// `intl_datetime_format_format`(実体は`val.to_js_string()`、つまり
508/// `toISOString()`相当のISO文字列をそのまま返すだけのスタブ)へ委譲
509/// されており、日付のみ/時刻のみ/日時両方という3種の呼び分けが一切
510/// 無かったバグ(`toLocaleDateString()`が`"2024-01-15T00:00:00.000Z"`を
511/// 返し、本来の`"2024-01-15"`にならない等)。本格的なCLDRロケール
512/// データベースは無いため、`en-US`をはじめとする最も一般的な数値のみの
513/// 書式(`YYYY-MM-DD`/`HH:MM:SS`)に限定した簡略実装とする(曜日名・
514/// 月名等のロケール依存文字列出力、`options`引数によるフォーマット
515/// カスタマイズは非対応のまま)。2026-07-18 発見・実装。
516fn format_locale_date(ms: f64) -> String {
517    let (y, mo, d, ..) = decompose_ms(ms);
518    format!("{:04}-{:02}-{:02}", y, mo, d)
519}
520fn format_locale_time(ms: f64) -> String {
521    let (_, _, _, hh, mm, ss, ..) = decompose_ms(ms);
522    format!("{:02}:{:02}:{:02}", hh, mm, ss)
523}
524pub(crate) fn date_to_locale_string(_it: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
525    let ms = date_ms(&t);
526    Ok(Value::str(format!("{} {}", format_locale_date(ms), format_locale_time(ms))))
527}
528
529pub(crate) fn date_to_locale_date_string(_it: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
530    Ok(Value::str(format_locale_date(date_ms(&t))))
531}
532
533pub(crate) fn date_to_locale_time_string(_it: &mut Interp, t: Value, _a: &[Value]) -> Result<Value, Value> {
534    Ok(Value::str(format_locale_time(date_ms(&t))))
535}
536