Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions src/jolt/time/fmt.clj
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,22 @@
(= \' (nth pattern j)) [(inc j) acc]
:else (recur (inc j) (str acc (nth pattern j)))))))

(defn format-pattern [pattern v locale]
;; The ISO_* formatters print the second's fraction the way java.time's
;; ISO_LOCAL_TIME does -- omitted when zero, else as many digits as carry a
;; value, up to nine -- and a pattern's fixed-width S never does; so the rule
;; is a flag on the formatter (`iso-frac`) that the seconds letter reads.
;; ISO_INSTANT is the exception: it prints the fraction in whole groups of three
;; (".500", ".000120"), which is the :instant mode.
(defn- iso-fraction [nano mode]
(if (zero? nano)
""
(let [s (u/pad-left (str nano) 9)
last-digit (loop [i 8] (if (= \0 (nth s i)) (recur (dec i)) i))
width (if (= :instant mode) (* 3 (quot (+ last-digit 3) 3)) (inc last-digit))]
(str "." (subs s 0 width)))))
(defn format-pattern
([pattern v locale] (format-pattern pattern v locale nil))
([pattern v locale iso-frac]
(let [p (parts v)]
(if-not p (str v)
(let [[y mo d hh mi se nano dow off zid] p n (count pattern)]
Expand All @@ -93,17 +108,20 @@
(= c \H) (recur (+ i k) (str out (if (= k 1) (str hh) (u/pad2 hh))))
(= c \h) (recur (+ i k) (str out (let [h12 (let [h (mod hh 12)] (if (zero? h) 12 h))] (if (= k 1) (str h12) (u/pad2 h12)))))
(= c \m) (recur (+ i k) (str out (if (= k 1) (str mi) (u/pad2 mi))))
(= c \s) (recur (+ i k) (str out (if (= k 1) (str se) (u/pad2 se))))
(= c \s) (recur (+ i k) (str out (if (= k 1) (str se) (u/pad2 se)) (if iso-frac (iso-fraction nano iso-frac) "")))
(= c \S) (recur (+ i k) (str out (u/pad-left (str (quot nano (u/pow10 (max 0 (- 9 k))))) k)))
(= c \a) (recur (+ i k) (str out (if (< hh 12) "AM" "PM")))
(= c \X) (recur (+ i k) (str out (off-iso off (>= k 3) true)))
(= c \x) (recur (+ i k) (str out (off-iso off (>= k 3) false)))
(= c \Z) (recur (+ i k) (str out (off-iso off (>= k 3) false)))
(or (= c \V) (= c \z)) (recur (+ i k) (str out (or zid (off-iso off true true))))
:else (recur (inc i) (str out c)))))))))))
:else (recur (inc i) (str out c))))))))))))

;; --- DateTimeFormatter -------------------------------------------------------
(defn formatter [pattern locale] (impl/value :jolt.time/dt-formatter {:pattern pattern :locale (or locale "en")}))
(defn- iso-formatter
([pattern] (iso-formatter pattern true))
([pattern mode] (impl/value :jolt.time/dt-formatter {:pattern pattern :locale "en" :iso mode})))
(defn- fmt-pattern [f]
(or (impl/field f :pattern)
(let [[kind style] (impl/field f :style)]
Expand All @@ -128,10 +146,11 @@
(declare parse-with-pattern parse-fields-strict parse-zoned-strict strict-parseable?)

(__register-class-methods! :jolt.time/dt-formatter
{"format" (fn [self v] (format-pattern (fmt-pattern self) v (fmt-locale self)))
{"format" (fn [self v] (format-pattern (fmt-pattern self) v (fmt-locale self) (impl/field self :iso)))
"withLocale" (fn [self l] (impl/value :jolt.time/dt-formatter
{:pattern (impl/field self :pattern)
:style (impl/field self :style)
:iso (impl/field self :iso)
:locale (locale-id l)}))
"withZone" (fn [self _z] self)
"parse" (fn
Expand Down Expand Up @@ -258,16 +277,19 @@

(statics! ["DateTimeFormatter" "java.time.format.DateTimeFormatter"]
{"ofPattern" (fn [p & r] (formatter (str p) (if (seq r) (locale-id (first r)) "en")))
"ISO_LOCAL_DATE" (formatter "yyyy-MM-dd" "en") "ISO_LOCAL_TIME" (formatter "HH:mm:ss" "en")
"ISO_LOCAL_DATE_TIME" (formatter "yyyy-MM-dd'T'HH:mm:ss" "en")
"ISO_DATE" (formatter "yyyy-MM-dd" "en") "ISO_TIME" (formatter "HH:mm:ss" "en")
"ISO_DATE_TIME" (formatter "yyyy-MM-dd'T'HH:mm:ss" "en")
"ISO_INSTANT" (formatter "yyyy-MM-dd'T'HH:mm:ssX" "en")
"ISO_OFFSET_DATE_TIME" (formatter "yyyy-MM-dd'T'HH:mm:ssXXX" "en")
"ISO_OFFSET_TIME" (formatter "HH:mm:ssXXX" "en")
"ISO_LOCAL_DATE" (formatter "yyyy-MM-dd" "en") "ISO_LOCAL_TIME" (iso-formatter "HH:mm:ss")
;; the ISO constants carry :iso, so the second's fraction prints as
;; java.time's do (see iso-fraction); the strict parser already takes an
;; optional fraction after the seconds when the pattern spells no S
"ISO_LOCAL_DATE_TIME" (iso-formatter "yyyy-MM-dd'T'HH:mm:ss")
"ISO_DATE" (formatter "yyyy-MM-dd" "en") "ISO_TIME" (iso-formatter "HH:mm:ss")
"ISO_DATE_TIME" (iso-formatter "yyyy-MM-dd'T'HH:mm:ss")
"ISO_INSTANT" (iso-formatter "yyyy-MM-dd'T'HH:mm:ssX" :instant)
"ISO_OFFSET_DATE_TIME" (iso-formatter "yyyy-MM-dd'T'HH:mm:ssXXX")
"ISO_OFFSET_TIME" (iso-formatter "HH:mm:ssXXX")
"ISO_OFFSET_DATE" (formatter "yyyy-MM-ddXXX" "en")
"ISO_ZONED_DATE_TIME" (impl/value :jolt.time/dt-formatter
{:pattern "yyyy-MM-dd'T'HH:mm:ss.SSSXXX'['VV']'" :locale "en" :iso :zoned})
{:pattern "yyyy-MM-dd'T'HH:mm:ssXXX'['VV']'" :locale "en" :iso :zoned})
"ISO_ORDINAL_DATE" (formatter "yyyy-DDD" "en")
"ISO_WEEK_DATE" (formatter "yyyy-'W'ww-e" "en")
"BASIC_ISO_DATE" (formatter "yyyyMMdd" "en")
Expand Down
30 changes: 30 additions & 0 deletions test/jolt/time/fmt_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,36 @@
(is (= "1:45 PM" (.format (DateTimeFormatter/ofPattern "h:mm a") (LocalDateTime/of 2020 3 5 13 45))))
(is (= "05/03/2020" (.format (DateTimeFormatter/ofPattern "dd/MM/yyyy") (LocalDate/of 2020 3 5)))))

;; java.time's ISO constants print the second's fraction only when it is
;; non-zero, with as many digits as carry a value (up to nine); values from
;; the reference.
(deftest iso-fraction
(let [z (java.time.ZoneId/of "Australia/Sydney")
zdt (fn [nano] (java.time.ZonedDateTime/of 2026 1 15 12 0 0 nano z))
f DateTimeFormatter/ISO_ZONED_DATE_TIME]
(is (= "2026-01-15T12:00:00+11:00[Australia/Sydney]" (.format f (zdt 0))))
(is (= "2026-01-15T12:00:00.123+11:00[Australia/Sydney]" (.format f (zdt 123000000))))
(is (= "2026-01-15T12:00:00.123456789+11:00[Australia/Sydney]" (.format f (zdt 123456789))))
(is (= "2026-01-15T12:00:00.00012+11:00[Australia/Sydney]" (.format f (zdt 120000))))
(is (= "2026-01-15T12:00:00.005+11:00" (.format DateTimeFormatter/ISO_OFFSET_DATE_TIME (zdt 5000000))))
(is (= "2026-01-15T12:00:00+11:00" (.format DateTimeFormatter/ISO_OFFSET_DATE_TIME (zdt 0))))
(is (= "12:00:00.5" (.format DateTimeFormatter/ISO_LOCAL_TIME (java.time.LocalTime/of 12 0 0 500000000))))
(is (= "2026-01-15T12:00:00" (.format DateTimeFormatter/ISO_LOCAL_DATE_TIME (LocalDateTime/of 2026 1 15 12 0 0))))
;; what it prints, it parses back
(is (= (zdt 123456789) (java.time.ZonedDateTime/parse (.format f (zdt 123456789)))))
(is (= (zdt 0) (java.time.ZonedDateTime/parse (.format f (zdt 0))))))
;; ISO_INSTANT prints the fraction in groups of three digits, not trimmed
(is (= ["1970-01-01T00:00:00Z" "1970-01-01T00:00:00.500Z" "1970-01-01T00:00:00.000120Z"
"1970-01-01T00:00:00.123456789Z" "1970-01-01T00:00:00.005Z"]
(mapv #(.format DateTimeFormatter/ISO_INSTANT (java.time.Instant/ofEpochSecond 0 %))
[0 500000000 120000 123456789 5000000])))
;; a locale does not change what an ISO constant is
(is (= "12:00:00.5" (.format (.withLocale DateTimeFormatter/ISO_LOCAL_TIME java.util.Locale/US)
(java.time.LocalTime/of 12 0 0 500000000))))
(is (= "2026-01-15T12:00:00.5+11:00[Australia/Sydney]"
(.format (.withLocale DateTimeFormatter/ISO_ZONED_DATE_TIME java.util.Locale/US)
(java.time.ZonedDateTime/of 2026 1 15 12 0 0 500000000 (java.time.ZoneId/of "Australia/Sydney"))))))

(deftest iso-constants
(is (= "2020-03-05" (.format DateTimeFormatter/ISO_LOCAL_DATE (LocalDate/of 2020 3 5))))
(is (= "13:45:30" (.format DateTimeFormatter/ISO_LOCAL_TIME (LocalDateTime/of 2020 3 5 13 45 30))))
Expand Down
Loading