diff --git a/src/jolt/time/fmt.clj b/src/jolt/time/fmt.clj index ba50ba2..862d2f8 100644 --- a/src/jolt/time/fmt.clj +++ b/src/jolt/time/fmt.clj @@ -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 @@ -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) diff --git a/test/jolt/time/enums_test.clj b/test/jolt/time/enums_test.clj index f65b579..21e0229 100644 --- a/test/jolt/time/enums_test.clj +++ b/test/jolt/time/enums_test.clj @@ -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)) diff --git a/test/jolt/time/fmt_test.clj b/test/jolt/time/fmt_test.clj index 8aa39ee..709d860 100644 --- a/test/jolt/time/fmt_test.clj +++ b/test/jolt/time/fmt_test.clj @@ -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])) @@ -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))))))