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
26 changes: 21 additions & 5 deletions src/jolt/time/fmt.clj
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,26 @@
;; has to run once.
(defonce ^:private locale-points-registered (boolean (register-locale-points!)))

;; FormatStyle is an enum: its constants in JDK declaration order, declared
;; through jolt.host/register-enum! (values, valueOf, ordinal, compareTo,
;; EnumSet, the #object print form). An older jolt without register-enum!
;; still gets the constants as plain statics.
(def ^:private style-names ["FULL" "LONG" "MEDIUM" "SHORT"])
(def ^:private style-key {"SHORT" :short "MEDIUM" :medium "LONG" :long "FULL" :full})
(statics! ["FormatStyle" "java.time.format.FormatStyle"]
(into {} (map (fn [s] [s (impl/value :jolt.time/format-style {:style s})]) ["SHORT" "MEDIUM" "LONG" "FULL"])))
(def ^:private style-ordinal (zipmap style-names (range)))
(def ^:private format-styles
(mapv (fn [s] (impl/value :jolt.time/format-style {:style s})) style-names))
(impl/register-type! :jolt.time/format-style
{:eq (fn [a b] (= (impl/field a :style) (impl/field b :style))) :hash (fn [s] (hash (impl/field s :style)))
:str (fn [s] (impl/field s :style)) :cmp nil :classes #{"java.time.format.FormatStyle" "FormatStyle"}})
:str (fn [s] (impl/field s :style))
:enum? true
;; Enum.compareTo: the ordinal difference
:cmp (fn [a b] (- (style-ordinal (impl/field a :style)) (style-ordinal (impl/field b :style))))
:classes #{"java.time.format.FormatStyle" "FormatStyle"
"java.lang.Comparable" "Comparable" "java.lang.Enum" "Enum"}})
(if-let [register-enum! (resolve 'jolt.host/register-enum!)]
(register-enum! "java.time.format.FormatStyle" (mapv vector style-names format-styles))
(statics! ["FormatStyle" "java.time.format.FormatStyle"] (zipmap style-names format-styles)))
(defn- style-of [fs] (impl/field fs :style))
;; ofLocalized* defers pattern resolution: the pattern depends on the locale,
;; which .withLocale supplies after construction, so the value carries
Expand Down Expand Up @@ -324,12 +338,14 @@
;; zeros toward minWidth, which a pattern string can't express. Only the
;; second-fraction fields map onto 'S'.
"appendFraction" (fn [b field _min-w max-w point]
(let [fname (str field)
;; .name, not str: a ChronoField's toString is its
;; display name (NanoOfSecond) on the JDK
(let [fname (if (instance? Enum field) (.name field) (str field))
width (case fname
"NANO_OF_SECOND" (min (u/->long max-w) 9)
"MICRO_OF_SECOND" (min (u/->long max-w) 6)
"MILLI_OF_SECOND" (min (u/->long max-w) 3)
(throw (ex-info (str "appendFraction: unsupported field " fname)
(throw (ex-info (str "appendFraction: unsupported field " field)
{:field fname})))]
(b-append! b (str (when point ".") (apply str (repeat width "S"))))))
"parseCaseInsensitive" (fn [b] b) "parseLenient" (fn [b] b)
Expand Down
4 changes: 3 additions & 1 deletion test/jolt/time/enums_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@

(deftest chrono-unit
(is (= "DAYS" (.name java.time.temporal.ChronoUnit/DAYS)))
(is (= "DAYS" (str java.time.temporal.ChronoUnit/DAYS)))
;; str is toString, which on the JDK is the display name ("Days"); which
;; one a given jolt answers is core's to certify, not this library's
(is (= (.toString java.time.temporal.ChronoUnit/DAYS) (str java.time.temporal.ChronoUnit/DAYS)))
(is (.isDateBased java.time.temporal.ChronoUnit/DAYS))
(is (.isTimeBased java.time.temporal.ChronoUnit/HOURS))
(is (instance? java.time.temporal.ChronoUnit java.time.temporal.ChronoUnit/SECONDS))
Expand Down
59 changes: 58 additions & 1 deletion test/jolt/time/fmt_test.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
(ns jolt.time.fmt-test
"Stage 6 gate: DateTimeFormatter. JVM-certified values. Run under JOLT_NO_JAVA_TIME=1."
(:require [clojure.test :refer [deftest is]]
[jolt.time.fmt])
[jolt.time.fmt]
[jolt.time.impl])
(:import [java.time LocalDate LocalDateTime]
[java.time.format DateTimeFormatter FormatStyle]))

Expand Down Expand Up @@ -156,3 +157,59 @@
(is (= "12:34:56+01:00"
(.format f (java.time.OffsetDateTime/of 2020 1 1 12 34 56 0
(java.time.ZoneOffset/ofHours 1)))))))

;; On the JDK (str ChronoField/NANO_OF_SECOND) is the display name
;; "NanoOfSecond"; only .name is the constant. Run the builder with the field's
;; str answering the display name, as it does there, so nothing in the builder
;; leans on str for the constant name.
(defn- display-name [nm]
(apply str (map (fn [part] (str (subs part 0 1) (.toLowerCase (subs part 1))))
(.split nm "_"))))

(defn- with-jdk-field-str [f]
(let [reg @#'jolt.time.impl/registry
spec (get @reg :jolt.time/chrono-field)]
(swap! reg assoc :jolt.time/chrono-field
(assoc spec :str (fn [fld] (display-name (jolt.time.impl/field fld :name)))))
(try (f) (finally (swap! reg assoc :jolt.time/chrono-field spec)))))

(deftest builder-reads-field-name-not-str
(with-jdk-field-str
(fn []
(is (= "NanoOfSecond" (str java.time.temporal.ChronoField/NANO_OF_SECOND)))
(let [t (java.time.LocalTime/of 12 34 56 123456789)
build (fn [field w]
(.toFormatter (doto (java.time.format.DateTimeFormatterBuilder.)
(.appendPattern "HH:mm:ss")
(.appendFraction field 0 w true))))]
(is (= "12:34:56.123456789"
(.format (build java.time.temporal.ChronoField/NANO_OF_SECOND 9) t)))
(is (= "12:34:56.123456"
(.format (build java.time.temporal.ChronoField/MICRO_OF_SECOND 6) t)))
(is (= "12:34:56.123"
(.format (build java.time.temporal.ChronoField/MILLI_OF_SECOND 3) t)))))))

;; FormatStyle is a JDK enum: constants in declaration order, the Enum methods,
;; and the #object print form. Values from JDK 21. Needs a jolt that has
;; jolt.host/register-enum!; an older one only gets the constants.
(deftest format-style-enum
(is (= "MEDIUM" (str FormatStyle/MEDIUM)))
(when (resolve 'jolt.host/register-enum!)
(is (= ["FULL" "LONG" "MEDIUM" "SHORT"] (mapv str (FormatStyle/values))))
(is (= ["FULL" "LONG" "MEDIUM" "SHORT"] (mapv #(.name %) (FormatStyle/values))))
(is (= [0 1 2 3] (mapv #(.ordinal %) (FormatStyle/values))))
(is (= -3 (.compareTo FormatStyle/FULL FormatStyle/SHORT)))
(is (= FormatStyle/MEDIUM (FormatStyle/valueOf "MEDIUM")))
(is (identical? FormatStyle/MEDIUM (FormatStyle/valueOf "MEDIUM")))
(is (= FormatStyle/LONG (Enum/valueOf FormatStyle "LONG")))
(is (thrown? IllegalArgumentException (FormatStyle/valueOf "NOPE")))
(is (.isEnum FormatStyle))
(is (instance? Enum FormatStyle/SHORT))
(is (= FormatStyle (.getDeclaringClass FormatStyle/SHORT)))
(is (= [FormatStyle/FULL FormatStyle/SHORT] (sort [FormatStyle/SHORT FormatStyle/FULL])))
(is (re-matches #"#object\[java\.time\.format\.FormatStyle 0x[0-9a-f]+ \"SHORT\"\]"
(pr-str FormatStyle/SHORT)))
(is (= 4 (count (java.util.EnumSet/allOf FormatStyle))))
;; still drives the localized formatters
(is (= "Mar 5, 2020" (.format (DateTimeFormatter/ofLocalizedDate (FormatStyle/valueOf "MEDIUM"))
(LocalDate/of 2020 3 5))))))
Loading