java.time: complete LocalTime/LocalDate/Year/YearMonth ChronoField coverage

tick's fields-test walks every ChronoField a temporal supports and reads it,
which crashed on fields jolt didn't implement. Fill the gaps:

- LocalTime: CLOCK_HOUR_OF_DAY (1..24), HOUR_OF_AMPM, CLOCK_HOUR_OF_AMPM,
  MICRO_OF_DAY — both isSupported and getLong.
- LocalDate: the aligned-* group (ALIGNED_DAY_OF_WEEK_IN_MONTH/_YEAR,
  ALIGNED_WEEK_OF_MONTH/_YEAR).
- LocalDateTime field routing now asks which part supports the field instead of a
  hardcoded date list, so a date field never misroutes to the time part (the actual
  cause of "LocalTime has no field ALIGNED_DAY_OF_WEEK_IN_MONTH" — a ZonedDateTime's
  date field fell through to its time).
- Year / YearMonth gain isSupported / get / getLong.

tick api_test 344/0/1 -> 599/0/0. Seven JVM-certified corpus rows. make test green,
0 new divergences. Runtime .ss — no re-mint.
This commit is contained in:
Yogthos 2026-06-26 15:12:35 -04:00
parent dcfa607dc2
commit 14cd4eba46
2 changed files with 55 additions and 3 deletions

View file

@ -1116,20 +1116,37 @@
((string=? f "PROLEPTIC_MONTH") (+ (* y 12) (- m 1)))
((string=? f "YEAR_OF_ERA") (if (>= y 1) y (- 1 y)))
((string=? f "ERA") (if (>= y 1) 1 0))
;; aligned-* group the day-of-month/year into 7-day blocks from the
;; 1st (java.time): the within-block weekday is ((n-1) mod 7)+1, the
;; block number is ((n-1) quotient 7)+1.
((string=? f "ALIGNED_DAY_OF_WEEK_IN_MONTH") (+ (modulo (- d 1) 7) 1))
((string=? f "ALIGNED_WEEK_OF_MONTH") (+ (quotient (- d 1) 7) 1))
((string=? f "ALIGNED_DAY_OF_WEEK_IN_YEAR")
(+ (modulo (- (ld-day-of-year (ld-epoch-day t)) 1) 7) 1))
((string=? f "ALIGNED_WEEK_OF_YEAR")
(+ (quotient (- (ld-day-of-year (ld-epoch-day t)) 1) 7) 1))
(else (error #f (string-append "LocalDate has no field " f)))))))
((jt-time? t)
(cond ((string=? f "HOUR_OF_DAY") (lt-hour t)) ((string=? f "MINUTE_OF_HOUR") (lt-minute t))
((string=? f "SECOND_OF_MINUTE") (lt-second t)) ((string=? f "NANO_OF_SECOND") (lt-nano t))
((string=? f "NANO_OF_DAY") (lt-nano-of-day t))
((string=? f "MILLI_OF_DAY") (quotient (lt-nano-of-day t) 1000000))
((string=? f "MICRO_OF_DAY") (quotient (lt-nano-of-day t) 1000))
((string=? f "SECOND_OF_DAY") (quotient (lt-nano-of-day t) nanos-per-sec))
((string=? f "MINUTE_OF_DAY") (quotient (lt-nano-of-day t) (* 60 nanos-per-sec)))
((string=? f "MILLI_OF_SECOND") (quotient (lt-nano t) 1000000))
((string=? f "MICRO_OF_SECOND") (quotient (lt-nano t) 1000))
;; CLOCK_HOUR_OF_DAY is 1..24 (midnight is 24), HOUR_OF_AMPM 0..11,
;; CLOCK_HOUR_OF_AMPM 1..12, AMPM_OF_DAY 0 (AM) / 1 (PM).
((string=? f "CLOCK_HOUR_OF_DAY") (let ((h (lt-hour t))) (if (= h 0) 24 h)))
((string=? f "HOUR_OF_AMPM") (modulo (lt-hour t) 12))
((string=? f "CLOCK_HOUR_OF_AMPM") (let ((h (modulo (lt-hour t) 12))) (if (= h 0) 12 h)))
((string=? f "AMPM_OF_DAY") (quotient (lt-hour t) 12))
(else (error #f (string-append "LocalTime has no field " f)))))
((jt-dt? t)
(if (member f '("YEAR" "MONTH_OF_YEAR" "DAY_OF_MONTH" "DAY_OF_WEEK" "DAY_OF_YEAR" "EPOCH_DAY" "PROLEPTIC_MONTH" "YEAR_OF_ERA" "ERA"))
;; route a field to whichever part supports it (date fields incl. the
;; aligned-* group to the date, the rest to the time).
(if (temporal-supports-field? (ldt-date t) f)
(temporal-get-field (ldt-date t) f)
(temporal-get-field (ldt-time t) f)))
((jt-instant? t)
@ -1138,6 +1155,17 @@
((string=? f "MILLI_OF_SECOND") (jt-floor-div (jt-floor-mod (inst-nanos t) nanos-per-sec) 1000000))
((string=? f "MICRO_OF_SECOND") (jt-floor-div (jt-floor-mod (inst-nanos t) nanos-per-sec) 1000))
(else (error #f (string-append "Instant has no field " f)))))
((and (jhost? t) (string=? (jhost-tag t) "year"))
(let ((y (year-val t)))
(cond ((string=? f "YEAR") y) ((string=? f "YEAR_OF_ERA") (if (>= y 1) y (- 1 y)))
((string=? f "ERA") (if (>= y 1) 1 0))
(else (error #f (string-append "Year has no field " f))))))
((and (jhost? t) (string=? (jhost-tag t) "year-month"))
(let ((y (ym-year t)) (m (ym-month t)))
(cond ((string=? f "YEAR") y) ((string=? f "MONTH_OF_YEAR") m)
((string=? f "PROLEPTIC_MONTH") (+ (* y 12) (- m 1)))
((string=? f "YEAR_OF_ERA") (if (>= y 1) y (- 1 y))) ((string=? f "ERA") (if (>= y 1) 1 0))
(else (error #f (string-append "YearMonth has no field " f))))))
(else (error #f "get(field): unsupported temporal")))))
;; field set: (with temporal ChronoField value) -> a new temporal.
@ -1168,10 +1196,17 @@
(else #f))))
(define (temporal-supports-field? t field)
(let ((f (string-upcase field)))
(cond ((jt-date? t) (and (member f '("YEAR" "MONTH_OF_YEAR" "DAY_OF_MONTH" "DAY_OF_WEEK" "DAY_OF_YEAR" "EPOCH_DAY" "PROLEPTIC_MONTH" "YEAR_OF_ERA" "ERA")) #t))
((jt-time? t) (and (member f '("HOUR_OF_DAY" "MINUTE_OF_HOUR" "SECOND_OF_MINUTE" "NANO_OF_SECOND" "NANO_OF_DAY" "MILLI_OF_DAY" "SECOND_OF_DAY" "MINUTE_OF_DAY" "MILLI_OF_SECOND" "MICRO_OF_SECOND" "AMPM_OF_DAY")) #t))
(cond ((jt-date? t) (and (member f '("YEAR" "MONTH_OF_YEAR" "DAY_OF_MONTH" "DAY_OF_WEEK" "DAY_OF_YEAR" "EPOCH_DAY" "PROLEPTIC_MONTH" "YEAR_OF_ERA" "ERA"
"ALIGNED_DAY_OF_WEEK_IN_MONTH" "ALIGNED_DAY_OF_WEEK_IN_YEAR" "ALIGNED_WEEK_OF_MONTH" "ALIGNED_WEEK_OF_YEAR")) #t))
((jt-time? t) (and (member f '("HOUR_OF_DAY" "CLOCK_HOUR_OF_DAY" "HOUR_OF_AMPM" "CLOCK_HOUR_OF_AMPM" "AMPM_OF_DAY"
"MINUTE_OF_HOUR" "MINUTE_OF_DAY" "SECOND_OF_MINUTE" "SECOND_OF_DAY"
"MILLI_OF_SECOND" "MILLI_OF_DAY" "MICRO_OF_SECOND" "MICRO_OF_DAY"
"NANO_OF_SECOND" "NANO_OF_DAY")) #t))
((jt-dt? t) (or (temporal-supports-field? (ldt-date t) field) (temporal-supports-field? (ldt-time t) field)))
((jt-instant? t) (and (member f '("INSTANT_SECONDS" "NANO_OF_SECOND" "MILLI_OF_SECOND" "MICRO_OF_SECOND")) #t))
((and (jhost? t) (string=? (jhost-tag t) "year")) (and (member f '("YEAR" "YEAR_OF_ERA" "ERA")) #t))
((and (jhost? t) (string=? (jhost-tag t) "year-month"))
(and (member f '("YEAR" "MONTH_OF_YEAR" "PROLEPTIC_MONTH" "YEAR_OF_ERA" "ERA")) #t))
(else #f))))
;; isSupported / get / getLong / with / range / plus / minus / until accept a
@ -1213,6 +1248,16 @@
(register-host-methods! "local-date" (mk-temporal-methods))
(register-host-methods! "local-time" (mk-temporal-methods))
(register-host-methods! "local-date-time" (mk-temporal-methods))
;; Year/YearMonth answer the field accessors too (a fields-over-all-temporals walk
;; queries them); their own plus/minus/with stay the specific methods above.
(let ((field-methods
(list (cons "isSupported" (lambda (t x) (cond ((arg-is-unit? x) (temporal-supports-unit? t (cu-name x)))
((arg-is-field? x) (temporal-supports-field? t (cf-name x)))
(else #f))))
(cons "get" (lambda (t f) (temporal-get-field t (arg-field-name f))))
(cons "getLong" (lambda (t f) (temporal-get-field t (arg-field-name f)))))))
(register-host-methods! "year" field-methods)
(register-host-methods! "year-month" field-methods))
(register-host-methods! "instant" (mk-temporal-methods))
;; --- TemporalAdjuster: a date->date transform applied via (.with t adjuster) --

View file

@ -551,6 +551,13 @@
{:suite "interop / java.time shims" :label "Instant instance" :expected "true" :actual "(instance? java.time.Instant (Instant/ofEpochMilli 0))"}
{:suite "interop / java.time shims" :label "getTime epoch ms" :expected "0" :actual "(.getTime #inst \"1970-01-01T00:00:00Z\")"}
{:suite "interop / java.time shims" :label "toEpochMilli round trip" :expected "1234" :actual "(.toEpochMilli (Instant/ofEpochMilli 1234))"}
{:suite "interop / java.time chrono-fields" :label "CLOCK_HOUR_OF_DAY midnight is 24" :expected "24" :actual "(.getLong (java.time.LocalTime/of 0 30) java.time.temporal.ChronoField/CLOCK_HOUR_OF_DAY)"}
{:suite "interop / java.time chrono-fields" :label "HOUR_OF_AMPM" :expected "2" :actual "(.getLong (java.time.LocalTime/of 14 0) java.time.temporal.ChronoField/HOUR_OF_AMPM)"}
{:suite "interop / java.time chrono-fields" :label "CLOCK_HOUR_OF_AMPM noon is 12" :expected "12" :actual "(.getLong (java.time.LocalTime/of 12 0) java.time.temporal.ChronoField/CLOCK_HOUR_OF_AMPM)"}
{:suite "interop / java.time chrono-fields" :label "ALIGNED_DAY_OF_WEEK_IN_MONTH" :expected "3" :actual "(.getLong (java.time.LocalDate/of 2020 3 17) java.time.temporal.ChronoField/ALIGNED_DAY_OF_WEEK_IN_MONTH)"}
{:suite "interop / java.time chrono-fields" :label "ALIGNED_WEEK_OF_MONTH" :expected "3" :actual "(.getLong (java.time.LocalDate/of 2020 3 17) java.time.temporal.ChronoField/ALIGNED_WEEK_OF_MONTH)"}
{:suite "interop / java.time chrono-fields" :label "Year isSupported + getLong" :expected "2020" :actual "(.getLong (java.time.Year/of 2020) java.time.temporal.ChronoField/YEAR)"}
{:suite "interop / java.time chrono-fields" :label "YearMonth PROLEPTIC_MONTH" :expected "24244" :actual "(.getLong (java.time.YearMonth/of 2020 5) java.time.temporal.ChronoField/PROLEPTIC_MONTH)"}
{:suite "interop / java.time shims" :label "Instant/now is current" :expected "true" :actual "(> (.toEpochMilli (Instant/now)) 1500000000000)"}
{:suite "interop / java.time shims" :label "sql types are not" :expected "false" :actual "(instance? java.sql.Timestamp #inst \"2020-01-01T00:00:00Z\")"}
{:suite "interop / StringReader & StringBuilder" :label "StringReader read" :expected "[97 98 -1]" :actual "(let [r (java.io.StringReader. \"ab\")] [(.read r) (.read r) (.read r)])"}