(ns metabase.lib.temporal-bucket (:require [clojure.string :as str] [medley.core :as m] [metabase.lib.dispatch :as lib.dispatch] [metabase.lib.hierarchy :as lib.hierarchy] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.temporal-bucketing :as lib.schema.temporal-bucketing] [metabase.lib.util :as lib.util] [metabase.util :as u] [metabase.util.i18n :as i18n] [metabase.util.malli :as mu] [metabase.util.time :as u.time])) | |
(mu/defn describe-temporal-unit :- :string "Get a translated description of a temporal bucketing unit." ([] (describe-temporal-unit 1 nil)) ([unit] (describe-temporal-unit 1 unit)) ([n :- :int unit :- [:maybe :keyword]] (if-not unit (let [n (abs n)] (case (keyword unit) :default (i18n/trun "Default period" "Default periods" n) :millisecond (i18n/trun "Millisecond" "Milliseconds" n) :second (i18n/trun "Second" "Seconds" n) :minute (i18n/trun "Minute" "Minutes" n) :hour (i18n/trun "Hour" "Hours" n) :day (i18n/trun "Day" "Days" n) :week (i18n/trun "Week" "Weeks" n) :month (i18n/trun "Month" "Months" n) :quarter (i18n/trun "Quarter" "Quarters" n) :year (i18n/trun "Year" "Years" n) :minute-of-hour (i18n/trun "Minute of hour" "Minutes of hour" n) :hour-of-day (i18n/trun "Hour of day" "Hours of day" n) :day-of-week (i18n/trun "Day of week" "Days of week" n) :day-of-month (i18n/trun "Day of month" "Days of month" n) :day-of-year (i18n/trun "Day of year" "Days of year" n) :week-of-year (i18n/trun "Week of year" "Weeks of year" n) :month-of-year (i18n/trun "Month of year" "Months of year" n) :quarter-of-year (i18n/trun "Quarter of year" "Quarters of year" n) ;; e.g. :unknown-unit => "Unknown unit" (let [[unit & more] (str/split (name unit) #"-")] (str/join \space (cons (str/capitalize unit) more)))))))) | |
(def ^:private TemporalIntervalAmount [:or [:enum :current :last :next] :int]) | |
(defn- interval-n->int [n] (if (number? n) n (case n :current 0 :next 1 :last -1 0))) | |
(mu/defn describe-temporal-interval :- ::lib.schema.common/non-blank-string "Get a translated description of a temporal bucketing interval. If unit is unspecified, assume `:day`." [n :- TemporalIntervalAmount unit :- [:maybe :keyword]] (let [n (interval-n->int n) unit (or unit :day)] (cond (zero? n) (if (= unit :day) (i18n/tru "Today") (i18n/tru "This {0}" (describe-temporal-unit unit))) (= n 1) (if (= unit :day) (i18n/tru "Tomorrow") (i18n/tru "Next {0}" (describe-temporal-unit unit))) (= n -1) (if (= unit :day) (i18n/tru "Yesterday") (i18n/tru "Previous {0}" (describe-temporal-unit unit))) (neg? n) (i18n/tru "Previous {0} {1}" (abs n) (describe-temporal-unit (abs n) unit)) (pos? n) (i18n/tru "Next {0} {1}" n (describe-temporal-unit n unit))))) | |
(mu/defn describe-relative-datetime :- ::lib.schema.common/non-blank-string "Get a translated description of a relative datetime interval, ported from `frontend/src/metabase-lib/queries/utils/query-time.js`. e.g. if the relative interval is `-1 days`, then `n` = `-1` and `unit` = `:day`. If `:unit` is unspecified, assume `:day`." [n :- TemporalIntervalAmount unit :- [:maybe :keyword]] (let [n (interval-n->int n) unit (or unit :day)] (cond (zero? n) (i18n/tru "Now") (neg? n) ;; this should legitimately be lowercasing in the user locale. I know system locale isn't necessarily the same ;; thing, but it might be. This will have to do until we have some sort of user-locale lower-case functionality #_{:clj-kondo/ignore [:discouraged-var]} (i18n/tru "{0} {1} ago" (abs n) (str/lower-case (describe-temporal-unit (abs n) unit))) :else #_{:clj-kondo/ignore [:discouraged-var]} (i18n/tru "{0} {1} from now" n (str/lower-case (describe-temporal-unit n unit)))))) | |
Implementation for [[temporal-bucket]]. Implement this to tell [[temporal-bucket]] how to add a bucket to a particular MBQL clause. | (defmulti with-temporal-bucket-method {:arglists '([x unit])} (fn [x _unit] (lib.dispatch/dispatch-value x)) :hierarchy lib.hierarchy/hierarchy) |
Add a temporal bucketing unit, e.g. (temporal some-field :day) => [:field 1 {:temporal-unit :day}] Pass a | (mu/defn with-temporal-bucket [x option-or-unit :- [:maybe [:or ::lib.schema.temporal-bucketing/option ::lib.schema.temporal-bucketing/unit]]] (with-temporal-bucket-method x (cond-> option-or-unit (not (keyword? option-or-unit)) :unit))) |
Implementation of [[temporal-bucket]]. Return the current temporal bucketing unit associated with | (defmulti temporal-bucket-method {:arglists '([x])} lib.dispatch/dispatch-value :hierarchy lib.hierarchy/hierarchy) |
(defmethod temporal-bucket-method :default [_x] nil) | |
(mu/defmethod temporal-bucket-method :option/temporal-bucketing :- ::lib.schema.temporal-bucketing/unit [option] (:unit option)) | |
(mu/defn raw-temporal-bucket :- [:maybe ::lib.schema.temporal-bucketing/unit] "Get the raw temporal bucketing `unit` associated with something e.g. a `:field` ref or a ColumnMetadata." [x] (temporal-bucket-method x)) | |
(mu/defn temporal-bucket :- [:maybe ::lib.schema.temporal-bucketing/option] "Get the current temporal bucketing option associated with something, if any." [x] (when-let [unit (raw-temporal-bucket x)] {:lib/type :option/temporal-bucketing :unit unit})) | |
Options that are technically legal in MBQL, but that should be hidden in the UI. | (def ^:private hidden-bucketing-options #{:millisecond :second :second-of-minute :year-of-era}) |
The temporal bucketing options for time type expressions. | (def time-bucket-options (into [] (comp (remove hidden-bucketing-options) (map (fn [unit] (cond-> {:lib/type :option/temporal-bucketing :unit unit} (= unit :hour) (assoc :default true))))) lib.schema.temporal-bucketing/ordered-time-bucketing-units)) |
The temporal bucketing options for date type expressions. | (def date-bucket-options (mapv (fn [unit] (cond-> {:lib/type :option/temporal-bucketing :unit unit} (= unit :day) (assoc :default true))) lib.schema.temporal-bucketing/ordered-date-bucketing-units)) |
The temporal bucketing units for datetime type expressions. | (def datetime-bucket-units (into [] (remove hidden-bucketing-options) lib.schema.temporal-bucketing/ordered-datetime-bucketing-units)) |
The temporal bucketing options for datetime type expressions. | (def datetime-bucket-options (mapv (fn [unit] (cond-> {:lib/type :option/temporal-bucketing :unit unit} (= unit :day) (assoc :default true))) datetime-bucket-units)) |
The temporal bucketing units for datetime type expressions. | (defn available-temporal-units [] datetime-bucket-units) |
(defmethod lib.metadata.calculation/display-name-method :option/temporal-bucketing [_query _stage-number {:keys [unit]} _style] (describe-temporal-unit unit)) | |
(defmethod lib.metadata.calculation/display-info-method :option/temporal-bucketing [query stage-number option] (merge {:display-name (lib.metadata.calculation/display-name query stage-number option) :short-name (u/qualified-name (raw-temporal-bucket option)) :is-temporal-extraction (let [bucket (raw-temporal-bucket option)] (and (contains? lib.schema.temporal-bucketing/datetime-extraction-units bucket) (not (contains? lib.schema.temporal-bucketing/datetime-truncation-units bucket))))} (select-keys option [:default :selected]))) | |
Implementation for [[available-temporal-buckets]]. Return a set of units from
| (defmulti available-temporal-buckets-method {:arglists '([query stage-number x])} (fn [_query _stage-number x] (lib.dispatch/dispatch-value x)) :hierarchy lib.hierarchy/hierarchy) |
(defmethod available-temporal-buckets-method :default [_query _stage-number _x] #{}) | |
(defn- mark-unit [options option-key unit] (cond->> options (some #(= (:unit %) unit) options) (mapv (fn [option] (cond-> option (contains? option option-key) (dissoc option option-key) (= (:unit option) unit) (assoc option-key true)))))) | |
Given the type of this column and nillable | (defn available-temporal-buckets-for-type [column-type default-unit selected-unit] (let [options (cond (isa? column-type :type/DateTime) datetime-bucket-options (isa? column-type :type/Date) date-bucket-options (isa? column-type :type/Time) time-bucket-options :else []) fallback-unit (if (isa? column-type :type/Time) :hour :month) default-unit (or default-unit fallback-unit)] (cond-> options (= :inherited default-unit) (->> (mapv #(dissoc % :default))) default-unit (mark-unit :default default-unit) selected-unit (mark-unit :selected selected-unit)))) |
(mu/defn available-temporal-buckets :- [:sequential [:ref ::lib.schema.temporal-bucketing/option]] "Get a set of available temporal bucketing units for `x`. Returns nil if no units are available." ([query x] (available-temporal-buckets query -1 x)) ([query :- ::lib.schema/query stage-number :- :int x] (available-temporal-buckets-method query stage-number x))) | |
(mu/defn describe-temporal-pair :- :string "Return a string describing the temporal pair. Used when comparing temporal values like `[:!= ... [:field {:temporal-unit :day-of-week} ...] \"2022-01-01\"]`" [temporal-column temporal-value :- [:or :int :string]] (u.time/format-unit temporal-value (:unit (temporal-bucket temporal-column)))) | |
Internal helper shared between a few implementations of [[with-temporal-bucket-method]]. Not intended to be called otherwise. | (defn add-temporal-bucket-to-ref [[tag options id-or-name] unit] ;; if `unit` is an extraction unit like `:month-of-year`, then the `:effective-type` of the ref changes to ;; `:type/Integer` (month of year returns an int). We need to record the ORIGINAL effective type somewhere in case ;; we need to refer back to it, e.g. to see what temporal buckets are available if we want to change the unit, or if ;; we want to remove it later. We will record this with the key `::original-effective-type`. Note that changing the ;; unit multiple times should keep the original first value of `::original-effective-type`. (if unit (let [original-temporal-unit ((some-fn :metabase.lib.field/original-temporal-unit :temporal-unit) options) extraction-unit? (contains? lib.schema.temporal-bucketing/datetime-extraction-units unit) original-effective-type ((some-fn :metabase.lib.field/original-effective-type :effective-type :base-type) options) new-effective-type (if extraction-unit? :type/Integer original-effective-type) options (-> options (assoc :temporal-unit unit :effective-type new-effective-type :metabase.lib.field/original-effective-type original-effective-type) (m/assoc-some :metabase.lib.field/original-temporal-unit original-temporal-unit))] [tag options id-or-name]) ;; `unit` is `nil`: remove the temporal bucket and remember it :metabase.lib.field/original-temporal-unit. (let [original-effective-type (:metabase.lib.field/original-effective-type options) original-temporal-unit ((some-fn :metabase.lib.field/original-temporal-unit :temporal-unit) options) options (cond-> (dissoc options :temporal-unit) original-effective-type (-> (assoc :effective-type original-effective-type) (dissoc :metabase.lib.field/original-effective-type)) original-temporal-unit (assoc :metabase.lib.field/original-temporal-unit original-temporal-unit))] [tag options id-or-name]))) |
(defn- ends-with-temporal-unit? [s temporal-unit] (str/ends-with? s (str ": " (describe-temporal-unit temporal-unit)))) | |
Append The | (defn ensure-ends-with-temporal-unit [s temporal-unit] (if (or (not (string? s)) ; ie. nil or something that definitely should not occur here (= :default temporal-unit) (ends-with-temporal-unit? s temporal-unit)) s (lib.util/format "%s: %s" s (describe-temporal-unit temporal-unit)))) |
Append temporal unit into This is expected to be called after | (defn ensure-temporal-unit-in-display-name [column-metadata] (if-some [temporal-unit (:unit column-metadata)] (update column-metadata :display_name ensure-ends-with-temporal-unit temporal-unit) column-metadata)) |