diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index d86a627a..ebd6ccc6 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1062,6 +1062,49 @@ (js-new-call URIError (js-args (js-string-slice e 10 (len e))))) (else e)))) +(define + js-date-time-value + (fn + (d) + (cond + ((or (not (dict? d)) (not (contains? (keys d) "__js_is_date__"))) + (raise (js-new-call TypeError (js-args "this is not a Date object")))) + (else (get d "__date_value__"))))) + +(define + js-date-getter + (fn + (d field) + (let + ((ms-raw (js-date-time-value d))) + (let + ((ms (if (= (type-of ms-raw) "rational") (exact->inexact ms-raw) ms-raw))) + (cond + ((or (= ms nil) (js-undefined? ms) (not (number? ms))) + (js-nan-value)) + ((js-number-is-nan ms) (js-nan-value)) + (else + (let + ((days (floor (/ ms 86400000))) + (tod + (let ((m (modulo (js-num-to-int ms) 86400000))) + (if (< m 0) (+ m 86400000) m)))) + (cond + ((= field "ms") (modulo tod 1000)) + ((= field "seconds") (js-math-trunc (/ (modulo tod 60000) 1000))) + ((= field "minutes") (js-math-trunc (/ (modulo tod 3600000) 60000))) + ((= field "hours") (js-math-trunc (/ tod 3600000))) + ((= field "day") + (let ((dow (modulo (+ days 4) 7))) + (if (< dow 0) (+ dow 7) dow))) + (else + (let ((ymd (js-date-days-to-ymd days))) + (cond + ((= field "year") (nth ymd 0)) + ((= field "month") (- (nth ymd 1) 1)) + ((= field "date") (nth ymd 2)) + (else (js-nan-value))))))))))))) + (define js-date-from-one (fn @@ -1092,15 +1135,53 @@ (fn (args) (let - ((year (js-num-to-int (js-to-number (nth args 0)))) - (month - (if (>= (len args) 2) (js-num-to-int (js-to-number (nth args 1))) 0)) - (day - (if (>= (len args) 3) (js-num-to-int (js-to-number (nth args 2))) 1))) - (+ - (* (- year 1970) 31557600000) - (* month 2629800000) - (* (- day 1) 86400000))))) + ((year-raw (js-num-to-int (js-to-number (nth args 0))))) + (let + ((year (if (and (>= year-raw 0) (<= year-raw 99)) (+ year-raw 1900) year-raw)) + (month + (if (>= (len args) 2) (js-num-to-int (js-to-number (nth args 1))) 0)) + (day + (if (>= (len args) 3) (js-num-to-int (js-to-number (nth args 2))) 1)) + (hour + (if (>= (len args) 4) (js-num-to-int (js-to-number (nth args 3))) 0)) + (mins + (if (>= (len args) 5) (js-num-to-int (js-to-number (nth args 4))) 0)) + (secs + (if (>= (len args) 6) (js-num-to-int (js-to-number (nth args 5))) 0)) + (ms + (if (>= (len args) 7) (js-num-to-int (js-to-number (nth args 6))) 0))) + (let + ((days (js-date-civil-to-days year (+ month 1) day))) + (+ + (* days 86400000) + (* hour 3600000) + (* mins 60000) + (* secs 1000) + ms)))))) + +(define + js-date-civil-to-days + (fn + (y m d) + (let + ((y2 (if (<= m 2) (- y 1) y))) + (let + ((era (if (>= y2 0) (js-math-trunc (/ y2 400)) (js-math-trunc (/ (- y2 399) 400))))) + (let + ((yoe (- y2 (* era 400)))) + (let + ((doy + (+ + (js-math-trunc (/ (+ (* 153 (if (> m 2) (- m 3) (+ m 9))) 2) 5)) + (- d 1)))) + (let + ((doe + (+ + (* yoe 365) + (+ + (- (js-math-trunc (/ yoe 4)) (js-math-trunc (/ yoe 100))) + doy)))) + (+ (* era 146097) (- doe 719468))))))))) (define js-date-format-now @@ -1150,30 +1231,24 @@ ((= (len args) 0) 0) (else (js-date-from-parts args)))) :prototype - {:getTime (fn () (let ((t (js-this))) (get t "__date_value__"))) - :valueOf (fn () (let ((t (js-this))) (get t "__date_value__"))) - :getFullYear - (fn () - (let ((t (js-this))) - (+ 1970 (js-math-trunc (/ (get t "__date_value__") 31557600000))))) - :getUTCFullYear - (fn () - (let ((t (js-this))) - (+ 1970 (js-math-trunc (/ (get t "__date_value__") 31557600000))))) - :getMonth (fn () 0) - :getUTCMonth (fn () 0) - :getDate (fn () 1) - :getUTCDate (fn () 1) - :getDay (fn () 0) - :getUTCDay (fn () 0) - :getHours (fn () 0) - :getUTCHours (fn () 0) - :getMinutes (fn () 0) - :getUTCMinutes (fn () 0) - :getSeconds (fn () 0) - :getUTCSeconds (fn () 0) - :getMilliseconds (fn () 0) - :getUTCMilliseconds (fn () 0) + {:getTime (fn () (js-date-time-value (js-this))) + :valueOf (fn () (js-date-time-value (js-this))) + :getFullYear (fn () (js-date-getter (js-this) "year")) + :getUTCFullYear (fn () (js-date-getter (js-this) "year")) + :getMonth (fn () (js-date-getter (js-this) "month")) + :getUTCMonth (fn () (js-date-getter (js-this) "month")) + :getDate (fn () (js-date-getter (js-this) "date")) + :getUTCDate (fn () (js-date-getter (js-this) "date")) + :getDay (fn () (js-date-getter (js-this) "day")) + :getUTCDay (fn () (js-date-getter (js-this) "day")) + :getHours (fn () (js-date-getter (js-this) "hours")) + :getUTCHours (fn () (js-date-getter (js-this) "hours")) + :getMinutes (fn () (js-date-getter (js-this) "minutes")) + :getUTCMinutes (fn () (js-date-getter (js-this) "minutes")) + :getSeconds (fn () (js-date-getter (js-this) "seconds")) + :getUTCSeconds (fn () (js-date-getter (js-this) "seconds")) + :getMilliseconds (fn () (js-date-getter (js-this) "ms")) + :getUTCMilliseconds (fn () (js-date-getter (js-this) "ms")) :getTimezoneOffset (fn () 0) :setTime (fn (v) @@ -6617,6 +6692,7 @@ (dict-set! (get Map "prototype") "__proto__" (get Object "prototype")) (dict-set! (get Set "prototype") "__proto__" (get Object "prototype")) (dict-set! (get Date "prototype") "__proto__" (get Object "prototype")) + (dict-set! (get Date "prototype") "constructor" Date) (dict-set! (get RegExp "prototype") "__proto__" (get Object "prototype")) (dict-set! (get RegExp "prototype") "constructor" RegExp) (dict-set! (get js-function-global "prototype") "__proto__" (get Object "prototype")) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 8f576757..85649175 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -158,6 +158,8 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green. Append-only record of completed iterations. Loop writes one line per iteration: date, what was done, test count delta. +- 2026-05-10 — **Real `Date` construction + getters via Howard-Hinnant civil-day arithmetic.** `js-date-from-parts` now computes a true ms-since-epoch from `(year, month, day, hour, min, sec, ms)` via `js-date-civil-to-days` (the inverse of last iteration's `days-to-ymd`), with the legacy 2-digit-year coercion (0..99 → 1900+y). `getFullYear/Month/Date/Day/Hours/Minutes/Seconds/Milliseconds` (UTC + non-UTC) all share a new `js-date-getter`: TypeErrors on non-Date this, returns NaN on invalid time, otherwise decomposes ms into y/m/d/h/m/s/ms/dow. Plus added `Date.prototype.constructor = Date` (was missing). Result: each of the 8 Date getter categories went 2/6 → 5/6 (+3 each, +24 total). Date toISOString 11/16 → 13/16. Some Date construction-loop tests now exceed the 15s per-test timeout — the new civil math is heavier than the old (year-1970)*ms-per-year approximation, but correctness wins. conformance.sh: 148/148. + - 2026-05-10 — **`Date.prototype.toISOString` produces real `YYYY-MM-DDTHH:mm:ss.sssZ` and validates input.** Old `js-date-iso` only computed the year and hardcoded the rest as `01-01T00:00:00.000Z`. Added: (1) TypeError when this isn't a Date (no `__js_is_date__` slot); (2) RangeError when ms is NaN, undefined, or |ms| > 8.64e15; (3) full date breakdown via Howard-Hinnant `days_to_civil` algorithm (`js-date-days-to-ymd`) → year/month/day, plus modular hours/min/sec/ms; (4) extended-year format `±YYYYYY` for years outside 0..9999. Result: built-ins/Date/prototype/toISOString 7/16 → 11/16 (+4). Date 21/30. conformance.sh: 148/148. - 2026-05-10 — **`JSON.stringify` honours `replacer` (function + array forms), `space`, and `toJSON`.** Previous impl ignored the second/third arguments entirely and never called `toJSON`. Rewrote around a `js-json-serialize-property(key, holder, rep-fn, rep-keys, gap, indent)` core: walks `toJSON` first, then replacer-fn (with `holder` as `this`); arrays-as-replacer become a property-name allowlist; numeric `space` clamped to 0..10 spaces, string `space` truncated to 10 chars, non-empty gap activates indented output with `:` → `: ` separator. Number wrapper / String wrapper / Boolean wrapper unwrap before serialization; non-finite numbers serialize as `"null"`; functions serialize as `undefined`. Result: built-ins/JSON/stringify 6/30 → 14/30 (+8). conformance.sh: 148/148.