diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 1d8f4763..d86a627a 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1193,12 +1193,137 @@ js-date-iso (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 + (let + ((ms-raw (get d "__date_value__"))) + (let + ((ms (if (= (type-of ms-raw) "rational") (exact->inexact ms-raw) ms-raw))) + (cond + ((or (= ms nil) (js-undefined? ms)) + (raise (js-new-call RangeError (js-args "Invalid time value")))) + ((not (number? ms)) + (raise (js-new-call RangeError (js-args "Invalid time value")))) + ((js-number-is-nan ms) + (raise (js-new-call RangeError (js-args "Invalid time value")))) + ((or (> ms 8640000000000000) (< ms -8640000000000000)) + (raise (js-new-call RangeError (js-args "Invalid time value")))) + (else (js-date-iso-format ms))))))))) + +(define + js-date-iso-format + (fn + (ms) (let - ((ms (get d "__date_value__"))) + ((day-ms 86400000) (sec-ms 1000) (min-ms 60000) (hr-ms 3600000)) (let - ((year - (+ 1970 (js-math-trunc (/ ms 31557600000))))) - (str (js-date-year-pad year) "-01-01T00:00:00.000Z"))))) + ((days (floor (/ ms day-ms))) + (time-of-day + (let ((m (modulo (js-num-to-int ms) day-ms))) + (if (< m 0) (+ m day-ms) m)))) + (let + ((hh (js-math-trunc (/ time-of-day hr-ms))) + (mm (js-math-trunc (/ (modulo time-of-day hr-ms) min-ms))) + (ss (js-math-trunc (/ (modulo time-of-day min-ms) sec-ms))) + (msec (modulo time-of-day sec-ms)) + (ymd (js-date-days-to-ymd days))) + (let + ((y (nth ymd 0)) (mo (nth ymd 1)) (d (nth ymd 2))) + (str + (js-date-iso-year y) + "-" + (js-pad2 mo) + "-" + (js-pad2 d) + "T" + (js-pad2 hh) + ":" + (js-pad2 mm) + ":" + (js-pad2 ss) + "." + (js-pad3 msec) + "Z"))))))) + +(define + js-date-iso-year + (fn + (y) + (cond + ((or (< y 0) (> y 9999)) + (let + ((sign (if (< y 0) "-" "+")) + (ay (if (< y 0) (- 0 y) y))) + (str sign (js-date-year-6 ay)))) + ((< y 10) (str "000" (js-to-string y))) + ((< y 100) (str "00" (js-to-string y))) + ((< y 1000) (str "0" (js-to-string y))) + (else (js-to-string y))))) + +(define + js-date-year-6 + (fn + (y) + (cond + ((< y 10) (str "00000" (js-to-string y))) + ((< y 100) (str "0000" (js-to-string y))) + ((< y 1000) (str "000" (js-to-string y))) + ((< y 10000) (str "00" (js-to-string y))) + ((< y 100000) (str "0" (js-to-string y))) + (else (js-to-string y))))) + +(define js-pad2 (fn (n) (if (< n 10) (str "0" (js-to-string n)) (js-to-string n)))) + +(define + js-pad3 + (fn + (n) + (cond + ((< n 10) (str "00" (js-to-string n))) + ((< n 100) (str "0" (js-to-string n))) + (else (js-to-string n))))) + +(define + js-date-days-to-ymd + (fn + (days-since-epoch) + (let + ((d (+ days-since-epoch 719468))) + (let + ((era (if (>= d 0) (js-math-trunc (/ d 146097)) (js-math-trunc (/ (- d 146096) 146097))))) + (let + ((doe (- d (* era 146097)))) + (let + ((yoe + (js-math-trunc + (/ + (- + (+ + (- doe (js-math-trunc (/ doe 1460))) + (js-math-trunc (/ doe 36524))) + (js-math-trunc (/ doe 146096))) + 365)))) + (let + ((y (+ yoe (* era 400))) + (doy + (- + doe + (+ + (* yoe 365) + (- + (js-math-trunc (/ yoe 4)) + (js-math-trunc (/ yoe 100))))))) + (let + ((mp (js-math-trunc (/ (+ (* 5 doy) 2) 153)))) + (let + ((day (- (+ doy 1) (js-math-trunc (/ (+ (* 153 mp) 2) 5)))) + (month (if (< mp 10) (+ mp 3) (- mp 9)))) + (list + (if (<= month 2) (+ y 1) y) + month + day)))))))))) (define js-date-year-pad diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index c13ede91..8f576757 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 — **`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. - 2026-05-10 — **`JSON.parse` raises spec-correct `SyntaxError` instances and rejects malformed input.** Previously `JSON.parse("12 34")` silently returned `12` (no trailing-content check), `JSON.parse('""')` accepted control chars in strings, an unterminated string read off the end, and the inner `(error "JSON: ...")` calls produced generic Errors not `instanceof SyntaxError`. Added: (1) post-value whitespace skip + trailing-content check in `js-json-parse`; (2) control-char rejection (code < 0x20) and unterminated-string check in `js-json-parse-string-loop`; (3) all internal "JSON: ..." errors now `(raise (js-new-call SyntaxError ...))`. Result: built-ins/JSON/parse 7/30 → 25/30 (+18). JSON 26/30. conformance.sh: 148/148.