Formatting for dates, times, and ranges.

(ns metabase.util.formatting.date
  (:require
   [metabase.util.formatting.constants :as constants]
   [metabase.util.formatting.internal.date-builder :as builder]
   [metabase.util.formatting.internal.date-formatters :as formatters]
   [metabase.util.formatting.internal.date-options :as options]
   [metabase.util.time :as u.time]))

The range separator is a Unicode en-dash, not an ASCII hyphen.

(def range-separator
  " \u2013 ")

-------------------------------------------- Parameter Formatting ---------------------------------------------

(def ^:private parameter-formatters
  {:month   (builder/->formatter [:year "-" :month-dd])
   :quarter (builder/->formatter ["Q" :quarter "-" :year])
   :day     formatters/big-endian-day})

Returns a formatting date string for a datetime used as a parameter to a Card.

(defn ^:export format-for-parameter
  [value options]
  (let [options (options/prepare-options options)
        t       (u.time/coerce-to-timestamp value options)]
    (if (not (u.time/valid? t))
      ;; Fall back to a basic string rendering if we couldn't parse it.
      (str value)
      (if-let [fmt (parameter-formatters (:unit options))]
        ;; A few units have special formats.
        (fmt t)
        ;; Otherwise, render as a day or day range.
        (let [[start end] (u.time/to-range t options)]
          (if (u.time/same-day? start end)
            (formatters/big-endian-day start)
            (str (formatters/big-endian-day start) "~" (formatters/big-endian-day end))))))))

------------------------------------------------ Format Range -------------------------------------------------

(defn- format-range-with-unit-inner [[start end] options]
  (cond
    ;; Uncondensed, or in different years: January 1, 2018 - January 23, 2019
    (or (not (constants/condense-ranges? options))
        (not (u.time/same-year? start end)))
    (let [fmt (formatters/month-day-year options)]
      (str (fmt start) range-separator (fmt end)))
    ;; Condensed, but different months: January 1 - February 2, 2018
    (not (u.time/same-month? start end))
    (str ((formatters/month-day options) start)
         range-separator
         ((formatters/month-day-year options) end))
    ;; Condensed, and same month: January 1 - 14, 2018
    :else (str ((formatters/month-day options) start)
               range-separator
               ((builder/->formatter [:day-of-month-d ", " :year]) end))))

Returns a string with this datetime formatted as a range, rounded to the given :unit.

(defn ^:export format-range-with-unit
  [value options]
  (let [options (options/prepare-options options)
        t       (u.time/coerce-to-timestamp value options)]
    (if (u.time/valid? t)
      (format-range-with-unit-inner (u.time/to-range t options) options)
      ;; Best-effort fallback if we failed to parse - .toString the input.
      (str value))))

Returns a string with this datetime formatted as a single value, rounded to the given :unit.

---------------------------------------------- Format Single Date -----------------------------------------------

(defn ^:export format-datetime-with-unit
  [value options]
  (let [{:keys [is-exclude no-range type unit]
         :as options}                          (options/prepare-options options)
        t                                      (u.time/coerce-to-timestamp value options)]
    (cond
      is-exclude (case unit
                   :hour-of-day (formatters/hour-only t)
                   :day-of-week (formatters/weekday t)
                   (throw (ex-info "is-exclude option is only compatible with hour-of-day and day-of-week units"
                                   {:options options})))
      ;; Weeks in tooltips and cells get formatted specially.
      (and (= unit :week) (#{"tooltip" "cell"} type) (not no-range))
      (format-range-with-unit value options)
      :else ((formatters/options->formatter options) t))))

Coerce date and format as big-endian-day string.

(defn ^:export date->iso-string
  [d]
  (formatters/big-endian-day (u.time/coerce-to-timestamp d)))

Coerce datetime and format as iso string.

(defn ^:export datetime->iso-string
  [dt]
  (formatters/->iso (u.time/coerce-to-timestamp dt)))