Utility functions for programatically building a DateTimeFormatter. Easier to understand than chaining a hundred Java calls and trying to keep the structure straight.

The basic idea here is you pass a number of sections to formatter to build a DateTimeFormatter — see metabase.util.date-2.parse for examples. Most of these sections are simple wrappers around corresponding DateTimeFormatterBuilder -- see https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatterBuilder.html for documenation.

TODO - this is a prime library candidate.

(ns metabase.util.date-2.parse.builder
  (:require
   [metabase.util.date-2.common :as u.date.common])
  (:import
   (java.time.format DateTimeFormatter DateTimeFormatterBuilder SignStyle)
   (java.time.temporal TemporalField)))
(set! *warn-on-reflection* true)
(defprotocol ^:private Section
  (^:private apply-section [this builder]))
(extend-protocol Section
  String
  (apply-section [s builder]
    (.appendLiteral ^DateTimeFormatterBuilder builder s))

  clojure.lang.Fn
  (apply-section [f builder]
    (f builder))

  clojure.lang.Sequential
  (apply-section [sections builder]
    (doseq [section sections]
      (apply-section section builder)))

  DateTimeFormatter
  (apply-section [formatter builder]
    (.append ^DateTimeFormatterBuilder builder formatter)))

Make wrapped sections optional.

(defn optional
  [& sections]
  (reify Section
    (apply-section [_ builder]
      (.optionalStart ^DateTimeFormatterBuilder builder)
      (apply-section sections builder)
      (.optionalEnd ^DateTimeFormatterBuilder builder))))
(defn- set-option [^DateTimeFormatterBuilder builder option]
  (case option
    :strict           (.parseStrict builder)
    :lenient          (.parseLenient builder)
    :case-sensitive   (.parseCaseSensitive builder)
    :case-insensitive (.parseCaseInsensitive builder)))
(def ^:private ^:dynamic *options*
  {:strictness       :strict
   :case-sensitivity :case-sensitive})
(defn- do-with-option [builder k new-value thunk]
  (let [old-value (get *options* k)]
    (if (= old-value new-value)
      (thunk)
      (binding [*options* (assoc *options* k new-value)]
        (set-option builder new-value)
        (thunk)
        (set-option builder old-value)))))
(defn- with-option-section [k v sections]
  (reify Section
    (apply-section [_ builder]
      (do-with-option builder k v (fn [] (apply-section sections builder))))))

Use strict parsing for wrapped sections.

(defn strict
  [& sections]
  (with-option-section :strictness :strict sections))

Use lenient parsing for wrapped sections.

(defn lenient
  [& sections]
  (with-option-section :strictness :lenient sections))

Make wrapped sections case-sensitive.

(defn case-sensitive
  [& sections]
  (with-option-section :case-sensitivity :case-sensitive sections))

Make wrapped sections case-insensitive.

(defn case-insensitive
  [& sections]
  (with-option-section :case-sensitivity :case-insensitive sections))
(def ^:private ^SignStyle sign-style
  (u.date.common/static-instances SignStyle))
(defn- temporal-field ^TemporalField [x]
  (let [field (if (keyword? x)
                (u.date.common/temporal-field x)
                x)]
    (assert (instance? TemporalField field)
            (format "Invalid TemporalField: %s" (pr-str field)))
    field))

Define a section for a specific field such as :hour-of-day or :minute-of-hour. Refer to metabase.util.date-2.common/temporal-field for all possible temporal fields names.

(defn value
  ([temporal-field-name]
   (fn [^DateTimeFormatterBuilder builder]
     (.appendValue builder (temporal-field temporal-field-name))))
  ([temporal-field-name width]
   (fn [^DateTimeFormatterBuilder builder]
     (.appendValue builder (temporal-field temporal-field-name) width)))
  ([temporal-field-name min-val max-val sign-style-name]
   (fn [^DateTimeFormatterBuilder builder]
     (.appendValue builder (temporal-field temporal-field-name) min-val max-val (sign-style sign-style-name)))))

Define a section that sets a default value for a field like :minute-of-hour.

(defn default-value
  [temporal-field-name default-value]
  (fn [^DateTimeFormatterBuilder builder]
    (.parseDefaulting builder (temporal-field temporal-field-name) default-value)))

Define a section for a fractional value, e.g. milliseconds or nanoseconds.

(defn fraction
  [temporal-field-name _min-val-width _max-val-width & {:keys [decimal-point?]}]
  (fn [^DateTimeFormatterBuilder builder]
    (.appendFraction builder (temporal-field temporal-field-name) 0 9 (boolean decimal-point?))))

Define a section for a timezone offset. e.g. -08:00.

(defn zone-offset
  []
  (lenient
   (fn [^DateTimeFormatterBuilder builder]
     (.appendOffsetId builder))))

An a section for a timezone ID wrapped in square brackets, e.g. [America/Los_Angeles].

(defn zone-id
  []
  (strict
   (case-sensitive
    (optional "[")
    (fn [^DateTimeFormatterBuilder builder]
      (.appendZoneRegionId builder))
    (optional "]"))))

Return a new DateTimeFormatter from sections. See examples in metabase.util.date-2.parse for more details.

(formatter (case-insensitive (value :hour-of-day 2) (optional ":" (value :minute-of-hour 2) (optional ":" (value :second-of-minute)))))

->

#object[java.time.format.DateTimeFormatter "ParseCaseSensitive(false)Value(HourOfDay,2)[':'Value(MinuteOfHour,2)[':'Value(SecondOfMinute)]]"]

(defn formatter
  ^DateTimeFormatter [& sections]
  (let [builder (DateTimeFormatterBuilder.)]
    (apply-section sections builder)
    (.toFormatter builder)))