| |
| ( 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 )
( 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 )
#_
( i18n/tru "{0} {1} ago" ( abs n ) ( str/lower-case ( describe-temporal-unit ( abs n ) unit ) ) )
:else
#_
( 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. :day or :day-of-year , to an MBQL clause or something that can be converted to
an MBQL clause. E.g. for a Field or Field metadata or :field clause, this might do something like this:
(temporal some-field :day)
=>
[:field 1 {:temporal-unit :day}]
Pass a nil unit to remove the temporal bucket.
| ( 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 x .
| ( 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
:metabase.lib.schema.temporal-bucketing/unit that are allowed to be used with x .
| ( 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 default-unit and selected-unit s, return the correct list of buckets.
| ( 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
( 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 ] )
( 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 temporal-unit into a string s if appropriate.
The :default temporal unit is not appended. If temporal-unit is already suffix of s , do not add it
for the second time. This function may be called multiple times during processing of a query stage.
| ( defn ensure-ends-with-temporal-unit
[ s temporal-unit ]
( if ( or ( not ( string? s ) )
( = :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 column-metadata 's :display_name when appropriate.
This is expected to be called after :unit is added into column metadata, ie. in terms of annotate middleware, after
the column metadata coming from a driver are merged with result of column-info .
| ( 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 ) )
|
|
| |