The gory details of transforming date and time styles, with units and other options, into formatting functions. This namespace deals with the options only, not with specific dates, and returns reusable formatter functions. | (ns metabase.util.formatting.internal.date-formatters (:require [clojure.string :as str] [metabase.util.formatting.constants :as constants] [metabase.util.formatting.internal.date-builder :as builder] [metabase.util.log :as log])) |
(defn- apply-date-separator [format-list date-separator] (if date-separator (for [fmt format-list] (if (string? fmt) (str/replace fmt #"/" date-separator) fmt)) format-list)) | |
(defn- apply-date-abbreviation [format-list] (for [k format-list] (case k :month-full :month-short ":month-full" :month-short :day-of-week-full :day-of-week-short ":day-of-week-full" :day-of-week-short k))) | |
Maps each unit to the default way of formatting that unit. This uses full month and weekday names; abbreviated output replaces these with the short forms later. | (def ^:private default-date-formats-for-unit ;; TODO Do we have (in i18n or utils) helpers for getting localized ordinals? {:year [:year] ; 2022 :quarter ["Q" :quarter " - " :year] ; Q4 - 2022 :minute-of-hour [:minute-d] ; 6, 24 :day-of-week [:day-of-week-full] ; Monday; Mon :day-of-month [:day-of-month-d] ; 7, 23 :day-of-year [:day-of-year] ; 1, 24, 365 :week-of-year [:week-of-year] ; CLJS: 1st, 42nd; CLJ: 1, 42 (no ordinals) :month-of-year [:month-full] ; October; Oct :quarter-of-year ["Q" :quarter]}) ; Q4 |
Map of | (def ^:private date-style-to-format-overrides (let [m-y [:month-d "/" :year] mmm-y [:month-full ", " :year]] {"M/D/YYYY" {:month m-y} "D/M/YYYY" {:month m-y} "YYYY/M/D" {:month [:year "/" :month-d] :quarter [:year " - Q" :quarter]} "MMMM D, YYYY" {:month mmm-y} "D MMMM, YYYY" {:month mmm-y} "dddd, MMMM D, YYYY" {:week [:month-full " " :day-of-month-d ", " :year] :month mmm-y}})) |
(def ^:private iso-format [:year "-" :month-dd "-" :day-of-month-dd "T" :hour-24-dd ":" :minute-dd ":" :second-dd]) | |
Datetime iso formatter. | (def ->iso (builder/->formatter iso-format)) |
The | (defn- resolve-date-style [{:keys [date-format date-style unit]}] (or date-format (get-in date-style-to-format-overrides [date-style unit]) (get default-date-formats-for-unit unit) (get constants/known-date-styles date-style) (do (log/warn "Unrecognized date style" {:date-style date-style :unit unit}) iso-format))) |
(defn- normalize-date-format [{:keys [date-format] :as options}] (merge options (get constants/known-datetime-styles date-format))) | |
(defn- prepend-weekday [date-format] (concat [:day-of-week-short ", "] date-format)) | |
Derives a date format data structure from an options map. There are three possible sources of the final date format:
1. A directly provided A string | (defn- date-format-for-options [{:keys [date-separator weekday-enabled] :as options}] (let [date-format (-> options normalize-date-format resolve-date-style)] (cond-> date-format date-separator (apply-date-separator date-separator) weekday-enabled prepend-weekday (constants/abbreviated? options) apply-date-abbreviation))) |
------------------------------------------ Standardized Formats ------------------------------------------------ | (def ^:private short-month-day (builder/->formatter [:month-short " " :day-of-month-d])) (def ^:private full-month-day (builder/->formatter [:month-full " " :day-of-month-d])) |
(def ^:private short-month-day-year (builder/->formatter [:month-short " " :day-of-month-d ", " :year])) (def ^:private full-month-day-year (builder/->formatter [:month-full " " :day-of-month-d ", " :year])) | |
(defn- short-months? [{:keys [type] :as options}] (and (constants/abbreviated? options) (not= type "tooltip"))) | |
Helper that gets the right month-day-year format based on the options: either full | (defn month-day-year [options] (if (short-months? options) short-month-day-year full-month-day-year)) |
Helper that gets the right month-day format based on the options: either full | (defn month-day [options] (if (short-months? options) short-month-day full-month-day)) |
(def ^:private big-endian-day-format [:year "-" :month-dd "-" :day-of-month-dd]) | |
A cached, commonly used formatter for dates in | (def big-endian-day (builder/->formatter big-endian-day-format)) |
A cached, commonly used formatter for times in 12-hour | (def hour-only (builder/->formatter [:hour-12-d " " :am-pm])) |
A cached, commonly used formatter for full weekday names. | (def weekday (builder/->formatter [:day-of-week-full])) |
--------------------------------------------- Time formatters ---------------------------------------------------- | (defn- english-time-seconds [inner] (vec (concat [:hour-12-d ":" :minute-dd ":" :second-dd] inner [" " :am-pm]))) |
(def ^:private iso-time-seconds [:hour-24-dd ":" :minute-dd ":" :second-dd]) | |
(def ^:private time-style-to-format {"h:mm A" {nil (english-time-seconds []) "seconds" (english-time-seconds []) "milliseconds" (english-time-seconds ["." :millisecond-ddd])} "HH:mm" {nil iso-time-seconds "seconds" iso-time-seconds "milliseconds" (into iso-time-seconds ["." :millisecond-ddd])}}) | |
(def ^:private fallback-iso-time [:hour-24-dd ":" :minute-dd ":" :second-dd]) | |
The time format is resolved as follows:
1. If a | (defn- time-format-for-options [{:keys [time-enabled time-format time-style] :as options}] (or (and (string? time-format) (or (get constants/known-time-styles time-format) (throw (ex-info "Unknown time format" options)))) time-format (get-in time-style-to-format [time-style time-enabled]) (get constants/known-time-styles time-style) (do (log/warn "Unrecognized time style" {:time-style time-style :time-enabled time-enabled}) fallback-iso-time))) |
------------------------------------- Custom formatters from options --------------------------------------------- These are cached, since the formatter is always identical for the same options. | |
(defn- options->formatter* [{:keys [date-enabled time-enabled] :as options}] ;; TODO The original emits a console warning if the date-style is not in the overrides map. Reproduce that? (let [date-format (when date-enabled (date-format-for-options options)) time-format (when time-enabled (time-format-for-options options)) format-list (if (and date-format time-format) (concat date-format [", "] time-format) ;; At most one format is given; use that one. ;; If neither is set, emit a warning and use ISO standard format. (or date-format time-format (do (log/warn "Unrecognized date/time format" options) iso-format)))] (builder/->formatter format-list))) | |
(def ^:private options->formatter-cache (atom {})) | |
Given the options map, this reduces it to a formatter function.
Expects The options and corresponding formatters are cached indefinitely, since there are generally only a few dozen different sets of options, and from hundreds to many thousands of dates will be formatted in a typical session. | (defn options->formatter [options] {:pre [(map? options)]} ;; options must be a Clojure map from date-options/prepare-options (if-let [fmt (get @options->formatter-cache options)] fmt (-> (swap! options->formatter-cache (fn [cache] (if (contains? cache options) cache (assoc cache options (options->formatter* options))))) (get options)))) |