;; java.time core value types: LocalDate, LocalTime, LocalDateTime, Instant.
;;
;; Each is a tz-free jhost over an integer state:
;;   local-date      (vector epoch-day)                 day number from 1970-01-01
;;   local-time      (vector nano-of-day)               [0, 86_400_000_000_000)
;;   local-dt        (vector epoch-day nano-of-day)
;;   instant         (vector epoch-ms)                  reused from inst-time.ss
;;
;; The cljc.java-time wrappers are thin interop over these — static factories
;; (LocalDate/of …) reach register-class-statics!, instance methods reach the
;; per-tag method tables. Equality / hash / compare / print / instance? / protocol
;; dispatch are wired through the host registries.
;;
;; Precision: Instant is millisecond-granular (it carries epoch-ms only). get-nano
;; reports (ms-sub-second * 1e6); plus-nanos / plus-millis round to the millisecond.
;; LocalTime / LocalDateTime carry full nanosecond precision.

;; --- shared helpers ----------------------------------------------------------
(define nanos-per-day 86400000000000)
(define nanos-per-sec 1000000000)

(define (jt-floor-div a b) (let ((q (quotient a b)) (r (remainder a b)))
                             (if (and (not (= r 0)) (< (* a b) 0)) (- q 1) q)))
(define (jt-floor-mod a b) (- a (* (jt-floor-div a b) b)))
(define (jt->exact n) (cond ((and (number? n) (exact? n) (integer? n)) n)
                            ((number? n) (exact (truncate n)))
                            (else (error #f "java.time: not an integer" n))))

(define (jt-leap? y) (and (= 0 (modulo y 4)) (or (not (= 0 (modulo y 100))) (= 0 (modulo y 400)))))
(define (jt-len-of-month y m)
  (cond ((= m 2) (if (jt-leap? y) 29 28))
        ((memv m '(4 6 9 11)) 30)
        (else 31)))

;; constructors
(define (jt-local-date ed) (make-jhost "local-date" (vector ed)))
(define (jt-local-time nod) (make-jhost "local-time" (vector nod)))
(define (jt-local-dt ed nod) (make-jhost "local-date-time" (vector ed nod)))

;; state accessors
(define (ld-epoch-day d) (vector-ref (jhost-state d) 0))
(define (lt-nano-of-day t) (vector-ref (jhost-state t) 0))
;; the LocalDateTime jhost uses tag "local-date-time" with [epoch-day nano-of-day].
;; inst-time.ss's ms-based "local-dt" tag stays for the #inst formatting shim.
(define (ldt-epoch-day x) (vector-ref (jhost-state x) 0))
(define (ldt-nano-of-day x) (vector-ref (jhost-state x) 1))

;; civil <-> epoch-day (inst-time.ss owns days-from-civil / civil-from-days)
(define (ymd->epoch-day y m d) (days-from-civil y m d))
(define (epoch-day->ymd ed) (civil-from-days ed))   ; -> (values y m d)

;; build a LocalDate, normalizing month/day overflow the way java.time does:
;; the year/month roll, then the day is clamped to the month length.
(define (jt-date-of y m d)
  (let* ((ym (+ (* y 12) (- m 1)))
         (y2 (jt-floor-div ym 12))
         (m2 (+ 1 (jt-floor-mod ym 12)))
         (dom (min d (jt-len-of-month y2 m2))))
    (jt-local-date (ymd->epoch-day y2 m2 dom))))

;; day-of-week: 1970-01-01 is Thursday; java.time DayOfWeek is 1=Mon..7=Sun.
(define (ld-dow ed) (+ 1 (jt-floor-mod (+ ed 3) 7)))
(define jt-day-names (vector "MONDAY" "TUESDAY" "WEDNESDAY" "THURSDAY" "FRIDAY" "SATURDAY" "SUNDAY"))
(define jt-month-names (vector "JANUARY" "FEBRUARY" "MARCH" "APRIL" "MAY" "JUNE" "JULY"
                               "AUGUST" "SEPTEMBER" "OCTOBER" "NOVEMBER" "DECEMBER"))

(define (ld-day-of-year ed)
  (call-with-values (lambda () (epoch-day->ymd ed))
    (lambda (y m d) (+ 1 (- ed (ymd->epoch-day y 1 1))))))

;; nano-of-day <-> (h m s nano)
(define (hmsn->nano h m s nano) (+ (* (+ (* h 3600) (* m 60) s) nanos-per-sec) nano))
(define (lt-hour t) (quotient (lt-nano-of-day t) (* 3600 nanos-per-sec)))
(define (lt-minute t) (modulo (quotient (lt-nano-of-day t) (* 60 nanos-per-sec)) 60))
(define (lt-second t) (modulo (quotient (lt-nano-of-day t) nanos-per-sec) 60))
(define (lt-nano t) (modulo (lt-nano-of-day t) nanos-per-sec))

;; --- ISO-8601 rendering ------------------------------------------------------
(define (iso-date-str ed)
  (call-with-values (lambda () (epoch-day->ymd ed))
    (lambda (y m d) (string-append (pad4 y) "-" (pad2 m) "-" (pad2 d)))))

;; LocalTime: "HH:mm", "HH:mm:ss", or with a fractional second (3/6/9 digits).
(define (frac-digits nano)
  (cond ((= 0 (modulo nano 1000000)) (let ((s (number->string (quotient nano 1000000))))
                                       (string-append (make-string (max 0 (- 3 (string-length s))) #\0) s)))
        ((= 0 (modulo nano 1000)) (let ((s (number->string (quotient nano 1000))))
                                    (string-append (make-string (max 0 (- 6 (string-length s))) #\0) s)))
        (else (let ((s (number->string nano)))
                (string-append (make-string (max 0 (- 9 (string-length s))) #\0) s)))))
(define (iso-time-str nod)
  (let ((h (quotient nod (* 3600 nanos-per-sec)))
        (mi (modulo (quotient nod (* 60 nanos-per-sec)) 60))
        (s (modulo (quotient nod nanos-per-sec) 60))
        (nano (modulo nod nanos-per-sec)))
    (string-append (pad2 h) ":" (pad2 mi)
                   (if (and (= s 0) (= nano 0)) ""
                       (string-append ":" (pad2 s) (if (= nano 0) "" (string-append "." (frac-digits nano))))))))
(define (iso-datetime-str ed nod)
  (string-append (iso-date-str ed) "T" (iso-time-str nod)))
;; Instant: always UTC with a trailing Z; seconds always shown, millis only when nonzero.
(define (iso-instant-str ms)
  (let* ((ems (exact (truncate ms)))
         (secs (jt-floor-div ems 1000))
         (frac (- ems (* secs 1000)))
         (ed (jt-floor-div secs 86400))
         (sod (jt-floor-mod secs 86400))
         (nod (* (+ (* (quotient sod 3600) 3600) (* (quotient (modulo sod 3600) 60) 60) (modulo sod 60))
                 nanos-per-sec)))
    (string-append (iso-date-str ed) "T"
                   (pad2 (quotient sod 3600)) ":" (pad2 (modulo (quotient sod 60) 60)) ":" (pad2 (modulo sod 60))
                   (if (= frac 0) "" (string-append "." (frac-digits (* frac 1000000))))
                   "Z")))
;; nano-precise ISO instant. The fraction is shown in groups of 3 digits (millis,
;; micros, or nanos), matching DateTimeFormatter.ISO_INSTANT.
(define (iso-instant-str-nanos en)
  (let* ((secs (jt-floor-div en nanos-per-sec))
         (nano (jt-floor-mod en nanos-per-sec))
         (ed (jt-floor-div secs 86400))
         (sod (jt-floor-mod secs 86400)))
    (string-append (iso-date-str ed) "T"
                   (pad2 (quotient sod 3600)) ":" (pad2 (modulo (quotient sod 60) 60)) ":" (pad2 (modulo sod 60))
                   (cond ((= nano 0) "")
                         ((= 0 (modulo nano 1000000)) (string-append "." (frac-fixed nano 3)))
                         ((= 0 (modulo nano 1000)) (string-append "." (frac-fixed nano 6)))
                         (else (string-append "." (frac-fixed nano 9))))
                   "Z")))
;; nano (0..1e9) -> the leading `digits` of its 9-digit zero-padded form.
(define (frac-fixed nano digits)
  (let ((s9 (let ((s (number->string nano))) (string-append (make-string (max 0 (- 9 (string-length s))) #\0) s))))
    (substring s9 0 digits)))

;; --- ISO parsing -------------------------------------------------------------
(define (jt-str x) (if (string? x) x (jolt-str-render-one x)))
;; "yyyy-MM-dd" -> epoch-day
(define (parse-iso-date s)
  (let ((y (digits-at s 0 4)) (m (digits-at s 5 2)) (d (digits-at s 8 2)))
    (if (and y m d (char=? (string-ref s 4) #\-) (char=? (string-ref s 7) #\-))
        (ymd->epoch-day y m d)
        (error #f (string-append "could not parse LocalDate: " s)))))
;; "HH:mm[:ss[.fff…]]" -> nano-of-day
(define (parse-iso-time s)
  (let ((len (string-length s)))
    (let ((h (digits-at s 0 2)) (mi (digits-at s 3 2)))
      (unless (and h mi (char=? (string-ref s 2) #\:))
        (error #f (string-append "could not parse LocalTime: " s)))
      (let ((s2 (and (> len 5) (char=? (string-ref s 5) #\:) (digits-at s 6 2))))
        (if (not s2)
            (hmsn->nano h mi 0 0)
            (let loop ((i 8) (nano 0))   ; optional .fraction
              (if (and (< i len) (char=? (string-ref s 8) #\.))
                  (let frac ((j 9) (k 0) (acc 0))
                    (if (and (< j len) (digit? (string-ref s j)))
                        (frac (+ j 1) (+ k 1) (+ (* acc 10) (- (char->integer (string-ref s j)) 48)))
                        (hmsn->nano h mi s2 (* acc (expt 10 (max 0 (- 9 k)))))))
                  (hmsn->nano h mi s2 0))))))))
;; "yyyy-MM-ddTHH:mm[:ss[.fff]]" -> (values epoch-day nano-of-day)
(define (parse-iso-datetime s)
  (let ((ti (let loop ((i 0)) (cond ((>= i (string-length s)) #f)
                                    ((or (char=? (string-ref s i) #\T) (char=? (string-ref s i) #\t)) i)
                                    (else (loop (+ i 1)))))))
    (unless ti (error #f (string-append "could not parse LocalDateTime: " s)))
    (values (parse-iso-date (substring s 0 ti))
            (parse-iso-time (substring s (+ ti 1) (string-length s))))))

;; the offset/zone suffix start in an ISO instant/offset string: the first
;; Z/z/+/- after the 'T', or #f if local-only. (parse-zone-offset / the [zone]
;; bracket handle the suffix itself.)
(define (iso-offset-pos s)
  (let ((tpos (let loop ((i 0)) (cond ((>= i (string-length s)) 0)
                                      ((memv (string-ref s i) '(#\T #\t)) i)
                                      (else (loop (+ i 1)))))))
    (let loop ((i (+ tpos 1)))
      (cond ((>= i (string-length s)) #f)
            ((memv (string-ref s i) '(#\Z #\z #\+ #\-)) i)
            (else (loop (+ i 1)))))))
;; parse an ISO-8601 instant ("…T…[.fffffffff](Z|±HH:mm)") -> UTC epoch-nanos,
;; preserving full fractional-second precision.
(define (parse-iso-instant-nanos s)
  (let* ((opos (iso-offset-pos s))
         (local (if opos (substring s 0 opos) s))
         (osuf (if opos (substring s opos (string-length s)) "Z"))
         (off (if (or (string=? osuf "Z") (string=? osuf "z")) 0 (parse-zone-offset osuf))))
    (call-with-values (lambda () (parse-iso-datetime local))
      (lambda (ed nod) (- (+ (* ed nanos-per-day) nod) (* off nanos-per-sec))))))

;; --- LocalDate ---------------------------------------------------------------
(define ld-min (jt-local-date (ymd->epoch-day -999999999 1 1)))
(define ld-max (jt-local-date (ymd->epoch-day 999999999 12 31)))

(register-class-statics! "LocalDate"
  (list (cons "of" (lambda (y m d) (jt-date-of (jt->exact y) (jt->exact m) (jt->exact d))))
        (cons "ofEpochDay" (lambda (n) (jt-local-date (jt->exact n))))
        (cons "ofYearDay" (lambda (y doy) (jt-local-date (+ (ymd->epoch-day (jt->exact y) 1 1) (- (jt->exact doy) 1)))))
        (cons "parse" (lambda (s . _) (jt-local-date (parse-iso-date (jt-str s)))))
        (cons "now" (lambda _ (mk-local-date (now-ms))))
        (cons "from" (lambda (t) (cond ((and (jhost? t) (string=? (jhost-tag t) "local-date")) t)
                                       ((and (jhost? t) (string=? (jhost-tag t) "local-date-time")) (jt-local-date (ldt-epoch-day t)))
                                       (else (mk-local-date (ms-of t))))))
        (cons "MIN" ld-min)
        (cons "MAX" ld-max)))

(define (ld-plus-months d n)
  (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
    (lambda (y m dom)
      (let* ((ym (+ (* y 12) (- m 1) n))
             (y2 (jt-floor-div ym 12)) (m2 (+ 1 (jt-floor-mod ym 12))))
        (jt-local-date (ymd->epoch-day y2 m2 (min dom (jt-len-of-month y2 m2))))))))
(define (ld-plus-years d n)
  (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
    (lambda (y m dom) (jt-local-date (ymd->epoch-day (+ y n) m (min dom (jt-len-of-month (+ y n) m)))))))

(define (ld-with-field d which v)
  (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
    (lambda (y m dom)
      (case which
        ((year) (jt-local-date (ymd->epoch-day v m (min dom (jt-len-of-month v m)))))
        ((month) (jt-local-date (ymd->epoch-day y v (min dom (jt-len-of-month y v)))))
        ((day) (jt-local-date (ymd->epoch-day y m v)))
        ((day-of-year) (jt-local-date (+ (ymd->epoch-day y 1 1) (- v 1))))))))

(register-host-methods! "local-date"
  (list (cons "getYear" (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) y))))
        (cons "getMonthValue" (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) m))))
        (cons "getDayOfMonth" (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) dd))))
        (cons "getMonth" (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
                                       (lambda (y m dd) (make-jhost "month-enum" (vector m))))))
        (cons "getDayOfWeek" (lambda (d) (make-jhost "dow-enum" (vector (ld-dow (ld-epoch-day d))))))
        (cons "getDayOfYear" (lambda (d) (ld-day-of-year (ld-epoch-day d))))
        (cons "toEpochDay" (lambda (d) (ld-epoch-day d)))
        (cons "lengthOfMonth" (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
                                            (lambda (y m dd) (jt-len-of-month y m)))))
        (cons "lengthOfYear" (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
                                           (lambda (y m dd) (if (jt-leap? y) 366 365)))))
        (cons "isLeapYear" (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
                                         (lambda (y m dd) (jt-leap? y)))))
        (cons "plusDays" (lambda (d n) (jt-local-date (+ (ld-epoch-day d) (jt->exact n)))))
        (cons "minusDays" (lambda (d n) (jt-local-date (- (ld-epoch-day d) (jt->exact n)))))
        (cons "plusWeeks" (lambda (d n) (jt-local-date (+ (ld-epoch-day d) (* 7 (jt->exact n))))))
        (cons "minusWeeks" (lambda (d n) (jt-local-date (- (ld-epoch-day d) (* 7 (jt->exact n))))))
        (cons "plusMonths" (lambda (d n) (ld-plus-months d (jt->exact n))))
        (cons "minusMonths" (lambda (d n) (ld-plus-months d (- (jt->exact n)))))
        (cons "plusYears" (lambda (d n) (ld-plus-years d (jt->exact n))))
        (cons "minusYears" (lambda (d n) (ld-plus-years d (- (jt->exact n)))))
        (cons "withYear" (lambda (d v) (ld-with-field d 'year (jt->exact v))))
        (cons "withMonth" (lambda (d v) (ld-with-field d 'month (jt->exact v))))
        (cons "withDayOfMonth" (lambda (d v) (ld-with-field d 'day (jt->exact v))))
        (cons "withDayOfYear" (lambda (d v) (ld-with-field d 'day-of-year (jt->exact v))))
        (cons "isBefore" (lambda (d o) (< (ld-epoch-day d) (ld-epoch-day o))))
        (cons "isAfter" (lambda (d o) (> (ld-epoch-day d) (ld-epoch-day o))))
        (cons "isEqual" (lambda (d o) (= (ld-epoch-day d) (ld-epoch-day o))))
        (cons "compareTo" (lambda (d o) (let ((a (ld-epoch-day d)) (b (ld-epoch-day o)))
                                          (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (d o) (and (jhost? o) (string=? (jhost-tag o) "local-date") (= (ld-epoch-day d) (ld-epoch-day o)))))
        (cons "hashCode" (lambda (d) (ld-epoch-day d)))
        (cons "atStartOfDay" (lambda (d . _) (jt-local-dt (ld-epoch-day d) 0)))
        (cons "atTime" (case-lambda
                         ((d t) (if (and (jhost? t) (string=? (jhost-tag t) "local-time"))
                                    (jt-local-dt (ld-epoch-day d) (lt-nano-of-day t))
                                    (error #f "atTime: expected LocalTime")))
                         ((d h m) (jt-local-dt (ld-epoch-day d) (hmsn->nano (jt->exact h) (jt->exact m) 0 0)))
                         ((d h m s) (jt-local-dt (ld-epoch-day d) (hmsn->nano (jt->exact h) (jt->exact m) (jt->exact s) 0)))
                         ((d h m s nano) (jt-local-dt (ld-epoch-day d) (hmsn->nano (jt->exact h) (jt->exact m) (jt->exact s) (jt->exact nano))))))
        (cons "toString" (lambda (d) (iso-date-str (ld-epoch-day d))))))

;; --- LocalTime ---------------------------------------------------------------
(define lt-min (jt-local-time 0))
(define lt-max (jt-local-time (- nanos-per-day 1)))

(register-class-statics! "LocalTime"
  (list (cons "of" (case-lambda
                     ((h m) (jt-local-time (hmsn->nano (jt->exact h) (jt->exact m) 0 0)))
                     ((h m s) (jt-local-time (hmsn->nano (jt->exact h) (jt->exact m) (jt->exact s) 0)))
                     ((h m s nano) (jt-local-time (hmsn->nano (jt->exact h) (jt->exact m) (jt->exact s) (jt->exact nano))))))
        (cons "ofNanoOfDay" (lambda (n) (jt-local-time (jt->exact n))))
        (cons "ofSecondOfDay" (lambda (n) (jt-local-time (* (jt->exact n) nanos-per-sec))))
        (cons "parse" (lambda (s . _) (jt-local-time (parse-iso-time (jt-str s)))))
        (cons "now" (lambda _ (jt-local-time (let ((ms (now-ms))) (* (jt-floor-mod (exact (truncate ms)) 86400000) 1000000)))))
        (cons "from" (lambda (t) (cond ((and (jhost? t) (string=? (jhost-tag t) "local-time")) t)
                                       ((and (jhost? t) (string=? (jhost-tag t) "local-date-time")) (jt-local-time (ldt-nano-of-day t)))
                                       (else (error #f "LocalTime/from: unsupported")))))
        (cons "MIN" lt-min)
        (cons "MAX" lt-max)
        (cons "MIDNIGHT" (jt-local-time 0))
        (cons "NOON" (jt-local-time (* 12 3600 nanos-per-sec)))))

(define (lt-plus t nanos) (jt-local-time (jt-floor-mod (+ (lt-nano-of-day t) nanos) nanos-per-day)))
(define (lt-with t which v)
  (let ((h (lt-hour t)) (mi (lt-minute t)) (s (lt-second t)) (nano (lt-nano t)))
    (case which
      ((hour) (jt-local-time (hmsn->nano v mi s nano)))
      ((minute) (jt-local-time (hmsn->nano h v s nano)))
      ((second) (jt-local-time (hmsn->nano h mi v nano)))
      ((nano) (jt-local-time (hmsn->nano h mi s v))))))

(register-host-methods! "local-time"
  (list (cons "getHour" (lambda (t) (lt-hour t)))
        (cons "getMinute" (lambda (t) (lt-minute t)))
        (cons "getSecond" (lambda (t) (lt-second t)))
        (cons "getNano" (lambda (t) (lt-nano t)))
        (cons "toNanoOfDay" (lambda (t) (lt-nano-of-day t)))
        (cons "toSecondOfDay" (lambda (t) (quotient (lt-nano-of-day t) nanos-per-sec)))
        (cons "plusHours" (lambda (t n) (lt-plus t (* (jt->exact n) 3600 nanos-per-sec))))
        (cons "minusHours" (lambda (t n) (lt-plus t (- (* (jt->exact n) 3600 nanos-per-sec)))))
        (cons "plusMinutes" (lambda (t n) (lt-plus t (* (jt->exact n) 60 nanos-per-sec))))
        (cons "minusMinutes" (lambda (t n) (lt-plus t (- (* (jt->exact n) 60 nanos-per-sec)))))
        (cons "plusSeconds" (lambda (t n) (lt-plus t (* (jt->exact n) nanos-per-sec))))
        (cons "minusSeconds" (lambda (t n) (lt-plus t (- (* (jt->exact n) nanos-per-sec)))))
        (cons "plusNanos" (lambda (t n) (lt-plus t (jt->exact n))))
        (cons "minusNanos" (lambda (t n) (lt-plus t (- (jt->exact n)))))
        (cons "withHour" (lambda (t v) (lt-with t 'hour (jt->exact v))))
        (cons "withMinute" (lambda (t v) (lt-with t 'minute (jt->exact v))))
        (cons "withSecond" (lambda (t v) (lt-with t 'second (jt->exact v))))
        (cons "withNano" (lambda (t v) (lt-with t 'nano (jt->exact v))))
        (cons "truncatedTo" (lambda (t u) (lt-truncate t u)))
        (cons "isBefore" (lambda (t o) (< (lt-nano-of-day t) (lt-nano-of-day o))))
        (cons "isAfter" (lambda (t o) (> (lt-nano-of-day t) (lt-nano-of-day o))))
        (cons "compareTo" (lambda (t o) (let ((a (lt-nano-of-day t)) (b (lt-nano-of-day o)))
                                          (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (t o) (and (jhost? o) (string=? (jhost-tag o) "local-time") (= (lt-nano-of-day t) (lt-nano-of-day o)))))
        (cons "hashCode" (lambda (t) (lt-nano-of-day t)))
        (cons "atDate" (lambda (t d) (jt-local-dt (ld-epoch-day d) (lt-nano-of-day t))))
        (cons "toString" (lambda (t) (iso-time-str (lt-nano-of-day t))))))

;; truncatedTo a ChronoUnit: zero out below the unit. The unit arrives as a
;; chrono-unit jhost (name) or a keyword/string; common units only.
(define (chrono-unit-name u)
  (cond ((and (jhost? u) (string=? (jhost-tag u) "chrono-unit")) (vector-ref (jhost-state u) 0))
        ((string? u) u)
        ((keyword? u) (keyword-t-name u))
        (else #f)))
(define (lt-truncate t u)
  (let* ((nod (lt-nano-of-day t))
         (unit (chrono-unit-name u))
         (div (cond ((not unit) 1)
                    ((string-ci=? unit "NANOS") 1)
                    ((string-ci=? unit "MICROS") 1000)
                    ((string-ci=? unit "MILLIS") 1000000)
                    ((string-ci=? unit "SECONDS") nanos-per-sec)
                    ((string-ci=? unit "MINUTES") (* 60 nanos-per-sec))
                    ((string-ci=? unit "HOURS") (* 3600 nanos-per-sec))
                    ((string-ci=? unit "DAYS") nanos-per-day)
                    (else 1))))
    (jt-local-time (* (quotient nod div) div))))

;; --- LocalDateTime -----------------------------------------------------------
(define ldt-min (jt-local-dt (ymd->epoch-day -999999999 1 1) 0))
(define ldt-max (jt-local-dt (ymd->epoch-day 999999999 12 31) (- nanos-per-day 1)))

;; epoch-seconds at a zero offset (the tz-free layer treats LocalDateTime as UTC).
(define (ldt->epoch-second x) (+ (* (ldt-epoch-day x) 86400) (quotient (ldt-nano-of-day x) nanos-per-sec)))
(define (ldt->ms x) (+ (* (ldt-epoch-day x) 86400000) (quotient (ldt-nano-of-day x) 1000000)))

(register-class-statics! "LocalDateTime"
  (list (cons "of" (case-lambda
                     ((d t) (jt-local-dt (ld-epoch-day d) (lt-nano-of-day t)))   ; (LocalDate LocalTime)
                     ((y mo d h mi) (jt-local-dt (ymd->epoch-day (jt->exact y) (jt->exact mo) (jt->exact d))
                                                 (hmsn->nano (jt->exact h) (jt->exact mi) 0 0)))
                     ((y mo d h mi s) (jt-local-dt (ymd->epoch-day (jt->exact y) (jt->exact mo) (jt->exact d))
                                                   (hmsn->nano (jt->exact h) (jt->exact mi) (jt->exact s) 0)))
                     ((y mo d h mi s nano) (jt-local-dt (ymd->epoch-day (jt->exact y) (jt->exact mo) (jt->exact d))
                                                        (hmsn->nano (jt->exact h) (jt->exact mi) (jt->exact s) (jt->exact nano))))))
        (cons "ofEpochSecond" (lambda (secs nano off)
                                (let ((es (jt->exact secs)))
                                  (jt-local-dt (jt-floor-div es 86400)
                                               (+ (* (jt-floor-mod es 86400) nanos-per-sec) (jt->exact nano))))))
        (cons "ofInstant" (lambda (inst . _) (let ((ms (ms-of inst)))
                                               (jt-local-dt (jt-floor-div (exact (truncate ms)) 86400000)
                                                            (* (jt-floor-mod (exact (truncate ms)) 86400000) 1000000)))))
        (cons "parse" (lambda (s . _) (call-with-values (lambda () (parse-iso-datetime (jt-str s)))
                                        (lambda (ed nod) (jt-local-dt ed nod)))))
        (cons "now" (lambda _ (let ((ms (exact (truncate (now-ms)))))
                                (jt-local-dt (jt-floor-div ms 86400000) (* (jt-floor-mod ms 86400000) 1000000)))))
        (cons "from" (lambda (t) (cond ((and (jhost? t) (string=? (jhost-tag t) "local-date-time")) t)
                                       (else (let ((ms (ms-of t)))
                                               (jt-local-dt (jt-floor-div (exact (truncate ms)) 86400000)
                                                            (* (jt-floor-mod (exact (truncate ms)) 86400000) 1000000)))))))
        (cons "MIN" ldt-min)
        (cons "MAX" ldt-max)))

;; date / time arithmetic on a LocalDateTime: round-trip through the components.
(define (ldt-date x) (jt-local-date (ldt-epoch-day x)))
(define (ldt-time x) (jt-local-time (ldt-nano-of-day x)))
(define (ldt-combine d t) (jt-local-dt (ld-epoch-day d) (lt-nano-of-day t)))
;; add nanos to a LocalDateTime, carrying whole days into the date.
(define (ldt-plus-nanos x nanos)
  (let* ((total (+ (ldt-nano-of-day x) nanos))
         (carry (jt-floor-div total nanos-per-day))
         (nod (jt-floor-mod total nanos-per-day)))
    (jt-local-dt (+ (ldt-epoch-day x) carry) nod)))

(register-host-methods! "local-date-time"
  (list (cons "getYear" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (ldt-epoch-day x))) (lambda (y m d) y))))
        (cons "getMonthValue" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (ldt-epoch-day x))) (lambda (y m d) m))))
        (cons "getMonth" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (ldt-epoch-day x))) (lambda (y m d) (make-jhost "month-enum" (vector m))))))
        (cons "getDayOfMonth" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (ldt-epoch-day x))) (lambda (y m d) d))))
        (cons "getDayOfWeek" (lambda (x) (make-jhost "dow-enum" (vector (ld-dow (ldt-epoch-day x))))))
        (cons "getDayOfYear" (lambda (x) (ld-day-of-year (ldt-epoch-day x))))
        (cons "getHour" (lambda (x) (lt-hour (ldt-time x))))
        (cons "getMinute" (lambda (x) (lt-minute (ldt-time x))))
        (cons "getSecond" (lambda (x) (lt-second (ldt-time x))))
        (cons "getNano" (lambda (x) (lt-nano (ldt-time x))))
        (cons "toLocalDate" (lambda (x) (ldt-date x)))
        (cons "toLocalTime" (lambda (x) (ldt-time x)))
        (cons "toEpochSecond" (lambda (x . _) (ldt->epoch-second x)))
        (cons "plusDays" (lambda (x n) (jt-local-dt (+ (ldt-epoch-day x) (jt->exact n)) (ldt-nano-of-day x))))
        (cons "minusDays" (lambda (x n) (jt-local-dt (- (ldt-epoch-day x) (jt->exact n)) (ldt-nano-of-day x))))
        (cons "plusWeeks" (lambda (x n) (jt-local-dt (+ (ldt-epoch-day x) (* 7 (jt->exact n))) (ldt-nano-of-day x))))
        (cons "minusWeeks" (lambda (x n) (jt-local-dt (- (ldt-epoch-day x) (* 7 (jt->exact n))) (ldt-nano-of-day x))))
        (cons "plusMonths" (lambda (x n) (ldt-combine (ld-plus-months (ldt-date x) (jt->exact n)) (ldt-time x))))
        (cons "minusMonths" (lambda (x n) (ldt-combine (ld-plus-months (ldt-date x) (- (jt->exact n))) (ldt-time x))))
        (cons "plusYears" (lambda (x n) (ldt-combine (ld-plus-years (ldt-date x) (jt->exact n)) (ldt-time x))))
        (cons "minusYears" (lambda (x n) (ldt-combine (ld-plus-years (ldt-date x) (- (jt->exact n))) (ldt-time x))))
        (cons "plusHours" (lambda (x n) (ldt-plus-nanos x (* (jt->exact n) 3600 nanos-per-sec))))
        (cons "minusHours" (lambda (x n) (ldt-plus-nanos x (- (* (jt->exact n) 3600 nanos-per-sec)))))
        (cons "plusMinutes" (lambda (x n) (ldt-plus-nanos x (* (jt->exact n) 60 nanos-per-sec))))
        (cons "minusMinutes" (lambda (x n) (ldt-plus-nanos x (- (* (jt->exact n) 60 nanos-per-sec)))))
        (cons "plusSeconds" (lambda (x n) (ldt-plus-nanos x (* (jt->exact n) nanos-per-sec))))
        (cons "minusSeconds" (lambda (x n) (ldt-plus-nanos x (- (* (jt->exact n) nanos-per-sec)))))
        (cons "plusNanos" (lambda (x n) (ldt-plus-nanos x (jt->exact n))))
        (cons "minusNanos" (lambda (x n) (ldt-plus-nanos x (- (jt->exact n)))))
        (cons "withYear" (lambda (x v) (ldt-combine (ld-with-field (ldt-date x) 'year (jt->exact v)) (ldt-time x))))
        (cons "withMonth" (lambda (x v) (ldt-combine (ld-with-field (ldt-date x) 'month (jt->exact v)) (ldt-time x))))
        (cons "withDayOfMonth" (lambda (x v) (ldt-combine (ld-with-field (ldt-date x) 'day (jt->exact v)) (ldt-time x))))
        (cons "withDayOfYear" (lambda (x v) (ldt-combine (ld-with-field (ldt-date x) 'day-of-year (jt->exact v)) (ldt-time x))))
        (cons "withHour" (lambda (x v) (ldt-combine (ldt-date x) (lt-with (ldt-time x) 'hour (jt->exact v)))))
        (cons "withMinute" (lambda (x v) (ldt-combine (ldt-date x) (lt-with (ldt-time x) 'minute (jt->exact v)))))
        (cons "withSecond" (lambda (x v) (ldt-combine (ldt-date x) (lt-with (ldt-time x) 'second (jt->exact v)))))
        (cons "withNano" (lambda (x v) (ldt-combine (ldt-date x) (lt-with (ldt-time x) 'nano (jt->exact v)))))
        (cons "truncatedTo" (lambda (x u) (ldt-combine (ldt-date x) (lt-truncate (ldt-time x) u))))
        (cons "isBefore" (lambda (x o) (ldt<? x o)))
        (cons "isAfter" (lambda (x o) (ldt<? o x)))
        (cons "isEqual" (lambda (x o) (ldt=? x o)))
        (cons "compareTo" (lambda (x o) (ldt-cmp x o)))
        (cons "equals" (lambda (x o) (and (jhost? o) (string=? (jhost-tag o) "local-date-time") (ldt=? x o))))
        (cons "hashCode" (lambda (x) (+ (* (ldt-epoch-day x) 31) (ldt-nano-of-day x))))
        (cons "atZone" (lambda (x zone) (zoned-of-ldt x zone)))
        (cons "atOffset" (lambda (x off) (offset-of-ldt x off)))
        (cons "toInstant" (lambda (x . _) (mk-instant (ldt->ms x))))
        (cons "toString" (lambda (x) (iso-datetime-str (ldt-epoch-day x) (ldt-nano-of-day x))))))

(define (ldt-cmp x o)
  (cond ((< (ldt-epoch-day x) (ldt-epoch-day o)) -1)
        ((> (ldt-epoch-day x) (ldt-epoch-day o)) 1)
        ((< (ldt-nano-of-day x) (ldt-nano-of-day o)) -1)
        ((> (ldt-nano-of-day x) (ldt-nano-of-day o)) 1)
        (else 0)))
(define (ldt<? x o) (< (ldt-cmp x o) 0))
(define (ldt=? x o) (= (ldt-cmp x o) 0))

;; --- Instant (extend the existing inst-time.ss "instant" jhost) --------------
;; nano-precise: state is epoch-nanos (inst-nanos). inst-ms projects to ms (floor)
;; for the ms-based zone/Date call sites; mk-instant-nanos / inst-nanos own the
;; nano arithmetic.
(define (inst-ms x) (jt-floor-div (inst-nanos x) 1000000))
;; truncatedTo unit -> nanos, or #f for an unsupported unit (no-op).
(define (instant-unit-nanos u)
  (let ((unit (chrono-unit-name u)))
    (cond ((and unit (string-ci=? unit "DAYS")) (* 86400 nanos-per-sec))
          ((and unit (string-ci=? unit "HALF_DAYS")) (* 43200 nanos-per-sec))
          ((and unit (string-ci=? unit "HOURS")) (* 3600 nanos-per-sec))
          ((and unit (string-ci=? unit "MINUTES")) (* 60 nanos-per-sec))
          ((and unit (string-ci=? unit "SECONDS")) nanos-per-sec)
          ((and unit (string-ci=? unit "MILLIS")) 1000000)
          ((and unit (string-ci=? unit "MICROS")) 1000)
          (else 1))))
(register-class-statics! "Instant"
  (list (cons "ofEpochSecond" (case-lambda
                                ((s) (mk-instant-nanos (* (jt->exact s) nanos-per-sec)))
                                ((s nano) (mk-instant-nanos (+ (* (jt->exact s) nanos-per-sec) (jt->exact nano))))))
        (cons "EPOCH" (mk-instant-nanos 0))
        (cons "MIN" (mk-instant-nanos (* (ymd->epoch-day -999999999 1 1) 86400 nanos-per-sec)))
        (cons "MAX" (mk-instant-nanos (+ (* (ymd->epoch-day 999999999 12 31) 86400 nanos-per-sec)
                                         (* 86399 nanos-per-sec) 999999999)))
        ;; nano-precise ISO parse, overriding the ms-granular inst-time.ss parse.
        ;; Falls back to the ms parser for extended-year (±999999999) MIN/MAX strings
        ;; the fixed-width date parser can't read.
        (cons "parse" (lambda (s . _)
                        (let ((str (if (string? s) s (jolt-str-render-one s))))
                          (guard (e (#t (mk-instant (jinst-ms (jolt-inst-from-string str)))))
                            (mk-instant-nanos (parse-iso-instant-nanos str))))))))

(register-host-methods! "instant"
  (list (cons "getEpochSecond" (lambda (x) (jt-floor-div (inst-nanos x) nanos-per-sec)))
        (cons "getNano" (lambda (x) (jt-floor-mod (inst-nanos x) nanos-per-sec)))
        (cons "toEpochMilli" (lambda (x) (jt-floor-div (inst-nanos x) 1000000)))
        (cons "plusMillis" (lambda (x n) (mk-instant-nanos (+ (inst-nanos x) (* (jt->exact n) 1000000)))))
        (cons "minusMillis" (lambda (x n) (mk-instant-nanos (- (inst-nanos x) (* (jt->exact n) 1000000)))))
        (cons "plusSeconds" (lambda (x n) (mk-instant-nanos (+ (inst-nanos x) (* (jt->exact n) nanos-per-sec)))))
        (cons "minusSeconds" (lambda (x n) (mk-instant-nanos (- (inst-nanos x) (* (jt->exact n) nanos-per-sec)))))
        (cons "plusNanos" (lambda (x n) (mk-instant-nanos (+ (inst-nanos x) (jt->exact n)))))
        (cons "minusNanos" (lambda (x n) (mk-instant-nanos (- (inst-nanos x) (jt->exact n)))))
        (cons "isBefore" (lambda (x o) (< (inst-nanos x) (inst-nanos o))))
        (cons "isAfter" (lambda (x o) (> (inst-nanos x) (inst-nanos o))))
        (cons "compareTo" (lambda (x o) (let ((a (inst-nanos x)) (b (inst-nanos o)))
                                          (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (x o) (and (jhost? o) (string=? (jhost-tag o) "instant") (= (inst-nanos x) (inst-nanos o)))))
        (cons "hashCode" (lambda (x) (inst-nanos x)))
        (cons "truncatedTo" (lambda (x u) (let ((d (instant-unit-nanos u)))
                                            (mk-instant-nanos (* (jt-floor-div (inst-nanos x) d) d)))))
        (cons "atOffset" (lambda (x off) (offset-of-instant-nanos (inst-nanos x) off)))
        (cons "atZone" (lambda (x zone) (zoned-of-instant-nanos (inst-nanos x) zone)))
        (cons "toString" (lambda (x) (iso-instant-str-nanos (inst-nanos x))))))

;; --- Month / DayOfWeek enums (returned by getMonth / getDayOfWeek) -----------
(define (jt-month n) (make-jhost "month-enum" (vector n)))
(define (jt-dow n) (make-jhost "dow-enum" (vector n)))
(define (month-val e) (vector-ref (jhost-state e) 0))
(define (dow-val e) (vector-ref (jhost-state e) 0))
(define (month-name n) (vector-ref jt-month-names (- n 1)))
(define (dow-name n) (vector-ref jt-day-names (- n 1)))
;; quarter starts: Jan/Apr/Jul/Oct.
(define (month-quarter-start m) (+ 1 (* 3 (quotient (- m 1) 3))))
;; day-of-year of the first day of month m (non-leap base; +1 from March on a leap year).
(define (month-first-day-of-year m leap)
  (let ((cum (vector 0 0 31 59 90 120 151 181 212 243 273 304 334)))
    (+ 1 (vector-ref cum m) (if (and leap (> m 2)) 1 0))))

(register-host-methods! "month-enum"
  (list (cons "getValue" (lambda (e) (month-val e)))
        (cons "ordinal" (lambda (e) (- (month-val e) 1)))
        (cons "name" (lambda (e) (month-name (month-val e))))
        (cons "toString" (lambda (e) (month-name (month-val e))))
        (cons "getDisplayName" (lambda (e . _) (month-name (month-val e))))
        (cons "plus" (lambda (e n) (jt-month (+ 1 (jt-floor-mod (+ (- (month-val e) 1) (jt->exact n)) 12)))))
        (cons "minus" (lambda (e n) (jt-month (+ 1 (jt-floor-mod (- (- (month-val e) 1) (jt->exact n)) 12)))))
        (cons "length" (lambda (e leap) (jt-len-of-month (if (jolt-truthy? leap) 4 1) (month-val e))))
        (cons "minLength" (lambda (e) (if (= (month-val e) 2) 28 (jt-len-of-month 1 (month-val e)))))
        (cons "maxLength" (lambda (e) (if (= (month-val e) 2) 29 (jt-len-of-month 1 (month-val e)))))
        (cons "firstMonthOfQuarter" (lambda (e) (jt-month (month-quarter-start (month-val e)))))
        (cons "firstDayOfYear" (lambda (e leap) (month-first-day-of-year (month-val e) (jolt-truthy? leap))))
        (cons "compareTo" (lambda (e o) (- (month-val e) (month-val o))))
        (cons "equals" (lambda (e o) (and (jhost? o) (string=? (jhost-tag o) "month-enum") (= (month-val e) (month-val o)))))
        (cons "hashCode" (lambda (e) (month-val e)))))
(register-host-methods! "dow-enum"
  (list (cons "getValue" (lambda (e) (dow-val e)))
        (cons "ordinal" (lambda (e) (- (dow-val e) 1)))
        (cons "name" (lambda (e) (dow-name (dow-val e))))
        (cons "toString" (lambda (e) (dow-name (dow-val e))))
        (cons "getDisplayName" (lambda (e . _) (dow-name (dow-val e))))
        (cons "plus" (lambda (e n) (jt-dow (+ 1 (jt-floor-mod (+ (- (dow-val e) 1) (jt->exact n)) 7)))))
        (cons "minus" (lambda (e n) (jt-dow (+ 1 (jt-floor-mod (- (- (dow-val e) 1) (jt->exact n)) 7)))))
        (cons "compareTo" (lambda (e o) (- (dow-val e) (dow-val o))))
        (cons "equals" (lambda (e o) (and (jhost? o) (string=? (jhost-tag o) "dow-enum") (= (dow-val e) (dow-val o)))))
        (cons "hashCode" (lambda (e) (dow-val e)))))

(define (month-from-temporal t)
  (cond ((and (jhost? t) (string=? (jhost-tag t) "month-enum")) t)
        ((jt-date? t) (jt-month (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day t))) (lambda (y m d) m))))
        ((jt-dt? t) (jt-month (call-with-values (lambda () (epoch-day->ymd (ldt-epoch-day t))) (lambda (y m d) m))))
        (else (error #f "Month/from: unsupported"))))
(register-class-statics! "Month"
  (append
   (list (cons "of" (lambda (n) (jt-month (jt->exact n))))
         (cons "valueOf" (lambda (s) (let ((nm (jt-str s)))
                                       (let loop ((i 0)) (cond ((= i 12) (error #f (string-append "No enum constant Month." nm)))
                                                               ((string=? (vector-ref jt-month-names i) nm) (jt-month (+ i 1)))
                                                               (else (loop (+ i 1))))))))
         (cons "from" (lambda (t) (month-from-temporal t)))
         (cons "values" (lambda () (make-pvec (list->vector (map jt-month '(1 2 3 4 5 6 7 8 9 10 11 12)))))))
   (map (lambda (i) (cons (vector-ref jt-month-names (- i 1)) (jt-month i))) '(1 2 3 4 5 6 7 8 9 10 11 12))))
(define (dow-from-temporal t)
  (cond ((and (jhost? t) (string=? (jhost-tag t) "dow-enum")) t)
        ((jt-date? t) (jt-dow (ld-dow (ld-epoch-day t))))
        ((jt-dt? t) (jt-dow (ld-dow (ldt-epoch-day t))))
        (else (error #f "DayOfWeek/from: unsupported"))))
(register-class-statics! "DayOfWeek"
  (append
   (list (cons "of" (lambda (n) (jt-dow (jt->exact n))))
         (cons "valueOf" (lambda (s) (let ((nm (jt-str s)))
                                       (let loop ((i 0)) (cond ((= i 7) (error #f (string-append "No enum constant DayOfWeek." nm)))
                                                               ((string=? (vector-ref jt-day-names i) nm) (jt-dow (+ i 1)))
                                                               (else (loop (+ i 1))))))))
         (cons "from" (lambda (t) (dow-from-temporal t)))
         (cons "values" (lambda () (make-pvec (list->vector (map jt-dow '(1 2 3 4 5 6 7)))))))
   (map (lambda (i) (cons (vector-ref jt-day-names (- i 1)) (jt-dow i))) '(1 2 3 4 5 6 7))))

;; --- Duration: (vector seconds nanos), nanos in [0, 1e9) ----------------------
(define (dur-normalize secs nanos)
  (let* ((carry (jt-floor-div nanos nanos-per-sec))
         (n (jt-floor-mod nanos nanos-per-sec)))
    (make-jhost "duration" (vector (+ secs carry) n))))
(define (dur-secs d) (vector-ref (jhost-state d) 0))
(define (dur-nanos d) (vector-ref (jhost-state d) 1))
(define (dur-total-nanos d) (+ (* (dur-secs d) nanos-per-sec) (dur-nanos d)))
(define (dur-of-total-nanos tn) (dur-normalize (jt-floor-div tn nanos-per-sec) (jt-floor-mod tn nanos-per-sec)))
(define dur-zero (make-jhost "duration" (vector 0 0)))

;; ISO-8601: PTnHnMnS. Components come from the normalized (secs,nanos>=0) state;
;; each H/M/S field carries its own sign (java.time prints them per-component). The
;; seconds field folds in the fractional nanos: a negative second with nanos shows
;; e.g. "-0.5" (rem-sec -1 + nanos 5e8 -> -0.5).
(define (dur->string d)
  (let ((secs (dur-secs d)) (nanos (dur-nanos d)))
    (if (and (= secs 0) (= nanos 0)) "PT0S"
        (let* ((hours (quotient secs 3600))
               (mins (quotient (remainder secs 3600) 60))
               (rem-secs (remainder secs 60))
               (out (open-output-string)))
          (display "PT" out)
          (unless (= hours 0) (display (number->string hours) out) (write-char #\H out))
          (unless (= mins 0) (display (number->string mins) out) (write-char #\M out))
          (when (or (not (= rem-secs 0)) (not (= nanos 0)) (and (= hours 0) (= mins 0)))
            (if (= nanos 0)
                (display (number->string rem-secs) out)
                ;; whole second + fraction; a negative rem-sec with positive nanos
                ;; rolls toward zero (rem+1) and shows fraction as 1e9-nanos.
                (let* ((neg (< rem-secs 0))
                       (whole (if neg (+ rem-secs 1) rem-secs))
                       (frac (if neg (- nanos-per-sec nanos) nanos)))
                  (when (and neg (= whole 0)) (write-char #\- out))
                  (display (number->string whole) out)
                  (write-char #\. out)
                  (display (dur-frac-digits frac) out)))
            (write-char #\S out))
          (get-output-string out)))))
;; nanos -> a 9-digit fraction with all trailing zeros stripped (Duration shows the
;; minimal fraction, e.g. .5, unlike LocalTime which pads to 3/6/9).
(define (dur-frac-digits nano)
  (let ((s9 (let ((s (number->string nano))) (string-append (make-string (max 0 (- 9 (string-length s))) #\0) s))))
    (let loop ((i 9)) (cond ((<= i 1) (substring s9 0 1))
                            ((char=? (string-ref s9 (- i 1)) #\0) (loop (- i 1)))
                            (else (substring s9 0 i))))))

(define (dur-temporal-nanos t)   ; instant/ldt/lt/zoned/offset -> a nanos count
  (cond ((jt-instant? t) (inst-nanos t))
        ((jt-dt? t) (+ (* (ldt-epoch-day t) nanos-per-day) (ldt-nano-of-day t)))
        ((jt-time? t) (lt-nano-of-day t))
        ((jt-date? t) (* (ld-epoch-day t) nanos-per-day))
        ;; zoned/offset date-times measure on the instant timeline (UTC nanos).
        ((jt-zoned-dt? t) (zdt->nanos t))
        ((jt-offset-dt? t) (odt->nanos t))
        (else (error #f "Duration/between: unsupported temporal"))))
(register-class-statics! "Duration"
  (list (cons "ZERO" dur-zero)
        (cons "of" (lambda (n unit) (dur-of-total-nanos (* (jt->exact n) (chrono-unit-nanos unit)))))
        (cons "ofDays" (lambda (n) (dur-normalize (* (jt->exact n) 86400) 0)))
        (cons "ofHours" (lambda (n) (dur-normalize (* (jt->exact n) 3600) 0)))
        (cons "ofMinutes" (lambda (n) (dur-normalize (* (jt->exact n) 60) 0)))
        (cons "ofSeconds" (case-lambda
                            ((s) (dur-normalize (jt->exact s) 0))
                            ((s na) (dur-normalize (jt->exact s) (jt->exact na)))))
        (cons "ofMillis" (lambda (n) (dur-of-total-nanos (* (jt->exact n) 1000000))))
        (cons "ofNanos" (lambda (n) (dur-of-total-nanos (jt->exact n))))
        (cons "between" (lambda (a b) (dur-of-total-nanos (- (dur-temporal-nanos b) (dur-temporal-nanos a)))))
        (cons "from" (lambda (t) (if (and (jhost? t) (string=? (jhost-tag t) "duration")) t (error #f "Duration/from"))))
        (cons "parse" (lambda (s) (parse-iso-duration (jt-str s))))))

(define (dur-plus a b) (dur-of-total-nanos (+ (dur-total-nanos a) (dur-total-nanos b))))
(register-host-methods! "duration"
  (list (cons "getSeconds" (lambda (d) (dur-secs d)))
        (cons "getNano" (lambda (d) (dur-nanos d)))
        (cons "toDays" (lambda (d) (quotient (dur-secs d) 86400)))
        (cons "toHours" (lambda (d) (quotient (dur-secs d) 3600)))
        (cons "toMinutes" (lambda (d) (quotient (dur-secs d) 60)))
        ;; toMillis/toSeconds truncate the TOTAL toward zero (JVM rounds the whole
        ;; value, not per-field): -1001 micros -> -1 ms, not -2.
        (cons "toMillis" (lambda (d) (quotient (dur-total-nanos d) 1000000)))
        (cons "toNanos" (lambda (d) (dur-total-nanos d)))
        (cons "plus" (lambda (d o) (cond ((and (jhost? o) (string=? (jhost-tag o) "duration")) (dur-plus d o))
                                         (else (error #f "Duration.plus: expected Duration")))))
        (cons "minus" (lambda (d o) (dur-of-total-nanos (- (dur-total-nanos d) (dur-total-nanos o)))))
        (cons "plusDays" (lambda (d n) (dur-of-total-nanos (+ (dur-total-nanos d) (* (jt->exact n) 86400 nanos-per-sec)))))
        (cons "plusHours" (lambda (d n) (dur-of-total-nanos (+ (dur-total-nanos d) (* (jt->exact n) 3600 nanos-per-sec)))))
        (cons "plusMinutes" (lambda (d n) (dur-of-total-nanos (+ (dur-total-nanos d) (* (jt->exact n) 60 nanos-per-sec)))))
        (cons "plusSeconds" (lambda (d n) (dur-of-total-nanos (+ (dur-total-nanos d) (* (jt->exact n) nanos-per-sec)))))
        (cons "plusMillis" (lambda (d n) (dur-of-total-nanos (+ (dur-total-nanos d) (* (jt->exact n) 1000000)))))
        (cons "plusNanos" (lambda (d n) (dur-of-total-nanos (+ (dur-total-nanos d) (jt->exact n)))))
        (cons "minusDays" (lambda (d n) (dur-of-total-nanos (- (dur-total-nanos d) (* (jt->exact n) 86400 nanos-per-sec)))))
        (cons "minusHours" (lambda (d n) (dur-of-total-nanos (- (dur-total-nanos d) (* (jt->exact n) 3600 nanos-per-sec)))))
        (cons "minusMinutes" (lambda (d n) (dur-of-total-nanos (- (dur-total-nanos d) (* (jt->exact n) 60 nanos-per-sec)))))
        (cons "minusSeconds" (lambda (d n) (dur-of-total-nanos (- (dur-total-nanos d) (* (jt->exact n) nanos-per-sec)))))
        (cons "minusMillis" (lambda (d n) (dur-of-total-nanos (- (dur-total-nanos d) (* (jt->exact n) 1000000)))))
        (cons "minusNanos" (lambda (d n) (dur-of-total-nanos (- (dur-total-nanos d) (jt->exact n)))))
        (cons "multipliedBy" (lambda (d n) (dur-of-total-nanos (* (dur-total-nanos d) (jt->exact n)))))
        (cons "dividedBy" (lambda (d n) (dur-of-total-nanos (quotient (dur-total-nanos d) (jt->exact n)))))
        (cons "negated" (lambda (d) (dur-of-total-nanos (- (dur-total-nanos d)))))
        (cons "abs" (lambda (d) (dur-of-total-nanos (abs (dur-total-nanos d)))))
        (cons "withSeconds" (lambda (d s) (dur-normalize (jt->exact s) (dur-nanos d))))
        (cons "withNanos" (lambda (d na) (dur-normalize (dur-secs d) (jt->exact na))))
        (cons "isZero" (lambda (d) (and (= (dur-secs d) 0) (= (dur-nanos d) 0))))
        (cons "isNegative" (lambda (d) (< (dur-total-nanos d) 0)))
        (cons "compareTo" (lambda (d o) (let ((a (dur-total-nanos d)) (b (dur-total-nanos o)))
                                          (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (d o) (and (jhost? o) (string=? (jhost-tag o) "duration") (= (dur-total-nanos d) (dur-total-nanos o)))))
        (cons "hashCode" (lambda (d) (jolt-hash (dur-total-nanos d))))
        ;; TemporalAmount: addTo/subtractFrom apply this duration to a temporal.
        (cons "addTo" (lambda (d t) (temporal-plus-nanos t (dur-total-nanos d))))
        (cons "subtractFrom" (lambda (d t) (temporal-plus-nanos t (- (dur-total-nanos d)))))
        ;; TemporalAmount: a Duration is measured in SECONDS + NANOS.
        (cons "getUnits" (lambda (d) (make-pvec (vector (jt-chrono-unit "SECONDS") (jt-chrono-unit "NANOS")))))
        (cons "get" (lambda (d u) (let ((nm (string-upcase (arg-unit-name u))))
                                    (cond ((string=? nm "SECONDS") (dur-secs d))
                                          ((string=? nm "NANOS") (dur-nanos d))
                                          (else (error #f (string-append "Duration has no unit " nm)))))))
        (cons "toString" (lambda (d) (dur->string d)))))

;; "PT…" parse: PnDTnHnMn.nS (we accept the time part; days fold into hours).
(define (parse-iso-duration s)
  (let ((len (string-length s)) (neg #f) (i 0) (total 0))
    (define (sign-at j) (cond ((>= j len) (values 1 j))
                              ((char=? (string-ref s j) #\-) (values -1 (+ j 1)))
                              ((char=? (string-ref s j) #\+) (values 1 (+ j 1)))
                              (else (values 1 j))))
    (when (and (< i len) (char=? (string-ref s i) #\-)) (set! neg #t) (set! i (+ i 1)))
    (unless (and (< i len) (or (char=? (string-ref s i) #\P) (char=? (string-ref s i) #\p)))
      (error #f (string-append "could not parse Duration: " s)))
    (set! i (+ i 1))
    (let loop ((in-time #f))
      (when (< i len)
        (let ((c (string-ref s i)))
          (cond
            ((or (char=? c #\T) (char=? c #\t)) (set! i (+ i 1)) (loop #t))
            (else
             (call-with-values (lambda () (sign-at i))
               (lambda (sg j)
                 ;; read number (with optional fraction)
                 (let num ((k j) (acc 0) (frac 0) (fdigits 0) (in-frac #f) (any #f))
                   (cond
                     ((and (< k len) (digit? (string-ref s k)))
                      (if in-frac
                          (num (+ k 1) acc (+ (* frac 10) (- (char->integer (string-ref s k)) 48)) (+ fdigits 1) #t #t)
                          (num (+ k 1) (+ (* acc 10) (- (char->integer (string-ref s k)) 48)) frac fdigits #f #t)))
                     ((and (< k len) (char=? (string-ref s k) #\.)) (num (+ k 1) acc frac fdigits #t any))
                     ((and any (< k len))
                      (let* ((unit (char-upcase (string-ref s k)))
                             (nanos (* (+ (* acc nanos-per-sec) (* frac (expt 10 (max 0 (- 9 fdigits))))) sg))
                             (mult (cond ((char=? unit #\D) 86400) ((char=? unit #\H) 3600)
                                         ((char=? unit #\M) (if in-time 60 (error #f "Duration months unsupported")))
                                         ((char=? unit #\S) 1)
                                         (else (error #f (string-append "bad Duration unit: " s))))))
                        (set! total (+ total (* nanos mult)))
                        (set! i (+ k 1))
                        (loop in-time)))
                     (else (error #f (string-append "could not parse Duration: " s))))))))))))
    (dur-of-total-nanos (if neg (- total) total))))

;; --- Period: (vector years months days) --------------------------------------
(define (jt-period y m d) (make-jhost "period" (vector y m d)))
(define (per-years p) (vector-ref (jhost-state p) 0))
(define (per-months p) (vector-ref (jhost-state p) 1))
(define (per-days p) (vector-ref (jhost-state p) 2))
(define per-zero (jt-period 0 0 0))
(define (per->string p)
  (let ((y (per-years p)) (m (per-months p)) (d (per-days p)))
    (if (and (= y 0) (= m 0) (= d 0)) "P0D"
        (let ((out (open-output-string)))
          (write-char #\P out)
          (unless (= y 0) (display (number->string y) out) (write-char #\Y out))
          (unless (= m 0) (display (number->string m) out) (write-char #\M out))
          (unless (= d 0) (display (number->string d) out) (write-char #\D out))
          (get-output-string out)))))
;; Period/between counts whole years/months/days from a to b (java.time semantics).
(define (per-between a b)
  (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day a)))
    (lambda (y1 m1 d1)
      (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day b)))
        (lambda (y2 m2 d2)
          (let* ((total-months (- (+ (* y2 12) m2) (+ (* y1 12) m1)))
                 (days (- d2 d1)))
            ;; if days go negative, borrow a month (using the prior month length of b)
            (let-values (((tm dd)
                          (if (and (> total-months 0) (< days 0))
                              (let* ((tm (- total-months 1))
                                     (bm (+ (* y1 12) (- m1 1) tm))
                                     (by (jt-floor-div bm 12)) (bmo (+ 1 (jt-floor-mod bm 12)))
                                     (len (jt-len-of-month by bmo)))
                                (values tm (+ days len)))
                              (if (and (< total-months 0) (> days 0))
                                  (values (+ total-months 1) (- days (jt-len-of-month y2 m2)))
                                  (values total-months days)))))
              (jt-period (jt-floor-div tm 12) (jt-floor-mod tm 12) dd))))))))
(define (per-normalize p)
  (let ((tm (+ (* (per-years p) 12) (per-months p))))
    (jt-period (jt-floor-div tm 12) (jt-floor-mod tm 12) (per-days p))))
(register-class-statics! "Period"
  (list (cons "ZERO" per-zero)
        (cons "of" (lambda (y m d) (jt-period (jt->exact y) (jt->exact m) (jt->exact d))))
        (cons "ofYears" (lambda (y) (jt-period (jt->exact y) 0 0)))
        (cons "ofMonths" (lambda (m) (jt-period 0 (jt->exact m) 0)))
        (cons "ofWeeks" (lambda (w) (jt-period 0 0 (* 7 (jt->exact w)))))
        (cons "ofDays" (lambda (d) (jt-period 0 0 (jt->exact d))))
        (cons "between" (lambda (a b) (per-between a b)))
        (cons "from" (lambda (t) (if (and (jhost? t) (string=? (jhost-tag t) "period")) t (error #f "Period/from"))))
        (cons "parse" (lambda (s) (parse-iso-period (jt-str s))))))
(register-host-methods! "period"
  (list (cons "getYears" (lambda (p) (per-years p)))
        (cons "getMonths" (lambda (p) (per-months p)))
        (cons "getDays" (lambda (p) (per-days p)))
        (cons "toTotalMonths" (lambda (p) (+ (* (per-years p) 12) (per-months p))))
        (cons "plusYears" (lambda (p n) (jt-period (+ (per-years p) (jt->exact n)) (per-months p) (per-days p))))
        (cons "plusMonths" (lambda (p n) (jt-period (per-years p) (+ (per-months p) (jt->exact n)) (per-days p))))
        (cons "plusDays" (lambda (p n) (jt-period (per-years p) (per-months p) (+ (per-days p) (jt->exact n)))))
        (cons "minusYears" (lambda (p n) (jt-period (- (per-years p) (jt->exact n)) (per-months p) (per-days p))))
        (cons "minusMonths" (lambda (p n) (jt-period (per-years p) (- (per-months p) (jt->exact n)) (per-days p))))
        (cons "minusDays" (lambda (p n) (jt-period (per-years p) (per-months p) (- (per-days p) (jt->exact n)))))
        (cons "withYears" (lambda (p n) (jt-period (jt->exact n) (per-months p) (per-days p))))
        (cons "withMonths" (lambda (p n) (jt-period (per-years p) (jt->exact n) (per-days p))))
        (cons "withDays" (lambda (p n) (jt-period (per-years p) (per-months p) (jt->exact n))))
        (cons "plus" (lambda (p o) (jt-period (+ (per-years p) (per-years o)) (+ (per-months p) (per-months o)) (+ (per-days p) (per-days o)))))
        (cons "minus" (lambda (p o) (jt-period (- (per-years p) (per-years o)) (- (per-months p) (per-months o)) (- (per-days p) (per-days o)))))
        (cons "multipliedBy" (lambda (p n) (jt-period (* (per-years p) (jt->exact n)) (* (per-months p) (jt->exact n)) (* (per-days p) (jt->exact n)))))
        (cons "negated" (lambda (p) (jt-period (- (per-years p)) (- (per-months p)) (- (per-days p)))))
        (cons "normalized" (lambda (p) (per-normalize p)))
        (cons "isZero" (lambda (p) (and (= (per-years p) 0) (= (per-months p) 0) (= (per-days p) 0))))
        (cons "isNegative" (lambda (p) (or (< (per-years p) 0) (< (per-months p) 0) (< (per-days p) 0))))
        (cons "equals" (lambda (p o) (and (jhost? o) (string=? (jhost-tag o) "period")
                                          (= (per-years p) (per-years o)) (= (per-months p) (per-months o)) (= (per-days p) (per-days o)))))
        (cons "hashCode" (lambda (p) (+ (per-years p) (bitwise-arithmetic-shift-left (per-months p) 8) (bitwise-arithmetic-shift-left (per-days p) 16))))
        ;; TemporalAmount: a Period adds its y/m/d to a date-bearing temporal.
        (cons "addTo" (lambda (p t) (temporal-plus-period t p 1)))
        (cons "subtractFrom" (lambda (p t) (temporal-plus-period t p -1)))
        ;; TemporalAmount: a Period is measured in YEARS + MONTHS + DAYS.
        (cons "getUnits" (lambda (p) (make-pvec (vector (jt-chrono-unit "YEARS") (jt-chrono-unit "MONTHS") (jt-chrono-unit "DAYS")))))
        (cons "get" (lambda (p u) (let ((nm (string-upcase (arg-unit-name u))))
                                    (cond ((string=? nm "YEARS") (per-years p))
                                          ((string=? nm "MONTHS") (per-months p))
                                          ((string=? nm "DAYS") (per-days p))
                                          (else (error #f (string-append "Period has no unit " nm)))))))
        (cons "toString" (lambda (p) (per->string p)))))
;; "PnYnMnWnD" -> a Period (weeks fold into days).
(define (parse-iso-period s)
  (let ((len (string-length s)) (sign 1) (i 0) (y 0) (m 0) (d 0))
    (when (and (< i len) (char=? (string-ref s i) #\-)) (set! sign -1) (set! i (+ i 1)))
    (unless (and (< i len) (or (char=? (string-ref s i) #\P) (char=? (string-ref s i) #\p)))
      (error #f (string-append "could not parse Period: " s)))
    (set! i (+ i 1))
    (let loop ()
      (when (< i len)
        (let ((vsign (cond ((char=? (string-ref s i) #\-) (set! i (+ i 1)) -1)
                           ((char=? (string-ref s i) #\+) (set! i (+ i 1)) 1)
                           (else 1))))
          (let num ((k i) (acc 0) (any #f))
            (cond
              ((and (< k len) (digit? (string-ref s k))) (num (+ k 1) (+ (* acc 10) (- (char->integer (string-ref s k)) 48)) #t))
              ((and any (< k len))
               (let ((u (char-upcase (string-ref s k))) (val (* vsign acc)))
                 (cond ((char=? u #\Y) (set! y val)) ((char=? u #\M) (set! m val))
                       ((char=? u #\W) (set! d (+ d (* 7 val)))) ((char=? u #\D) (set! d (+ d val)))
                       (else (error #f (string-append "bad Period unit: " s))))
                 (set! i (+ k 1)) (loop)))
              (else (error #f (string-append "could not parse Period: " s))))))))
    (jt-period (* sign y) (* sign m) (* sign d))))

;; --- Year --------------------------------------------------------------------
(define (jt-year y) (make-jhost "year" (vector y)))
(define (year-val y) (vector-ref (jhost-state y) 0))
(define (year-from-temporal t)
  (cond ((and (jhost? t) (string=? (jhost-tag t) "year")) t)
        ((jt-date? t) (jt-year (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day t))) (lambda (y m d) y))))
        ((jt-dt? t) (jt-year (call-with-values (lambda () (epoch-day->ymd (ldt-epoch-day t))) (lambda (y m d) y))))
        ((and (jhost? t) (string=? (jhost-tag t) "year-month")) (jt-year (ym-year t)))
        (else (error #f "Year/from: unsupported"))))
(register-class-statics! "Year"
  (list (cons "of" (lambda (y) (jt-year (jt->exact y))))
        (cons "now" (lambda _ (jt-year (call-with-values (lambda () (epoch-day->ymd (jt-floor-div (exact (truncate (now-ms))) 86400000))) (lambda (y m d) y)))))
        (cons "isLeap" (lambda (y) (jt-leap? (jt->exact y))))
        (cons "parse" (lambda (s . _) (jt-year (or (string->number (jt-str s)) (error #f "could not parse Year")))))
        (cons "from" (lambda (t) (year-from-temporal t)))
        (cons "MIN_VALUE" -999999999)
        (cons "MAX_VALUE" 999999999)))
(register-host-methods! "year"
  (list (cons "getValue" (lambda (y) (year-val y)))
        (cons "isLeap" (lambda (y) (jt-leap? (year-val y))))
        (cons "length" (lambda (y) (if (jt-leap? (year-val y)) 366 365)))
        (cons "plusYears" (lambda (y n) (jt-year (+ (year-val y) (jt->exact n)))))
        (cons "minusYears" (lambda (y n) (jt-year (- (year-val y) (jt->exact n)))))
        (cons "atMonth" (lambda (y m) (jt-year-month (year-val y) (jt->exact m))))
        (cons "atDay" (lambda (y doy) (jt-local-date (+ (ymd->epoch-day (year-val y) 1 1) (- (jt->exact doy) 1)))))
        (cons "atMonthDay" (lambda (y md) (error #f "Year.atMonthDay: MonthDay unsupported")))
        (cons "isBefore" (lambda (y o) (< (year-val y) (year-val o))))
        (cons "isAfter" (lambda (y o) (> (year-val y) (year-val o))))
        (cons "isValidMonthDay" (lambda (y md) #f))
        (cons "compareTo" (lambda (y o) (let ((a (year-val y)) (b (year-val o))) (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (y o) (and (jhost? o) (string=? (jhost-tag o) "year") (= (year-val y) (year-val o)))))
        (cons "hashCode" (lambda (y) (year-val y)))
        (cons "toString" (lambda (y) (number->string (year-val y))))))

;; --- YearMonth: (vector year month) ------------------------------------------
(define (jt-year-month y m)
  (let* ((ym (+ (* y 12) (- m 1))) (y2 (jt-floor-div ym 12)) (m2 (+ 1 (jt-floor-mod ym 12))))
    (make-jhost "year-month" (vector y2 m2))))
(define (ym-year x) (vector-ref (jhost-state x) 0))
(define (ym-month x) (vector-ref (jhost-state x) 1))
(define (ym-plus-months x n)
  (let ((tm (+ (* (ym-year x) 12) (- (ym-month x) 1) n)))
    (make-jhost "year-month" (vector (jt-floor-div tm 12) (+ 1 (jt-floor-mod tm 12))))))
(define (ym->string x) (string-append (pad4 (ym-year x)) "-" (pad2 (ym-month x))))
(define (ym-from-temporal t)
  (cond ((and (jhost? t) (string=? (jhost-tag t) "year-month")) t)
        ((jt-date? t) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day t))) (lambda (y m d) (jt-year-month y m))))
        ((jt-dt? t) (call-with-values (lambda () (epoch-day->ymd (ldt-epoch-day t))) (lambda (y m d) (jt-year-month y m))))
        (else (error #f "YearMonth/from: unsupported"))))
(register-class-statics! "YearMonth"
  (list (cons "of" (lambda (y m) (jt-year-month (jt->exact y) (jt->exact m))))
        (cons "now" (lambda _ (call-with-values (lambda () (epoch-day->ymd (jt-floor-div (exact (truncate (now-ms))) 86400000)))
                                (lambda (y m d) (jt-year-month y m)))))
        (cons "parse" (lambda (s . fmt)
                        (if (null? fmt)
                            (let ((str (jt-str s)))
                              (jt-year-month (or (digits-at str 0 4) (error #f "could not parse YearMonth"))
                                             (or (digits-at str 5 2) (error #f "could not parse YearMonth"))))
                            (call-with-values (lambda () (formatter-parse-fields (car fmt) s))
                              (lambda (ed nod) (call-with-values (lambda () (epoch-day->ymd ed)) (lambda (y m d) (jt-year-month y m))))))))
        (cons "from" (lambda (t) (ym-from-temporal t)))))
(register-host-methods! "year-month"
  (list (cons "getYear" (lambda (x) (ym-year x)))
        (cons "getMonthValue" (lambda (x) (ym-month x)))
        (cons "getMonth" (lambda (x) (jt-month (ym-month x))))
        (cons "lengthOfMonth" (lambda (x) (jt-len-of-month (ym-year x) (ym-month x))))
        (cons "lengthOfYear" (lambda (x) (if (jt-leap? (ym-year x)) 366 365)))
        (cons "isLeapYear" (lambda (x) (jt-leap? (ym-year x))))
        (cons "plusMonths" (lambda (x n) (ym-plus-months x (jt->exact n))))
        (cons "minusMonths" (lambda (x n) (ym-plus-months x (- (jt->exact n)))))
        (cons "plusYears" (lambda (x n) (jt-year-month (+ (ym-year x) (jt->exact n)) (ym-month x))))
        (cons "minusYears" (lambda (x n) (jt-year-month (- (ym-year x) (jt->exact n)) (ym-month x))))
        (cons "withYear" (lambda (x v) (jt-year-month (jt->exact v) (ym-month x))))
        (cons "withMonth" (lambda (x v) (jt-year-month (ym-year x) (jt->exact v))))
        (cons "atDay" (lambda (x d) (jt-local-date (ymd->epoch-day (ym-year x) (ym-month x) (jt->exact d)))))
        (cons "atEndOfMonth" (lambda (x) (jt-local-date (ymd->epoch-day (ym-year x) (ym-month x) (jt-len-of-month (ym-year x) (ym-month x))))))
        (cons "isValidDay" (lambda (x d) (let ((dd (jt->exact d))) (and (>= dd 1) (<= dd (jt-len-of-month (ym-year x) (ym-month x)))))))
        (cons "isBefore" (lambda (x o) (< (ym-cmp x o) 0)))
        (cons "isAfter" (lambda (x o) (> (ym-cmp x o) 0)))
        (cons "compareTo" (lambda (x o) (ym-cmp x o)))
        (cons "equals" (lambda (x o) (and (jhost? o) (string=? (jhost-tag o) "year-month") (= (ym-cmp x o) 0))))
        (cons "hashCode" (lambda (x) (+ (* (ym-year x) 13) (ym-month x))))
        (cons "toString" (lambda (x) (ym->string x)))))
(define (ym-cmp x o)
  (cond ((< (ym-year x) (ym-year o)) -1) ((> (ym-year x) (ym-year o)) 1)
        ((< (ym-month x) (ym-month o)) -1) ((> (ym-month x) (ym-month o)) 1) (else 0)))

;; --- ChronoUnit / ChronoField ------------------------------------------------
;; Each ChronoUnit is a jhost (vector name nanos-or-#f) — nanos is the fixed
;; duration in nanoseconds for time-based units, or an estimate for date-based ones.
;; The conversion lets Duration/of and unit-based between/plus/until work.
(define seconds-per-year (* (/ 146097 400) 86400))   ; 365.2425 days
(define chrono-unit-table
  ;; (name . nanos-per-unit)   month/year use the average-length estimate java.time uses.
  (list (cons "NANOS" 1)
        (cons "MICROS" 1000)
        (cons "MILLIS" 1000000)
        (cons "SECONDS" nanos-per-sec)
        (cons "MINUTES" (* 60 nanos-per-sec))
        (cons "HOURS" (* 3600 nanos-per-sec))
        (cons "HALF_DAYS" (* 43200 nanos-per-sec))
        (cons "DAYS" (* 86400 nanos-per-sec))
        (cons "WEEKS" (* 7 86400 nanos-per-sec))
        (cons "MONTHS" (exact (round (* (/ seconds-per-year 12) nanos-per-sec))))
        (cons "YEARS" (exact (round (* seconds-per-year nanos-per-sec))))
        (cons "DECADES" (exact (round (* 10 seconds-per-year nanos-per-sec))))
        (cons "CENTURIES" (exact (round (* 100 seconds-per-year nanos-per-sec))))
        (cons "MILLENNIA" (exact (round (* 1000 seconds-per-year nanos-per-sec))))
        (cons "ERAS" (exact (round (* 1000000000 seconds-per-year nanos-per-sec))))
        (cons "FOREVER" #f)))
(define (jt-chrono-unit name) (make-jhost "chrono-unit" (vector name)))
(define (cu-name u) (vector-ref (jhost-state u) 0))
(define (chrono-unit-nanos u)
  (let* ((nm (chrono-unit-name u)) (row (and nm (assoc (string-upcase nm) chrono-unit-table))))
    (if (and row (cdr row)) (cdr row) (error #f (string-append "no fixed duration for unit " (or nm "?"))))))
;; register the 16 unit constants under ChronoUnit statics.
(register-class-statics! "ChronoUnit"
  (append
   (map (lambda (row) (cons (car row) (jt-chrono-unit (car row)))) chrono-unit-table)
   (list (cons "valueOf" (lambda (s) (jt-chrono-unit (jt-str s))))
         (cons "values" (lambda () (make-pvec (list->vector (map (lambda (r) (jt-chrono-unit (car r))) chrono-unit-table))))))))
(register-host-methods! "chrono-unit"
  (list (cons "name" (lambda (u) (cu-name u)))
        (cons "toString" (lambda (u) (cu-name u)))
        (cons "ordinal" (lambda (u) (let loop ((t chrono-unit-table) (i 0))
                                      (cond ((null? t) -1) ((string=? (caar t) (cu-name u)) i) (else (loop (cdr t) (+ i 1)))))))
        (cons "getDuration" (lambda (u) (dur-of-total-nanos (chrono-unit-nanos u))))
        (cons "isDateBased" (lambda (u) (and (member (cu-name u) '("DAYS" "WEEKS" "MONTHS" "YEARS" "DECADES" "CENTURIES" "MILLENNIA" "ERAS")) #t)))
        (cons "isTimeBased" (lambda (u) (and (member (cu-name u) '("NANOS" "MICROS" "MILLIS" "SECONDS" "MINUTES" "HOURS" "HALF_DAYS")) #t)))
        (cons "isDurationEstimated" (lambda (u) (and (member (cu-name u) '("DAYS" "WEEKS" "MONTHS" "YEARS" "DECADES" "CENTURIES" "MILLENNIA" "ERAS" "FOREVER")) #t)))
        (cons "between" (lambda (u a b) (unit-between (cu-name u) a b)))
        (cons "addTo" (lambda (u t n) (temporal-plus-unit t (jt->exact n) (cu-name u))))
        (cons "equals" (lambda (u o) (and (jhost? o) (string=? (jhost-tag o) "chrono-unit") (string=? (cu-name u) (cu-name o)))))
        (cons "hashCode" (lambda (u) (string-hash (cu-name u))))))

;; ChronoField: a jhost (vector name). get/getLong/with project the field onto a
;; temporal via the field-projection table below.
;; the full ChronoField enum (the cljc wrapper defs every constant at load). Only
;; the common ones are projected by temporal-get-field; the rest exist as tokens.
(define chrono-field-names
  '("NANO_OF_SECOND" "NANO_OF_DAY" "MICRO_OF_SECOND" "MICRO_OF_DAY" "MILLI_OF_SECOND" "MILLI_OF_DAY"
    "SECOND_OF_MINUTE" "SECOND_OF_DAY" "MINUTE_OF_HOUR" "MINUTE_OF_DAY"
    "HOUR_OF_AMPM" "CLOCK_HOUR_OF_AMPM" "HOUR_OF_DAY" "CLOCK_HOUR_OF_DAY" "AMPM_OF_DAY"
    "DAY_OF_WEEK" "ALIGNED_DAY_OF_WEEK_IN_MONTH" "ALIGNED_DAY_OF_WEEK_IN_YEAR"
    "DAY_OF_MONTH" "DAY_OF_YEAR" "EPOCH_DAY"
    "ALIGNED_WEEK_OF_MONTH" "ALIGNED_WEEK_OF_YEAR"
    "MONTH_OF_YEAR" "PROLEPTIC_MONTH" "YEAR_OF_ERA" "YEAR" "ERA"
    "INSTANT_SECONDS" "OFFSET_SECONDS"))
(define (jt-chrono-field name) (make-jhost "chrono-field" (vector name)))
(define (cf-name f) (vector-ref (jhost-state f) 0))
(register-class-statics! "ChronoField"
  (append
   (map (lambda (n) (cons n (jt-chrono-field n))) chrono-field-names)
   (list (cons "valueOf" (lambda (s) (jt-chrono-field (jt-str s))))
         (cons "values" (lambda () (make-pvec (list->vector (map jt-chrono-field chrono-field-names))))))))
(register-host-methods! "chrono-field"
  (list (cons "name" (lambda (f) (cf-name f)))
        (cons "toString" (lambda (f) (cf-name f)))
        (cons "getDisplayName" (lambda (f . _) (cf-name f)))
        (cons "isDateBased" (lambda (f) (and (member (cf-name f) '("DAY_OF_WEEK" "DAY_OF_MONTH" "DAY_OF_YEAR" "EPOCH_DAY" "MONTH_OF_YEAR" "PROLEPTIC_MONTH" "YEAR_OF_ERA" "YEAR" "ERA")) #t)))
        (cons "isTimeBased" (lambda (f) (not (member (cf-name f) '("DAY_OF_WEEK" "DAY_OF_MONTH" "DAY_OF_YEAR" "EPOCH_DAY" "MONTH_OF_YEAR" "PROLEPTIC_MONTH" "YEAR_OF_ERA" "YEAR" "ERA" "INSTANT_SECONDS" "OFFSET_SECONDS")))))
        (cons "getFrom" (lambda (f t) (temporal-get-field t (cf-name f))))
        (cons "equals" (lambda (f o) (and (jhost? o) (string=? (jhost-tag o) "chrono-field") (string=? (cf-name f) (cf-name o)))))
        (cons "hashCode" (lambda (f) (string-hash (cf-name f))))))

;; --- ValueRange (minimal min/max holder) -------------------------------------
(define (jt-value-range smin lmin smax lmax) (make-jhost "value-range" (vector smin lmin smax lmax)))
(register-host-methods! "value-range"
  (list (cons "getMinimum" (lambda (r) (vector-ref (jhost-state r) 1)))
        (cons "getLargestMinimum" (lambda (r) (vector-ref (jhost-state r) 1)))
        (cons "getSmallestMaximum" (lambda (r) (vector-ref (jhost-state r) 2)))
        (cons "getMaximum" (lambda (r) (vector-ref (jhost-state r) 3)))
        (cons "isFixed" (lambda (r) (and (= (vector-ref (jhost-state r) 0) (vector-ref (jhost-state r) 1))
                                         (= (vector-ref (jhost-state r) 2) (vector-ref (jhost-state r) 3)))))
        (cons "isValidValue" (lambda (r v) (let ((x (jt->exact v))) (and (>= x (vector-ref (jhost-state r) 1)) (<= x (vector-ref (jhost-state r) 3))))))
        (cons "toString" (lambda (r) (string-append (number->string (vector-ref (jhost-state r) 1)) " - " (number->string (vector-ref (jhost-state r) 3)))))))

;; --- temporal field/unit machinery: plus/minus/until/get/with on core types --
;; These power the generic cljc.java-time.temporal dispatchers, which forward to
;; the receiver's plus/minus/until/get/getLong/with/range/isSupported.

;; add n of `unit` to a temporal (local-date / local-time / local-date-time / instant).
(define (temporal-plus-unit t n unit)
  (let ((u (string-upcase unit)))
    (cond
      ((jt-date? t)
       (cond ((string=? u "DAYS") (jt-local-date (+ (ld-epoch-day t) n)))
             ((string=? u "WEEKS") (jt-local-date (+ (ld-epoch-day t) (* 7 n))))
             ((string=? u "MONTHS") (ld-plus-months t n))
             ((string=? u "YEARS") (ld-plus-years t n))
             ((string=? u "DECADES") (ld-plus-years t (* 10 n)))
             ((string=? u "CENTURIES") (ld-plus-years t (* 100 n)))
             ((string=? u "MILLENNIA") (ld-plus-years t (* 1000 n)))
             (else (error #f (string-append "LocalDate plus unsupported unit " u)))))
      ((jt-time? t) (lt-plus t (* n (chrono-unit-nanos (jt-chrono-unit u)))))
      ((jt-dt? t)
       (cond ((string=? u "DAYS") (jt-local-dt (+ (ldt-epoch-day t) n) (ldt-nano-of-day t)))
             ((string=? u "WEEKS") (jt-local-dt (+ (ldt-epoch-day t) (* 7 n)) (ldt-nano-of-day t)))
             ((string=? u "MONTHS") (ldt-combine (ld-plus-months (ldt-date t) n) (ldt-time t)))
             ((string=? u "YEARS") (ldt-combine (ld-plus-years (ldt-date t) n) (ldt-time t)))
             ((string=? u "DECADES") (ldt-combine (ld-plus-years (ldt-date t) (* 10 n)) (ldt-time t)))
             ((string=? u "CENTURIES") (ldt-combine (ld-plus-years (ldt-date t) (* 100 n)) (ldt-time t)))
             ((string=? u "MILLENNIA") (ldt-combine (ld-plus-years (ldt-date t) (* 1000 n)) (ldt-time t)))
             (else (ldt-plus-nanos t (* n (chrono-unit-nanos (jt-chrono-unit u)))))))
      ((jt-instant? t)
       (cond ((string=? u "DAYS") (mk-instant-nanos (+ (inst-nanos t) (* n 86400 nanos-per-sec))))
             (else (mk-instant-nanos (+ (inst-nanos t) (* n (chrono-unit-nanos (jt-chrono-unit u))))))))
      (else (error #f "plus: unsupported temporal")))))

;; add raw nanos (for Duration.addTo).
(define (temporal-plus-nanos t nanos)
  (cond ((jt-time? t) (lt-plus t nanos))
        ((jt-dt? t) (ldt-plus-nanos t nanos))
        ((jt-instant? t) (mk-instant-nanos (+ (inst-nanos t) nanos)))
        ((jt-date? t) (jt-local-date (+ (ld-epoch-day t) (quotient nanos (* 86400 nanos-per-sec)))))
        (else (error #f "plus(Duration): unsupported temporal"))))

;; add a Period (scaled by sign) to a date-bearing temporal.
(define (temporal-plus-period t p sign)
  (let ((y (* sign (per-years p))) (m (* sign (per-months p))) (d (* sign (per-days p))))
    (cond ((jt-date? t) (jt-local-date (+ (ld-epoch-day (ld-plus-months (ld-plus-years t y) m)) d)))
          ((jt-dt? t) (let ((nd (ld-plus-months (ld-plus-years (ldt-date t) y) m)))
                        (jt-local-dt (+ (ld-epoch-day nd) d) (ldt-nano-of-day t))))
          (else (error #f "plus(Period): unsupported temporal")))))

;; count whole `unit`s from a to b.
(define (unit-between unit a b)
  (let ((u (string-upcase unit)))
    (cond
      ((and (jt-date? a) (jt-date? b))
       (let ((da (ld-epoch-day a)) (db (ld-epoch-day b)))
         (cond ((string=? u "DAYS") (- db da))
               ((string=? u "WEEKS") (quotient (- db da) 7))
               ((string=? u "MONTHS") (date-months-between da db))
               ((string=? u "YEARS") (quotient (date-months-between da db) 12))
               ((string=? u "DECADES") (quotient (date-months-between da db) 120))
               ((string=? u "CENTURIES") (quotient (date-months-between da db) 1200))
               (else (error #f (string-append "between unsupported unit " u))))))
      ((and (jt-time? a) (jt-time? b)) (quotient (- (lt-nano-of-day b) (lt-nano-of-day a)) (chrono-unit-nanos (jt-chrono-unit u))))
      ((and (jt-dt? a) (jt-dt? b))
       (cond ((member u '("YEARS" "MONTHS" "DECADES" "CENTURIES" "MILLENNIA"))
              (unit-between u (ldt-date a) (ldt-date b)))   ; date-based: ignore time-of-day below months on LDT? use date months adjusted
             (else (quotient (- (+ (* (ldt-epoch-day b) nanos-per-day) (ldt-nano-of-day b))
                                 (+ (* (ldt-epoch-day a) nanos-per-day) (ldt-nano-of-day a)))
                             (chrono-unit-nanos (jt-chrono-unit u))))))
      ((and (jt-instant? a) (jt-instant? b))
       (quotient (- (inst-nanos b) (inst-nanos a)) (chrono-unit-nanos (jt-chrono-unit u))))
      (else (error #f "between: unsupported temporals")))))
;; whole months between two epoch-days (java.time: months, then -1 if day-of-month
;; of b hasn't reached a's).
(define (date-months-between da db)
  (call-with-values (lambda () (epoch-day->ymd da))
    (lambda (y1 m1 d1)
      (call-with-values (lambda () (epoch-day->ymd db))
        (lambda (y2 m2 d2)
          (let ((months (- (+ (* y2 12) m2) (+ (* y1 12) m1))))
            (cond ((and (> months 0) (< d2 d1)) (- months 1))
                  ((and (< months 0) (> d2 d1)) (+ months 1))
                  (else months))))))))

;; field get: project a ChronoField onto a temporal.
(define (temporal-get-field t field)
  (let ((f (string-upcase field)))
    (cond
      ((jt-date? t)
       (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day t)))
         (lambda (y m d)
           (cond ((string=? f "YEAR") y) ((string=? f "MONTH_OF_YEAR") m) ((string=? f "DAY_OF_MONTH") d)
                 ((string=? f "DAY_OF_WEEK") (ld-dow (ld-epoch-day t)))
                 ((string=? f "DAY_OF_YEAR") (ld-day-of-year (ld-epoch-day t)))
                 ((string=? f "EPOCH_DAY") (ld-epoch-day t))
                 ((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 "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 "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))
             ((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"))
           (temporal-get-field (ldt-date t) f)
           (temporal-get-field (ldt-time t) f)))
      ((jt-instant? t)
       (cond ((string=? f "INSTANT_SECONDS") (jt-floor-div (inst-nanos t) nanos-per-sec))
             ((string=? f "NANO_OF_SECOND") (jt-floor-mod (inst-nanos t) nanos-per-sec))
             ((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)))))
      (else (error #f "get(field): unsupported temporal")))))

;; field set: (with temporal ChronoField value) -> a new temporal.
(define (temporal-with-field t field v)
  (let ((f (string-upcase field)))
    (cond
      ((jt-date? t)
       (cond ((string=? f "YEAR") (ld-with-field t 'year v)) ((string=? f "MONTH_OF_YEAR") (ld-with-field t 'month v))
             ((string=? f "DAY_OF_MONTH") (ld-with-field t 'day v)) ((string=? f "DAY_OF_YEAR") (ld-with-field t 'day-of-year v))
             ((string=? f "EPOCH_DAY") (jt-local-date v))
             (else (error #f (string-append "LocalDate.with unsupported field " f)))))
      ((jt-time? t)
       (cond ((string=? f "HOUR_OF_DAY") (lt-with t 'hour v)) ((string=? f "MINUTE_OF_HOUR") (lt-with t 'minute v))
             ((string=? f "SECOND_OF_MINUTE") (lt-with t 'second v)) ((string=? f "NANO_OF_SECOND") (lt-with t 'nano v))
             ((string=? f "NANO_OF_DAY") (jt-local-time v))
             (else (error #f (string-append "LocalTime.with unsupported field " f)))))
      ((jt-dt? t)
       (if (member f '("YEAR" "MONTH_OF_YEAR" "DAY_OF_MONTH" "DAY_OF_YEAR" "EPOCH_DAY"))
           (ldt-combine (temporal-with-field (ldt-date t) f v) (ldt-time t))
           (ldt-combine (ldt-date t) (temporal-with-field (ldt-time t) f v))))
      (else (error #f "with(field): unsupported temporal")))))

(define (temporal-supports-unit? t unit)
  (let ((u (string-upcase unit)))
    (cond ((jt-date? t) (and (member u '("DAYS" "WEEKS" "MONTHS" "YEARS" "DECADES" "CENTURIES" "MILLENNIA" "ERAS")) #t))
          ((jt-time? t) (and (member u '("NANOS" "MICROS" "MILLIS" "SECONDS" "MINUTES" "HOURS" "HALF_DAYS")) #t))
          ((or (jt-dt? t) (jt-instant? t)) (not (member u '("FOREVER"))))
          (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))
          ((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))
          (else #f))))

;; isSupported / get / getLong / with / range / plus / minus / until accept a
;; chrono-unit OR chrono-field jhost (or a string). These method names extend the
;; existing per-tag tables.
(define (unit-or-field-arg x) x)
(define (arg-is-unit? x) (and (jhost? x) (string=? (jhost-tag x) "chrono-unit")))
(define (arg-is-field? x) (and (jhost? x) (string=? (jhost-tag x) "chrono-field")))
(define (arg-is-amount? x) (and (jhost? x) (member (jhost-tag x) '("duration" "period"))))
(define (arg-unit-name x) (cond ((arg-is-unit? x) (cu-name x)) ((string? x) x) ((keyword? x) (keyword-t-name x)) (else #f)))
(define (arg-field-name x) (cond ((arg-is-field? x) (cf-name x)) ((string? x) x) ((keyword? x) (keyword-t-name x)) (else #f)))

;; the generic plus/minus/until/get/getLong/with/range/isSupported, shared by all
;; four core tags. plus/minus accept (n unit) or (amount); with accepts (field val).
(define (mk-temporal-methods)
  (list
   (cons "plus" (case-lambda
                  ((t a) (cond ((arg-is-amount? a) (if (string=? (jhost-tag a) "duration")
                                                       (temporal-plus-nanos t (dur-total-nanos a))
                                                       (temporal-plus-period t a 1)))
                               (else (error #f "plus: bad amount"))))
                  ((t n u) (temporal-plus-unit t (jt->exact n) (arg-unit-name u)))))
   (cons "minus" (case-lambda
                   ((t a) (cond ((arg-is-amount? a) (if (string=? (jhost-tag a) "duration")
                                                        (temporal-plus-nanos t (- (dur-total-nanos a)))
                                                        (temporal-plus-period t a -1)))
                                (else (error #f "minus: bad amount"))))
                   ((t n u) (temporal-plus-unit t (- (jt->exact n)) (arg-unit-name u)))))
   (cons "until" (lambda (t o u) (unit-between (arg-unit-name u) t o)))
   (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))))
   (cons "with" (case-lambda
                  ((t adj) (apply-adjuster t adj))                         ; (.with t adjuster)
                  ((t f v) (temporal-with-field t (arg-field-name f) (jt->exact v)))))
   (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 "range" (lambda (t f) (temporal-range t (arg-field-name f))))))
(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))
(register-host-methods! "instant" (mk-temporal-methods))

;; --- TemporalAdjuster: a date->date transform applied via (.with t adjuster) --
;; Stored as a jhost over a procedure that takes/returns a LocalDate; for a
;; LocalDateTime the date part is adjusted and the time kept.
(define (jt-adjuster proc) (make-jhost "temporal-adjuster" (vector proc)))
(define (adjuster-proc a) (vector-ref (jhost-state a) 0))
(define (apply-adjuster t adj)
  (let ((proc (cond ((and (jhost? adj) (string=? (jhost-tag adj) "temporal-adjuster")) (adjuster-proc adj))
                    ((procedure? adj) adj)
                    (else (error #f "with: expected a TemporalAdjuster")))))
    (cond ((jt-date? t) (proc t))
          ((jt-dt? t) (ldt-combine (proc (ldt-date t)) (ldt-time t)))
          (else (proc t)))))
;; the next/previous day-of-week adjusters (dow target 1=Mon..7=Sun).
(define (adj-next-dow d target same?)
  (let* ((cur (ld-dow (ld-epoch-day d)))
         (delta (jt-floor-mod (- target cur) 7))
         (step (if (and same? (= delta 0)) 0 (if (= delta 0) 7 delta))))
    (jt-local-date (+ (ld-epoch-day d) step))))
(define (adj-prev-dow d target same?)
  (let* ((cur (ld-dow (ld-epoch-day d)))
         (delta (jt-floor-mod (- cur target) 7))
         (step (if (and same? (= delta 0)) 0 (if (= delta 0) 7 delta))))
    (jt-local-date (- (ld-epoch-day d) step))))
(define (dow-target dow) (cond ((and (jhost? dow) (string=? (jhost-tag dow) "dow-enum")) (dow-val dow))
                               (else (jt->exact dow))))
(define (adj-first-day-of-month d)
  (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) (jt-local-date (ymd->epoch-day y m 1)))))
(define (adj-last-day-of-month d)
  (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) (jt-local-date (ymd->epoch-day y m (jt-len-of-month y m))))))
(define (adj-day-of-week-in-month d ordinal target)
  ;; the ordinal-th (1-based) `target` weekday in d's month (negative counts from end).
  (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d)))
    (lambda (y m dd)
      (if (>= ordinal 0)
          (let* ((first (jt-local-date (ymd->epoch-day y m 1)))
                 (first-match (adj-next-dow first target #t)))
            (jt-local-date (+ (ld-epoch-day first-match) (* 7 (- (max 1 ordinal) 1)))))
          (let* ((last (adj-last-day-of-month d))
                 (last-match (adj-prev-dow last target #t)))
            (jt-local-date (- (ld-epoch-day last-match) (* 7 (- (- ordinal) 1)))))))))
(register-class-statics! "TemporalAdjusters"
  (list (cons "firstDayOfMonth" (lambda () (jt-adjuster adj-first-day-of-month)))
        (cons "lastDayOfMonth" (lambda () (jt-adjuster adj-last-day-of-month)))
        (cons "firstDayOfNextMonth" (lambda () (jt-adjuster (lambda (d) (jt-local-date (+ (ld-epoch-day (adj-last-day-of-month d)) 1))))))
        (cons "firstDayOfYear" (lambda () (jt-adjuster (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) (jt-local-date (ymd->epoch-day y 1 1))))))))
        (cons "lastDayOfYear" (lambda () (jt-adjuster (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) (jt-local-date (ymd->epoch-day y 12 31))))))))
        (cons "firstDayOfNextYear" (lambda () (jt-adjuster (lambda (d) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day d))) (lambda (y m dd) (jt-local-date (ymd->epoch-day (+ y 1) 1 1))))))))
        (cons "dayOfWeekInMonth" (lambda (ordinal dow) (jt-adjuster (lambda (d) (adj-day-of-week-in-month d (jt->exact ordinal) (dow-target dow))))))
        (cons "firstInMonth" (lambda (dow) (jt-adjuster (lambda (d) (adj-day-of-week-in-month d 1 (dow-target dow))))))
        (cons "lastInMonth" (lambda (dow) (jt-adjuster (lambda (d) (adj-day-of-week-in-month d -1 (dow-target dow))))))
        (cons "next" (lambda (dow) (jt-adjuster (lambda (d) (adj-next-dow d (dow-target dow) #f)))))
        (cons "nextOrSame" (lambda (dow) (jt-adjuster (lambda (d) (adj-next-dow d (dow-target dow) #t)))))
        (cons "previous" (lambda (dow) (jt-adjuster (lambda (d) (adj-prev-dow d (dow-target dow) #f)))))
        (cons "previousOrSame" (lambda (dow) (jt-adjuster (lambda (d) (adj-prev-dow d (dow-target dow) #t)))))
        (cons "ofDateAdjuster" (lambda (f) (jt-adjuster (lambda (d) (jolt-invoke f d)))))))
(register-host-methods! "temporal-adjuster"
  (list (cons "adjustInto" (lambda (a t) (apply-adjuster t a)))))

;; range(field): a ValueRange. A small set of common fields; others fall back to a
;; generous range so callers that only read min/max don't crash.
(define (temporal-range t field)
  (let ((f (string-upcase field)))
    (cond
      ((and (jt-date? t) (string=? f "DAY_OF_MONTH"))
       (jt-value-range 1 1 28 (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day t))) (lambda (y m d) (jt-len-of-month y m)))))
      ((string=? f "MONTH_OF_YEAR") (jt-value-range 1 1 12 12))
      ((string=? f "DAY_OF_WEEK") (jt-value-range 1 1 7 7))
      ((string=? f "HOUR_OF_DAY") (jt-value-range 0 0 23 23))
      ((string=? f "MINUTE_OF_HOUR") (jt-value-range 0 0 59 59))
      ((string=? f "SECOND_OF_MINUTE") (jt-value-range 0 0 59 59))
      ((string=? f "NANO_OF_SECOND") (jt-value-range 0 0 999999999 999999999))
      (else (jt-value-range 0 0 999999999999 999999999999)))))

;; --- equality / hash / compare / print / instance? --------------------------
(define (jt-date? x) (and (jhost? x) (string=? (jhost-tag x) "local-date")))
(define (jt-time? x) (and (jhost? x) (string=? (jhost-tag x) "local-time")))
(define (jt-dt? x) (and (jhost? x) (string=? (jhost-tag x) "local-date-time")))

(register-eq-arm! (lambda (a b) (or (jt-date? a) (jt-date? b)))
                  (lambda (a b) (and (jt-date? a) (jt-date? b) (= (ld-epoch-day a) (ld-epoch-day b)))))
(register-hash-arm! jt-date? (lambda (x) (jolt-hash (ld-epoch-day x))))
(register-eq-arm! (lambda (a b) (or (jt-time? a) (jt-time? b)))
                  (lambda (a b) (and (jt-time? a) (jt-time? b) (= (lt-nano-of-day a) (lt-nano-of-day b)))))
(register-hash-arm! jt-time? (lambda (x) (jolt-hash (lt-nano-of-day x))))
(register-eq-arm! (lambda (a b) (or (jt-dt? a) (jt-dt? b)))
                  (lambda (a b) (and (jt-dt? a) (jt-dt? b) (ldt=? a b))))
(register-hash-arm! jt-dt? (lambda (x) (jolt-hash (+ (* (ldt-epoch-day x) 31) (ldt-nano-of-day x)))))

(register-str-render! jt-date? (lambda (x) (iso-date-str (ld-epoch-day x))))
(register-pr-arm! jt-date? (lambda (x) (iso-date-str (ld-epoch-day x))))
(register-str-render! jt-time? (lambda (x) (iso-time-str (lt-nano-of-day x))))
(register-pr-arm! jt-time? (lambda (x) (iso-time-str (lt-nano-of-day x))))
(register-str-render! jt-dt? (lambda (x) (iso-datetime-str (ldt-epoch-day x) (ldt-nano-of-day x))))
(register-pr-arm! jt-dt? (lambda (x) (iso-datetime-str (ldt-epoch-day x) (ldt-nano-of-day x))))

;; the "instant" jhost prints as a java.time ISO instant (…Z), not the bare record.
(define (jt-instant? x) (and (jhost? x) (string=? (jhost-tag x) "instant")))
(register-str-render! jt-instant? (lambda (x) (iso-instant-str-nanos (inst-nanos x))))
(register-pr-arm! jt-instant? (lambda (x) (iso-instant-str-nanos (inst-nanos x))))

;; Phase-2 value types: amounts, enums, and the chrono-unit/field tokens. Each
;; prints as its java.time toString and is = / hashed on its canonical state.
(define (jt-tagged? tag) (lambda (x) (and (jhost? x) (string=? (jhost-tag x) tag))))
(define (register-jt-value! tag str-fn hash-fn)
  (let ((pred (jt-tagged? tag)))
    (register-str-render! pred str-fn)
    (register-pr-arm! pred str-fn)
    (register-eq-arm! (lambda (a b) (or (pred a) (pred b)))
                      (lambda (a b) (and (pred a) (pred b) (equal? (jhost-state a) (jhost-state b)))))
    (register-hash-arm! pred hash-fn)))
(register-jt-value! "duration" dur->string (lambda (x) (jolt-hash (dur-total-nanos x))))
(register-jt-value! "period" per->string (lambda (x) (jolt-hash (+ (per-years x) (per-months x) (per-days x)))))
(register-jt-value! "month-enum" (lambda (x) (month-name (month-val x))) (lambda (x) (jolt-hash (month-val x))))
(register-jt-value! "dow-enum" (lambda (x) (dow-name (dow-val x))) (lambda (x) (jolt-hash (dow-val x))))
(register-jt-value! "year" (lambda (x) (number->string (year-val x))) (lambda (x) (jolt-hash (year-val x))))
(register-jt-value! "year-month" ym->string (lambda (x) (jolt-hash (+ (* (ym-year x) 13) (ym-month x)))))
(register-jt-value! "chrono-unit" cu-name (lambda (x) (jolt-hash (cu-name x))))
(register-jt-value! "chrono-field" cf-name (lambda (x) (jolt-hash (cf-name x))))

;; compare: same-type java.time values compare on their canonical state.
(define %jt-prev-compare jolt-compare)
(set! jolt-compare
  (lambda (a b)
    (cond
      ((and (jt-date? a) (jt-date? b)) (cond ((< (ld-epoch-day a) (ld-epoch-day b)) -1) ((> (ld-epoch-day a) (ld-epoch-day b)) 1) (else 0)))
      ((and (jt-time? a) (jt-time? b)) (cond ((< (lt-nano-of-day a) (lt-nano-of-day b)) -1) ((> (lt-nano-of-day a) (lt-nano-of-day b)) 1) (else 0)))
      ((and (jt-dt? a) (jt-dt? b)) (ldt-cmp a b))
      ((and (jt-instant? a) (jt-instant? b)) (cond ((< (inst-nanos a) (inst-nanos b)) -1) ((> (inst-nanos a) (inst-nanos b)) 1) (else 0)))
      ((and (jt-tagged-as? a "duration") (jt-tagged-as? b "duration"))
       (let ((x (dur-total-nanos a)) (y (dur-total-nanos b))) (cond ((< x y) -1) ((> x y) 1) (else 0))))
      ((and (jt-tagged-as? a "month-enum") (jt-tagged-as? b "month-enum")) (- (month-val a) (month-val b)))
      ((and (jt-tagged-as? a "dow-enum") (jt-tagged-as? b "dow-enum")) (- (dow-val a) (dow-val b)))
      ((and (jt-tagged-as? a "year") (jt-tagged-as? b "year"))
       (let ((x (year-val a)) (y (year-val b))) (cond ((< x y) -1) ((> x y) 1) (else 0))))
      ((and (jt-tagged-as? a "year-month") (jt-tagged-as? b "year-month")) (ym-cmp a b))
      (else (%jt-prev-compare a b)))))
(define (jt-tagged-as? x tag) (and (jhost? x) (string=? (jhost-tag x) tag)))
(def-var! "clojure.core" "compare" jolt-compare)

;; instance? for the three new tags (inst-time.ss already answers "instant").
(register-instance-check-arm!
  (lambda (type-sym val)
    (let ((tn (short-class-name (symbol-t-name type-sym))))
      (cond
        ((jt-date? val) (if (string=? tn "LocalDate") #t 'pass))
        ((jt-time? val) (if (string=? tn "LocalTime") #t 'pass))
        ((jt-dt? val) (if (string=? tn "LocalDateTime") #t 'pass))
        ((jt-tagged-as? val "duration") (if (member tn '("Duration" "TemporalAmount")) #t 'pass))
        ((jt-tagged-as? val "period") (if (member tn '("Period" "TemporalAmount")) #t 'pass))
        ((jt-tagged-as? val "month-enum") (if (string=? tn "Month") #t 'pass))
        ((jt-tagged-as? val "dow-enum") (if (string=? tn "DayOfWeek") #t 'pass))
        ((jt-tagged-as? val "year") (if (string=? tn "Year") #t 'pass))
        ((jt-tagged-as? val "year-month") (if (string=? tn "YearMonth") #t 'pass))
        ((jt-tagged-as? val "chrono-unit") (if (member tn '("ChronoUnit" "TemporalUnit")) #t 'pass))
        ((jt-tagged-as? val "chrono-field") (if (member tn '("ChronoField" "TemporalField")) #t 'pass))
        (else 'pass)))))

;; ====================================================================
;; Phase 3: zones, offset/zoned date-times, clocks, formatter integration.
;; ====================================================================
;;
;; Chez exposes local + UTC + fixed offsets, not an IANA tz database. Fixed-offset
;; and UTC zones are exact. A named zone (America/New_York) maps to a representative
;; fixed offset from a small table; arbitrary-instant DST transitions are NOT modeled.
;;
;;   zone-offset       (vector total-seconds)
;;   zone-id           (vector id offset-seconds)        offset-seconds: resolved fixed offset
;;   offset-time       (vector nano-of-day offset-seconds)
;;   offset-date-time  (vector epoch-day nano-of-day offset-seconds)
;;   zoned-date-time   (vector epoch-day nano-of-day offset-seconds zone-id)
;;   clock             (vector kind fixed-ms zone-id [base-clock])   kind: 'system | 'fixed | 'offset | 'tick

;; --- ZoneOffset --------------------------------------------------------------
(define (jt-zone-offset secs) (make-jhost "zone-offset" (vector secs)))
(define (zo-secs z) (vector-ref (jhost-state z) 0))
(define (jt-zone-offset? x) (and (jhost? x) (string=? (jhost-tag x) "zone-offset")))
;; ZoneOffset id: "Z" for 0, else ±HH:mm[:ss] with the seconds field elided at :00.
(define (zo-id secs)
  (if (= secs 0) "Z"
      (let* ((neg (< secs 0)) (a (abs secs))
             (h (quotient a 3600)) (m (quotient (modulo a 3600) 60)) (s (modulo a 60)))
        (string-append (if neg "-" "+") (pad2 h) ":" (pad2 m)
                       (if (= s 0) "" (string-append ":" (pad2 s)))))))
;; parse a ZoneOffset id: "Z"/"+00:00" -> 0, "+HH:mm[:ss]" / "+HHmm" / "+HH".
(define (parse-zone-offset s)
  (let ((str (jt-str s)))
    (cond
      ((or (string=? str "Z") (string=? str "z") (string=? str "UTC") (string=? str "GMT") (string=? str "+00:00")) 0)
      (else
       (let* ((sign (cond ((char=? (string-ref str 0) #\-) -1) (else 1)))
              (body (if (memv (string-ref str 0) '(#\+ #\-)) (substring str 1 (string-length str)) str))
              (parts (let loop ((i 0) (cur '()) (acc '()))
                       (cond ((>= i (string-length body))
                              (reverse (cons (list->string (reverse cur)) acc)))
                             ((char=? (string-ref body i) #\:) (loop (+ i 1) '() (cons (list->string (reverse cur)) acc)))
                             (else (loop (+ i 1) (cons (string-ref body i) cur) acc))))))
         (if (and (= (length parts) 1) (> (string-length (car parts)) 2))
             ;; "+HHmm" / "+HHmmss" compact form
             (let ((b (car parts)))
               (* sign (+ (* (or (string->number (substring b 0 2)) 0) 3600)
                          (* (if (>= (string-length b) 4) (or (string->number (substring b 2 4)) 0) 0) 60)
                          (if (>= (string-length b) 6) (or (string->number (substring b 4 6)) 0) 0))))
             (* sign (+ (* (or (string->number (list-ref parts 0)) 0) 3600)
                        (* (if (> (length parts) 1) (or (string->number (list-ref parts 1)) 0) 0) 60)
                        (if (> (length parts) 2) (or (string->number (list-ref parts 2)) 0) 0)))))))))

(register-class-statics! "ZoneOffset"
  (list (cons "of" (lambda (s) (jt-zone-offset (parse-zone-offset s))))
        (cons "ofTotalSeconds" (lambda (n) (jt-zone-offset (jt->exact n))))
        (cons "ofHours" (lambda (h) (jt-zone-offset (* (jt->exact h) 3600))))
        ;; java.time requires h/m/s to share a sign; the total is the plain sum.
        (cons "ofHoursMinutes" (lambda (h m) (jt-zone-offset (+ (* (jt->exact h) 3600) (* (jt->exact m) 60)))))
        (cons "ofHoursMinutesSeconds" (lambda (h m s) (jt-zone-offset (+ (* (jt->exact h) 3600) (* (jt->exact m) 60) (jt->exact s)))))
        (cons "from" (lambda (t) (cond ((jt-zone-offset? t) t)
                                       ((jt-offset-dt? t) (jt-zone-offset (odt-offset t)))
                                       ((jt-zoned-dt? t) (jt-zone-offset (zdt-offset t)))
                                       (else (error #f "ZoneOffset/from: unsupported")))))
        (cons "UTC" (jt-zone-offset 0))
        (cons "MIN" (jt-zone-offset (* -18 3600)))
        (cons "MAX" (jt-zone-offset (* 18 3600)))))
(register-host-methods! "zone-offset"
  (list (cons "getId" (lambda (z) (zo-id (zo-secs z))))
        (cons "getTotalSeconds" (lambda (z) (zo-secs z)))
        (cons "getRules" (lambda (z) (make-jhost "zone-rules" (vector (zo-secs z)))))
        (cons "normalized" (lambda (z) z))
        (cons "compareTo" (lambda (z o) (let ((a (zo-secs z)) (b (zo-secs o))) (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (z o) (and (jt-zone-offset? o) (= (zo-secs z) (zo-secs o)))))
        (cons "hashCode" (lambda (z) (zo-secs z)))
        (cons "toString" (lambda (z) (zo-id (zo-secs z))))))

;; --- ZoneId ------------------------------------------------------------------
;; Named-zone offsets the corpus/tick set the JVM tz to. A fixed best-effort offset;
;; arbitrary-instant DST is not modeled.
(define zone-offset-table
  '(("UTC" . 0) ("GMT" . 0) ("Z" . 0) ("Etc/UTC" . 0) ("Etc/GMT" . 0)
    ("America/New_York" . -18000) ("America/Chicago" . -21600) ("America/Denver" . -25200)
    ("America/Los_Angeles" . -28800) ("America/Toronto" . -18000) ("America/Sao_Paulo" . -10800)
    ("Europe/London" . 0) ("Europe/Paris" . 3600) ("Europe/Berlin" . 3600) ("Europe/Madrid" . 3600)
    ("Europe/Moscow" . 10800) ("Asia/Tokyo" . 32400) ("Asia/Shanghai" . 28800)
    ("Asia/Kolkata" . 19800) ("Australia/Sydney" . 36000) ("Pacific/Auckland" . 43200)))
;; java.time ZoneId.SHORT_IDS: 3-letter ids -> region/offset ids.
(define short-ids-pairs
  '(("ACT" . "Australia/Darwin") ("AET" . "Australia/Sydney") ("AGT" . "America/Argentina/Buenos_Aires")
    ("ART" . "Africa/Cairo") ("AST" . "America/Anchorage") ("BET" . "America/Sao_Paulo")
    ("BST" . "Asia/Dhaka") ("CAT" . "Africa/Harare") ("CNT" . "America/St_Johns")
    ("CST" . "America/Chicago") ("CTT" . "Asia/Shanghai") ("EAT" . "Africa/Addis_Ababa")
    ("ECT" . "Europe/Paris") ("IET" . "America/Indiana/Indianapolis") ("IST" . "Asia/Kolkata")
    ("JST" . "Asia/Tokyo") ("MIT" . "Pacific/Apia") ("NET" . "Asia/Yerevan")
    ("NST" . "Pacific/Auckland") ("PLT" . "Asia/Karachi") ("PNT" . "America/Phoenix")
    ("PRT" . "America/Puerto_Rico") ("PST" . "America/Los_Angeles") ("SST" . "Pacific/Guadalcanal")
    ("VST" . "Asia/Ho_Chi_Minh") ("EST" . "-05:00") ("MST" . "-07:00") ("HST" . "-10:00")))
(define (jt-zone-id id off) (make-jhost "zone-id" (vector id off)))
(define (zid-id z) (vector-ref (jhost-state z) 0))
(define (zid-off z) (vector-ref (jhost-state z) 1))
(define (jt-zone-id? x) (and (jhost? x) (string=? (jhost-tag x) "zone-id")))
;; system zone offset. The inst/SimpleDateFormat layer is UTC-centric (tz-free,
;; machine-independent by design), so systemDefault resolves to UTC: a LocalDateTime
;; round-tripped through atZone/toInstant stays aligned with the UTC #inst model.
;; (Chez's real machine offset is available via date-zone-offset, but using it would
;; make results machine-tz-dependent and break that round-trip.)
(define (system-zone-offset-secs) 0)
;; --- DST rules ---------------------------------------------------------------
;; A DST-observing named zone has a standard offset plus a rule family (US/Canada
;; or EU) that puts the clock one hour ahead for part of the year. The rule yields
;; the two transition points; outside the table a zone keeps a fixed offset.
;; Southern-hemisphere zones (Sydney/Auckland) stay fixed — their reversed DST
;; isn't exercised and a fixed offset preserves existing behavior.
(define dst-saving 3600)
;; day-of-week of an epoch-day, 0=Sunday (1970-01-01 is a Thursday).
(define (epoch-day-dow ed) (modulo (+ ed 4) 7))
;; epoch-day of the nth (1-based) weekday `dow` in (year, month); n<0 counts from the end.
(define (nth-dow-epoch-day year month dow n)
  (if (> n 0)
      (let* ((first-ed (ymd->epoch-day year month 1))
             (shift (modulo (- dow (epoch-day-dow first-ed)) 7)))
        (+ first-ed shift (* 7 (- n 1))))
      (let* ((last-ed (ymd->epoch-day year month (jt-len-of-month year month)))
             (shift (modulo (- (epoch-day-dow last-ed) dow) 7)))
        (- last-ed shift))))
;; year of a UTC/local epoch-seconds value.
(define (secs->year s)
  (call-with-values (lambda () (civil-from-days (jt-floor-div s 86400)))
    (lambda (y m d) y)))
;; zone-id -> (standard-offset-secs . rule-symbol: us | eu).
(define dst-zone-table
  '(("America/New_York" -18000 . us) ("America/Toronto" -18000 . us)
    ("America/Chicago" -21600 . us) ("America/Denver" -25200 . us)
    ("America/Los_Angeles" -28800 . us)
    ("Europe/London" 0 . eu) ("Europe/Paris" 3600 . eu)
    ("Europe/Berlin" 3600 . eu) ("Europe/Madrid" 3600 . eu)))
(define (dst-entry id) (assoc id dst-zone-table))
;; offset for a DST zone at a UTC instant (epoch-seconds).
(define (dst-offset-at-instant id std rule secs)
  (let ((year (secs->year secs)))
    (case rule
      ;; US: spring 2nd Sun Mar 02:00 local std; fall 1st Sun Nov 02:00 local DST.
      ((us)
       (let ((spring (- (+ (* (nth-dow-epoch-day year 3 0 2) 86400) (* 2 3600)) std))
             (fall   (- (+ (* (nth-dow-epoch-day year 11 0 1) 86400) (* 2 3600)) (+ std dst-saving))))
         (if (and (<= spring secs) (< secs fall)) (+ std dst-saving) std)))
      ;; EU: spring last Sun Mar 01:00 UTC; fall last Sun Oct 01:00 UTC.
      ((eu)
       (let ((spring (+ (* (nth-dow-epoch-day year 3 0 -1) 86400) 3600))
             (fall   (+ (* (nth-dow-epoch-day year 10 0 -1) 86400) 3600)))
         (if (and (<= spring secs) (< secs fall)) (+ std dst-saving) std)))
      (else std))))
;; offset for a DST zone given a local wall-time (epoch-seconds in local time).
;; Boundary hours (the spring gap / fall overlap) resolve to the simple window test.
(define (dst-offset-at-local id std rule lsecs)
  (let ((year (secs->year lsecs)))
    (case rule
      ((us)
       (let ((spring (+ (* (nth-dow-epoch-day year 3 0 2) 86400) (* 2 3600)))
             (fall   (+ (* (nth-dow-epoch-day year 11 0 1) 86400) (* 2 3600))))
         (if (and (<= spring lsecs) (< lsecs fall)) (+ std dst-saving) std)))
      ((eu)
       (let ((spring (+ (* (nth-dow-epoch-day year 3 0 -1) 86400) 3600 std))
             (fall   (+ (* (nth-dow-epoch-day year 10 0 -1) 86400) 7200 std)))
         (if (and (<= spring lsecs) (< lsecs fall)) (+ std dst-saving) std)))
      (else std))))
;; DST-aware offset for a resolved (id . std-off) at a UTC instant / local wall-time.
;; Falls back to the resolved fixed offset for non-DST zones.
(define (zone-offset-at-instant id std secs)
  (let ((e (dst-entry id))) (if e (dst-offset-at-instant id (cadr e) (cddr e) secs) std)))
(define (zone-offset-at-local id std lsecs)
  (let ((e (dst-entry id))) (if e (dst-offset-at-local id (cadr e) (cddr e) lsecs) std)))

;; resolve any zone designator (string / ZoneId / ZoneOffset) to a (id . offset).
(define (resolve-zone z)
  (cond
    ((jt-zone-offset? z) (cons (zo-id (zo-secs z)) (zo-secs z)))
    ((jt-zone-id? z) (cons (zid-id z) (zid-off z)))
    (else
     (let ((id (jt-str z)))
       (cond
         ((string=? id "system") (cons (zo-id (system-zone-offset-secs)) (system-zone-offset-secs)))
         ((or (string=? id "Z") (string=? id "UTC") (string=? id "GMT")) (cons "Z" 0))
         ((and (> (string-length id) 0) (memv (string-ref id 0) '(#\+ #\-)))
          (let ((s (parse-zone-offset id))) (cons (zo-id s) s)))
         ((assoc id zone-offset-table) (cons id (cdr (assoc id zone-offset-table))))
         (else (cons id 0)))))))           ; unknown named zone: treat as UTC, id preserved
(define (zone-id-of z)
  (let ((r (resolve-zone z))) (jt-zone-id (car r) (cdr r))))

(register-class-statics! "ZoneId"
  (list (cons "of" (lambda (id . _) (zone-id-of id)))
        (cons "systemDefault" (lambda () (let ((s (system-zone-offset-secs))) (jt-zone-id (zo-id s) s))))
        (cons "getAvailableZoneIds" (lambda () (fold-left pset-conj empty-pset (map car zone-offset-table))))
        (cons "SHORT_IDS" (apply jolt-hash-map (apply append (map (lambda (p) (list (car p) (cdr p))) short-ids-pairs))))
        (cons "from" (lambda (t) (cond ((jt-zone-id? t) t) ((jt-zone-offset? t) (jt-zone-id (zo-id (zo-secs t)) (zo-secs t)))
                                       ((jt-zoned-dt? t) (zdt-zone t)) (else (error #f "ZoneId/from: unsupported")))))))
(register-host-methods! "zone-id"
  (list (cons "getId" (lambda (z) (zid-id z)))
        (cons "getRules" (lambda (z) (make-jhost "zone-rules" (vector (zid-id z) (zid-off z)))))
        (cons "normalized" (lambda (z) (if (and (> (string-length (zid-id z)) 0) (memv (string-ref (zid-id z) 0) '(#\+ #\- #\Z)))
                                           (jt-zone-offset (zid-off z)) z)))
        (cons "getDisplayName" (lambda (z . _) (zid-id z)))
        (cons "equals" (lambda (z o) (and (jt-zone-id? o) (string=? (zid-id z) (zid-id o)))))
        (cons "hashCode" (lambda (z) (string-hash (zid-id z))))
        (cons "toString" (lambda (z) (zid-id z)))))
;; ZoneRules carries the zone id + standard offset. getOffset is DST-aware: given
;; an Instant it resolves the offset at that instant, given a LocalDateTime at that
;; local wall time; with no argument it yields the standard offset.
(define (zr-id r) (vector-ref (jhost-state r) 0))
(define (zr-std r) (vector-ref (jhost-state r) 1))
(register-host-methods! "zone-rules"
  (list (cons "getOffset"
              (lambda (r . args)
                (if (null? args)
                    (jt-zone-offset (zr-std r))
                    (let ((a (car args)))
                      (cond
                        ((jt-instant-tag? a)
                         (jt-zone-offset (zone-offset-at-instant (zr-id r) (zr-std r) (jt-floor-div (inst-nanos a) nanos-per-sec))))
                        ((jinst? a)
                         (jt-zone-offset (zone-offset-at-instant (zr-id r) (zr-std r) (jt-floor-div (exact (truncate (jinst-ms a))) 1000))))
                        ((and (jhost? a) (string=? (jhost-tag a) "local-date-time"))
                         (jt-zone-offset (zone-offset-at-local (zr-id r) (zr-std r)
                                          (+ (* (ldt-epoch-day a) 86400) (jt-floor-div (ldt-nano-of-day a) nanos-per-sec)))))
                        (else (jt-zone-offset (zr-std r))))))))
        (cons "isFixedOffset" (lambda (r) (not (dst-entry (zr-id r)))))
        (cons "getStandardOffset" (lambda (r . _) (jt-zone-offset (zr-std r))))
        (cons "toString" (lambda (r) (if (dst-entry (zr-id r)) "ZoneRules" "ZoneRules[fixed]")))))

;; --- OffsetTime --------------------------------------------------------------
(define (jt-offset-time nod off) (make-jhost "offset-time" (vector nod off)))
(define (ot-nod x) (vector-ref (jhost-state x) 0))
(define (ot-offset x) (vector-ref (jhost-state x) 1))
(define (jt-offset-time? x) (and (jhost? x) (string=? (jhost-tag x) "offset-time")))
(define (ot->string x) (string-append (iso-time-str (ot-nod x)) (offset-suffix (ot-offset x))))
;; the offset suffix used in ISO offset/zoned rendering: "Z" for 0 else ±HH:mm[:ss].
(define (offset-suffix secs) (zo-id secs))

;; --- OffsetDateTime ----------------------------------------------------------
(define (jt-offset-dt ed nod off) (make-jhost "offset-date-time" (vector ed nod off)))
(define (odt-epoch-day x) (vector-ref (jhost-state x) 0))
(define (odt-nano-of-day x) (vector-ref (jhost-state x) 1))
(define (odt-offset x) (vector-ref (jhost-state x) 2))
(define (jt-offset-dt? x) (and (jhost? x) (string=? (jhost-tag x) "offset-date-time")))
(define (odt-ldt x) (jt-local-dt (odt-epoch-day x) (odt-nano-of-day x)))
;; epoch-ms of the instant this offset-dt denotes (subtract the offset to reach UTC).
(define (odt->ms x) (- (ldt->ms (odt-ldt x)) (* (odt-offset x) 1000)))
(define (odt->nanos x) (- (+ (* (odt-epoch-day x) nanos-per-day) (odt-nano-of-day x)) (* (odt-offset x) nanos-per-sec)))
(define (odt->string x) (string-append (iso-datetime-str (odt-epoch-day x) (odt-nano-of-day x)) (offset-suffix (odt-offset x))))

;; --- ZonedDateTime -----------------------------------------------------------
(define (jt-zoned-dt ed nod off zone) (make-jhost "zoned-date-time" (vector ed nod off zone)))
(define (zdt-epoch-day x) (vector-ref (jhost-state x) 0))
(define (zdt-nano-of-day x) (vector-ref (jhost-state x) 1))
(define (zdt-offset x) (vector-ref (jhost-state x) 2))
(define (zdt-zone x) (vector-ref (jhost-state x) 3))
(define (jt-zoned-dt? x) (and (jhost? x) (string=? (jhost-tag x) "zoned-date-time")))
(define (zdt-ldt x) (jt-local-dt (zdt-epoch-day x) (zdt-nano-of-day x)))
(define (zdt->ms x) (- (ldt->ms (zdt-ldt x)) (* (zdt-offset x) 1000)))
(define (zdt->nanos x) (- (+ (* (zdt-epoch-day x) nanos-per-day) (zdt-nano-of-day x)) (* (zdt-offset x) nanos-per-sec)))
;; ISO zoned: local + offset, then [zone-id] unless the zone IS the offset.
(define (zdt->string x)
  (let* ((zid (zdt-zone x)) (id (zid-id zid)))
    (string-append (iso-datetime-str (zdt-epoch-day x) (zdt-nano-of-day x)) (offset-suffix (zdt-offset x))
                   (if (or (string=? id (offset-suffix (zdt-offset x)))
                           (and (> (string-length id) 0) (memv (string-ref id 0) '(#\+ #\- #\Z))))
                       "" (string-append "[" id "]")))))

;; build a ZonedDateTime/OffsetDateTime from a LocalDateTime + zone designator. The
;; offset is the zone's fixed offset (no DST resolution).
(define (zoned-of-ldt ldt zone)
  (let* ((r (resolve-zone zone))
         (lsecs (+ (* (ldt-epoch-day ldt) 86400) (jt-floor-div (ldt-nano-of-day ldt) 1000000000)))
         (off (zone-offset-at-local (car r) (cdr r) lsecs)))
    (jt-zoned-dt (ldt-epoch-day ldt) (ldt-nano-of-day ldt) off (jt-zone-id (car r) (cdr r)))))
(define (offset-of-ldt ldt off)
  (let ((secs (cond ((jt-zone-offset? off) (zo-secs off)) ((jt-zone-id? off) (zid-off off)) (else (parse-zone-offset off)))))
    (jt-offset-dt (ldt-epoch-day ldt) (ldt-nano-of-day ldt) secs)))
;; from an epoch-ms instant + zone: apply the zone offset to get the local fields.
(define (zoned-of-instant-ms ms zone)
  (let* ((r (resolve-zone zone))
         (off (zone-offset-at-instant (car r) (cdr r) (jt-floor-div (exact (truncate ms)) 1000)))
         (local-ms (+ (exact (truncate ms)) (* off 1000)))
         (ed (jt-floor-div local-ms 86400000)) (nod (* (jt-floor-mod local-ms 86400000) 1000000)))
    (jt-zoned-dt ed nod off (jt-zone-id (car r) (cdr r)))))
(define (offset-of-instant-ms ms off-or-zone)
  (let* ((secs (cond ((jt-zone-offset? off-or-zone) (zo-secs off-or-zone))
                     ((jt-zone-id? off-or-zone) (zid-off off-or-zone))
                     (else (let ((r (resolve-zone off-or-zone)))
                             (zone-offset-at-instant (car r) (cdr r) (jt-floor-div (exact (truncate ms)) 1000))))))
         (local-ms (+ (exact (truncate ms)) (* secs 1000)))
         (ed (jt-floor-div local-ms 86400000)) (nod (* (jt-floor-mod local-ms 86400000) 1000000)))
    (jt-offset-dt ed nod secs)))
;; nano-precise instant -> zoned/offset (Instant carries epoch-nanos; ms versions
;; above stay for the ms-based Date/Calendar callers).
(define (zoned-of-instant-nanos nanos zone)
  (let* ((r (resolve-zone zone))
         (off (zone-offset-at-instant (car r) (cdr r) (jt-floor-div nanos nanos-per-sec)))
         (local-nanos (+ nanos (* off nanos-per-sec)))
         (ed (jt-floor-div local-nanos nanos-per-day)) (nod (jt-floor-mod local-nanos nanos-per-day)))
    (jt-zoned-dt ed nod off (jt-zone-id (car r) (cdr r)))))
(define (offset-of-instant-nanos nanos off-or-zone)
  (let* ((off (cond ((jt-zone-offset? off-or-zone) (zo-secs off-or-zone))
                    ((jt-zone-id? off-or-zone) (zid-off off-or-zone))
                    (else (let ((r (resolve-zone off-or-zone)))
                            (zone-offset-at-instant (car r) (cdr r) (jt-floor-div nanos nanos-per-sec))))))
         (local-nanos (+ nanos (* off nanos-per-sec)))
         (ed (jt-floor-div local-nanos nanos-per-day)) (nod (jt-floor-mod local-nanos nanos-per-day)))
    (jt-offset-dt ed nod off)))

;; redefine mk-zoned (used by Phase-1/2 atZone/atOffset) to yield a real
;; ZonedDateTime at UTC. Older inst-time.ss "zoned-dt" callers route through here.
(set! mk-zoned (lambda (ms) (zoned-of-instant-ms ms (jt-zone-id "Z" 0))))

;; --- now / Clock-aware statics -----------------------------------------------
;; A Clock yields an instant (epoch-ms) and a zone. now-from-clock reads ms.
(define (clock-now-ms clk)
  (cond ((not (jhost? clk)) (now-ms))
        ((string=? (jhost-tag clk) "clock") (clk-millis clk))
        (else (now-ms))))
(define (clock-zone clk)
  (if (and (jhost? clk) (string=? (jhost-tag clk) "clock")) (vector-ref (jhost-state clk) 2)
      (jt-zone-id "Z" 0)))
;; now-ms-arg: an optional first arg may be a Clock or a ZoneId; pick the ms.
(define (now-ms* args)
  (if (and (pair? args) (jhost? (car args)) (string=? (jhost-tag (car args)) "clock"))
      (clock-now-ms (car args))
      (now-ms)))

;; rewire the Phase-1/2 now statics to accept an optional Clock/ZoneId argument.
(register-class-statics! "Instant"
  (list (cons "now" (lambda args (mk-instant (now-ms* args))))))
(register-class-statics! "LocalDate"
  (list (cons "now" (lambda args (mk-local-date (now-ms* args))))))
(register-class-statics! "LocalTime"
  (list (cons "now" (lambda args (jt-local-time (* (jt-floor-mod (exact (truncate (now-ms* args))) 86400000) 1000000))))))
(register-class-statics! "LocalDateTime"
  (list (cons "now" (lambda args (mk-local (now-ms* args))))))

;; --- Clock -------------------------------------------------------------------
;; (vector kind fixed-ms zone [base])  kind 'system | 'fixed | 'offset | 'tick
(define (jt-clock kind ms zone . base) (make-jhost "clock" (vector kind ms zone (and (pair? base) (car base)))))
(define (clk-millis clk)
  (let ((kind (vector-ref (jhost-state clk) 0)))
    (case kind
      ((fixed) (vector-ref (jhost-state clk) 1))
      ((offset) (+ (clk-millis (vector-ref (jhost-state clk) 3)) (vector-ref (jhost-state clk) 1)))
      ((tick) (let* ((base (vector-ref (jhost-state clk) 3)) (dur-ms (vector-ref (jhost-state clk) 1))
                     (m (clk-millis base)))
                (if (> dur-ms 0) (* (jt-floor-div m dur-ms) dur-ms) m)))
      (else (now-ms)))))
(register-class-statics! "Clock"
  (list (cons "systemUTC" (lambda () (jt-clock 'system 0 (jt-zone-id "Z" 0))))
        (cons "systemDefaultZone" (lambda () (let ((s (system-zone-offset-secs))) (jt-clock 'system 0 (jt-zone-id (zo-id s) s)))))
        (cons "system" (lambda (zone) (jt-clock 'system 0 (zone-id-of zone))))
        (cons "fixed" (lambda (inst zone) (jt-clock 'fixed (exact (truncate (ms-of inst))) (zone-id-of zone))))
        (cons "offset" (lambda (clk dur) (jt-clock 'offset (quotient (dur-total-nanos dur) 1000000) (clock-zone clk) clk)))
        (cons "tick" (lambda (clk dur) (jt-clock 'tick (quotient (dur-total-nanos dur) 1000000) (clock-zone clk) clk)))
        (cons "tickMinutes" (lambda (zone) (jt-clock 'tick 60000 (zone-id-of zone) (jt-clock 'system 0 (zone-id-of zone)))))
        (cons "tickSeconds" (lambda (zone) (jt-clock 'tick 1000 (zone-id-of zone) (jt-clock 'system 0 (zone-id-of zone)))))))
(register-host-methods! "clock"
  (list (cons "instant" (lambda (clk) (mk-instant (clk-millis clk))))
        (cons "millis" (lambda (clk) (clk-millis clk)))
        (cons "getZone" (lambda (clk) (vector-ref (jhost-state clk) 2)))
        (cons "withZone" (lambda (clk zone) (make-jhost "clock" (vector (vector-ref (jhost-state clk) 0)
                                                                        (vector-ref (jhost-state clk) 1)
                                                                        (zone-id-of zone)
                                                                        (vector-ref (jhost-state clk) 3)))))
        (cons "equals" (lambda (clk o) (and (jhost? o) (string=? (jhost-tag o) "clock") (equal? (jhost-state clk) (jhost-state o)))))
        (cons "toString" (lambda (clk) "Clock"))))

;; --- ZonedDateTime statics + methods -----------------------------------------
(register-class-statics! "ZonedDateTime"
  (list (cons "of" (case-lambda
                     ((ldt zone) (zoned-of-ldt ldt zone))
                     ((d t zone) (zoned-of-ldt (jt-local-dt (ld-epoch-day d) (lt-nano-of-day t)) zone))
                     ((y mo d h mi s nano zone)
                      (zoned-of-ldt (jt-local-dt (ymd->epoch-day (jt->exact y) (jt->exact mo) (jt->exact d))
                                                 (hmsn->nano (jt->exact h) (jt->exact mi) (jt->exact s) (jt->exact nano))) zone))))
        (cons "ofInstant" (case-lambda
                            ((inst zone) (zoned-of-instant-ms (ms-of inst) zone))
                            ((ldt off zone) (zoned-of-ldt ldt zone))))
        (cons "ofLocal" (lambda (ldt zone . _) (zoned-of-ldt ldt zone)))
        (cons "now" (lambda args (let ((ms (now-ms* args)))
                                   (zoned-of-instant-ms ms (if (and (pair? args) (jhost? (car args)) (string=? (jhost-tag (car args)) "clock"))
                                                               (clock-zone (car args)) (jt-zone-id "Z" 0))))))
        (cons "parse" (lambda (s . fmt)
                        (if (null? fmt) (parse-zoned-date-time (jt-str s))
                            (call-with-values (lambda () (formatter-parse-fields (car fmt) s))
                              (lambda (ed nod) (jt-zoned-dt ed nod 0 (jt-zone-id "Z" 0)))))))
        (cons "from" (lambda (t) (cond ((jt-zoned-dt? t) t)
                                       ((jt-offset-dt? t) (jt-zoned-dt (odt-epoch-day t) (odt-nano-of-day t) (odt-offset t) (jt-zone-id (zo-id (odt-offset t)) (odt-offset t))))
                                       (else (zoned-of-instant-ms (ms-of t) (jt-zone-id "Z" 0))))))))

;; apply nano arithmetic to a zoned/offset value, keeping its offset & zone.
(define (zdt-plus-nanos x nanos)
  (let ((nldt (ldt-plus-nanos (zdt-ldt x) nanos)))
    (jt-zoned-dt (ldt-epoch-day nldt) (ldt-nano-of-day nldt) (zdt-offset x) (zdt-zone x))))
(define (zdt-with-ldt x ldt) (jt-zoned-dt (ldt-epoch-day ldt) (ldt-nano-of-day ldt) (zdt-offset x) (zdt-zone x)))

(register-host-methods! "zoned-date-time"
  (list (cons "getYear" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (zdt-epoch-day x))) (lambda (y m d) y))))
        (cons "getMonthValue" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (zdt-epoch-day x))) (lambda (y m d) m))))
        (cons "getMonth" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (zdt-epoch-day x))) (lambda (y m d) (jt-month m)))))
        (cons "getDayOfMonth" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (zdt-epoch-day x))) (lambda (y m d) d))))
        (cons "getDayOfWeek" (lambda (x) (jt-dow (ld-dow (zdt-epoch-day x)))))
        (cons "getDayOfYear" (lambda (x) (ld-day-of-year (zdt-epoch-day x))))
        (cons "getHour" (lambda (x) (lt-hour (zdt-ldt x))))
        (cons "getMinute" (lambda (x) (lt-minute (zdt-ldt x))))
        (cons "getSecond" (lambda (x) (lt-second (zdt-ldt x))))
        (cons "getNano" (lambda (x) (lt-nano (zdt-ldt x))))
        (cons "getOffset" (lambda (x) (jt-zone-offset (zdt-offset x))))
        (cons "getZone" (lambda (x) (zdt-zone x)))
        (cons "toInstant" (lambda (x) (mk-instant-nanos (zdt->nanos x))))
        (cons "toLocalDate" (lambda (x) (jt-local-date (zdt-epoch-day x))))
        (cons "toLocalTime" (lambda (x) (jt-local-time (zdt-nano-of-day x))))
        (cons "toLocalDateTime" (lambda (x) (zdt-ldt x)))
        (cons "toOffsetDateTime" (lambda (x) (jt-offset-dt (zdt-epoch-day x) (zdt-nano-of-day x) (zdt-offset x))))
        (cons "toEpochSecond" (lambda (x) (jt-floor-div (zdt->ms x) 1000)))
        (cons "plusDays" (lambda (x n) (zdt-with-ldt x (jt-local-dt (+ (zdt-epoch-day x) (jt->exact n)) (zdt-nano-of-day x)))))
        (cons "minusDays" (lambda (x n) (zdt-with-ldt x (jt-local-dt (- (zdt-epoch-day x) (jt->exact n)) (zdt-nano-of-day x)))))
        (cons "plusWeeks" (lambda (x n) (zdt-with-ldt x (jt-local-dt (+ (zdt-epoch-day x) (* 7 (jt->exact n))) (zdt-nano-of-day x)))))
        (cons "minusWeeks" (lambda (x n) (zdt-with-ldt x (jt-local-dt (- (zdt-epoch-day x) (* 7 (jt->exact n))) (zdt-nano-of-day x)))))
        (cons "plusMonths" (lambda (x n) (zdt-with-ldt x (ldt-combine (ld-plus-months (ldt-date (zdt-ldt x)) (jt->exact n)) (ldt-time (zdt-ldt x))))))
        (cons "minusMonths" (lambda (x n) (zdt-with-ldt x (ldt-combine (ld-plus-months (ldt-date (zdt-ldt x)) (- (jt->exact n))) (ldt-time (zdt-ldt x))))))
        (cons "plusYears" (lambda (x n) (zdt-with-ldt x (ldt-combine (ld-plus-years (ldt-date (zdt-ldt x)) (jt->exact n)) (ldt-time (zdt-ldt x))))))
        (cons "minusYears" (lambda (x n) (zdt-with-ldt x (ldt-combine (ld-plus-years (ldt-date (zdt-ldt x)) (- (jt->exact n))) (ldt-time (zdt-ldt x))))))
        (cons "plusHours" (lambda (x n) (zdt-plus-nanos x (* (jt->exact n) 3600 nanos-per-sec))))
        (cons "minusHours" (lambda (x n) (zdt-plus-nanos x (- (* (jt->exact n) 3600 nanos-per-sec)))))
        (cons "plusMinutes" (lambda (x n) (zdt-plus-nanos x (* (jt->exact n) 60 nanos-per-sec))))
        (cons "minusMinutes" (lambda (x n) (zdt-plus-nanos x (- (* (jt->exact n) 60 nanos-per-sec)))))
        (cons "plusSeconds" (lambda (x n) (zdt-plus-nanos x (* (jt->exact n) nanos-per-sec))))
        (cons "minusSeconds" (lambda (x n) (zdt-plus-nanos x (- (* (jt->exact n) nanos-per-sec)))))
        (cons "plusNanos" (lambda (x n) (zdt-plus-nanos x (jt->exact n))))
        (cons "minusNanos" (lambda (x n) (zdt-plus-nanos x (- (jt->exact n)))))
        (cons "withYear" (lambda (x v) (zdt-with-ldt x (ldt-combine (ld-with-field (ldt-date (zdt-ldt x)) 'year (jt->exact v)) (ldt-time (zdt-ldt x))))))
        (cons "withMonth" (lambda (x v) (zdt-with-ldt x (ldt-combine (ld-with-field (ldt-date (zdt-ldt x)) 'month (jt->exact v)) (ldt-time (zdt-ldt x))))))
        (cons "withDayOfMonth" (lambda (x v) (zdt-with-ldt x (ldt-combine (ld-with-field (ldt-date (zdt-ldt x)) 'day (jt->exact v)) (ldt-time (zdt-ldt x))))))
        (cons "withDayOfYear" (lambda (x v) (zdt-with-ldt x (ldt-combine (ld-with-field (ldt-date (zdt-ldt x)) 'day-of-year (jt->exact v)) (ldt-time (zdt-ldt x))))))
        (cons "withHour" (lambda (x v) (zdt-with-ldt x (ldt-combine (ldt-date (zdt-ldt x)) (lt-with (ldt-time (zdt-ldt x)) 'hour (jt->exact v))))))
        (cons "withMinute" (lambda (x v) (zdt-with-ldt x (ldt-combine (ldt-date (zdt-ldt x)) (lt-with (ldt-time (zdt-ldt x)) 'minute (jt->exact v))))))
        (cons "withSecond" (lambda (x v) (zdt-with-ldt x (ldt-combine (ldt-date (zdt-ldt x)) (lt-with (ldt-time (zdt-ldt x)) 'second (jt->exact v))))))
        (cons "withNano" (lambda (x v) (zdt-with-ldt x (ldt-combine (ldt-date (zdt-ldt x)) (lt-with (ldt-time (zdt-ldt x)) 'nano (jt->exact v))))))
        (cons "truncatedTo" (lambda (x u) (zdt-with-ldt x (ldt-combine (ldt-date (zdt-ldt x)) (lt-truncate (ldt-time (zdt-ldt x)) u)))))
        (cons "withZoneSameInstant" (lambda (x zone) (zoned-of-instant-nanos (zdt->nanos x) zone)))
        (cons "withZoneSameLocal" (lambda (x zone) (zoned-of-ldt (zdt-ldt x) zone)))
        (cons "withFixedOffsetZone" (lambda (x) (jt-zoned-dt (zdt-epoch-day x) (zdt-nano-of-day x) (zdt-offset x) (jt-zone-id (zo-id (zdt-offset x)) (zdt-offset x)))))
        (cons "plus" (case-lambda ((x a) (zdt-plus-amount x a 1)) ((x n u) (zdt-with-ldt x (temporal-plus-unit (zdt-ldt x) (jt->exact n) (arg-unit-name u))))))
        (cons "minus" (case-lambda ((x a) (zdt-plus-amount x a -1)) ((x n u) (zdt-with-ldt x (temporal-plus-unit (zdt-ldt x) (- (jt->exact n)) (arg-unit-name u))))))
        (cons "until" (lambda (x o u) (unit-between (arg-unit-name u) (zdt-ldt x) (zdt-ldt o))))
        (cons "get" (lambda (x f) (zdt-get-field x (arg-field-name f))))
        (cons "getLong" (lambda (x f) (zdt-get-field x (arg-field-name f))))
        (cons "with" (case-lambda ((x adj) (zdt-with-ldt x (apply-adjuster (zdt-ldt x) adj)))
                                  ((x f v) (zdt-with-ldt x (temporal-with-field (zdt-ldt x) (arg-field-name f) (jt->exact v))))))
        (cons "isSupported" (lambda (x a) (cond ((arg-is-unit? a) (not (string-ci=? (cu-name a) "FOREVER")))
                                                ((arg-is-field? a) #t) (else #f))))
        (cons "range" (lambda (x f) (temporal-range (zdt-ldt x) (arg-field-name f))))
        (cons "isBefore" (lambda (x o) (< (zdt->ms x) (time-ms o))))
        (cons "isAfter" (lambda (x o) (> (zdt->ms x) (time-ms o))))
        (cons "isEqual" (lambda (x o) (= (zdt->ms x) (time-ms o))))
        (cons "compareTo" (lambda (x o) (let ((a (zdt->ms x)) (b (zdt->ms o))) (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (x o) (and (jt-zoned-dt? o) (equal? (jhost-state x) (jhost-state o)))))
        (cons "hashCode" (lambda (x) (jolt-hash (zdt->ms x))))
        (cons "format" (lambda (x fmt) (fmt-format fmt x)))
        (cons "toString" (lambda (x) (zdt->string x)))))
(define (zdt-plus-amount x a sign)
  (cond ((arg-is-amount? a) (if (string=? (jhost-tag a) "duration")
                                (zdt-plus-nanos x (* sign (dur-total-nanos a)))
                                (zdt-with-ldt x (temporal-plus-period (zdt-ldt x) a sign))))
        (else (error #f "ZonedDateTime.plus: bad amount"))))
(define (zdt-get-field x field)
  (let ((f (string-upcase field)))
    (cond ((string=? f "OFFSET_SECONDS") (zdt-offset x))
          ((string=? f "INSTANT_SECONDS") (jt-floor-div (zdt->ms x) 1000))
          (else (temporal-get-field (zdt-ldt x) f)))))
;; epoch-ms of any time-bearing value (for cross-type before/after).
(define (time-ms o)
  (cond ((jt-zoned-dt? o) (zdt->ms o)) ((jt-offset-dt? o) (odt->ms o)) ((jt-instant? o) (inst-ms o))
        ((jt-dt? o) (ldt->ms o)) (else (ms-of o))))

;; --- OffsetDateTime statics + methods ----------------------------------------
(register-class-statics! "OffsetDateTime"
  (list (cons "of" (case-lambda
                     ((ldt off) (offset-of-ldt ldt off))
                     ((d t off) (offset-of-ldt (jt-local-dt (ld-epoch-day d) (lt-nano-of-day t)) off))
                     ((y mo d h mi s nano off)
                      (offset-of-ldt (jt-local-dt (ymd->epoch-day (jt->exact y) (jt->exact mo) (jt->exact d))
                                                  (hmsn->nano (jt->exact h) (jt->exact mi) (jt->exact s) (jt->exact nano))) off))))
        (cons "ofInstant" (lambda (inst zone) (offset-of-instant-ms (ms-of inst) zone)))
        (cons "now" (lambda args (offset-of-instant-ms (now-ms* args) (jt-zone-offset 0))))
        (cons "parse" (lambda (s . fmt)
                        (if (null? fmt) (parse-offset-date-time (jt-str s))
                            (call-with-values (lambda () (formatter-parse-fields (car fmt) s))
                              (lambda (ed nod) (jt-offset-dt ed nod 0))))))
        (cons "from" (lambda (t) (cond ((jt-offset-dt? t) t)
                                       ((jt-zoned-dt? t) (jt-offset-dt (zdt-epoch-day t) (zdt-nano-of-day t) (zdt-offset t)))
                                       (else (offset-of-instant-ms (ms-of t) (jt-zone-offset 0))))))
        (cons "MIN" (jt-offset-dt (ymd->epoch-day -999999999 1 1) 0 (* 18 3600)))
        (cons "MAX" (jt-offset-dt (ymd->epoch-day 999999999 12 31) (- nanos-per-day 1) (* -18 3600)))))
(define (odt-with-ldt x ldt) (jt-offset-dt (ldt-epoch-day ldt) (ldt-nano-of-day ldt) (odt-offset x)))
(define (odt-plus-nanos x nanos) (odt-with-ldt x (ldt-plus-nanos (odt-ldt x) nanos)))
(register-host-methods! "offset-date-time"
  (list (cons "getYear" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (odt-epoch-day x))) (lambda (y m d) y))))
        (cons "getMonthValue" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (odt-epoch-day x))) (lambda (y m d) m))))
        (cons "getMonth" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (odt-epoch-day x))) (lambda (y m d) (jt-month m)))))
        (cons "getDayOfMonth" (lambda (x) (call-with-values (lambda () (epoch-day->ymd (odt-epoch-day x))) (lambda (y m d) d))))
        (cons "getDayOfWeek" (lambda (x) (jt-dow (ld-dow (odt-epoch-day x)))))
        (cons "getDayOfYear" (lambda (x) (ld-day-of-year (odt-epoch-day x))))
        (cons "getHour" (lambda (x) (lt-hour (odt-ldt x))))
        (cons "getMinute" (lambda (x) (lt-minute (odt-ldt x))))
        (cons "getSecond" (lambda (x) (lt-second (odt-ldt x))))
        (cons "getNano" (lambda (x) (lt-nano (odt-ldt x))))
        (cons "getOffset" (lambda (x) (jt-zone-offset (odt-offset x))))
        (cons "toInstant" (lambda (x) (mk-instant-nanos (odt->nanos x))))
        (cons "toLocalDate" (lambda (x) (jt-local-date (odt-epoch-day x))))
        (cons "toLocalTime" (lambda (x) (jt-local-time (odt-nano-of-day x))))
        (cons "toLocalDateTime" (lambda (x) (odt-ldt x)))
        (cons "toOffsetTime" (lambda (x) (jt-offset-time (odt-nano-of-day x) (odt-offset x))))
        (cons "toZonedDateTime" (lambda (x) (jt-zoned-dt (odt-epoch-day x) (odt-nano-of-day x) (odt-offset x) (jt-zone-id (zo-id (odt-offset x)) (odt-offset x)))))
        (cons "toEpochSecond" (lambda (x) (jt-floor-div (odt->ms x) 1000)))
        (cons "atZoneSameInstant" (lambda (x zone) (zoned-of-instant-nanos (odt->nanos x) zone)))
        (cons "atZoneSimilarLocal" (lambda (x zone) (zoned-of-ldt (odt-ldt x) zone)))
        (cons "withOffsetSameInstant" (lambda (x off) (offset-of-instant-ms (odt->ms x) off)))
        (cons "withOffsetSameLocal" (lambda (x off) (offset-of-ldt (odt-ldt x) off)))
        (cons "plusDays" (lambda (x n) (odt-with-ldt x (jt-local-dt (+ (odt-epoch-day x) (jt->exact n)) (odt-nano-of-day x)))))
        (cons "minusDays" (lambda (x n) (odt-with-ldt x (jt-local-dt (- (odt-epoch-day x) (jt->exact n)) (odt-nano-of-day x)))))
        (cons "plusWeeks" (lambda (x n) (odt-with-ldt x (jt-local-dt (+ (odt-epoch-day x) (* 7 (jt->exact n))) (odt-nano-of-day x)))))
        (cons "minusWeeks" (lambda (x n) (odt-with-ldt x (jt-local-dt (- (odt-epoch-day x) (* 7 (jt->exact n))) (odt-nano-of-day x)))))
        (cons "plusMonths" (lambda (x n) (odt-with-ldt x (ldt-combine (ld-plus-months (ldt-date (odt-ldt x)) (jt->exact n)) (ldt-time (odt-ldt x))))))
        (cons "minusMonths" (lambda (x n) (odt-with-ldt x (ldt-combine (ld-plus-months (ldt-date (odt-ldt x)) (- (jt->exact n))) (ldt-time (odt-ldt x))))))
        (cons "plusYears" (lambda (x n) (odt-with-ldt x (ldt-combine (ld-plus-years (ldt-date (odt-ldt x)) (jt->exact n)) (ldt-time (odt-ldt x))))))
        (cons "minusYears" (lambda (x n) (odt-with-ldt x (ldt-combine (ld-plus-years (ldt-date (odt-ldt x)) (- (jt->exact n))) (ldt-time (odt-ldt x))))))
        (cons "plusHours" (lambda (x n) (odt-plus-nanos x (* (jt->exact n) 3600 nanos-per-sec))))
        (cons "minusHours" (lambda (x n) (odt-plus-nanos x (- (* (jt->exact n) 3600 nanos-per-sec)))))
        (cons "plusMinutes" (lambda (x n) (odt-plus-nanos x (* (jt->exact n) 60 nanos-per-sec))))
        (cons "minusMinutes" (lambda (x n) (odt-plus-nanos x (- (* (jt->exact n) 60 nanos-per-sec)))))
        (cons "plusSeconds" (lambda (x n) (odt-plus-nanos x (* (jt->exact n) nanos-per-sec))))
        (cons "minusSeconds" (lambda (x n) (odt-plus-nanos x (- (* (jt->exact n) nanos-per-sec)))))
        (cons "plusNanos" (lambda (x n) (odt-plus-nanos x (jt->exact n))))
        (cons "minusNanos" (lambda (x n) (odt-plus-nanos x (- (jt->exact n)))))
        (cons "withYear" (lambda (x v) (odt-with-ldt x (ldt-combine (ld-with-field (ldt-date (odt-ldt x)) 'year (jt->exact v)) (ldt-time (odt-ldt x))))))
        (cons "withMonth" (lambda (x v) (odt-with-ldt x (ldt-combine (ld-with-field (ldt-date (odt-ldt x)) 'month (jt->exact v)) (ldt-time (odt-ldt x))))))
        (cons "withDayOfMonth" (lambda (x v) (odt-with-ldt x (ldt-combine (ld-with-field (ldt-date (odt-ldt x)) 'day (jt->exact v)) (ldt-time (odt-ldt x))))))
        (cons "withDayOfYear" (lambda (x v) (odt-with-ldt x (ldt-combine (ld-with-field (ldt-date (odt-ldt x)) 'day-of-year (jt->exact v)) (ldt-time (odt-ldt x))))))
        (cons "withHour" (lambda (x v) (odt-with-ldt x (ldt-combine (ldt-date (odt-ldt x)) (lt-with (ldt-time (odt-ldt x)) 'hour (jt->exact v))))))
        (cons "withMinute" (lambda (x v) (odt-with-ldt x (ldt-combine (ldt-date (odt-ldt x)) (lt-with (ldt-time (odt-ldt x)) 'minute (jt->exact v))))))
        (cons "withSecond" (lambda (x v) (odt-with-ldt x (ldt-combine (ldt-date (odt-ldt x)) (lt-with (ldt-time (odt-ldt x)) 'second (jt->exact v))))))
        (cons "withNano" (lambda (x v) (odt-with-ldt x (ldt-combine (ldt-date (odt-ldt x)) (lt-with (ldt-time (odt-ldt x)) 'nano (jt->exact v))))))
        (cons "truncatedTo" (lambda (x u) (odt-with-ldt x (ldt-combine (ldt-date (odt-ldt x)) (lt-truncate (ldt-time (odt-ldt x)) u)))))
        (cons "plus" (case-lambda ((x a) (odt-plus-amount x a 1)) ((x n u) (odt-with-ldt x (temporal-plus-unit (odt-ldt x) (jt->exact n) (arg-unit-name u))))))
        (cons "minus" (case-lambda ((x a) (odt-plus-amount x a -1)) ((x n u) (odt-with-ldt x (temporal-plus-unit (odt-ldt x) (- (jt->exact n)) (arg-unit-name u))))))
        (cons "until" (lambda (x o u) (unit-between (arg-unit-name u) (odt-ldt x) (odt-ldt o))))
        (cons "get" (lambda (x f) (odt-get-field x (arg-field-name f))))
        (cons "getLong" (lambda (x f) (odt-get-field x (arg-field-name f))))
        (cons "with" (case-lambda ((x adj) (odt-with-ldt x (apply-adjuster (odt-ldt x) adj)))
                                  ((x f v) (odt-with-ldt x (temporal-with-field (odt-ldt x) (arg-field-name f) (jt->exact v))))))
        (cons "isSupported" (lambda (x a) (cond ((arg-is-unit? a) (not (string-ci=? (cu-name a) "FOREVER"))) ((arg-is-field? a) #t) (else #f))))
        (cons "range" (lambda (x f) (temporal-range (odt-ldt x) (arg-field-name f))))
        (cons "isBefore" (lambda (x o) (< (odt->ms x) (time-ms o))))
        (cons "isAfter" (lambda (x o) (> (odt->ms x) (time-ms o))))
        (cons "isEqual" (lambda (x o) (= (odt->ms x) (time-ms o))))
        (cons "compareTo" (lambda (x o) (let ((a (odt->ms x)) (b (odt->ms o))) (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (x o) (and (jt-offset-dt? o) (equal? (jhost-state x) (jhost-state o)))))
        (cons "hashCode" (lambda (x) (jolt-hash (odt->ms x))))
        (cons "format" (lambda (x fmt) (fmt-format fmt x)))
        (cons "toString" (lambda (x) (odt->string x)))))
(define (odt-plus-amount x a sign)
  (cond ((arg-is-amount? a) (if (string=? (jhost-tag a) "duration")
                                (odt-plus-nanos x (* sign (dur-total-nanos a)))
                                (odt-with-ldt x (temporal-plus-period (odt-ldt x) a sign))))
        (else (error #f "OffsetDateTime.plus: bad amount"))))
(define (odt-get-field x field)
  (let ((f (string-upcase field)))
    (cond ((string=? f "OFFSET_SECONDS") (odt-offset x))
          ((string=? f "INSTANT_SECONDS") (jt-floor-div (odt->ms x) 1000))
          (else (temporal-get-field (odt-ldt x) f)))))

;; --- OffsetTime statics + methods --------------------------------------------
(register-class-statics! "OffsetTime"
  (list (cons "of" (case-lambda
                     ((t off) (jt-offset-time (lt-nano-of-day t) (zo-secs* off)))
                     ((h m s nano off) (jt-offset-time (hmsn->nano (jt->exact h) (jt->exact m) (jt->exact s) (jt->exact nano)) (zo-secs* off)))))
        (cons "ofInstant" (lambda (inst zone) (let ((od (offset-of-instant-ms (ms-of inst) zone)))
                                                (jt-offset-time (odt-nano-of-day od) (odt-offset od)))))
        (cons "now" (lambda args (jt-offset-time (* (jt-floor-mod (exact (truncate (now-ms* args))) 86400000) 1000000) 0)))
        (cons "parse" (lambda (s . _) (parse-offset-time (jt-str s))))
        (cons "from" (lambda (t) (cond ((jt-offset-time? t) t)
                                       ((jt-offset-dt? t) (jt-offset-time (odt-nano-of-day t) (odt-offset t)))
                                       (else (error #f "OffsetTime/from: unsupported")))))
        (cons "MIN" (jt-offset-time 0 (* 18 3600)))
        (cons "MAX" (jt-offset-time (- nanos-per-day 1) (* -18 3600)))))
(define (zo-secs* off) (cond ((jt-zone-offset? off) (zo-secs off)) ((jt-zone-id? off) (zid-off off)) (else (parse-zone-offset off))))
(define (ot-with x nod) (jt-offset-time nod (ot-offset x)))
(register-host-methods! "offset-time"
  (list (cons "getHour" (lambda (x) (lt-hour (jt-local-time (ot-nod x)))))
        (cons "getMinute" (lambda (x) (lt-minute (jt-local-time (ot-nod x)))))
        (cons "getSecond" (lambda (x) (lt-second (jt-local-time (ot-nod x)))))
        (cons "getNano" (lambda (x) (lt-nano (jt-local-time (ot-nod x)))))
        (cons "getOffset" (lambda (x) (jt-zone-offset (ot-offset x))))
        (cons "toLocalTime" (lambda (x) (jt-local-time (ot-nod x))))
        (cons "atDate" (lambda (x d) (jt-offset-dt (ld-epoch-day d) (ot-nod x) (ot-offset x))))
        (cons "plusHours" (lambda (x n) (ot-with x (jt-floor-mod (+ (ot-nod x) (* (jt->exact n) 3600 nanos-per-sec)) nanos-per-day))))
        (cons "minusHours" (lambda (x n) (ot-with x (jt-floor-mod (- (ot-nod x) (* (jt->exact n) 3600 nanos-per-sec)) nanos-per-day))))
        (cons "plusMinutes" (lambda (x n) (ot-with x (jt-floor-mod (+ (ot-nod x) (* (jt->exact n) 60 nanos-per-sec)) nanos-per-day))))
        (cons "minusMinutes" (lambda (x n) (ot-with x (jt-floor-mod (- (ot-nod x) (* (jt->exact n) 60 nanos-per-sec)) nanos-per-day))))
        (cons "plusSeconds" (lambda (x n) (ot-with x (jt-floor-mod (+ (ot-nod x) (* (jt->exact n) nanos-per-sec)) nanos-per-day))))
        (cons "minusSeconds" (lambda (x n) (ot-with x (jt-floor-mod (- (ot-nod x) (* (jt->exact n) nanos-per-sec)) nanos-per-day))))
        (cons "plusNanos" (lambda (x n) (ot-with x (jt-floor-mod (+ (ot-nod x) (jt->exact n)) nanos-per-day))))
        (cons "minusNanos" (lambda (x n) (ot-with x (jt-floor-mod (- (ot-nod x) (jt->exact n)) nanos-per-day))))
        (cons "withHour" (lambda (x v) (ot-with x (lt-nano-of-day (lt-with (jt-local-time (ot-nod x)) 'hour (jt->exact v))))))
        (cons "withMinute" (lambda (x v) (ot-with x (lt-nano-of-day (lt-with (jt-local-time (ot-nod x)) 'minute (jt->exact v))))))
        (cons "withSecond" (lambda (x v) (ot-with x (lt-nano-of-day (lt-with (jt-local-time (ot-nod x)) 'second (jt->exact v))))))
        (cons "withNano" (lambda (x v) (ot-with x (lt-nano-of-day (lt-with (jt-local-time (ot-nod x)) 'nano (jt->exact v))))))
        (cons "truncatedTo" (lambda (x u) (ot-with x (lt-nano-of-day (lt-truncate (jt-local-time (ot-nod x)) u)))))
        (cons "withOffsetSameLocal" (lambda (x off) (jt-offset-time (ot-nod x) (zo-secs* off))))
        (cons "isBefore" (lambda (x o) (< (- (ot-nod x) (* (ot-offset x) nanos-per-sec)) (- (ot-nod o) (* (ot-offset o) nanos-per-sec)))))
        (cons "isAfter" (lambda (x o) (> (- (ot-nod x) (* (ot-offset x) nanos-per-sec)) (- (ot-nod o) (* (ot-offset o) nanos-per-sec)))))
        (cons "isEqual" (lambda (x o) (= (- (ot-nod x) (* (ot-offset x) nanos-per-sec)) (- (ot-nod o) (* (ot-offset o) nanos-per-sec)))))
        (cons "compareTo" (lambda (x o) (let ((a (- (ot-nod x) (* (ot-offset x) nanos-per-sec))) (b (- (ot-nod o) (* (ot-offset o) nanos-per-sec))))
                                          (cond ((< a b) -1) ((> a b) 1) (else 0)))))
        (cons "equals" (lambda (x o) (and (jt-offset-time? o) (equal? (jhost-state x) (jhost-state o)))))
        (cons "hashCode" (lambda (x) (jolt-hash (ot-nod x))))
        (cons "format" (lambda (x fmt) (fmt-format fmt x)))
        (cons "toString" (lambda (x) (ot->string x)))))

;; --- ISO parse for the offset/zoned types ------------------------------------
;; split off an offset/zone suffix at the end of an ISO datetime string; -> (values
;; local-part offset-secs zone-id-or-#f). offset starts at the first +/-/Z after T.
(define (split-offset-zone s)
  (let ((len (string-length s))
        (ti (let loop ((i 0)) (cond ((>= i (string-length s)) #f)
                                    ((or (char=? (string-ref s i) #\T) (char=? (string-ref s i) #\t)) i)
                                    (else (loop (+ i 1)))))))
    ;; bracket zone "[Region/City]" at the very end
    (let* ((zone-id (and (> len 0) (char=? (string-ref s (- len 1)) #\])
                         (let loop ((i (- len 2))) (cond ((< i 0) #f)
                                                         ((char=? (string-ref s i) #\[) (substring s (+ i 1) (- len 1)))
                                                         (else (loop (- i 1)))))))
           (s2 (if zone-id (let loop ((i (- len 1))) (if (char=? (string-ref s i) #\[) (substring s 0 i) (loop (- i 1)))) s))
           (len2 (string-length s2))
           ;; find the offset start (after the T)
           (oi (let loop ((i (if ti (+ ti 1) 0)))
                 (cond ((>= i len2) #f)
                       ((or (char=? (string-ref s2 i) #\Z) (char=? (string-ref s2 i) #\z)) i)
                       ((and (> i (if ti (+ ti 1) 0)) (or (char=? (string-ref s2 i) #\+) (char=? (string-ref s2 i) #\-))) i)
                       (else (loop (+ i 1)))))))
      (if oi
          (values (substring s2 0 oi) (parse-zone-offset (substring s2 oi len2)) zone-id)
          (values s2 0 zone-id)))))
(define (parse-zoned-date-time s)
  (call-with-values (lambda () (split-offset-zone s))
    (lambda (local off zone-id)
      (call-with-values (lambda () (parse-iso-datetime local))
        (lambda (ed nod)
          (let ((zid (if zone-id (zone-id-of zone-id) (jt-zone-id (zo-id off) off))))
            (jt-zoned-dt ed nod off zid)))))))
(define (parse-offset-date-time s)
  (call-with-values (lambda () (split-offset-zone s))
    (lambda (local off zone-id)
      (call-with-values (lambda () (parse-iso-datetime local)) (lambda (ed nod) (jt-offset-dt ed nod off))))))
(define (parse-offset-time s)
  ;; "HH:mm[:ss]±HH:mm" / "...Z"
  (let ((oi (let loop ((i 0)) (cond ((>= i (string-length s)) #f)
                                    ((or (char=? (string-ref s i) #\Z) (char=? (string-ref s i) #\z)) i)
                                    ((and (> i 0) (or (char=? (string-ref s i) #\+) (char=? (string-ref s i) #\-))) i)
                                    (else (loop (+ i 1)))))))
    (if oi (jt-offset-time (parse-iso-time (substring s 0 oi)) (parse-zone-offset (substring s oi (string-length s))))
        (jt-offset-time (parse-iso-time s) 0))))

;; --- formatter integration ---------------------------------------------------
;; format any java.time value through a dt-formatter pattern. Builds the broken-down
;; fields from the value's local representation; X/Z/x render its offset, z its zone.
(define (jt-value-format-parts v)         ; -> (vector y mo d hh mi se nano dow offset-secs zone-id)
  (define (from-ed-nod ed nod off zid)
    (call-with-values (lambda () (epoch-day->ymd ed))
      (lambda (y m d)
        (let ((t (jt-local-time nod)))
          (vector y m d (lt-hour t) (lt-minute t) (lt-second t) (lt-nano t) (jt-floor-mod (+ ed 4) 7) off zid)))))
  (cond
    ((jt-date? v) (call-with-values (lambda () (epoch-day->ymd (ld-epoch-day v)))
                    (lambda (y m d) (vector y m d 0 0 0 0 (jt-floor-mod (+ (ld-epoch-day v) 4) 7) 0 #f))))
    ((jt-time? v) (let ((t v)) (vector 1970 1 1 (lt-hour t) (lt-minute t) (lt-second t) (lt-nano t) 4 0 #f)))
    ((jt-dt? v) (from-ed-nod (ldt-epoch-day v) (ldt-nano-of-day v) 0 #f))
    ((jt-zoned-dt? v) (from-ed-nod (zdt-epoch-day v) (zdt-nano-of-day v) (zdt-offset v) (zid-id (zdt-zone v))))
    ((jt-offset-dt? v) (from-ed-nod (odt-epoch-day v) (odt-nano-of-day v) (odt-offset v) #f))
    ((jt-offset-time? v) (let ((t (jt-local-time (ot-nod v)))) (vector 1970 1 1 (lt-hour t) (lt-minute t) (lt-second t) (lt-nano t) 4 (ot-offset v) #f)))
    ((jt-instant? v) #f)                  ; instants render via format-ms (UTC)
    (else #f)))
;; Locale month/day names. Only the locales tick exercises (en, fr) are tabled;
;; an unknown locale falls back to English. French abbreviations follow CLDR/
;; java.time (most carry a trailing period; "mars"/"mai"/"juin"/"août" don't).
(define fr-month-full
  (vector "janvier" "février" "mars" "avril" "mai" "juin"
          "juillet" "août" "septembre" "octobre" "novembre" "décembre"))
(define fr-month-abbr
  (vector "janv." "févr." "mars" "avr." "mai" "juin"
          "juil." "août" "sept." "oct." "nov." "déc."))
(define fr-day-full   ; indexed by day-of-week 0=Sunday
  (vector "dimanche" "lundi" "mardi" "mercredi" "jeudi" "vendredi" "samedi"))
(define fr-day-abbr
  (vector "dim." "lun." "mar." "mer." "jeu." "ven." "sam."))
(define (locale-fr? loc) (and (>= (string-length loc) 2) (string=? (substring loc 0 2) "fr")))
;; month display name for a 1-based month under a locale; full? picks MMMM over MMM.
(define (locale-month-name loc mo full?)
  (if (locale-fr? loc)
      (vector-ref (if full? fr-month-full fr-month-abbr) (- mo 1))
      (if full? (vector-ref month-names (- mo 1)) (substring (vector-ref month-names (- mo 1)) 0 3))))
(define (locale-day-name loc dow full?)
  (if (locale-fr? loc)
      (vector-ref (if full? fr-day-full fr-day-abbr) dow)
      (if full? (vector-ref day-names dow) (substring (vector-ref day-names dow) 0 3))))

;; the pattern engine for java.time values (extends inst-time.ss's format-ms letters
;; with fractional S and real X/x/Z/z from the value's own offset/zone).
(define (jt-format-pattern pattern v . loc)
  (define locale (if (null? loc) "en" (car loc)))
  (let ((parts (jt-value-format-parts v)))
    (if (not parts)
        (format-ms pattern (ms-of v))     ; instant: UTC engine
        (let ((y (vector-ref parts 0)) (mo (vector-ref parts 1)) (d (vector-ref parts 2))
              (hh (vector-ref parts 3)) (mi (vector-ref parts 4)) (se (vector-ref parts 5))
              (nano (vector-ref parts 6)) (dow (vector-ref parts 7))
              (off (vector-ref parts 8)) (zid (vector-ref parts 9))
              (n (string-length pattern)) (out (open-output-string)))
          (define (run-len i c) (let loop ((j i)) (if (and (< j n) (char=? (string-ref pattern j) c)) (loop (+ j 1)) (- j i))))
          (define (off-iso secs colon allow-z)
            (if (and allow-z (= secs 0)) "Z"
                (let* ((neg (< secs 0)) (a (abs secs)) (h (quotient a 3600)) (m (quotient (modulo a 3600) 60)) (s (modulo a 60)))
                  (string-append (if neg "-" "+") (pad2 h) (if colon ":" "") (pad2 m)
                                 (if (= s 0) "" (string-append (if colon ":" "") (pad2 s)))))))
          (let loop ((i 0))
            (when (< i n)
              (let* ((c (string-ref pattern i)) (k (run-len i c)))
                (cond
                  ((char=? c #\')
                   (if (and (< (+ i 1) n) (char=? (string-ref pattern (+ i 1)) #\'))
                       (begin (write-char #\' out) (loop (+ i 2)))
                       (let close ((j (+ i 1)))
                         (cond ((>= j n) (loop j))
                               ((char=? (string-ref pattern j) #\') (loop (+ j 1)))
                               (else (write-char (string-ref pattern j) out) (close (+ j 1)))))))
                  ;; y = year-of-era, Y = week-based-year; for whole dates they agree,
                  ;; so render Y like y (no ISO-week calendar here).
                  ((or (char=? c #\y) (char=? c #\Y)) (display (if (>= k 4) (pad4 y) (pad2 (modulo y 100))) out) (loop (+ i k)))
                  ((char=? c #\M)
                   (display (cond ((= k 1) (number->string mo)) ((= k 2) (pad2 mo))
                                  ((= k 3) (locale-month-name locale mo #f))
                                  (else (locale-month-name locale mo #t))) out) (loop (+ i k)))
                  ((char=? c #\d) (display (if (= k 1) (number->string d) (pad2 d)) out) (loop (+ i k)))
                  ((char=? c #\E) (display (locale-day-name locale dow (>= k 4)) out) (loop (+ i k)))
                  ((char=? c #\H) (display (if (= k 1) (number->string hh) (pad2 hh)) out) (loop (+ i k)))
                  ((char=? c #\h) (let ((h12 (let ((h (modulo hh 12))) (if (= h 0) 12 h)))) (display (if (= k 1) (number->string h12) (pad2 h12)) out)) (loop (+ i k)))
                  ((char=? c #\m) (display (if (= k 1) (number->string mi) (pad2 mi)) out) (loop (+ i k)))
                  ((char=? c #\s) (display (if (= k 1) (number->string se) (pad2 se)) out) (loop (+ i k)))
                  ((char=? c #\S) (let ((str (let ((p (number->string (quotient nano (expt 10 (max 0 (- 9 k))))))) p)))
                                    (display (string-append (make-string (max 0 (- k (string-length str))) #\0) str) out)) (loop (+ i k)))
                  ((char=? c #\a) (display (if (< hh 12) "AM" "PM") out) (loop (+ i k)))
                  ((char=? c #\X) (display (off-iso off (>= k 3) #t) out) (loop (+ i k)))
                  ((char=? c #\x) (display (off-iso off (>= k 3) #f) out) (loop (+ i k)))
                  ((char=? c #\Z) (display (if (>= k 3) (off-iso off #t #f) (off-iso off #f #f)) out) (loop (+ i k)))
                  ((char=? c #\V) (display (or zid (off-iso off #t #t)) out) (loop (+ i k)))
                  ((char=? c #\z) (display (or zid (off-iso off #t #t)) out) (loop (+ i k)))
                  (else (write-char c out) (loop (+ i 1)))))))
          (get-output-string out)))))

;; .format on a dt-formatter applied to a java.time value. The pattern comes from the
;; formatter; ISO_* constants carry a literal pattern that round-trips through here.
(define (fmt-format fmt v)
  (cond ((and (jhost? fmt) (string=? (jhost-tag fmt) "dt-formatter")) (jt-format-pattern (fmt-pat fmt) v (fmt-locale fmt)))
        ((string? fmt) (jt-format-pattern fmt v))
        (else (jt-value->iso v))))
(define (jt-value->iso v)
  (cond ((jt-date? v) (iso-date-str (ld-epoch-day v))) ((jt-time? v) (iso-time-str (lt-nano-of-day v)))
        ((jt-dt? v) (iso-datetime-str (ldt-epoch-day v) (ldt-nano-of-day v)))
        ((jt-zoned-dt? v) (zdt->string v)) ((jt-offset-dt? v) (odt->string v))
        ((jt-offset-time? v) (ot->string v)) ((jt-instant? v) (iso-instant-str-nanos (inst-nanos v)))
        (else (jolt-str-render-one v))))

;; .format on the new value types reaches the per-tag method tables above. Add
;; .format to local-date/time/date-time too (they previously had none).
(register-host-methods! "local-date" (list (cons "format" (lambda (x fmt) (fmt-format fmt x)))))
(register-host-methods! "local-time" (list (cons "format" (lambda (x fmt) (fmt-format fmt x)))))
(register-host-methods! "local-date-time" (list (cons "format" (lambda (x fmt) (fmt-format fmt x)))))
(register-host-methods! "instant" (list (cons "format" (lambda (x fmt) (fmt-format fmt x)))))

;; extend the dt-formatter method table: .format routes java.time values through the
;; richer engine; .parse picks the value type from the parsed fields.
(register-host-methods! "dt-formatter"
  (list (cons "format" (lambda (self d) (jt-format-pattern (fmt-pat self) d (fmt-locale self))))
        (cons "getZone" (lambda (self) (jt-zone-id "Z" 0)))
        (cons "getLocale" (lambda (self) (make-jhost "locale" (vector (fmt-locale self)))))
        (cons "toString" (lambda (self) (fmt-pat self)))))

;; DateTimeFormatter ISO constants (richer engine; the pattern strings drive parse +
;; format). Re-registers ofPattern so the format-aware Local*/parse statics see them.
(register-class-statics! "DateTimeFormatter"
  (list (cons "ofPattern" (lambda (p . _) (mk-formatter (jt-str p))))
        (cons "ISO_LOCAL_DATE" (mk-formatter "yyyy-MM-dd"))
        (cons "ISO_LOCAL_TIME" (mk-formatter "HH:mm:ss"))
        (cons "ISO_LOCAL_DATE_TIME" (mk-formatter "yyyy-MM-dd'T'HH:mm:ss"))
        (cons "ISO_DATE" (mk-formatter "yyyy-MM-dd"))
        (cons "ISO_TIME" (mk-formatter "HH:mm:ss"))
        (cons "ISO_DATE_TIME" (mk-formatter "yyyy-MM-dd'T'HH:mm:ss"))
        (cons "ISO_INSTANT" (mk-formatter "yyyy-MM-dd'T'HH:mm:ssX"))
        (cons "ISO_OFFSET_DATE_TIME" (mk-formatter "yyyy-MM-dd'T'HH:mm:ssXXX"))
        (cons "ISO_OFFSET_TIME" (mk-formatter "HH:mm:ssXXX"))
        (cons "ISO_OFFSET_DATE" (mk-formatter "yyyy-MM-ddXXX"))
        (cons "ISO_ZONED_DATE_TIME" (mk-formatter "yyyy-MM-dd'T'HH:mm:ssXXX"))
        (cons "ISO_ORDINAL_DATE" (mk-formatter "yyyy-DDD"))
        (cons "ISO_WEEK_DATE" (mk-formatter "yyyy-'W'ww-e"))
        (cons "BASIC_ISO_DATE" (mk-formatter "yyyyMMdd"))
        (cons "RFC_1123_DATE_TIME" (mk-formatter "EEE, dd MMM yyyy HH:mm:ss Z"))))

;; Local*/parse with a formatter: parse via the pattern, then build the right value.
;; The parse path reuses inst-time.ss parse-ms (UTC) and reads back the fields.
(define (formatter-parse-fields fmt s)
  (let ((ms (jinst-ms (parse-ms (fmt-pat fmt) (jt-str s)))))
    (let ((ed (jt-floor-div (exact (truncate ms)) 86400000)) (nod (* (jt-floor-mod (exact (truncate ms)) 86400000) 1000000)))
      (values ed nod))))
(register-class-statics! "LocalDate"
  (list (cons "parse" (lambda (s . fmt)
                        (if (null? fmt) (jt-local-date (parse-iso-date (jt-str s)))
                            (call-with-values (lambda () (formatter-parse-fields (car fmt) s)) (lambda (ed nod) (jt-local-date ed))))))))
(register-class-statics! "LocalTime"
  (list (cons "parse" (lambda (s . fmt)
                        (if (null? fmt) (jt-local-time (parse-iso-time (jt-str s)))
                            (call-with-values (lambda () (formatter-parse-fields (car fmt) s)) (lambda (ed nod) (jt-local-time nod))))))))
(register-class-statics! "LocalDateTime"
  (list (cons "parse" (lambda (s . fmt)
                        (if (null? fmt)
                            (call-with-values (lambda () (parse-iso-datetime (jt-str s))) (lambda (ed nod) (jt-local-dt ed nod)))
                            (call-with-values (lambda () (formatter-parse-fields (car fmt) s)) (lambda (ed nod) (jt-local-dt ed nod))))))))

;; --- ms-of / equality / hash / compare / print / instance? for Phase-3 types --
;; extend ms-of so ZonedDateTime/OffsetDateTime/OffsetTime route through to an epoch-ms.
(define %p3-ms-of ms-of)
(set! ms-of (lambda (d)
              (cond ((jt-zoned-dt? d) (zdt->ms d))
                    ((jt-offset-dt? d) (odt->ms d))
                    (else (%p3-ms-of d)))))

(register-jt-value! "zone-offset" (lambda (x) (zo-id (zo-secs x))) (lambda (x) (jolt-hash (zo-secs x))))
(register-jt-value! "zone-id" (lambda (x) (zid-id x)) (lambda (x) (jolt-hash (string-hash (zid-id x)))))
(register-str-render! jt-zoned-dt? zdt->string) (register-pr-arm! jt-zoned-dt? zdt->string)
(register-str-render! jt-offset-dt? odt->string) (register-pr-arm! jt-offset-dt? odt->string)
(register-str-render! jt-offset-time? ot->string) (register-pr-arm! jt-offset-time? ot->string)
(register-hash-arm! jt-zoned-dt? (lambda (x) (jolt-hash (zdt->ms x))))
(register-hash-arm! jt-offset-dt? (lambda (x) (jolt-hash (odt->ms x))))
(register-hash-arm! jt-offset-time? (lambda (x) (jolt-hash (ot-nod x))))
;; ZonedDateTime equality is local-date-time + offset + zone id (java.time
;; ZonedDateTime.equals). Compare fields, not the raw state: the state holds a
;; nested zone-id record that Chez equal? compares by identity, not contents.
(register-eq-arm! (lambda (a b) (or (jt-zoned-dt? a) (jt-zoned-dt? b)))
                  (lambda (a b) (and (jt-zoned-dt? a) (jt-zoned-dt? b)
                                     (= (zdt-epoch-day a) (zdt-epoch-day b))
                                     (= (zdt-nano-of-day a) (zdt-nano-of-day b))
                                     (= (zdt-offset a) (zdt-offset b))
                                     (string=? (zid-id (zdt-zone a)) (zid-id (zdt-zone b))))))
(register-eq-arm! (lambda (a b) (or (jt-offset-dt? a) (jt-offset-dt? b)))
                  (lambda (a b) (and (jt-offset-dt? a) (jt-offset-dt? b) (equal? (jhost-state a) (jhost-state b)))))
(register-eq-arm! (lambda (a b) (or (jt-offset-time? a) (jt-offset-time? b)))
                  (lambda (a b) (and (jt-offset-time? a) (jt-offset-time? b) (equal? (jhost-state a) (jhost-state b)))))

;; compare for Phase-3 types (same-type only).
(define %p3-prev-compare jolt-compare)
(set! jolt-compare
  (lambda (a b)
    (cond
      ((and (jt-zoned-dt? a) (jt-zoned-dt? b)) (let ((x (zdt->ms a)) (y (zdt->ms b))) (cond ((< x y) -1) ((> x y) 1) (else 0))))
      ((and (jt-offset-dt? a) (jt-offset-dt? b)) (let ((x (odt->ms a)) (y (odt->ms b))) (cond ((< x y) -1) ((> x y) 1) (else 0))))
      ((and (jt-zone-offset? a) (jt-zone-offset? b)) (let ((x (zo-secs a)) (y (zo-secs b))) (cond ((< x y) -1) ((> x y) 1) (else 0))))
      ;; #inst (java.util.Date) values compare by epoch-ms.
      ((and (jinst? a) (jinst? b)) (let ((x (jinst-ms a)) (y (jinst-ms b))) (cond ((< x y) -1) ((> x y) 1) (else 0))))
      (else (%p3-prev-compare a b)))))
(def-var! "clojure.core" "compare" jolt-compare)

;; instance? for the Phase-3 tags.
(register-instance-check-arm!
  (lambda (type-sym val)
    (let ((tn (short-class-name (symbol-t-name type-sym))))
      (cond
        ((jt-zone-offset? val) (if (member tn '("ZoneOffset" "ZoneId")) #t 'pass))
        ((jt-zone-id? val) (if (string=? tn "ZoneId") #t 'pass))
        ((jt-zoned-dt? val) (if (string=? tn "ZonedDateTime") #t 'pass))
        ((jt-offset-dt? val) (if (string=? tn "OffsetDateTime") #t 'pass))
        ((jt-offset-time? val) (if (string=? tn "OffsetTime") #t 'pass))
        ((and (jhost? val) (string=? (jhost-tag val) "clock")) (if (string=? tn "Clock") #t 'pass))
        ((and (jhost? val) (string=? (jhost-tag val) "dt-formatter")) (if (string=? tn "DateTimeFormatter") #t 'pass))
        ((and (jhost? val) (string=? (jhost-tag val) "temporal-adjuster")) (if (member tn '("TemporalAdjuster")) #t 'pass))
        (else 'pass)))))
