From 5dd1cca580c7c623561f51af047c1cbcd1a6480c Mon Sep 17 00:00:00 2001 From: Braden Shepherdson <braden@metabase.com> Date: Tue, 5 Nov 2024 09:20:03 -0500 Subject: [PATCH] [util] Add `u.time/local-date`, `local-time` and `local-date-time` (#49445) These are helpful for creating literal dates and times in a cross-platform way. Useful for generating queries, in particular for expression and filter values. --- src/metabase/util/time.cljc | 5 ++- src/metabase/util/time/impl.clj | 46 +++++++++++++++++++++ src/metabase/util/time/impl.cljs | 53 +++++++++++++++++++++++++ src/metabase/util/time/impl_common.cljc | 17 ++++++++ test/metabase/util/time_test.cljc | 39 ++++++++++++++++++ 5 files changed, 159 insertions(+), 1 deletion(-) diff --git a/src/metabase/util/time.cljc b/src/metabase/util/time.cljc index cc3b3858c3c..9cc46d7d269 100644 --- a/src/metabase/util/time.cljc +++ b/src/metabase/util/time.cljc @@ -30,7 +30,10 @@ unit-diff truncate add - format-for-base-type]) + format-for-base-type + local-date + local-date-time + local-time]) (defn- prep-options [options] (merge internal/default-options (u/normalize-map options))) diff --git a/src/metabase/util/time/impl.clj b/src/metabase/util/time/impl.clj index 50311ec5aa2..d04448a54b0 100644 --- a/src/metabase/util/time/impl.clj +++ b/src/metabase/util/time/impl.clj @@ -222,6 +222,52 @@ [value] (t/local-time value)) +;;; ----------------------------------------------- constructors ----------------------------------------------------- +(defn local-time + "Constructs a platform time value (eg. Moment, LocalTime) for the given hour and minute, plus optional seconds and + milliseconds. + + If called with no arguments, returns the current time." + ([] + (t/local-time)) + ([hours minutes] + (local-time hours minutes 0 0)) + ([hours minutes seconds] + (local-time hours minutes seconds 0)) + ([hours minutes seconds millis] + (t/local-time hours minutes seconds (* 1000000 millis)))) + +(defn local-date + "Constructs a platform date value (eg. Moment, LocalDate) for the given year, month and day. + + Day is 1-31. January = 1, or you can specify keywords like `:jan`, `:jun`. + + If called with no arguments, returns the current date." + ([] + (t/local-date)) + ([year month day] + (t/local-date year + (or (common/month-keywords month) month) + day))) + +(defn local-date-time + "Constructs a platform datetime (eg. Moment, LocalDateTime). + + Accepts either: + - no arguments (returns the current datetime) + - a local date and local time (see [[local-date]] and [[local-time]]); or + - year, month, day, hour, and minute, plus optional seconds and millis." + ([] + (t/local-date-time)) + ([a-date a-time] + (t/local-date-time a-date a-time)) + ([year month day hours minutes] + (local-date-time (local-date year month day) (local-time hours minutes))) + ([year month day hours minutes seconds] + (local-date-time (local-date year month day) (local-time hours minutes seconds))) + ([year month day hours minutes seconds millis] + (local-date-time (local-date year month day) (local-time hours minutes seconds millis)))) + ;;; ------------------------------------------------ arithmetic ------------------------------------------------------ (defn unit-diff diff --git a/src/metabase/util/time/impl.cljs b/src/metabase/util/time/impl.cljs index a17d33ac582..56cef3a7d4b 100644 --- a/src/metabase/util/time/impl.cljs +++ b/src/metabase/util/time/impl.cljs @@ -158,6 +158,59 @@ [value] (moment value parse-time-formats)) +;;; ----------------------------------------------- constructors ----------------------------------------------------- +(defn local-time + "Constructs a platform time value (eg. Moment, LocalTime) for the given hour and minute, plus optional seconds and + milliseconds. + + If called without arguments, returns the current time." + ([] + ;; Actually a full datetime, but Moment doesn't have freestanding time values. + (moment)) + ([hours minutes] + (moment #js {:hours hours, :minutes minutes})) + ([hours minutes seconds] + (moment #js {:hours hours, :minutes minutes, :seconds seconds})) + ([hours minutes seconds millis] + (moment #js {:hours hours, :minutes minutes, :seconds seconds, :milliseconds millis}))) + +(declare truncate) + +(defn local-date + "Constructs a platform date value (eg. Moment, LocalDate) for the given year, month and day. + + Day is 1-31. January = 1, or you can specify keywords like `:jan`, `:jun`." + ([] (truncate (moment) :day)) + ([year month day] + (moment #js {:year year + :day day + ;; Moment uses 0-based months, unlike Metabase. + :month (dec (or (common/month-keywords month) month))}))) + +(defn local-date-time + "Constructs a platform datetime (eg. Moment, LocalDateTime). + + Accepts either: + - no arguments (current datetime) + - a local date and local time (see [[local-date]] and [[local-time]]); or + - year, month, day, hour, and minute, plus optional seconds and millis." + ([] (moment)) + ([a-date a-time] + (when-not (and (valid? a-date) (valid? a-time)) + (throw (ex-info "Expected valid Moments for date and time" {:date a-date + :time a-time}))) + (let [^moment/Moment d (.clone a-date) + ^moment/Moment t a-time] + (doseq [unit ["hour" "minute" "second" "millisecond"]] + (.set d unit (.get t unit))) + d)) + ([year month day hours minutes] + (local-date-time (local-date year month day) (local-time hours minutes))) + ([year month day hours minutes seconds] + (local-date-time (local-date year month day) (local-time hours minutes seconds))) + ([year month day hours minutes seconds millis] + (local-date-time (local-date year month day) (local-time hours minutes seconds millis)))) + ;;; ------------------------------------------------ arithmetic ------------------------------------------------------ (declare unit-diff) diff --git a/src/metabase/util/time/impl_common.cljc b/src/metabase/util/time/impl_common.cljc index 34038996b3a..8d8a1b88327 100644 --- a/src/metabase/util/time/impl_common.cljc +++ b/src/metabase/util/time/impl_common.cljc @@ -111,3 +111,20 @@ [time-str] (or (second (re-matches (re-pattern (str "(.*?)" (optional offset-part) \$)) time-str)) time-str)) + +(def ^:const month-keywords + "Mapping of human-friendly keywords to literal month numbers. + + 1 = January." + {:jan 1 + :feb 2 + :mar 3 + :apr 4 + :may 5 + :jun 6 + :jul 7 + :aug 8 + :sep 9 + :oct 10 + :nov 11 + :dec 12}) diff --git a/test/metabase/util/time_test.cljc b/test/metabase/util/time_test.cljc index dd58ac53e35..8a72e15d48e 100644 --- a/test/metabase/util/time_test.cljc +++ b/test/metabase/util/time_test.cljc @@ -14,6 +14,21 @@ #?(:cljs (moment/utc time-str) :clj (t/offset-date-time time-str))) +(defn- from-local [time-str] + #?(:cljs (moment time-str) + :clj (t/local-date-time time-str))) + +(defn- from-local-date [time-str] + #?(:cljs (moment time-str) + :clj (t/local-date time-str))) + +(defn- from-local-time [time-str] + #?(:cljs (moment time-str + #js [(.. moment -HTML5_FMT -TIME_MS) + (.. moment -HTML5_FMT -TIME_SECONDS) + (.. moment -HTML5_FMT -TIME)]) + :clj (t/local-time time-str))) + (defn- same? "True if these two datetimes are equivalent. JVM objects are [[=]] but Moment.js values are not, so use the Moment.isSame method in CLJS." @@ -392,3 +407,27 @@ :second "00:00:02" :minute "00:02" :hour "02:00")) + +(deftest ^:parallel local-date-test + (are [exp-str act] (same? (from-local-date exp-str) act) + "2024-11-01" (shared.ut/local-date 2024 11 1) + "2000-02-29" (shared.ut/local-date 2000 2 29) + "2037-12-31" (shared.ut/local-date 2037 12 31))) + +(deftest ^:parallel local-time-test + (are [exp-str act] (same? (from-local-time exp-str) act) + "12:34:00" (shared.ut/local-time 12 34) + "12:34:56" (shared.ut/local-time 12 34 56) + "12:34:56.789" (shared.ut/local-time 12 34 56 789))) + +(deftest ^:parallel local-date-time-test + (let [nov4 (shared.ut/local-date 2024 11 4)] + (are [exp-str act] (same? (from-local exp-str) act) + ;; Local date + local time + "2024-11-04T12:34:00" (shared.ut/local-date-time nov4 (shared.ut/local-time 12 34)) + "2024-11-04T12:34:56" (shared.ut/local-date-time nov4 (shared.ut/local-time 12 34 56)) + "2024-11-04T12:34:56.789" (shared.ut/local-date-time nov4 (shared.ut/local-time 12 34 56 789)) + ;; Literal values + "2024-11-04T12:34:00" (shared.ut/local-date-time 2024 11 4 12 34) + "2024-11-04T12:34:56" (shared.ut/local-date-time 2024 11 4 12 34 56) + "2024-11-04T12:34:56.789" (shared.ut/local-date-time 2024 11 4 12 34 56 789)))) -- GitLab