Merge pull request #195 from jolt-lang/spike/java-time-phase1

java.time on Chez: LocalDate..ZonedDateTime + formatters — runs tick (api_test 352/7)
This commit is contained in:
Dmitri Sotnikov 2026-06-24 22:51:33 +00:00 committed by GitHub
commit dcfc764b69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 2238 additions and 46 deletions

View file

@ -45,11 +45,23 @@
(and (digit? (string-ref s j))
(loop (+ j 1) (+ (* acc 10) (- (char->integer (string-ref s j)) 48))))))))
(define (jolt-inst-from-string ts)
(define (jolt-inst-from-string ts0)
;; a leading '-' marks a negative (proleptic) year; the rest of the field may be
;; more than 4 digits (java.time prints -999999999-…). Read the year up to the
;; first '-' that separates it from the month.
(define neg-year (and (> (string-length ts0) 0) (char=? (string-ref ts0 0) #\-)))
(define ts (if neg-year (substring ts0 1 (string-length ts0)) ts0))
(define len (string-length ts))
(define (fail) (error #f (string-append "Unrecognized #inst timestamp: " ts)))
(let* ((year (or (digits-at ts 0 4) (fail)))
(i 4) (month 1) (day 1) (hh 0) (mm 0) (ss 0) (frac-ms 0) (off-s 0))
(define (fail) (error #f (string-append "Unrecognized #inst timestamp: " ts0)))
(define (read-year)
;; >=4 digits up to a non-digit; java.time uses min-4 but allows more.
(let loop ((j 0) (acc 0) (n 0))
(if (and (< j len) (digit? (string-ref ts j)))
(loop (+ j 1) (+ (* acc 10) (- (char->integer (string-ref ts j)) 48)) (+ n 1))
(if (>= n 4) (cons acc j) #f))))
(let* ((yr (or (read-year) (fail)))
(year (if neg-year (- (car yr)) (car yr)))
(i (cdr yr)) (month 1) (day 1) (hh 0) (mm 0) (ss 0) (frac-ms 0) (off-s 0))
;; -MM
(when (and (< i len) (char=? (string-ref ts i) #\-) (digits-at ts (+ i 1) 2))
(set! month (digits-at ts (+ i 1) 2)) (set! i (+ i 3)))
@ -170,11 +182,14 @@
(y 1970) (mo 1) (d 1) (hh 0) (mi 0) (ss 0) (pm 'none))
(define (pfail) (error #f (string-append "ParseException: unparseable date \"" input "\"")))
(define (run-len i c) (let loop ((j i)) (if (and (< j pn) (char=? (string-ref pattern j) c)) (loop (+ j 1)) (- j i))))
(define (read-digits ii) ; -> (val . next), pfail if none
(let loop ((j ii) (acc 0) (any #f))
(if (and (< j inn) (digit? (string-ref input j)))
(loop (+ j 1) (+ (* acc 10) (- (char->integer (string-ref input j)) 48)) #t)
;; read up to `maxw` digits (#f = unbounded). A fixed-width field (k>=2, e.g.
;; HHmm) caps the read at its run length so adjacent numeric fields split.
(define (read-digits-w ii maxw) ; -> (val . next), pfail if none
(let loop ((j ii) (acc 0) (n 0) (any #f))
(if (and (< j inn) (digit? (string-ref input j)) (or (not maxw) (< n maxw)))
(loop (+ j 1) (+ (* acc 10) (- (char->integer (string-ref input j)) 48)) (+ n 1) #t)
(if any (cons acc j) (pfail)))))
(define (read-digits ii) (read-digits-w ii #f))
(define (read-alpha ii) ; -> (str . next)
(let loop ((j ii)) (if (and (< j inn) (char-alphabetic? (string-ref input j))) (loop (+ j 1))
(cons (substring input ii j) j))))
@ -195,23 +210,23 @@
((char-alphabetic? c)
(let ((k (run-len pi c)))
(cond
((char=? c #\y) (let ((r (read-digits ii)))
((char=? c #\y) (let ((r (read-digits-w ii (if (>= k 3) #f k))))
;; 2-digit year (value < 100): JVM sliding window — 00-68 -> 20xx,
;; 69-99 -> 19xx (rfc1036 HTTP dates). A full year stays as-is.
(set! y (let ((v (car r))) (if (< v 100) (if (< v 69) (+ 2000 v) (+ 1900 v)) v)))
(set! y (let ((v (car r))) (if (and (= k 2) (< v 100)) (if (< v 69) (+ 2000 v) (+ 1900 v)) v)))
(loop (+ pi k) (cdr r))))
((char=? c #\M) (if (>= k 3)
(let ((r (read-alpha ii))) (set! mo (or (month-from-name (car r)) (pfail))) (loop (+ pi k) (cdr r)))
(let ((r (read-digits ii))) (set! mo (car r)) (loop (+ pi k) (cdr r)))))
((char=? c #\d) (let ((r (read-digits ii))) (set! d (car r)) (loop (+ pi k) (cdr r))))
((or (char=? c #\H) (char=? c #\h)) (let ((r (read-digits ii))) (set! hh (car r)) (loop (+ pi k) (cdr r))))
((char=? c #\m) (let ((r (read-digits ii))) (set! mi (car r)) (loop (+ pi k) (cdr r))))
((char=? c #\s) (let ((r (read-digits ii))) (set! ss (car r)) (loop (+ pi k) (cdr r))))
(let ((r (read-digits-w ii (if (>= k 2) k #f)))) (set! mo (car r)) (loop (+ pi k) (cdr r)))))
((char=? c #\d) (let ((r (read-digits-w ii (if (>= k 2) k #f)))) (set! d (car r)) (loop (+ pi k) (cdr r))))
((or (char=? c #\H) (char=? c #\h)) (let ((r (read-digits-w ii (if (>= k 2) k #f)))) (set! hh (car r)) (loop (+ pi k) (cdr r))))
((char=? c #\m) (let ((r (read-digits-w ii (if (>= k 2) k #f)))) (set! mi (car r)) (loop (+ pi k) (cdr r))))
((char=? c #\s) (let ((r (read-digits-w ii (if (>= k 2) k #f)))) (set! ss (car r)) (loop (+ pi k) (cdr r))))
((char=? c #\E) (loop (+ pi k) (cdr (read-alpha ii))))
((char=? c #\a) (let ((r (read-alpha ii)))
(set! pm (if (string=? (ascii-string-down (car r)) "pm") 'pm 'am))
(loop (+ pi k) (cdr r))))
((or (char=? c #\z) (char=? c #\Z) (char=? c #\X)) (loop (+ pi k) (read-tz ii)))
((or (char=? c #\z) (char=? c #\Z) (char=? c #\X) (char=? c #\x) (char=? c #\V) (char=? c #\v)) (loop (+ pi k) (read-tz ii)))
(else (loop (+ pi k) ii)))))
((char=? c #\')
(if (and (< (+ pi 1) pn) (char=? (string-ref pattern (+ pi 1)) #\'))
@ -245,7 +260,9 @@
;; java.time.Instant/ZonedDateTime/LocalDateTime values (mk-instant/mk-zoned/mk-local
;; jhosts) are equal when same kind + same epoch-ms — two parsed Instants compare =.
(define (time-jhost? x) (and (jhost? x) (member (jhost-tag x) '("instant" "zoned-dt" "local-dt" "local-date" "sql-date")) #t))
;; java.time LocalDate/LocalTime/LocalDateTime own their = / hash in java-time.ss;
;; this arm covers the ms-based shim values (instant / zoned-dt / sql-date).
(define (time-jhost? x) (and (jhost? x) (member (jhost-tag x) '("instant" "zoned-dt" "sql-date")) #t))
(register-eq-arm! (lambda (a b) (or (time-jhost? a) (time-jhost? b)))
(lambda (a b) (and (time-jhost? a) (time-jhost? b)
(string=? (jhost-tag a) (jhost-tag b))
@ -274,7 +291,6 @@
((string=? tn "Timestamp") #f)
(else 'pass)))
((and (jhost? val) (string=? (jhost-tag val) "instant")) (if (string=? tn "Instant") #t 'pass))
((and (jhost? val) (string=? (jhost-tag val) "local-dt")) (if (string=? tn "LocalDateTime") #t 'pass))
;; java.sql.Date is a java.util.Date subclass (but not a Timestamp).
((and (jhost? val) (string=? (jhost-tag val) "sql-date"))
(cond ((or (string=? tn "Date")) #t) ((string=? tn "Timestamp") #f) (else 'pass)))
@ -284,16 +300,31 @@
(def-var! "clojure.core" "inst-ms*" (lambda (i) (jinst-ms i)))
;; --- java.time shim values (jhost objects over host-static.ss registries) -----
;; "local-date" stores an epoch-day (java-time.ss owns the type); ms-of projects it
;; to UTC midnight so existing date math keeps working. "local-dt" stores epoch-day +
;; nano-of-day; the others store epoch-ms.
(define (ms-of d)
(cond ((number? d) d)
((jinst? d) (jinst-ms d))
((and (jhost? d) (member (jhost-tag d) '("instant" "zoned-dt" "local-dt" "local-date" "calendar" "sql-date")))
((and (jhost? d) (string=? (jhost-tag d) "local-date"))
(* (vector-ref (jhost-state d) 0) 86400000))
((and (jhost? d) (string=? (jhost-tag d) "local-date-time"))
(+ (* (vector-ref (jhost-state d) 0) 86400000)
(quotient (vector-ref (jhost-state d) 1) 1000000)))
((and (jhost? d) (member (jhost-tag d) '("instant" "zoned-dt" "calendar" "sql-date")))
(vector-ref (jhost-state d) 0))
(else (error #f "not a date value" d))))
(define (mk-instant ms) (make-jhost "instant" (vector ms)))
(define (mk-zoned ms) (make-jhost "zoned-dt" (vector ms)))
(define (mk-local ms) (make-jhost "local-dt" (vector ms)))
(define (mk-local-date ms) (make-jhost "local-date" (vector ms)))
;; LocalDateTime from epoch-ms (UTC): the java-time.ss "local-date-time" jhost,
;; state [epoch-day nano-of-day].
(define (mk-local ms)
(let* ((ems (exact (truncate ms)))
(ed (inst-floor-div ems 86400000))
(mod (inst-floor-mod ems 86400000)))
(make-jhost "local-date-time" (vector ed (* mod 1000000)))))
;; local-date from epoch-ms: the epoch-day of the UTC day containing ms.
(define (mk-local-date ms) (make-jhost "local-date" (vector (inst-floor-div (exact (truncate ms)) 86400000))))
;; start of the UTC day containing ms.
(define (start-of-utc-day ms)
(* (inst-floor-div (exact (truncate ms)) 86400000) 86400000))
@ -310,13 +341,10 @@
(register-host-methods! "zoned-dt"
(list (cons "toLocalDateTime" (lambda (self) (mk-local (ms-of self))))
(cons "toInstant" (lambda (self) (mk-instant (ms-of self))))))
(register-host-methods! "local-dt"
(list (cons "atZone" (lambda (self zone) (mk-zoned (ms-of self))))
(cons "toLocalDate" (lambda (self) (mk-local-date (ms-of self))))))
;; LocalDate.atStartOfDay(zone): midnight of the UTC day (the layer is UTC).
;; LocalDate.atZone(zone): the UTC layer treats it as a zoned value at midnight.
;; (java-time.ss registers atStartOfDay and the rest of the local-date surface.)
(register-host-methods! "local-date"
(list (cons "atStartOfDay" (lambda (self . zone) (mk-zoned (start-of-utc-day (ms-of self)))))
(cons "atZone" (lambda (self zone) (mk-zoned (ms-of self))))))
(list (cons "atZone" (lambda (self zone) (mk-zoned (ms-of self))))))
(register-host-methods! "dt-formatter"
(list (cons "withLocale" (lambda (self locale) (mk-formatter (fmt-pat self))))
(cons "withZone" (lambda (self zone) (mk-formatter (fmt-pat self))))
@ -380,6 +408,9 @@
(list (cons "getDefault" (lambda () (make-jhost "locale" (vector "default"))))
(cons "ENGLISH" (make-jhost "locale" (vector "en")))
(cons "US" (make-jhost "locale" (vector "en-US")))
(cons "FRENCH" (make-jhost "locale" (vector "fr")))
(cons "FRANCE" (make-jhost "locale" (vector "fr-FR")))
(cons "GERMAN" (make-jhost "locale" (vector "de")))
(cons "ROOT" (make-jhost "locale" (vector "root")))))
;; java.util.Date / java.sql.Timestamp: #inst's classes. (Date.) = now, (Date. ms)
@ -391,6 +422,10 @@
(register-class-ctor! "java.util.Date" date-ctor)
(register-class-ctor! "Timestamp" date-ctor)
(register-class-ctor! "java.sql.Timestamp" date-ctor)
;; Date/from(Instant) -> a java.util.Date at the instant's epoch-ms.
(let ((date-statics (list (cons "from" (lambda (inst) (make-jinst (ms->exact (ms-of inst))))))))
(register-class-statics! "Date" date-statics)
(register-class-statics! "java.util.Date" date-statics))
;; java.sql.Date: a distinct class from java.util.Date (a "sql-date" jhost over
;; epoch-ms) so a protocol extended to both routes a sql.Date to its own impl.
;; (Date. year-1900 month0 day) builds UTC midnight of that civil date; valueOf

2072
host/chez/java-time.ss Normal file

File diff suppressed because it is too large Load diff

View file

@ -141,6 +141,23 @@
((jinst? obj) '("Date" "java.util.Date" "Timestamp" "java.sql.Timestamp" "Object"))
((jbigdec? obj) '("BigDecimal" "java.math.BigDecimal" "Number" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "instant")) '("Instant" "java.time.Instant" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "local-date")) '("LocalDate" "java.time.LocalDate" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "local-time")) '("LocalTime" "java.time.LocalTime" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "local-date-time")) '("LocalDateTime" "java.time.LocalDateTime" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "duration")) '("Duration" "java.time.Duration" "TemporalAmount" "java.time.temporal.TemporalAmount" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "period")) '("Period" "java.time.Period" "TemporalAmount" "java.time.temporal.TemporalAmount" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "month-enum")) '("Month" "java.time.Month" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "dow-enum")) '("DayOfWeek" "java.time.DayOfWeek" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "year")) '("Year" "java.time.Year" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "year-month")) '("YearMonth" "java.time.YearMonth" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "chrono-unit")) '("ChronoUnit" "java.time.temporal.ChronoUnit" "TemporalUnit" "java.time.temporal.TemporalUnit" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "chrono-field")) '("ChronoField" "java.time.temporal.ChronoField" "TemporalField" "java.time.temporal.TemporalField" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "zone-offset")) '("ZoneOffset" "java.time.ZoneOffset" "ZoneId" "java.time.ZoneId" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "zone-id")) '("ZoneId" "java.time.ZoneId" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "zoned-date-time")) '("ZonedDateTime" "java.time.ZonedDateTime" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "offset-date-time")) '("OffsetDateTime" "java.time.OffsetDateTime" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "offset-time")) '("OffsetTime" "java.time.OffsetTime" "Object"))
((and (jhost? obj) (string=? (jhost-tag obj) "clock")) '("Clock" "java.time.Clock" "Object"))
;; java.sql.Date — a distinct class from java.util.Date so a protocol
;; extended to both (data.json's JSONWriter) routes a sql.Date to its impl.
((and (jhost? obj) (string=? (jhost-tag obj) "sql-date")) '("java.sql.Date" "Date" "java.util.Date" "Object"))
@ -195,7 +212,12 @@
"ASeq" "ISeq" "IPersistentCollection" "Associative" "Sequential"
"Map" "java.util.Map" "List" "java.util.List" "Set" "java.util.Set"
"Collection" "java.util.Collection"
"UUID" "BigDecimal" "Date" "Timestamp" "Instant" "java.sql.Date"))
"UUID" "BigDecimal" "Date" "Timestamp" "Instant" "java.sql.Date"
;; java.time value types (extend-protocol Duration / ZonedDateTime / …)
"Duration" "Period" "LocalDate" "LocalTime" "LocalDateTime"
"ZonedDateTime" "OffsetDateTime" "OffsetTime" "ZoneId" "ZoneOffset"
"Clock" "Year" "YearMonth" "Month" "DayOfWeek"
"ChronoUnit" "ChronoField" "TemporalAmount" "TemporalUnit" "TemporalField"))
h))
(define (strip-prefix s p)
(let ((pl (string-length p)))

View file

@ -351,6 +351,12 @@
;; jolt-pr-str / jolt-type / instance-check and uses host-static.ss's registries.
(load "host/chez/inst-time.ss")
;; java.time value types: LocalDate / LocalTime / LocalDateTime / Instant as
;; tz-free jhost values (epoch-day / nano-of-day / epoch-ms). Loads after
;; inst-time.ss — it reuses its civil<->days helpers, the jhost registries, and
;; re-registers a few LocalDateTime/Instant statics to use the richer reps.
(load "host/chez/java-time.ss")
;; Chez-side data reader: read-string / __parse-next /
;; __read-tagged. Loads after inst-time.ss — __read-tagged reuses its #uuid/#inst
;; constructors, and the reader needs the full value/collection layer above.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -217,17 +217,19 @@
as-sym (get pat :as)
bound (conj (conj (conj (conj acc g) init) gm) coerce)
base (if as-sym (conj (conj bound as-sym) gm) bound)
;; group binds a :keys/:strs/:syms list. dnsp is the destructuring
;; namespace from a qualified key like :ns/keys — it both prefixes
;; the lookup key and overrides a bare symbol's namespace.
group
(fn* [a kw kind]
(let* [names (get pat kw)]
(if names
(fn* group [a names kind dnsp]
(if names
(reduce
;; s is a symbol (a b) or a keyword (:a :b); name/
;; namespace handle both, so :keys [:major] binds
;; `major` looking up :major (str would keep the colon).
(fn* [aa s]
(let* [local (name s)
nsp (namespace s)
nsp (or (namespace s) dnsp)
keyform (cond
(= kind :kw) (keyword (if nsp (str nsp "/" local) local))
(= kind :str) local
@ -238,13 +240,21 @@
`(get ~gm ~keyform ~(nth fo 1))
`(get ~gm ~keyform)))))
a names)
a)))
g1 (group base :keys :kw)
g2 (group g1 :strs :str)
g3 (group g2 :syms :sym)]
a))
g1 (group base (get pat :keys) :kw nil)
g2 (group g1 (get pat :strs) :str nil)
g3 (group g2 (get pat :syms) :sym nil)]
;; remaining keys: a qualified :ns/keys|:ns/strs|:ns/syms groups under
;; its namespace; any other keyword is skipped; a non-keyword is a
;; nested binding pattern.
(reduce (fn* [a k]
(if (keyword? k)
a
(let* [kn (name k) kns (namespace k)]
(cond
(and kns (= kn "keys")) (group a (get pat k) :kw kns)
(and kns (= kn "strs")) (group a (get pat k) :str kns)
(and kns (= kn "syms")) (group a (get pat k) :sym kns)
:else a))
(proc k `(get ~gm ~(get pat k)) a)))
g3 (keys pat)))
:else (throw (str "unsupported destructuring pattern: " (pr-str pat)))))

View file

@ -478,12 +478,23 @@
(defn- analyze-dot [ctx items env]
(when (< (count items) 3)
(throw (str "Malformed (. target member ...) form")))
(let [member (nth items 2)]
(let [target (nth items 1)
member (nth items 2)
;; (. Class method args*) with a class target is a static call —
;; equivalent to (Class/method args*). resolve-global tags a class
;; symbol :kind :class; a local of the same name shadows it.
class-target (when (and (form-sym? target)
(not (local? env (form-sym-name target))))
(let [r (resolve-global ctx target)]
(when (= :class (:kind r)) (:name r))))]
(cond
(and class-target (form-sym? member))
(invoke (host-static class-target (form-sym-name member))
(mapv #(analyze ctx % env) (drop 3 items)))
(form-sym? member)
{:op :host-call
:method (form-sym-name member)
:target (analyze ctx (nth items 1) env)
:target (analyze ctx target env)
:args (mapv #(analyze ctx % env) (drop 3 items))}
;; (. obj :kw) is a keyword lookup — invoke the keyword on the target.
(form-keyword? member)

View file

@ -2999,4 +2999,40 @@
{:suite "interop / numbers & classes" :label "instance? PushbackReader" :expected "true" :actual "(instance? java.io.PushbackReader (java.io.PushbackReader. (java.io.StringReader. \"x\")))"}
{:suite "interop / numbers & classes" :label "EOFException catch by class" :expected "\"boom\"" :actual "(try (throw (java.io.EOFException. \"boom\")) (catch java.io.EOFException e (.getMessage e)))"}
{:suite "interop / numbers & classes" :label "Reader.read into char[]" :expected "[4 \"abcd\"]" :actual "(let [r (java.io.StringReader. \"abcd\") b (char-array 4)] (let [n (.read r b 0 4)] [n (String. b 0 n)]))"}
{:suite "interop / java.time" :label "LocalDate toString" :expected "\"2020-01-15\"" :actual "(str (java.time.LocalDate/of 2020 1 15))"}
{:suite "interop / java.time" :label "LocalDate plusDays over leap day" :expected "\"2020-02-29\"" :actual "(str (.plusDays (java.time.LocalDate/of 2020 2 28) 1))"}
{:suite "interop / java.time" :label "LocalDate equality" :expected "true" :actual "(= (java.time.LocalDate/of 2020 1 15) (java.time.LocalDate/of 2020 1 15))"}
{:suite "interop / java.time" :label "LocalDate compare" :expected "-1" :actual "(compare (java.time.LocalDate/of 2020 1 15) (java.time.LocalDate/of 2020 1 16))"}
{:suite "interop / java.time" :label "LocalDate ofEpochDay round-trip" :expected "1" :actual "(.toEpochDay (java.time.LocalDate/of 1970 1 2))"}
{:suite "interop / java.time" :label "LocalDate parse" :expected "\"2020-01-15\"" :actual "(str (java.time.LocalDate/parse \"2020-01-15\"))"}
{:suite "interop / java.time" :label "LocalDate getDayOfWeek name" :expected "\"WEDNESDAY\"" :actual "(.name (.getDayOfWeek (java.time.LocalDate/of 2020 1 15)))"}
{:suite "interop / java.time" :label "LocalTime toString" :expected "\"10:30:15\"" :actual "(str (java.time.LocalTime/of 10 30 15))"}
{:suite "interop / java.time" :label "LocalTime plusHours wraps midnight" :expected "\"01:30\"" :actual "(str (.plusHours (java.time.LocalTime/of 23 30) 2))"}
{:suite "interop / java.time" :label "LocalDateTime toString" :expected "\"2020-01-15T10:30\"" :actual "(str (java.time.LocalDateTime/of 2020 1 15 10 30 0))"}
{:suite "interop / java.time" :label "Instant ofEpochMilli toString" :expected "\"2020-01-15T10:30:00Z\"" :actual "(str (java.time.Instant/ofEpochMilli 1579084200000))"}
{:suite "interop / java.time" :label "Instant ofEpochMilli round-trip" :expected "1579084200000" :actual "(.toEpochMilli (java.time.Instant/ofEpochMilli 1579084200000))"}
{:suite "interop / java.time" :label "Duration ofSeconds toString" :expected "\"PT1M30S\"" :actual "(str (java.time.Duration/ofSeconds 90))"}
{:suite "interop / java.time" :label "Duration between instants" :expected "\"PT1H1M1S\"" :actual "(str (java.time.Duration/between (java.time.Instant/ofEpochSecond 0) (java.time.Instant/ofEpochSecond 3661)))"}
{:suite "interop / java.time" :label "Period between dates" :expected "\"P1Y2M2D\"" :actual "(str (java.time.Period/between (java.time.LocalDate/of 2020 1 1) (java.time.LocalDate/of 2021 3 3)))"}
{:suite "interop / java.time" :label "Period parse round-trip" :expected "\"P1Y2M3D\"" :actual "(str (java.time.Period/parse \"P1Y2M3D\"))"}
{:suite "interop / java.time" :label "Period normalized" :expected "\"P2Y1M5D\"" :actual "(str (.normalized (java.time.Period/of 1 13 5)))"}
{:suite "interop / java.time" :label "Month of toString" :expected "\"FEBRUARY\"" :actual "(.toString (java.time.Month/of 2))"}
{:suite "interop / java.time" :label "Month length leap" :expected "29" :actual "(.length (java.time.Month/of 2) true)"}
{:suite "interop / java.time" :label "DayOfWeek constant prints name" :expected "\"MONDAY\"" :actual "(str java.time.DayOfWeek/MONDAY)"}
{:suite "interop / java.time" :label "YearMonth toString" :expected "\"2020-02\"" :actual "(str (java.time.YearMonth/of 2020 2))"}
{:suite "interop / java.time" :label "ChronoUnit DAYS between" :expected "14" :actual "(.between java.time.temporal.ChronoUnit/DAYS (java.time.LocalDate/of 2020 1 1) (java.time.LocalDate/of 2020 1 15))"}
{:suite "interop / java.time" :label "LocalDate plus ChronoUnit DAYS" :expected "\"2020-01-04\"" :actual "(str (.plus (java.time.LocalDate/of 2020 1 1) 3 java.time.temporal.ChronoUnit/DAYS))"}
{:suite "interop / java.time" :label "LocalDate until ChronoUnit DAYS" :expected "31" :actual "(.until (java.time.LocalDate/of 2020 1 1) (java.time.LocalDate/of 2020 2 1) java.time.temporal.ChronoUnit/DAYS)"}
{:suite "interop / java.time" :label "ZoneOffset ofHoursMinutes toString" :expected "\"+02:00\"" :actual "(str (java.time.ZoneOffset/ofHoursMinutes 2 0))"}
{:suite "interop / java.time" :label "ZoneOffset ofTotalSeconds toString" :expected "\"-04:30\"" :actual "(str (java.time.ZoneOffset/ofTotalSeconds -16200))"}
{:suite "interop / java.time" :label "ZoneOffset UTC toString" :expected "\"Z\"" :actual "(str java.time.ZoneOffset/UTC)"}
{:suite "interop / java.time" :label "ZoneOffset of getTotalSeconds" :expected "7200" :actual "(.getTotalSeconds (java.time.ZoneOffset/of \"+02:00\"))"}
{:suite "interop / java.time" :label "ZoneOffset ofHoursMinutes negative" :expected "\"-04:30\"" :actual "(.getId (java.time.ZoneOffset/ofHoursMinutes -4 -30))"}
{:suite "interop / java.time" :label "OffsetDateTime toInstant" :expected "\"2020-01-15T08:30:00Z\"" :actual "(str (.toInstant (java.time.OffsetDateTime/of 2020 1 15 10 30 0 0 (java.time.ZoneOffset/ofHours 2))))"}
{:suite "interop / java.time" :label "OffsetDateTime toString" :expected "\"2020-01-15T10:30+02:00\"" :actual "(str (java.time.OffsetDateTime/of 2020 1 15 10 30 0 0 (java.time.ZoneOffset/ofHours 2)))"}
{:suite "interop / java.time" :label "ZonedDateTime withZoneSameInstant UTC" :expected "\"2020-01-15T08:30Z\"" :actual "(str (.withZoneSameInstant (java.time.ZonedDateTime/parse \"2020-01-15T10:30:00+02:00[Europe/Paris]\") java.time.ZoneOffset/UTC))"}
{:suite "interop / java.time" :label "ZonedDateTime of toInstant fixed offset" :expected "\"2020-01-15T08:30:00Z\"" :actual "(str (.toInstant (java.time.ZonedDateTime/of 2020 1 15 10 30 0 0 (java.time.ZoneId/of \"+02:00\"))))"}
{:suite "interop / java.time" :label "DateTimeFormatter ofPattern format" :expected "\"2020-01-15 10:30:00\"" :actual "(.format (java.time.format.DateTimeFormatter/ofPattern \"yyyy-MM-dd HH:mm:ss\") (java.time.LocalDateTime/of 2020 1 15 10 30 0))"}
{:suite "interop / java.time" :label "ISO_LOCAL_DATE_TIME format" :expected "\"2020-01-15T10:30:00\"" :actual "(.format java.time.format.DateTimeFormatter/ISO_LOCAL_DATE_TIME (java.time.LocalDateTime/of 2020 1 15 10 30 0))"}
{:suite "interop / java.time" :label "ISO_OFFSET_DATE_TIME format" :expected "\"2020-01-15T10:30:00+02:00\"" :actual "(.format java.time.format.DateTimeFormatter/ISO_OFFSET_DATE_TIME (java.time.OffsetDateTime/of 2020 1 15 10 30 0 0 (java.time.ZoneOffset/ofHours 2)))"}
]