From fa10fa24ec9a9d1c78b0367eaf2b833731606c51 Mon Sep 17 00:00:00 2001 From: Ryan Senior <ryan@metabase.com> Date: Fri, 4 May 2018 10:00:09 -0500 Subject: [PATCH] Move date related functions from `util` to `util.date` --- .../metabase/sample_dataset/generate.clj | 13 +- src/metabase/api/dataset.clj | 3 +- src/metabase/cmd.clj | 5 +- src/metabase/db/migrations.clj | 3 +- src/metabase/driver.clj | 5 +- src/metabase/driver/bigquery.clj | 13 +- src/metabase/driver/crate/util.clj | 6 +- src/metabase/driver/druid/query_processor.clj | 7 +- .../driver/generic_sql/query_processor.clj | 6 +- .../driver/generic_sql/util/unprepare.clj | 6 +- .../googleanalytics/query_processor.clj | 15 +- src/metabase/driver/mongo/query_processor.clj | 17 +- src/metabase/driver/mysql.clj | 3 +- src/metabase/driver/presto.clj | 17 +- src/metabase/driver/sqlite.clj | 8 +- src/metabase/driver/vertica.clj | 3 +- src/metabase/email/messages.clj | 5 +- src/metabase/events/last_login.clj | 3 +- src/metabase/feature_extraction/async.clj | 5 +- src/metabase/middleware.clj | 5 +- src/metabase/models/activity.clj | 3 +- src/metabase/models/collection_revision.clj | 3 +- src/metabase/models/dependency.clj | 4 +- src/metabase/models/interface.clj | 5 +- src/metabase/models/permissions_revision.clj | 3 +- src/metabase/models/revision.clj | 3 +- src/metabase/models/session.clj | 3 +- src/metabase/models/user.clj | 3 +- src/metabase/models/view_log.clj | 3 +- src/metabase/pulse/render.clj | 9 +- src/metabase/query_processor.clj | 6 +- src/metabase/query_processor/interface.clj | 8 +- .../query_processor/middleware/cache.clj | 5 +- .../middleware/cache_backend/db.clj | 12 +- .../query_processor/middleware/expand.clj | 8 +- .../middleware/format_rows.clj | 10 +- .../middleware/parameters/sql.clj | 14 +- .../query_processor/middleware/resolve.clj | 7 +- src/metabase/sync/analyze.clj | 3 +- .../sync/analyze/fingerprint/datetime.clj | 3 +- src/metabase/sync/util.clj | 3 +- src/metabase/util.clj | 345 ----------------- src/metabase/util/date.clj | 355 ++++++++++++++++++ test/metabase/api/activity_test.clj | 9 +- test/metabase/api/card_test.clj | 11 +- test/metabase/driver/googleanalytics_test.clj | 21 +- test/metabase/driver/mysql_test.clj | 16 +- test/metabase/http_client.clj | 5 +- test/metabase/middleware_test.clj | 5 +- test/metabase/models/dependency_test.clj | 7 +- test/metabase/models/session_test.clj | 11 +- .../query_processor/expand_resolve_test.clj | 5 +- .../middleware/fetch_source_query_test.clj | 5 +- .../middleware/parameters/mbql_test.clj | 11 +- test/metabase/sync/analyze/classify_test.clj | 5 +- .../sync/analyze/fingerprint_test.clj | 3 +- test/metabase/sync/analyze_test.clj | 11 +- test/metabase/task/sync_databases_test.clj | 3 +- test/metabase/test/data/bigquery.clj | 3 +- test/metabase/test/data/generic_sql.clj | 8 +- test/metabase/test/data/sqlite.clj | 8 +- test/metabase/util/date_test.clj | 85 +++++ test/metabase/util_test.clj | 83 ---- 63 files changed, 680 insertions(+), 601 deletions(-) create mode 100644 src/metabase/util/date.clj create mode 100644 test/metabase/util/date_test.clj diff --git a/sample_dataset/metabase/sample_dataset/generate.clj b/sample_dataset/metabase/sample_dataset/generate.clj index ad8b6a82531..20da4b57e71 100644 --- a/sample_dataset/metabase/sample_dataset/generate.clj +++ b/sample_dataset/metabase/sample_dataset/generate.clj @@ -16,7 +16,8 @@ [jdistlib.core :as dist] [medley.core :as m] [metabase.db.spec :as dbspec] - [metabase.util :as u]) + [metabase.util :as u] + [metabase.util.date :as du]) (:import java.util.Date)) (def ^:private ^:const sample-dataset-filename @@ -67,7 +68,7 @@ {:name (format "%s %s" first last) :email (internet/free-email (format "%s.%s" first last)) :password (str (java.util.UUID/randomUUID)) - :birth_date (random-date-between (u/relative-date :year -60) (u/relative-date :year -18)) + :birth_date (random-date-between (du/relative-date :year -60) (du/relative-date :year -18)) :address (str (:house-number addr) " " (:street addr)) :city (:city addr) :zip (:zip addr) @@ -75,7 +76,7 @@ :latitude (:lat addr) :longitude (:lon addr) :source (rand-nth ["Google" "Twitter" "Facebook" "Organic" "Affiliate"]) - :created_at (random-date-between (u/relative-date :year -2) (u/relative-date :year 1))})) + :created_at (random-date-between (du/relative-date :year -2) (du/relative-date :year 1))})) ;;; ## PRODUCTS @@ -141,7 +142,7 @@ :category (rand-nth ["Widget" "Gizmo" "Gadget" "Doohickey"]) :vendor (random-company-name) :price (random-price 12 100) - :created_at (random-date-between (u/relative-date :year -2) (u/relative-date :year 1))}) + :created_at (random-date-between (du/relative-date :year -2) (du/relative-date :year 1))}) ;;; ## ORDERS @@ -240,7 +241,7 @@ (format "No tax rate found for state '%s'." state)) created-at (random-date-between (min-date (:created_at person) (:created_at product)) - (u/relative-date :year 2)) + (du/relative-date :year 2)) price (if (> (.getTime created-at) (.getTime (Date. 118 0 1))) (* 1.5 price) price) @@ -266,7 +267,7 @@ 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5]) :body (first (lorem/paragraphs)) - :created_at (random-date-between (:created_at product) (u/relative-date :year 2))}) + :created_at (random-date-between (:created_at product) (du/relative-date :year 2))}) (defn- add-ids [objs] (map-indexed diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj index 4ead2c8ee2b..6c3c1ca6a10 100644 --- a/src/metabase/api/dataset.clj +++ b/src/metabase/api/dataset.clj @@ -18,6 +18,7 @@ [metabase.query-processor.util :as qputil] [metabase.util :as util] [metabase.util + [date :as du] [export :as ex] [schema :as su]] [puppetlabs.i18n.core :refer [trs tru]] @@ -108,7 +109,7 @@ {:status 200 :body ((:export-fn export-conf) columns (maybe-modify-date-values cols rows)) :headers {"Content-Type" (str (:content-type export-conf) "; charset=utf-8") - "Content-Disposition" (str "attachment; filename=\"query_result_" (u/date->iso-8601) "." (:ext export-conf) "\"")}} + "Content-Disposition" (str "attachment; filename=\"query_result_" (du/date->iso-8601) "." (:ext export-conf) "\"")}} ;; failed query, send error message {:status 500 :body (:error response)}))) diff --git a/src/metabase/cmd.clj b/src/metabase/cmd.clj index 0efcf920bfa..4dd3dcaadbc 100644 --- a/src/metabase/cmd.clj +++ b/src/metabase/cmd.clj @@ -19,7 +19,8 @@ [metabase [config :as config] [db :as mdb] - [util :as u]])) + [util :as u]] + [metabase.util.date :as du])) (defn ^:command migrate "Run database migrations. Valid options for DIRECTION are `up`, `force`, `down-one`, `print`, or `release-locks`." @@ -41,7 +42,7 @@ ;; override env var that would normally make Jetty block forever (require 'environ.core) (intern 'environ.core 'env (assoc @(resolve 'environ.core/env) :mb-jetty-join "false")) - (u/profile "start-normally" ((resolve 'metabase.core/start-normally)))) + (du/profile "start-normally" ((resolve 'metabase.core/start-normally)))) (defn ^:command reset-password "Reset the password for a user with EMAIL-ADDRESS." diff --git a/src/metabase/db/migrations.clj b/src/metabase/db/migrations.clj index 69d3813bf16..afe2b56b9d2 100644 --- a/src/metabase/db/migrations.clj +++ b/src/metabase/db/migrations.clj @@ -31,6 +31,7 @@ [table :as table :refer [Table]] [user :refer [User]]] [metabase.query-processor.util :as qputil] + [metabase.util.date :as du] [toucan [db :as db] [models :as models]])) @@ -51,7 +52,7 @@ (@migration-var) (db/insert! DataMigrations :id migration-name - :timestamp (u/new-sql-timestamp))))) + :timestamp (du/new-sql-timestamp))))) (def ^:private data-migrations (atom [])) diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj index 36dcbe3e927..7cc70b1f659 100644 --- a/src/metabase/driver.clj +++ b/src/metabase/driver.clj @@ -24,6 +24,7 @@ table] [metabase.sync.interface :as si] [metabase.util :as u] + [metabase.util.date :as du] [puppetlabs.i18n.core :refer [tru]] [schema.core :as s] [puppetlabs.i18n.core :refer [trs tru]] @@ -73,7 +74,7 @@ (date-interval [this, ^Keyword unit, ^Number amount] "*OPTIONAL* Return an driver-appropriate representation of a moment relative to the current moment in time. By - default, this returns an `Timestamp` by calling `metabase.util/relative-date`; but when possible drivers should + default, this returns an `Timestamp` by calling `metabase.util.date/relative-date`; but when possible drivers should return a native form so we can be sure the correct timezone is applied. For example, SQL drivers should return a HoneySQL form to call the appropriate SQL fns: @@ -235,7 +236,7 @@ (def IDriverDefaultsMixin "Default implementations of `IDriver` methods marked *OPTIONAL*." - {:date-interval (u/drop-first-arg u/relative-date) + {:date-interval (u/drop-first-arg du/relative-date) :describe-table-fks (constantly nil) :features (constantly nil) :format-custom-field-name (u/drop-first-arg identity) diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj index 3a1d012cac1..634a907af09 100644 --- a/src/metabase/driver/bigquery.clj +++ b/src/metabase/driver/bigquery.clj @@ -15,6 +15,7 @@ [config :as config] [driver :as driver] [util :as u]] + [metabase.util.date :as du] [metabase.driver [generic-sql :as sql] [google :as google]] @@ -153,15 +154,15 @@ (defn- parse-timestamp-str [s] ;; Timestamp strings either come back as ISO-8601 strings or Unix timestamps in µs, e.g. "1.3963104E9" (or - (u/->Timestamp s) + (du/->Timestamp s) ;; If parsing as ISO-8601 fails parse as a double then convert to ms. Add the appropriate number of milliseconds to ;; the number to convert it to the local timezone. We do this because the dates come back in UTC but we want the ;; grouping to match the local time (HUH?) This gives us the same results as the other ;; `has-questionable-timezone-support?` drivers. Not sure if this is actually desirable, but if it's not, it ;; probably means all of those other drivers are doing it wrong - (u/->Timestamp (- (* (Double/parseDouble s) 1000) - (.getDSTSavings default-timezone) - (.getRawOffset default-timezone))))) + (du/->Timestamp (- (* (Double/parseDouble s) 1000) + (.getDSTSavings default-timezone) + (.getRawOffset default-timezone))))) (def ^:private bigquery-time-format (tformat/formatter "HH:mm:SS" time/utc)) @@ -377,7 +378,7 @@ (defmethod sqlqp/->honeysql [BigQueryDriver Date] [_ date] - (hsql/call :timestamp (hx/literal (u/date->iso-8601 date)))) + (hsql/call :timestamp (hx/literal (du/date->iso-8601 date)))) (defmethod sqlqp/->honeysql [BigQueryDriver TimeValue] [driver {:keys [value]}] @@ -452,7 +453,7 @@ (hsql/call :length field-key)) (defn- date-interval [driver unit amount] - (sqlqp/->honeysql driver (u/relative-date unit amount))) + (sqlqp/->honeysql driver (du/relative-date unit amount))) ;; BigQuery doesn't return a timezone with it's time strings as it's always UTC, JodaTime parsing also defaults to UTC diff --git a/src/metabase/driver/crate/util.clj b/src/metabase/driver/crate/util.clj index 8d9c014cfaa..ebcd933ec88 100644 --- a/src/metabase/driver/crate/util.clj +++ b/src/metabase/driver/crate/util.clj @@ -4,7 +4,9 @@ [format :as hformat]) [metabase.driver.generic-sql.query-processor :as qp] [metabase.util :as u] - [metabase.util.honeysql-extensions :as hx]) + [metabase.util + [date :as du] + [honeysql-extensions :as hx]]) (:import java.sql.Timestamp)) ;; register the try_cast function with HoneySQL @@ -59,7 +61,7 @@ "ISQLDriver `date` implementation" [_ unit expr] (let [v (if (instance? Timestamp expr) - (hx/literal (u/date->iso-8601 expr)) + (hx/literal (du/date->iso-8601 expr)) expr)] (case unit :default (date-format (str "%Y-%m-%d %H:%i:%s") v) diff --git a/src/metabase/driver/druid/query_processor.clj b/src/metabase/driver/druid/query_processor.clj index 3a4de54aea4..d46d0c40698 100644 --- a/src/metabase/driver/druid/query_processor.clj +++ b/src/metabase/driver/druid/query_processor.clj @@ -12,7 +12,8 @@ [metabase.query-processor [annotate :as annotate] [interface :as i]] - [metabase.util :as u]) + [metabase.util :as u] + [metabase.util.date :as du]) (:import java.util.TimeZone [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Expression Field RelativeDateTimeValue Value] @@ -64,8 +65,8 @@ Field (->rvalue [this] (:field-name this)) DateTimeField (->rvalue [this] (->rvalue (:field this))) Value (->rvalue [this] (:value this)) - DateTimeValue (->rvalue [{{unit :unit} :field, value :value}] (u/date->iso-8601 (u/date-trunc unit value (get-timezone-id)))) - RelativeDateTimeValue (->rvalue [{:keys [unit amount]}] (u/date->iso-8601 (u/date-trunc unit (u/relative-date unit amount) (get-timezone-id))))) + DateTimeValue (->rvalue [{{unit :unit} :field, value :value}] (du/date->iso-8601 (du/date-trunc unit value (get-timezone-id)))) + RelativeDateTimeValue (->rvalue [{:keys [unit amount]}] (du/date->iso-8601 (du/date-trunc unit (du/relative-date unit amount) (get-timezone-id))))) (defprotocol ^:private IDimensionOrMetric (^:private dimension-or-metric? [this] diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj index d4d823418ce..409bfaa0c00 100644 --- a/src/metabase/driver/generic_sql/query_processor.clj +++ b/src/metabase/driver/generic_sql/query_processor.clj @@ -14,7 +14,9 @@ [annotate :as annotate] [interface :as i] [util :as qputil]] - [metabase.util.honeysql-extensions :as hx]) + [metabase.util + [date :as du] + [honeysql-extensions :as hx]]) (:import clojure.lang.Keyword [java.sql PreparedStatement ResultSet ResultSetMetaData SQLException] [java.util Calendar Date TimeZone] @@ -383,7 +385,7 @@ land" [^TimeZone tz ^ResultSet rs ^Integer i] (let [date-string (.getString rs i)] - (if-let [parsed-date (u/str->date-time tz date-string)] + (if-let [parsed-date (du/str->date-time date-string tz)] parsed-date (throw (Exception. (format "Unable to parse date '%s'" date-string)))))) diff --git a/src/metabase/driver/generic_sql/util/unprepare.clj b/src/metabase/driver/generic_sql/util/unprepare.clj index 55b028dca91..1a3be243d6d 100644 --- a/src/metabase/driver/generic_sql/util/unprepare.clj +++ b/src/metabase/driver/generic_sql/util/unprepare.clj @@ -3,7 +3,9 @@ (:require [clojure.string :as str] [honeysql.core :as hsql] [metabase.util :as u] - [metabase.util.honeysql-extensions :as hx]) + [metabase.util + [date :as du] + [honeysql-extensions :as hx]]) (:import java.sql.Time java.util.Date)) @@ -11,7 +13,7 @@ (^:private unprepare-arg ^String [this settings])) (defn- unprepare-date [date-or-time iso-8601-fn] - (hsql/call iso-8601-fn (hx/literal (u/date->iso-8601 date-or-time)))) + (hsql/call iso-8601-fn (hx/literal (du/date->iso-8601 date-or-time)))) (extend-protocol IUnprepare nil (unprepare-arg [this _] "NULL") diff --git a/src/metabase/driver/googleanalytics/query_processor.clj b/src/metabase/driver/googleanalytics/query_processor.clj index 41e95cf52b8..07c6aaca94f 100644 --- a/src/metabase/driver/googleanalytics/query_processor.clj +++ b/src/metabase/driver/googleanalytics/query_processor.clj @@ -5,7 +5,8 @@ [clojure.tools.reader.edn :as edn] [medley.core :as m] [metabase.query-processor.util :as qputil] - [metabase.util :as u]) + [metabase.util :as u] + [metabase.util.date :as du]) (:import [com.google.api.services.analytics.model GaData GaData$ColumnHeaders] [metabase.query_processor.interface AgFieldRef DateTimeField DateTimeValue Field RelativeDateTimeValue Value])) @@ -33,13 +34,13 @@ Field (->rvalue [this] (:field-name this)) DateTimeField (->rvalue [this] (->rvalue (:field this))) Value (->rvalue [this] (:value this)) - DateTimeValue (->rvalue [{{unit :unit} :field, value :value}] (u/format-date "yyyy-MM-dd" (u/date-trunc unit value))) + DateTimeValue (->rvalue [{{unit :unit} :field, value :value}] (du/format-date "yyyy-MM-dd" (du/date-trunc unit value))) RelativeDateTimeValue (->rvalue [{:keys [unit amount]}] (cond (and (= unit :day) (= amount 0)) "today" (and (= unit :day) (= amount -1)) "yesterday" (and (= unit :day) (< amount -1)) (str (- amount) "daysAgo") - :else (u/format-date "yyyy-MM-dd" (u/date-trunc unit (u/relative-date unit amount)))))) + :else (du/format-date "yyyy-MM-dd" (du/date-trunc unit (du/relative-date unit amount)))))) (defn- char-escape-map @@ -206,14 +207,14 @@ (def ^:private ga-dimension->date-format-fn {"ga:minute" parse-number - "ga:dateHour" (partial u/parse-date "yyyyMMddHH") + "ga:dateHour" (partial du/parse-date "yyyyMMddHH") "ga:hour" parse-number - "ga:date" (partial u/parse-date "yyyyMMdd") + "ga:date" (partial du/parse-date "yyyyMMdd") "ga:dayOfWeek" (comp inc parse-number) "ga:day" parse-number - "ga:isoYearIsoWeek" (partial u/parse-date "YYYYww") + "ga:isoYearIsoWeek" (partial du/parse-date "YYYYww") "ga:week" parse-number - "ga:yearMonth" (partial u/parse-date "yyyyMM") + "ga:yearMonth" (partial du/parse-date "yyyyMM") "ga:month" parse-number "ga:year" parse-number}) diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj index 7ec04960d42..dbf43bac3c1 100644 --- a/src/metabase/driver/mongo/query_processor.clj +++ b/src/metabase/driver/mongo/query_processor.clj @@ -13,6 +13,7 @@ [annotate :as annotate] [interface :as i]] [metabase.util :as u] + [metabase.util.date :as du] [monger joda-time [collection :as mc] [operators :refer :all]]) @@ -181,10 +182,10 @@ ([format-string] (stringify format-string value)) ([format-string v] - {:___date (u/format-date format-string v)})) - extract (u/rpartial u/date-extract value)] + {:___date (du/format-date format-string v)})) + extract (u/rpartial du/date-extract value)] (case (or unit :default) - :default (some-> value u/->Date) + :default (some-> value du/->Date) :minute (stringify "yyyy-MM-dd'T'HH:mm:00") :minute-of-hour (extract :minute) :hour (stringify "yyyy-MM-dd'T'HH:00:00") @@ -193,17 +194,17 @@ :day-of-week (extract :day-of-week) :day-of-month (extract :day-of-month) :day-of-year (extract :day-of-year) - :week (stringify "yyyy-MM-dd" (u/date-trunc :week value)) + :week (stringify "yyyy-MM-dd" (du/date-trunc :week value)) :week-of-year (extract :week-of-year) :month (stringify "yyyy-MM") :month-of-year (extract :month) - :quarter (stringify "yyyy-MM" (u/date-trunc :quarter value)) + :quarter (stringify "yyyy-MM" (du/date-trunc :quarter value)) :quarter-of-year (extract :quarter-of-year) :year (extract :year)))) RelativeDateTimeValue (->rvalue [{:keys [amount unit field]}] - (->rvalue (i/map->DateTimeValue {:value (u/relative-date (or unit :day) amount) + (->rvalue (i/map->DateTimeValue {:value (du/relative-date (or unit :day) amount) :field field})))) @@ -416,7 +417,7 @@ (into {} (for [[k v] row] {k (if (and (map? v) (:___date v)) - (u/->Timestamp (:___date v)) + (du/->Timestamp (:___date v)) v)})))) @@ -439,7 +440,7 @@ ;; it looks like Date() just ignores any arguments return a date string formatted the same way the Mongo console ;; does :Date (fn [& _] - (u/format-date "EEE MMM dd yyyy HH:mm:ss z")) + (du/format-date "EEE MMM dd yyyy HH:mm:ss z")) :NumberLong (fn [^String s] (Long/parseLong s)) :NumberInt (fn [^String s] diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj index fcc46e09633..32df4c94906 100644 --- a/src/metabase/driver/mysql.clj +++ b/src/metabase/driver/mysql.clj @@ -15,6 +15,7 @@ [metabase.driver.generic-sql :as sql] [metabase.driver.generic-sql.query-processor :as sqlqp] [metabase.util + [date :as du] [honeysql-extensions :as hx] [ssh :as ssh]]) (:import java.sql.Time @@ -149,7 +150,7 @@ ;; preferable to have timezones slightly wrong in these rare theoretical situations, instead of all the time, as ;; was the previous behavior. (hsql/call :convert_tz - (hx/literal (u/format-date :date-hour-minute-second-ms date)) + (hx/literal (du/format-date :date-hour-minute-second-ms date)) (hx/literal system-timezone-offset-str) (hx/literal report-timezone-offset-str)) ;; otherwise if we don't have a report timezone we can continue to pass the object as-is, e.g. as a prepared diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj index eef652b53c9..7482002e05e 100644 --- a/src/metabase/driver/presto.clj +++ b/src/metabase/driver/presto.clj @@ -20,6 +20,7 @@ [metabase.driver.generic-sql.util.unprepare :as unprepare] [metabase.query-processor.util :as qputil] [metabase.util + [date :as du] [honeysql-extensions :as hx] [ssh :as ssh]]) (:import java.sql.Time @@ -48,16 +49,16 @@ (defn- parse-time-with-tz [s] ;; Try parsing with offset first then with full ZoneId - (or (u/ignore-exceptions (u/parse-date "HH:mm:ss.SSS ZZ" s)) - (u/parse-date "HH:mm:ss.SSS ZZZ" s))) + (or (u/ignore-exceptions (du/parse-date "HH:mm:ss.SSS ZZ" s)) + (du/parse-date "HH:mm:ss.SSS ZZZ" s))) (defn- parse-timestamp-with-tz [s] ;; Try parsing with offset first then with full ZoneId - (or (u/ignore-exceptions (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZ" s)) - (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZZ" s))) + (or (u/ignore-exceptions (du/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZ" s)) + (du/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZZ" s))) (def ^:private presto-date-time-formatter - (u/->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS")) + (du/->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS")) (defn- parse-presto-time "Parsing time from presto using a specific formatter rather than the @@ -65,7 +66,7 @@ performance is important" [time-str] (->> time-str - (u/parse-date :hour-minute-second-ms) + (du/parse-date :hour-minute-second-ms) tcoerce/to-long Time.)) @@ -74,7 +75,7 @@ #"decimal.*" bigdec #"time" parse-presto-time #"time with time zone" parse-time-with-tz - #"timestamp" (partial u/parse-date + #"timestamp" (partial du/parse-date (if-let [report-tz (and report-timezone (time/time-zone-for-id report-timezone))] (tformat/with-zone presto-date-time-formatter report-tz) @@ -215,7 +216,7 @@ (defmethod sqlqp/->honeysql [PrestoDriver Date] [_ date] - (hsql/call :from_iso8601_timestamp (hx/literal (u/date->iso-8601 date)))) + (hsql/call :from_iso8601_timestamp (hx/literal (du/date->iso-8601 date)))) (def ^:private time-format (tformat/formatter "HH:mm:SS.SSS")) diff --git a/src/metabase/driver/sqlite.clj b/src/metabase/driver/sqlite.clj index 557d6adc5fd..5d830f2e549 100644 --- a/src/metabase/driver/sqlite.clj +++ b/src/metabase/driver/sqlite.clj @@ -12,7 +12,9 @@ [util :as u]] [metabase.driver.generic-sql :as sql] [metabase.driver.generic-sql.query-processor :as sqlqp] - [metabase.util.honeysql-extensions :as hx]) + [metabase.util + [date :as du] + [honeysql-extensions :as hx]]) (:import [java.sql Time Timestamp])) (defrecord SQLiteDriver [] @@ -67,7 +69,7 @@ [unit expr] ;; Convert Timestamps to ISO 8601 strings before passing to SQLite, otherwise they don't seem to work correctly (let [v (if (instance? Timestamp expr) - (hx/literal (u/date->iso-8601 expr)) + (hx/literal (du/date->iso-8601 expr)) expr)] (case unit :default v @@ -147,7 +149,7 @@ ;; for anything that's a Date (usually a java.sql.Timestamp) convert it to a yyyy-MM-dd formatted date literal ;; string For whatever reason the SQL generated from parameters ends up looking like `WHERE date(some_field) = ?` ;; sometimes so we need to use just the date rather than a full ISO-8601 string - (u/format-date "yyyy-MM-dd" obj) + (du/format-date "yyyy-MM-dd" obj) ;; every other prepared statement arg can be returned as-is obj)) diff --git a/src/metabase/driver/vertica.clj b/src/metabase/driver/vertica.clj index 4e1d34addd9..f64c0dab0f0 100644 --- a/src/metabase/driver/vertica.clj +++ b/src/metabase/driver/vertica.clj @@ -8,6 +8,7 @@ [util :as u]] [metabase.driver.generic-sql :as sql] [metabase.util + [date :as du] [honeysql-extensions :as hx] [ssh :as ssh]])) @@ -52,7 +53,7 @@ before date operations can be performed. This function will add that cast if it is a timestamp, otherwise this is a noop." [expr] - (if (u/is-temporal? expr) + (if (du/is-temporal? expr) (hx/cast :timestamp expr) expr)) diff --git a/src/metabase/email/messages.clj b/src/metabase/email/messages.clj index 4639f218e56..5d7f1c58e34 100644 --- a/src/metabase/email/messages.clj +++ b/src/metabase/email/messages.clj @@ -12,6 +12,7 @@ [util :as u]] [metabase.pulse.render :as render] [metabase.util + [date :as du] [export :as export] [quotation :as quotation] [urls :as url]] @@ -62,7 +63,7 @@ :invitorEmail (:email invitor) :company company :joinUrl join-url - :today (u/format-date "MMM' 'dd,' 'yyyy") + :today (du/format-date "MMM' 'dd,' 'yyyy") :logoHeader true} (random-quote-context)))] (email/send-message! @@ -97,7 +98,7 @@ :joinedUserName (:first_name new-user) :joinedViaSSO google-auth? :joinedUserEmail (:email new-user) - :joinedDate (u/format-date "EEEE, MMMM d") ; e.g. "Wednesday, July 13". TODO - is this what we want? + :joinedDate (du/format-date "EEEE, MMMM d") ; e.g. "Wednesday, July 13". TODO - is this what we want? :adminEmail (first recipients) :joinedUserEditUrl (str (public-settings/site-url) "/admin/people")} (random-quote-context)))))) diff --git a/src/metabase/events/last_login.clj b/src/metabase/events/last_login.clj index aef66c07f42..c5852694352 100644 --- a/src/metabase/events/last_login.clj +++ b/src/metabase/events/last_login.clj @@ -5,6 +5,7 @@ [events :as events] [util :as u]] [metabase.models.user :refer [User]] + [metabase.util.date :as du] [toucan.db :as db])) (def ^:const last-login-topics @@ -27,7 +28,7 @@ (when-let [{object :item} last-login-event] ;; just make a simple attempt to set the `:last_login` for the given user to now (when-let [user-id (:user_id object)] - (db/update! User user-id, :last_login (u/new-sql-timestamp)))) + (db/update! User user-id, :last_login (du/new-sql-timestamp)))) (catch Throwable e (log/warn (format "Failed to process sync-database event. %s" (:topic last-login-event)) e)))) diff --git a/src/metabase/feature_extraction/async.clj b/src/metabase/feature_extraction/async.clj index 8c82265b268..0588872725c 100644 --- a/src/metabase/feature_extraction/async.clj +++ b/src/metabase/feature_extraction/async.clj @@ -7,6 +7,7 @@ [computation-job :refer [ComputationJob]] [computation-job-result :refer [ComputationJobResult]]] [metabase.util :as u] + [metabase.util.date :as du] [toucan.db :as db])) (defonce ^:private running-jobs (atom {})) @@ -44,7 +45,7 @@ :payload payload) (db/update! ComputationJob id :status :done - :ended_at (u/new-sql-timestamp))) + :ended_at (du/new-sql-timestamp))) (when callback (callback job payload))) (swap! running-jobs dissoc id) @@ -63,7 +64,7 @@ :payload error) (db/update! ComputationJob id :status :error - :ended_at (u/new-sql-timestamp))) + :ended_at (du/new-sql-timestamp))) (when callback (callback job error))) (swap! running-jobs dissoc id) diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj index 5376e25d3f3..fc3304cfc86 100644 --- a/src/metabase/middleware.clj +++ b/src/metabase/middleware.clj @@ -15,6 +15,7 @@ [session :refer [Session]] [setting :refer [defsetting]] [user :as user :refer [User]]] + [metabase.util.date :as du] monger.json [puppetlabs.i18n.core :refer [tru]] [toucan @@ -173,7 +174,7 @@ [] {"Cache-Control" "max-age=0, no-cache, must-revalidate, proxy-revalidate" "Expires" "Tue, 03 Jul 2001 06:00:00 GMT" - "Last-Modified" (u/format-date :rfc822)}) + "Last-Modified" (du/format-date :rfc822)}) (def ^:private ^:const strict-transport-security-header "Tell browsers to only access this resource over HTTPS for the next year (prevent MTM attacks). (This only applies if @@ -347,7 +348,7 @@ (let [start-time (System/nanoTime)] (db/with-call-counting [call-count] (u/prog1 (handler request) - (log-response jetty-stats-fn request <> (u/format-nanoseconds (- (System/nanoTime) start-time)) (call-count)))))))) + (log-response jetty-stats-fn request <> (du/format-nanoseconds (- (System/nanoTime) start-time)) (call-count)))))))) ;;; ----------------------------------------------- EXCEPTION HANDLING ----------------------------------------------- diff --git a/src/metabase/models/activity.clj b/src/metabase/models/activity.clj index 9d62f80f3bb..a1247eee02c 100644 --- a/src/metabase/models/activity.clj +++ b/src/metabase/models/activity.clj @@ -9,6 +9,7 @@ [metric :refer [Metric]] [pulse :refer [Pulse]] [segment :refer [Segment]]] + [metabase.util.date :as du] [toucan [db :as db] [models :as models]])) @@ -34,7 +35,7 @@ (models/defmodel Activity :activity) (defn- pre-insert [activity] - (let [defaults {:timestamp (u/new-sql-timestamp) + (let [defaults {:timestamp (du/new-sql-timestamp) :details {}}] (merge defaults activity))) diff --git a/src/metabase/models/collection_revision.clj b/src/metabase/models/collection_revision.clj index c13317bad50..c367c5b14b1 100644 --- a/src/metabase/models/collection_revision.clj +++ b/src/metabase/models/collection_revision.clj @@ -1,5 +1,6 @@ (ns metabase.models.collection-revision (:require [metabase.util :as u] + [metabase.util.date :as du] [puppetlabs.i18n.core :refer [tru]] [toucan [db :as db] @@ -8,7 +9,7 @@ (models/defmodel CollectionRevision :collection_revision) (defn- pre-insert [revision] - (assoc revision :created_at (u/new-sql-timestamp))) + (assoc revision :created_at (du/new-sql-timestamp))) (u/strict-extend (class CollectionRevision) models/IModel diff --git a/src/metabase/models/dependency.clj b/src/metabase/models/dependency.clj index 529ca83e26e..ee140d38b1c 100644 --- a/src/metabase/models/dependency.clj +++ b/src/metabase/models/dependency.clj @@ -1,6 +1,6 @@ (ns metabase.models.dependency (:require [clojure.set :as set] - [metabase.util :as u] + [metabase.util.date :as du] [toucan [db :as db] [models :as models]])) @@ -52,7 +52,7 @@ dependencies+ (set/difference dependencies-new dependencies-old) dependencies- (set/difference dependencies-old dependencies-new)] (when (seq dependencies+) - (let [vs (map #(merge % {:model entity-name, :model_id id, :created_at (u/new-sql-timestamp)}) dependencies+)] + (let [vs (map #(merge % {:model entity-name, :model_id id, :created_at (du/new-sql-timestamp)}) dependencies+)] (db/insert-many! Dependency vs))) (when (seq dependencies-) (doseq [{:keys [dependent_on_model dependent_on_id]} dependencies-] diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj index 3b3dc7eb8ae..17e47a64b0b 100644 --- a/src/metabase/models/interface.clj +++ b/src/metabase/models/interface.clj @@ -4,6 +4,7 @@ [metabase.util :as u] [metabase.util [cron :as cron-util] + [date :as du] [encryption :as encryption]] [schema.core :as s] [taoensso.nippy :as nippy] @@ -82,10 +83,10 @@ ;;; properties (defn- add-created-at-timestamp [obj & _] - (assoc obj :created_at (u/new-sql-timestamp))) + (assoc obj :created_at (du/new-sql-timestamp))) (defn- add-updated-at-timestamp [obj & _] - (assoc obj :updated_at (u/new-sql-timestamp))) + (assoc obj :updated_at (du/new-sql-timestamp))) (models/add-property! :timestamped? :insert (comp add-created-at-timestamp add-updated-at-timestamp) diff --git a/src/metabase/models/permissions_revision.clj b/src/metabase/models/permissions_revision.clj index 10892a6f3c5..07ea81ff83b 100644 --- a/src/metabase/models/permissions_revision.clj +++ b/src/metabase/models/permissions_revision.clj @@ -1,5 +1,6 @@ (ns metabase.models.permissions-revision (:require [metabase.util :as u] + [metabase.util.date :as du] [puppetlabs.i18n.core :refer [tru]] [toucan [db :as db] @@ -8,7 +9,7 @@ (models/defmodel PermissionsRevision :permissions_revision) (defn- pre-insert [revision] - (assoc revision :created_at (u/new-sql-timestamp))) + (assoc revision :created_at (du/new-sql-timestamp))) (u/strict-extend (class PermissionsRevision) models/IModel diff --git a/src/metabase/models/revision.clj b/src/metabase/models/revision.clj index c96cfee15c6..c79fd82e03c 100644 --- a/src/metabase/models/revision.clj +++ b/src/metabase/models/revision.clj @@ -3,6 +3,7 @@ [metabase.models.revision.diff :refer [diff-string]] [metabase.models.user :refer [User]] [metabase.util :as u] + [metabase.util.date :as du] [puppetlabs.i18n.core :refer [tru]] [toucan [db :as db] @@ -63,7 +64,7 @@ (models/defmodel Revision :revision) (defn- pre-insert [revision] - (assoc revision :timestamp (u/new-sql-timestamp))) + (assoc revision :timestamp (du/new-sql-timestamp))) (u/strict-extend (class Revision) models/IModel diff --git a/src/metabase/models/session.clj b/src/metabase/models/session.clj index 7156847ce94..285218a277d 100644 --- a/src/metabase/models/session.clj +++ b/src/metabase/models/session.clj @@ -1,5 +1,6 @@ (ns metabase.models.session (:require [metabase.util :as u] + [metabase.util.date :as du] [toucan [db :as db] [models :as models]])) @@ -7,7 +8,7 @@ (models/defmodel Session :core_session) (defn- pre-insert [session] - (assoc session :created_at (u/new-sql-timestamp))) + (assoc session :created_at (du/new-sql-timestamp))) (u/strict-extend (class Session) models/IModel diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj index ed3a769376c..2827fc8b163 100644 --- a/src/metabase/models/user.clj +++ b/src/metabase/models/user.clj @@ -9,6 +9,7 @@ [metabase.models [permissions-group :as group] [permissions-group-membership :as perm-membership :refer [PermissionsGroupMembership]]] + [metabase.util.date :as du] [toucan [db :as db] [models :as models]]) @@ -26,7 +27,7 @@ (assert (not (:password_salt user)) "Don't try to pass an encrypted password to (insert! User). Password encryption is handled by pre-insert.") (let [salt (str (UUID/randomUUID)) - defaults {:date_joined (u/new-sql-timestamp) + defaults {:date_joined (du/new-sql-timestamp) :last_login nil :is_active true :is_superuser false}] diff --git a/src/metabase/models/view_log.clj b/src/metabase/models/view_log.clj index 8430b56d4b4..ec7a07eed8a 100644 --- a/src/metabase/models/view_log.clj +++ b/src/metabase/models/view_log.clj @@ -2,12 +2,13 @@ "The ViewLog is used to log an event where a given User views a given object such as a Table or Card (Question)." (:require [metabase.models.interface :as i] [metabase.util :as u] + [metabase.util.date :as du] [toucan.models :as models])) (models/defmodel ViewLog :view_log) (defn- pre-insert [log-entry] - (let [defaults {:timestamp (u/new-sql-timestamp)}] + (let [defaults {:timestamp (du/new-sql-timestamp)}] (merge defaults log-entry))) (u/strict-extend (class ViewLog) diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj index 5c38a745653..7bcabaa4ee6 100644 --- a/src/metabase/pulse/render.clj +++ b/src/metabase/pulse/render.clj @@ -13,6 +13,7 @@ [util :as hutil]] [metabase.util :as u] [metabase.util + [date :as du] [ui-logic :as ui-logic] [urls :as urls]] [puppetlabs.i18n.core :refer [tru trs]] @@ -212,7 +213,7 @@ (defn- reformat-timestamp [timezone old-format-timestamp new-format-string] (f/unparse (f/with-zone (f/formatter new-format-string) (DateTimeZone/forTimeZone timezone)) - (u/str->date-time old-format-timestamp timezone))) + (du/str->date-time old-format-timestamp timezone))) (defn- format-timestamp "Formats timestamps with human friendly absolute dates based on the column :unit" @@ -221,7 +222,7 @@ :hour (reformat-timestamp timezone timestamp "h a - MMM YYYY") :week (str "Week " (reformat-timestamp timezone timestamp "w - YYYY")) :month (reformat-timestamp timezone timestamp "MMMM YYYY") - :quarter (let [timestamp-obj (u/str->date-time timestamp timezone)] + :quarter (let [timestamp-obj (du/str->date-time timestamp timezone)] (str "Q" (inc (int (/ (t/month timestamp-obj) 3))) @@ -249,7 +250,7 @@ (defn- format-timestamp-relative "Formats timestamps with relative names (today, yesterday, this *, last *) based on column :unit, if possible, otherwie returns nil" [timezone timestamp, {:keys [unit]}] - (let [parsed-timestamp (u/str->date-time timestamp timezone)] + (let [parsed-timestamp (du/str->date-time timestamp timezone)] (case unit :day (date->interval-name parsed-timestamp (t/date-midnight (year) (month) (day)) @@ -649,7 +650,7 @@ [render-type timezone card {:keys [rows cols] :as data}] (let [[x-axis-rowfn y-axis-rowfn] (graphing-columns card data) ft-row (if (datetime-field? (x-axis-rowfn cols)) - #(.getTime ^Date (u/->Timestamp %)) + #(.getTime ^Date (du/->Timestamp %)) identity) rows (if (> (ft-row (x-axis-rowfn (first rows))) (ft-row (x-axis-rowfn (last rows)))) diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj index fe8b7012c50..6cd7a9150fd 100644 --- a/src/metabase/query_processor.clj +++ b/src/metabase/query_processor.clj @@ -34,7 +34,9 @@ [resolve :as resolve] [source-table :as source-table]] [metabase.query-processor.util :as qputil] - [metabase.util.schema :as su] + [metabase.util + [date :as du] + [schema :as su]] [schema.core :as s] [toucan.db :as db])) @@ -229,7 +231,7 @@ :hash (or query-hash (throw (Exception. "Missing query hash!"))) :native (= query-type "native") :json_query (dissoc query :info) - :started_at (u/new-sql-timestamp) + :started_at (du/new-sql-timestamp) :running_time 0 :result_rows 0 :start_time_millis (System/currentTimeMillis)}) diff --git a/src/metabase/query_processor/interface.clj b/src/metabase/query_processor/interface.clj index 6fd4af9cd16..db2b7a8b0fe 100644 --- a/src/metabase/query_processor/interface.clj +++ b/src/metabase/query_processor/interface.clj @@ -7,7 +7,9 @@ [field :as field]] [metabase.sync.interface :as i] [metabase.util :as u] - [metabase.util.schema :as su] + [metabase.util + [date :as du] + [schema :as su]] [schema.core :as s]) (:import clojure.lang.Keyword [java.sql Time Timestamp])) @@ -269,7 +271,7 @@ (def LiteralDatetimeString "Schema for an MBQL datetime string literal, in ISO-8601 format." - (s/constrained su/NonBlankString u/date-string? "Valid ISO-8601 datetime string literal")) + (s/constrained su/NonBlankString du/date-string? "Valid ISO-8601 datetime string literal")) (def LiteralDatetime "Schema for an MBQL literal datetime value: and ISO-8601 string or `java.sql.Date`." @@ -333,7 +335,7 @@ (extend-protocol IDateTimeValue DateTimeValue (unit [this] (:unit (:field this))) - (add-date-time-units [this n] (assoc this :value (u/relative-date (unit this) n (:value this)))) + (add-date-time-units [this n] (assoc this :value (du/relative-date (unit this) n (:value this)))) RelativeDateTimeValue (unit [this] (:unit this)) diff --git a/src/metabase/query_processor/middleware/cache.clj b/src/metabase/query_processor/middleware/cache.clj index 384d18f6963..ce8db38cea6 100644 --- a/src/metabase/query_processor/middleware/cache.clj +++ b/src/metabase/query_processor/middleware/cache.clj @@ -18,7 +18,8 @@ [public-settings :as public-settings] [util :as u]] [metabase.query-processor.middleware.cache-backend.interface :as i] - [metabase.query-processor.util :as qputil])) + [metabase.query-processor.util :as qputil] + [metabase.util.date :as du])) (def ^:dynamic ^Boolean *ignore-cached-results* "Should we force the query to run, ignoring cached results even if they're available? @@ -70,7 +71,7 @@ (defn- cached-results [query-hash max-age-seconds] (when-not *ignore-cached-results* (when-let [results (i/cached-results @backend-instance query-hash max-age-seconds)] - (assert (u/is-temporal? (:updated_at results)) + (assert (du/is-temporal? (:updated_at results)) "cached-results should include an `:updated_at` field containing the date when the query was last ran.") (log/info "Returning cached results for query" (u/emoji "💾")) (assoc results :cached true)))) diff --git a/src/metabase/query_processor/middleware/cache_backend/db.clj b/src/metabase/query_processor/middleware/cache_backend/db.clj index bb426ba5f3e..4e33785fc42 100644 --- a/src/metabase/query_processor/middleware/cache_backend/db.clj +++ b/src/metabase/query_processor/middleware/cache_backend/db.clj @@ -2,10 +2,12 @@ (:require [metabase [public-settings :as public-settings] [util :as u]] + [metabase.util.date :as du] [metabase.models [interface :as models] [query-cache :refer [QueryCache]]] [metabase.query-processor.middleware.cache-backend.interface :as i] + [metabase.util.date :as du] [toucan.db :as db])) (defn- cached-results @@ -13,16 +15,16 @@ [query-hash max-age-seconds] (when-let [{:keys [results updated_at]} (db/select-one [QueryCache :results :updated_at] :query_hash query-hash - :updated_at [:>= (u/->Timestamp (- (System/currentTimeMillis) - (* 1000 max-age-seconds)))])] + :updated_at [:>= (du/->Timestamp (- (System/currentTimeMillis) + (* 1000 max-age-seconds)))])] (assoc results :updated_at updated_at))) (defn- purge-old-cache-entries! "Delete any cache entries that are older than the global max age `max-cache-entry-age-seconds` (currently 3 months)." [] (db/simple-delete! QueryCache - :updated_at [:<= (u/->Timestamp (- (System/currentTimeMillis) - (* 1000 (public-settings/query-caching-max-ttl))))])) + :updated_at [:<= (du/->Timestamp (- (System/currentTimeMillis) + (* 1000 (public-settings/query-caching-max-ttl))))])) (defn- save-results! "Save the RESULTS of query with QUERY-HASH, updating an existing QueryCache entry @@ -30,7 +32,7 @@ [query-hash results] (purge-old-cache-entries!) (or (db/update-where! QueryCache {:query_hash query-hash} - :updated_at (u/new-sql-timestamp) + :updated_at (du/new-sql-timestamp) :results (models/compress results)) ; have to manually call these here since Toucan doesn't call type conversion fns for update-where! (yet) (db/insert! QueryCache :query_hash query-hash diff --git a/src/metabase/query_processor/middleware/expand.clj b/src/metabase/query_processor/middleware/expand.clj index 0c1d5c968f0..59c949370d6 100644 --- a/src/metabase/query_processor/middleware/expand.clj +++ b/src/metabase/query_processor/middleware/expand.clj @@ -9,7 +9,9 @@ [interface :as i] [util :as qputil]] [metabase.util :as u] - [metabase.util.schema :as su] + [metabase.util + [date :as du] + [schema :as su]] [schema.core :as s]) (:import [metabase.query_processor.interface AgFieldRef BetweenFilter ComparisonFilter CompoundFilter DateTimeValue DateTimeField Expression ExpressionRef FieldLiteral FieldPlaceholder RelativeDatetime @@ -110,9 +112,9 @@ (instance? RelativeDateTimeValue v) v (instance? DateTimeValue v) v (instance? RelativeDatetime v) (i/map->RelativeDateTimeValue (assoc v :unit (datetime-unit f v), :field (datetime-field f (datetime-unit f v)))) - (instance? DateTimeField f) (i/map->DateTimeValue {:value (u/->Timestamp v), :field f}) + (instance? DateTimeField f) (i/map->DateTimeValue {:value (du/->Timestamp v), :field f}) (instance? FieldLiteral f) (if (isa? (:base-type f) :type/DateTime) - (i/map->DateTimeValue {:value (u/->Timestamp v) + (i/map->DateTimeValue {:value (du/->Timestamp v) :field (i/map->DateTimeField {:field f :unit :default})}) (i/map->Value {:value v, :field f})) :else (i/map->ValuePlaceholder {:field-placeholder (field f), :value v}))) diff --git a/src/metabase/query_processor/middleware/format_rows.clj b/src/metabase/query_processor/middleware/format_rows.clj index 114a60ed307..6e30c30ca9a 100644 --- a/src/metabase/query_processor/middleware/format_rows.clj +++ b/src/metabase/query_processor/middleware/format_rows.clj @@ -1,7 +1,7 @@ (ns metabase.query-processor.middleware.format-rows "Middleware that formats the results of a query. Currently, the only thing this does is convert datetime types to ISO-8601 strings in the appropriate timezone." - (:require [metabase.util :as u])) + (:require [metabase.util.date :as du])) (defn- format-rows* [{:keys [report-timezone]} rows] (let [timezone (or report-timezone (System/getProperty "user.timezone"))] @@ -11,11 +11,11 @@ ;; this ensures alignment between the way dates are processed by JDBC and our returned data ;; GH issues: #2282, #2035 (cond - (u/is-time? v) - (u/format-time v timezone) + (du/is-time? v) + (du/format-time v timezone) - (u/is-temporal? v) - (u/->iso-8601-datetime v timezone) + (du/is-temporal? v) + (du/->iso-8601-datetime v timezone) :else v))))) diff --git a/src/metabase/query_processor/middleware/parameters/sql.clj b/src/metabase/query_processor/middleware/parameters/sql.clj index 759c3cf8e34..8734775fd8a 100644 --- a/src/metabase/query_processor/middleware/parameters/sql.clj +++ b/src/metabase/query_processor/middleware/parameters/sql.clj @@ -13,7 +13,9 @@ [metabase.query-processor.middleware.parameters.dates :as date-params] [metabase.query-processor.middleware.expand :as ql] [metabase.util :as u] - [metabase.util.schema :as su] + [metabase.util + [date :as du] + [schema :as su]] [puppetlabs.i18n.core :refer [tru]] [schema.core :as s] [toucan.db :as db]) @@ -373,15 +375,15 @@ Date (->replacement-snippet-info [{:keys [s]}] - (honeysql->replacement-snippet-info (u/->Timestamp s))) + (honeysql->replacement-snippet-info (du/->Timestamp s))) DateRange (->replacement-snippet-info [{:keys [start end]}] (cond - (= start end) {:replacement-snippet "= ?", :prepared-statement-args [(u/->Timestamp start)]} - (nil? start) {:replacement-snippet "< ?", :prepared-statement-args [(u/->Timestamp end)]} - (nil? end) {:replacement-snippet "> ?", :prepared-statement-args [(u/->Timestamp start)]} - :else {:replacement-snippet "BETWEEN ? AND ?", :prepared-statement-args [(u/->Timestamp start) (u/->Timestamp end)]})) + (= start end) {:replacement-snippet "= ?", :prepared-statement-args [(du/->Timestamp start)]} + (nil? start) {:replacement-snippet "< ?", :prepared-statement-args [(du/->Timestamp end)]} + (nil? end) {:replacement-snippet "> ?", :prepared-statement-args [(du/->Timestamp start)]} + :else {:replacement-snippet "BETWEEN ? AND ?", :prepared-statement-args [(du/->Timestamp start) (du/->Timestamp end)]})) ;; TODO - clean this up if possible! Dimension diff --git a/src/metabase/query_processor/middleware/resolve.clj b/src/metabase/query_processor/middleware/resolve.clj index f627d5afdec..683819eb619 100644 --- a/src/metabase/query_processor/middleware/resolve.clj +++ b/src/metabase/query_processor/middleware/resolve.clj @@ -20,6 +20,7 @@ [metabase.query-processor [interface :as i] [util :as qputil]] + [metabase.util.date :as du] [schema.core :as s] [toucan [db :as db] @@ -233,8 +234,8 @@ (let [tz (when-let [tz-id ^String (setting/get :report-timezone)] (TimeZone/getTimeZone tz-id)) parsed-string-date (some-> value - (u/str->date-time tz) - u/->Timestamp)] + (du/str->date-time tz) + du/->Timestamp)] (cond parsed-string-date (s/validate DateTimeValue (i/map->DateTimeValue {:field this, :value parsed-string-date})) @@ -256,7 +257,7 @@ tz (when tz-id (TimeZone/getTimeZone tz-id)) parsed-string-time (some-> value - (u/str->time tz))] + (du/str->time tz))] (cond parsed-string-time (s/validate TimeValue (i/map->TimeValue {:field this, :value parsed-string-time :timezone-id tz-id})) diff --git a/src/metabase/sync/analyze.clj b/src/metabase/sync/analyze.clj index 9976fa930ea..956e6f03e09 100644 --- a/src/metabase/sync/analyze.clj +++ b/src/metabase/sync/analyze.clj @@ -13,6 +13,7 @@ [fingerprint :as fingerprint] #_[table-row-count :as table-row-count]] [metabase.util :as u] + [metabase.util.date :as du] [schema.core :as s] [toucan.db :as db])) @@ -61,7 +62,7 @@ (db/update-where! Field {:table_id [:in ids] :fingerprint_version i/latest-fingerprint-version :last_analyzed nil} - :last_analyzed (u/new-sql-timestamp)))) + :last_analyzed (du/new-sql-timestamp)))) (s/defn ^:private update-fields-last-analyzed! "Update the `last_analyzed` date for all the recently re-fingerprinted/re-classified Fields in TABLE." diff --git a/src/metabase/sync/analyze/fingerprint/datetime.clj b/src/metabase/sync/analyze/fingerprint/datetime.clj index 7c0bcded644..21c284ea5aa 100644 --- a/src/metabase/sync/analyze/fingerprint/datetime.clj +++ b/src/metabase/sync/analyze/fingerprint/datetime.clj @@ -6,13 +6,14 @@ [medley.core :as m] [metabase.sync.interface :as i] [metabase.util :as u] + [metabase.util.date :as du] [redux.core :as redux] [schema.core :as s])) (s/defn datetime-fingerprint :- i/DateTimeFingerprint "Generate a fingerprint containing information about values that belong to a `DateTime` Field." [values :- i/FieldSample] - (transduce (map u/str->date-time) + (transduce (map du/str->date-time) (redux/post-complete (redux/fuse {:earliest t/min-date :latest t/max-date}) diff --git a/src/metabase/sync/util.clj b/src/metabase/sync/util.clj index 5bab0ce1530..d5988306c8f 100644 --- a/src/metabase/sync/util.clj +++ b/src/metabase/sync/util.clj @@ -13,6 +13,7 @@ [metabase.models.table :refer [Table]] [metabase.query-processor.interface :as qpi] [metabase.sync.interface :as i] + [metabase.util.date :as du] [ring.util.codec :as codec] [taoensso.nippy :as nippy] [toucan.db :as db])) @@ -95,7 +96,7 @@ (f) (log/info (u/format-color 'magenta "FINISHED: %s (%s)" message - (u/format-nanoseconds (- (System/nanoTime) start-time))))))) + (du/format-nanoseconds (- (System/nanoTime) start-time))))))) (defn- with-db-logging-disabled diff --git a/src/metabase/util.clj b/src/metabase/util.clj index 05aa6728af0..3597631b370 100644 --- a/src/metabase/util.clj +++ b/src/metabase/util.clj @@ -41,286 +41,6 @@ [& body] `(try ~@body (catch Throwable ~'_))) - -(defprotocol ITimestampCoercible - "Coerce object to a `java.sql.Timestamp`." - (->Timestamp ^java.sql.Timestamp [this] - "Coerce this object to a `java.sql.Timestamp`. Strings are parsed as ISO-8601.")) - -(declare str->date-time) - -(extend-protocol ITimestampCoercible - nil (->Timestamp [_] - nil) - Timestamp (->Timestamp [this] - this) - Date (->Timestamp [this] - (Timestamp. (.getTime this))) - ;; Number is assumed to be a UNIX timezone in milliseconds (UTC) - Number (->Timestamp [this] - (Timestamp. this)) - Calendar (->Timestamp [this] - (->Timestamp (.getTime this))) - ;; Strings are expected to be in ISO-8601 format. `YYYY-MM-DD` strings *are* valid ISO-8601 dates. - String (->Timestamp [this] - (->Timestamp (str->date-time this))) - DateTime (->Timestamp [this] - (->Timestamp (.getMillis this)))) - - -(defprotocol IDateTimeFormatterCoercible - "Protocol for converting objects to `DateTimeFormatters`." - (->DateTimeFormatter ^org.joda.time.format.DateTimeFormatter [this] - "Coerce object to a `DateTimeFormatter`.")) - -(declare pprint-to-str) - -(extend-protocol IDateTimeFormatterCoercible - ;; Specify a format string like "yyyy-MM-dd" - String (->DateTimeFormatter [this] (time/formatter this)) - DateTimeFormatter (->DateTimeFormatter [this] this) - ;; Keyword will be used to get matching formatter from time/formatters - Keyword (->DateTimeFormatter [this] - (or (time/formatters this) - (throw (Exception. (format "Invalid formatter name, must be one of:\n%s" - (pprint-to-str (sort (keys time/formatters))))))))) - - -(defn parse-date - "Parse a datetime string S with a custom DATE-FORMAT, which can be a format string, clj-time formatter keyword, or - anything else that can be coerced to a `DateTimeFormatter`. - - (parse-date \"yyyyMMdd\" \"20160201\") -> #inst \"2016-02-01\" - (parse-date :date-time \"2016-02-01T00:00:00.000Z\") -> #inst \"2016-02-01\"" - ^java.sql.Timestamp [date-format, ^String s] - (->Timestamp (time/parse (->DateTimeFormatter date-format) s))) - - -(defprotocol ISO8601 - "Protocol for converting objects to ISO8601 formatted strings." - (->iso-8601-datetime ^String [this timezone-id] - "Coerce object to an ISO8601 date-time string such as \"2015-11-18T23:55:03.841Z\" with a given TIMEZONE.")) - -(def ^:private ^{:arglists '([timezone-id])} ISO8601Formatter - ;; memoize this because the formatters are static. They must be distinct per timezone though. - (memoize (fn [timezone-id] - (if timezone-id - (time/with-zone (time/formatters :date-time) (t/time-zone-for-id timezone-id)) - (time/formatters :date-time))))) - -(extend-protocol ISO8601 - nil (->iso-8601-datetime [_ _] nil) - java.util.Date (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-date this))) - java.sql.Date (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-date this))) - java.sql.Timestamp (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-time this))) - org.joda.time.DateTime (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) this))) - -(def ^:private ^{:arglists '([timezone-id])} time-formatter - ;; memoize this because the formatters are static. They must be distinct per timezone though. - (memoize (fn [timezone-id] - (if timezone-id - (time/with-zone (time/formatters :time) (t/time-zone-for-id timezone-id)) - (time/formatters :time))))) - -(defn format-time - "Returns a string representation of the time found in `T`" - [t time-zone-id] - (time/unparse (time-formatter time-zone-id) (coerce/to-date-time t))) - -(defn is-time? - "Returns true if `V` is a Time object" - [v] - (and v (instance? Time v))) - -;;; ## Date Stuff - -(defn is-temporal? - "Is VALUE an instance of a datetime class like `java.util.Date` or `org.joda.time.DateTime`?" - [v] - (or (instance? java.util.Date v) - (instance? org.joda.time.DateTime v))) - -(defn new-sql-timestamp - "`java.sql.Date` doesn't have an empty constructor so this is a convenience that lets you make one with the current - date. (Some DBs like Postgres will get snippy if you don't use a `java.sql.Timestamp`)." - ^java.sql.Timestamp [] - (->Timestamp (System/currentTimeMillis))) - -(defn format-date - "Format DATE using a given DATE-FORMAT. - - DATE is anything that can coerced to a `Timestamp` via `->Timestamp`, such as a `Date`, `Timestamp`, - `Long` (ms since the epoch), or an ISO-8601 `String`. DATE defaults to the current moment in time. - - DATE-FORMAT is anything that can be passed to `->DateTimeFormatter`, such as `String` - (using [the usual date format args](http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html)), - `Keyword`, or `DateTimeFormatter`. - - - (format-date \"yyyy-MM-dd\") -> \"2015-11-18\" - (format-date :year (java.util.Date.)) -> \"2015\" - (format-date :date-time (System/currentTimeMillis)) -> \"2015-11-18T23:55:03.841Z\"" - (^String [date-format] - (format-date date-format (System/currentTimeMillis))) - (^String [date-format date] - (time/unparse (->DateTimeFormatter date-format) (coerce/from-sql-time (->Timestamp date))))) - -(def ^{:arglists '([] [date])} date->iso-8601 - "Format DATE a an ISO-8601 string." - (partial format-date :date-time)) - -(defn date-string? - "Is S a valid ISO 8601 date string?" - [^String s] - (boolean (when (string? s) - (ignore-exceptions - (->Timestamp s))))) - - -(defn ->Date - "Coerece DATE to a `java.util.Date`." - (^java.util.Date [] - (java.util.Date.)) - (^java.util.Date [date] - (java.util.Date. (.getTime (->Timestamp date))))) - - -(defn ->Calendar - "Coerce DATE to a `java.util.Calendar`." - (^java.util.Calendar [] - (doto (Calendar/getInstance) - (.setTimeZone (TimeZone/getTimeZone "UTC")))) - (^java.util.Calendar [date] - (doto (->Calendar) - (.setTime (->Timestamp date)))) - (^java.util.Calendar [date, ^String timezone-id] - (doto (->Calendar date) - (.setTimeZone (TimeZone/getTimeZone timezone-id))))) - - -(defn relative-date - "Return a new `Timestamp` relative to the current time using a relative date UNIT. - - (relative-date :year -1) -> #inst 2014-11-12 ..." - (^java.sql.Timestamp [unit amount] - (relative-date unit amount (Calendar/getInstance))) - (^java.sql.Timestamp [unit amount date] - (let [cal (->Calendar date) - [unit multiplier] (case unit - :second [Calendar/SECOND 1] - :minute [Calendar/MINUTE 1] - :hour [Calendar/HOUR 1] - :day [Calendar/DATE 1] - :week [Calendar/DATE 7] - :month [Calendar/MONTH 1] - :quarter [Calendar/MONTH 3] - :year [Calendar/YEAR 1])] - (.set cal unit (+ (.get cal unit) - (* amount multiplier))) - (->Timestamp cal)))) - - -(def ^:private ^:const date-extract-units - #{:minute-of-hour :hour-of-day :day-of-week :day-of-month :day-of-year :week-of-year :month-of-year :quarter-of-year - :year}) - -(defn date-extract - "Extract UNIT from DATE. DATE defaults to now. - - (date-extract :year) -> 2015" - ([unit] - (date-extract unit (System/currentTimeMillis) "UTC")) - ([unit date] - (date-extract unit date "UTC")) - ([unit date timezone-id] - (let [cal (->Calendar date timezone-id)] - (case unit - :minute-of-hour (.get cal Calendar/MINUTE) - :hour-of-day (.get cal Calendar/HOUR_OF_DAY) - ;; 1 = Sunday <-> 6 = Saturday - :day-of-week (.get cal Calendar/DAY_OF_WEEK) - :day-of-month (.get cal Calendar/DAY_OF_MONTH) - :day-of-year (.get cal Calendar/DAY_OF_YEAR) - ;; 1 = First week of year - :week-of-year (.get cal Calendar/WEEK_OF_YEAR) - :month-of-year (inc (.get cal Calendar/MONTH)) - :quarter-of-year (let [month (date-extract :month-of-year date timezone-id)] - (int (/ (+ 2 month) - 3))) - :year (.get cal Calendar/YEAR))))) - - -(def ^:private ^:const date-trunc-units - #{:minute :hour :day :week :month :quarter :year}) - -(defn- trunc-with-format [format-string date timezone-id] - (->Timestamp (format-date (time/with-zone (time/formatter format-string) - (t/time-zone-for-id timezone-id)) - date))) - -(defn- trunc-with-floor [date amount-ms] - (->Timestamp (* (math/floor (/ (.getTime (->Timestamp date)) - amount-ms)) - amount-ms))) - -(defn- ->first-day-of-week [date timezone-id] - (let [day-of-week (date-extract :day-of-week date timezone-id)] - (relative-date :day (- (dec day-of-week)) date))) - -(defn- format-string-for-quarter ^String [date timezone-id] - (let [year (date-extract :year date timezone-id) - quarter (date-extract :quarter-of-year date timezone-id) - month (- (* 3 quarter) 2)] - (format "%d-%02d-01'T'ZZ" year month))) - -(defn date-trunc - "Truncate DATE to UNIT. DATE defaults to now. - - (date-trunc :month). - ;; -> #inst \"2015-11-01T00:00:00\"" - (^java.sql.Timestamp [unit] - (date-trunc unit (System/currentTimeMillis) "UTC")) - (^java.sql.Timestamp [unit date] - (date-trunc unit date "UTC")) - (^java.sql.Timestamp [unit date timezone-id] - (case unit - ;; For minute and hour truncation timezone should not be taken into account - :minute (trunc-with-floor date (* 60 1000)) - :hour (trunc-with-floor date (* 60 60 1000)) - :day (trunc-with-format "yyyy-MM-dd'T'ZZ" date timezone-id) - :week (trunc-with-format "yyyy-MM-dd'T'ZZ" (->first-day-of-week date timezone-id) timezone-id) - :month (trunc-with-format "yyyy-MM-01'T'ZZ" date timezone-id) - :quarter (trunc-with-format (format-string-for-quarter date timezone-id) date timezone-id) - :year (trunc-with-format "yyyy-01-01'T'ZZ" date timezone-id)))) - - -(defn date-trunc-or-extract - "Apply date bucketing with UNIT to DATE. DATE defaults to now." - ([unit] - (date-trunc-or-extract unit (System/currentTimeMillis) "UTC")) - ([unit date] - (date-trunc-or-extract unit date "UTC")) - ([unit date timezone-id] - (cond - (= unit :default) date - - (contains? date-extract-units unit) - (date-extract unit date timezone-id) - - (contains? date-trunc-units unit) - (date-trunc unit date timezone-id)))) - -(defn format-nanoseconds - "Format a time interval in nanoseconds to something more readable (µs/ms/etc.) - Useful for logging elapsed time when using `(System/nanotime)`" - ^String [nanoseconds] - (loop [n nanoseconds, [[unit divisor] & more] [[:ns 1000] [:µs 1000] [:ms 1000] [:s 60] [:mins 60] [:hours Integer/MAX_VALUE]]] - (if (and (> n divisor) - (seq more)) - (recur (/ n divisor) more) - (format "%.0f %s" (double n) (name unit))))) - - ;;; ## Etc (defprotocol ^:private IClobToStr @@ -767,19 +487,6 @@ (integer? object-or-id) object-or-id :else (throw (Exception. (str "Not something with an ID: " object-or-id))))) -(defmacro profile - "Like `clojure.core/time`, but lets you specify a MESSAGE that gets printed with the total time, - and formats the time nicely using `format-nanoseconds`." - {:style/indent 1} - ([form] - `(profile ~(str form) ~form)) - ([message & body] - `(let [start-time# (System/nanoTime)] - (prog1 (do ~@body) - (println (format-color '~'green "%s took %s" - ~message - (format-nanoseconds (- (System/nanoTime) start-time#)))))))) - (def metabase-namespace-symbols "Delay to a vector of symbols of all Metabase namespaces, excluding test namespaces. This is intended for use by various routines that load related namespaces, such as task and events initialization. @@ -885,58 +592,6 @@ (apply update-in m k f args) m)) -(defn- str->date-time-with-formatters - "Attempt to parse `DATE-STR` using `FORMATTERS`. First successful - parse is returned, or nil" - ([formatters date-str] - (str->date-time-with-formatters formatters date-str nil)) - ([formatters ^String date-str ^TimeZone tz] - (let [dtz (some-> tz .getID t/time-zone-for-id)] - (first - (for [formatter formatters - :let [formatter-with-tz (time/with-zone formatter dtz) - parsed-date (ignore-exceptions (time/parse formatter-with-tz date-str))] - :when parsed-date] - parsed-date))))) - -(def ^:private date-time-with-millis-no-t - "This primary use for this formatter is for Dates formatted by the built-in SQLite functions" - (->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS")) - -(def ^:private ordered-date-parsers - "When using clj-time.format/parse without a formatter, it tries all default formatters, but not ordered by how - likely the date formatters will succeed. This leads to very slow parsing as many attempts fail before the right one - is found. Using this retains that flexibility but improves performance by trying the most likely ones first" - (let [most-likely-default-formatters [:mysql :date-hour-minute-second :date-time :date - :basic-date-time :basic-date-time-no-ms - :date-time :date-time-no-ms]] - (concat (map time/formatters most-likely-default-formatters) - [date-time-with-millis-no-t] - (vals (apply dissoc time/formatters most-likely-default-formatters))))) - -(defn str->date-time - "Like clj-time.format/parse but uses an ordered list of parsers to be faster. Returns the parsed date or nil if it - was unable to be parsed." - (^org.joda.time.DateTime [^String date-str] - (str->date-time date-str nil)) - ([^String date-str ^TimeZone tz] - (str->date-time-with-formatters ordered-date-parsers date-str tz))) - -(def ^:private ordered-time-parsers - (let [most-likely-default-formatters [:hour-minute :hour-minute-second :hour-minute-second-fraction]] - (concat (map time/formatters most-likely-default-formatters) - [(time/formatter "HH:mmZ") (time/formatter "HH:mm:SSZ") (time/formatter "HH:mm:SS.SSSZ")]))) - -(defn str->time - "Parse `TIME-STR` and return a `java.sql.Time` instance. Returns nil - if `TIME-STR` can't be parsed." - ([^String date-str] - (str->time date-str nil)) - ([^String date-str ^TimeZone tz] - (some-> (str->date-time-with-formatters ordered-time-parsers date-str tz) - coerce/to-long - Time.))) - (defn index-of "Return index of the first element in `coll` for which `pred` reutrns true." [pred coll] diff --git a/src/metabase/util/date.clj b/src/metabase/util/date.clj new file mode 100644 index 00000000000..8f987cf9a16 --- /dev/null +++ b/src/metabase/util/date.clj @@ -0,0 +1,355 @@ +(ns metabase.util.date + (:require [clj-time + [coerce :as coerce] + [core :as t] + [format :as time]] + [clojure.tools.logging :as log] + [clojure.math.numeric-tower :as math] + [metabase.util :as u] + [puppetlabs.i18n.core :refer [trs]]) + (:import [clojure.lang Keyword] + [java.util Calendar Date TimeZone] + [java.sql Time Timestamp] + [org.joda.time DateTime DateTimeZone] + [org.joda.time.format DateTimeFormatter])) + + +(defprotocol ITimestampCoercible + "Coerce object to a `java.sql.Timestamp`." + (->Timestamp ^java.sql.Timestamp [this] + "Coerce this object to a `java.sql.Timestamp`. Strings are parsed as ISO-8601.")) + +(declare str->date-time) + +(extend-protocol ITimestampCoercible + nil (->Timestamp [_] + nil) + Timestamp (->Timestamp [this] + this) + Date (->Timestamp [this] + (Timestamp. (.getTime this))) + ;; Number is assumed to be a UNIX timezone in milliseconds (UTC) + Number (->Timestamp [this] + (Timestamp. this)) + Calendar (->Timestamp [this] + (->Timestamp (.getTime this))) + ;; Strings are expected to be in ISO-8601 format. `YYYY-MM-DD` strings *are* valid ISO-8601 dates. + String (->Timestamp [this] + (->Timestamp (str->date-time this))) + DateTime (->Timestamp [this] + (->Timestamp (.getMillis this)))) + + +(defprotocol IDateTimeFormatterCoercible + "Protocol for converting objects to `DateTimeFormatters`." + (->DateTimeFormatter ^org.joda.time.format.DateTimeFormatter [this] + "Coerce object to a `DateTimeFormatter`.")) + +(extend-protocol IDateTimeFormatterCoercible + ;; Specify a format string like "yyyy-MM-dd" + String (->DateTimeFormatter [this] (time/formatter this)) + DateTimeFormatter (->DateTimeFormatter [this] this) + ;; Keyword will be used to get matching formatter from time/formatters + Keyword (->DateTimeFormatter [this] + (or (time/formatters this) + (throw (Exception. (format "Invalid formatter name, must be one of:\n%s" + (u/pprint-to-str (sort (keys time/formatters))))))))) + + +(defn parse-date + "Parse a datetime string S with a custom DATE-FORMAT, which can be a format string, clj-time formatter keyword, or + anything else that can be coerced to a `DateTimeFormatter`. + + (parse-date \"yyyyMMdd\" \"20160201\") -> #inst \"2016-02-01\" + (parse-date :date-time \"2016-02-01T00:00:00.000Z\") -> #inst \"2016-02-01\"" + ^java.sql.Timestamp [date-format, ^String s] + (->Timestamp (time/parse (->DateTimeFormatter date-format) s))) + + +(defprotocol ISO8601 + "Protocol for converting objects to ISO8601 formatted strings." + (->iso-8601-datetime ^String [this timezone-id] + "Coerce object to an ISO8601 date-time string such as \"2015-11-18T23:55:03.841Z\" with a given TIMEZONE.")) + +(def ^:private ^{:arglists '([timezone-id])} ISO8601Formatter + ;; memoize this because the formatters are static. They must be distinct per timezone though. + (memoize (fn [timezone-id] + (if timezone-id + (time/with-zone (time/formatters :date-time) (t/time-zone-for-id timezone-id)) + (time/formatters :date-time))))) + +(extend-protocol ISO8601 + nil (->iso-8601-datetime [_ _] nil) + java.util.Date (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-date this))) + java.sql.Date (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-date this))) + java.sql.Timestamp (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-time this))) + org.joda.time.DateTime (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) this))) + +(def ^:private ^{:arglists '([timezone-id])} time-formatter + ;; memoize this because the formatters are static. They must be distinct per timezone though. + (memoize (fn [timezone-id] + (if timezone-id + (time/with-zone (time/formatters :time) (t/time-zone-for-id timezone-id)) + (time/formatters :time))))) + +(defn format-time + "Returns a string representation of the time found in `T`" + [t time-zone-id] + (time/unparse (time-formatter time-zone-id) (coerce/to-date-time t))) + +(defn is-time? + "Returns true if `V` is a Time object" + [v] + (and v (instance? Time v))) + +;;; ## Date Stuff + +(defn is-temporal? + "Is VALUE an instance of a datetime class like `java.util.Date` or `org.joda.time.DateTime`?" + [v] + (or (instance? java.util.Date v) + (instance? org.joda.time.DateTime v))) + +(defn new-sql-timestamp + "`java.sql.Date` doesn't have an empty constructor so this is a convenience that lets you make one with the current + date. (Some DBs like Postgres will get snippy if you don't use a `java.sql.Timestamp`)." + ^java.sql.Timestamp [] + (->Timestamp (System/currentTimeMillis))) + +(defn format-date + "Format DATE using a given DATE-FORMAT. + + DATE is anything that can coerced to a `Timestamp` via `->Timestamp`, such as a `Date`, `Timestamp`, + `Long` (ms since the epoch), or an ISO-8601 `String`. DATE defaults to the current moment in time. + + DATE-FORMAT is anything that can be passed to `->DateTimeFormatter`, such as `String` + (using [the usual date format args](http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html)), + `Keyword`, or `DateTimeFormatter`. + + + (format-date \"yyyy-MM-dd\") -> \"2015-11-18\" + (format-date :year (java.util.Date.)) -> \"2015\" + (format-date :date-time (System/currentTimeMillis)) -> \"2015-11-18T23:55:03.841Z\"" + (^String [date-format] + (format-date date-format (System/currentTimeMillis))) + (^String [date-format date] + (time/unparse (->DateTimeFormatter date-format) (coerce/from-sql-time (->Timestamp date))))) + +(def ^{:arglists '([] [date])} date->iso-8601 + "Format DATE a an ISO-8601 string." + (partial format-date :date-time)) + +(defn date-string? + "Is S a valid ISO 8601 date string?" + [^String s] + (boolean (when (string? s) + (u/ignore-exceptions + (->Timestamp s))))) + +(defn ->Date + "Coerece DATE to a `java.util.Date`." + (^java.util.Date [] + (java.util.Date.)) + (^java.util.Date [date] + (java.util.Date. (.getTime (->Timestamp date))))) + + +(defn ->Calendar + "Coerce DATE to a `java.util.Calendar`." + (^java.util.Calendar [] + (doto (Calendar/getInstance) + (.setTimeZone (TimeZone/getTimeZone "UTC")))) + (^java.util.Calendar [date] + (doto (->Calendar) + (.setTime (->Timestamp date)))) + (^java.util.Calendar [date, ^String timezone-id] + (doto (->Calendar date) + (.setTimeZone (TimeZone/getTimeZone timezone-id))))) + + +(defn relative-date + "Return a new `Timestamp` relative to the current time using a relative date UNIT. + + (relative-date :year -1) -> #inst 2014-11-12 ..." + (^java.sql.Timestamp [unit amount] + (relative-date unit amount (Calendar/getInstance))) + (^java.sql.Timestamp [unit amount date] + (let [cal (->Calendar date) + [unit multiplier] (case unit + :second [Calendar/SECOND 1] + :minute [Calendar/MINUTE 1] + :hour [Calendar/HOUR 1] + :day [Calendar/DATE 1] + :week [Calendar/DATE 7] + :month [Calendar/MONTH 1] + :quarter [Calendar/MONTH 3] + :year [Calendar/YEAR 1])] + (.set cal unit (+ (.get cal unit) + (* amount multiplier))) + (->Timestamp cal)))) + + +(def ^:private ^:const date-extract-units + #{:minute-of-hour :hour-of-day :day-of-week :day-of-month :day-of-year :week-of-year :month-of-year :quarter-of-year + :year}) + +(defn date-extract + "Extract UNIT from DATE. DATE defaults to now. + + (date-extract :year) -> 2015" + ([unit] + (date-extract unit (System/currentTimeMillis) "UTC")) + ([unit date] + (date-extract unit date "UTC")) + ([unit date timezone-id] + (let [cal (->Calendar date timezone-id)] + (case unit + :minute-of-hour (.get cal Calendar/MINUTE) + :hour-of-day (.get cal Calendar/HOUR_OF_DAY) + ;; 1 = Sunday <-> 6 = Saturday + :day-of-week (.get cal Calendar/DAY_OF_WEEK) + :day-of-month (.get cal Calendar/DAY_OF_MONTH) + :day-of-year (.get cal Calendar/DAY_OF_YEAR) + ;; 1 = First week of year + :week-of-year (.get cal Calendar/WEEK_OF_YEAR) + :month-of-year (inc (.get cal Calendar/MONTH)) + :quarter-of-year (let [month (date-extract :month-of-year date timezone-id)] + (int (/ (+ 2 month) + 3))) + :year (.get cal Calendar/YEAR))))) + + +(def ^:private ^:const date-trunc-units + #{:minute :hour :day :week :month :quarter :year}) + +(defn- trunc-with-format [format-string date timezone-id] + (->Timestamp (format-date (time/with-zone (time/formatter format-string) + (t/time-zone-for-id timezone-id)) + date))) + +(defn- trunc-with-floor [date amount-ms] + (->Timestamp (* (math/floor (/ (.getTime (->Timestamp date)) + amount-ms)) + amount-ms))) + +(defn- ->first-day-of-week [date timezone-id] + (let [day-of-week (date-extract :day-of-week date timezone-id)] + (relative-date :day (- (dec day-of-week)) date))) + +(defn- format-string-for-quarter ^String [date timezone-id] + (let [year (date-extract :year date timezone-id) + quarter (date-extract :quarter-of-year date timezone-id) + month (- (* 3 quarter) 2)] + (format "%d-%02d-01'T'ZZ" year month))) + +(defn date-trunc + "Truncate DATE to UNIT. DATE defaults to now. + + (date-trunc :month). + ;; -> #inst \"2015-11-01T00:00:00\"" + (^java.sql.Timestamp [unit] + (date-trunc unit (System/currentTimeMillis) "UTC")) + (^java.sql.Timestamp [unit date] + (date-trunc unit date "UTC")) + (^java.sql.Timestamp [unit date timezone-id] + (case unit + ;; For minute and hour truncation timezone should not be taken into account + :minute (trunc-with-floor date (* 60 1000)) + :hour (trunc-with-floor date (* 60 60 1000)) + :day (trunc-with-format "yyyy-MM-dd'T'ZZ" date timezone-id) + :week (trunc-with-format "yyyy-MM-dd'T'ZZ" (->first-day-of-week date timezone-id) timezone-id) + :month (trunc-with-format "yyyy-MM-01'T'ZZ" date timezone-id) + :quarter (trunc-with-format (format-string-for-quarter date timezone-id) date timezone-id) + :year (trunc-with-format "yyyy-01-01'T'ZZ" date timezone-id)))) + + +(defn date-trunc-or-extract + "Apply date bucketing with UNIT to DATE. DATE defaults to now." + ([unit] + (date-trunc-or-extract unit (System/currentTimeMillis) "UTC")) + ([unit date] + (date-trunc-or-extract unit date "UTC")) + ([unit date timezone-id] + (cond + (= unit :default) date + + (contains? date-extract-units unit) + (date-extract unit date timezone-id) + + (contains? date-trunc-units unit) + (date-trunc unit date timezone-id)))) + +(defn format-nanoseconds + "Format a time interval in nanoseconds to something more readable (µs/ms/etc.) + Useful for logging elapsed time when using `(System/nanotime)`" + ^String [nanoseconds] + (loop [n nanoseconds, [[unit divisor] & more] [[:ns 1000] [:µs 1000] [:ms 1000] [:s 60] [:mins 60] [:hours Integer/MAX_VALUE]]] + (if (and (> n divisor) + (seq more)) + (recur (/ n divisor) more) + (format "%.0f %s" (double n) (name unit))))) + +(defmacro profile + "Like `clojure.core/time`, but lets you specify a MESSAGE that gets printed with the total time, + and formats the time nicely using `format-nanoseconds`." + {:style/indent 1} + ([form] + `(profile ~(str form) ~form)) + ([message & body] + `(let [start-time# (System/nanoTime)] + (u/prog1 (do ~@body) + (println (u/format-color '~'green "%s took %s" + ~message + (format-nanoseconds (- (System/nanoTime) start-time#)))))))) + +(defn- str->date-time-with-formatters + "Attempt to parse `DATE-STR` using `FORMATTERS`. First successful + parse is returned, or nil" + ([formatters date-str] + (str->date-time-with-formatters formatters date-str nil)) + ([formatters ^String date-str ^TimeZone tz] + (let [dtz (some-> tz .getID t/time-zone-for-id)] + (first + (for [formatter formatters + :let [formatter-with-tz (time/with-zone formatter dtz) + parsed-date (u/ignore-exceptions (time/parse formatter-with-tz date-str))] + :when parsed-date] + parsed-date))))) + +(def ^:private date-time-with-millis-no-t + "This primary use for this formatter is for Dates formatted by the built-in SQLite functions" + (->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS")) + +(def ^:private ordered-date-parsers + "When using clj-time.format/parse without a formatter, it tries all default formatters, but not ordered by how + likely the date formatters will succeed. This leads to very slow parsing as many attempts fail before the right one + is found. Using this retains that flexibility but improves performance by trying the most likely ones first" + (let [most-likely-default-formatters [:mysql :date-hour-minute-second :date-time :date + :basic-date-time :basic-date-time-no-ms + :date-time :date-time-no-ms]] + (concat (map time/formatters most-likely-default-formatters) + [date-time-with-millis-no-t] + (vals (apply dissoc time/formatters most-likely-default-formatters))))) + +(defn str->date-time + "Like clj-time.format/parse but uses an ordered list of parsers to be faster. Returns the parsed date or nil if it + was unable to be parsed." + (^org.joda.time.DateTime [^String date-str] + (str->date-time date-str nil)) + ([^String date-str ^TimeZone tz] + (str->date-time-with-formatters ordered-date-parsers date-str tz))) + +(def ^:private ordered-time-parsers + (let [most-likely-default-formatters [:hour-minute :hour-minute-second :hour-minute-second-fraction]] + (concat (map time/formatters most-likely-default-formatters) + [(time/formatter "HH:mmZ") (time/formatter "HH:mm:SSZ") (time/formatter "HH:mm:SS.SSSZ")]))) + +(defn str->time + "Parse `TIME-STR` and return a `java.sql.Time` instance. Returns nil + if `TIME-STR` can't be parsed." + ([^String date-str] + (str->time date-str nil)) + ([^String date-str ^TimeZone tz] + (some-> (str->date-time-with-formatters ordered-time-parsers date-str tz) + coerce/to-long + Time.))) diff --git a/test/metabase/api/activity_test.clj b/test/metabase/api/activity_test.clj index 4343e153f55..f3dd9990af5 100644 --- a/test/metabase/api/activity_test.clj +++ b/test/metabase/api/activity_test.clj @@ -10,6 +10,7 @@ [metabase.test.data.users :refer :all] [metabase.test.util :as tu :refer [match-$]] [metabase.util :as u] + [metabase.util.date :as du] [toucan.db :as db] [toucan.util.test :as tt])) @@ -22,19 +23,19 @@ ;; NOTE: timestamp matching was being a real PITA so I cheated a bit. ideally we'd fix that (tt/expect-with-temp [Activity [activity1 {:topic "install" :details {} - :timestamp (u/->Timestamp "2015-09-09T12:13:14.888Z")}] + :timestamp (du/->Timestamp "2015-09-09T12:13:14.888Z")}] Activity [activity2 {:topic "dashboard-create" :user_id (user->id :crowberto) :model "dashboard" :model_id 1234 :details {:description "Because I can!" :name "Bwahahaha"} - :timestamp (u/->Timestamp "2015-09-10T18:53:01.632Z")}] + :timestamp (du/->Timestamp "2015-09-10T18:53:01.632Z")}] Activity [activity3 {:topic "user-joined" :user_id (user->id :rasta) :model "user" :details {} - :timestamp (u/->Timestamp "2015-09-10T05:33:43.641Z")}]] + :timestamp (du/->Timestamp "2015-09-10T05:33:43.641Z")}]] [(match-$ (Activity (:id activity2)) {:id $ :topic "dashboard-create" @@ -116,7 +117,7 @@ :user_id user :model model :model_id model-id - :timestamp (u/new-sql-timestamp)) + :timestamp (du/new-sql-timestamp)) ;; we sleep a bit to ensure no events have the same timestamp ;; sadly, MySQL doesn't support milliseconds so we have to wait a second ;; otherwise our records are out of order and this test fails :( diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj index 5ddc3f6333f..bff21c86d4d 100644 --- a/test/metabase/api/card_test.clj +++ b/test/metabase/api/card_test.clj @@ -29,6 +29,7 @@ [data :as data :refer :all] [util :as tu :refer [match-$ random-name]]] [metabase.test.data.users :refer :all] + [metabase.util.date :as du] [toucan.db :as db] [toucan.util.test :as tt]) (:import java.io.ByteArrayInputStream @@ -140,15 +141,15 @@ ;; 3 was viewed most recently, followed by 4, then 1. Card 2 was viewed by a different user so ;; shouldn't be returned ViewLog [_ {:model "card", :model_id card-1-id, :user_id (user->id :rasta) - :timestamp (u/->Timestamp #inst "2015-12-01")}] + :timestamp (du/->Timestamp "2015-12-01")}] ViewLog [_ {:model "card", :model_id card-2-id, :user_id (user->id :trashbird) - :timestamp (u/->Timestamp #inst "2016-01-01")}] + :timestamp (du/->Timestamp "2016-01-01")}] ViewLog [_ {:model "card", :model_id card-3-id, :user_id (user->id :rasta) - :timestamp (u/->Timestamp #inst "2016-02-01")}] + :timestamp (du/->Timestamp "2016-02-01")}] ViewLog [_ {:model "card", :model_id card-4-id, :user_id (user->id :rasta) - :timestamp (u/->Timestamp #inst "2016-03-01")}] + :timestamp (du/->Timestamp "2016-03-01")}] ViewLog [_ {:model "card", :model_id card-3-id, :user_id (user->id :rasta) - :timestamp (u/->Timestamp #inst "2016-04-01")}]] + :timestamp (du/->Timestamp "2016-04-01")}]] [card-3-id card-4-id card-1-id] (mapv :id ((user->client :rasta) :get 200 "card", :f :recent))) diff --git a/test/metabase/driver/googleanalytics_test.clj b/test/metabase/driver/googleanalytics_test.clj index ceb95d91e17..b6cb1885e42 100644 --- a/test/metabase/driver/googleanalytics_test.clj +++ b/test/metabase/driver/googleanalytics_test.clj @@ -10,6 +10,7 @@ [metabase.query-processor.interface :as qpi] [metabase.test.data.users :as users] [metabase.util :as u] + [metabase.util.date :as du] [toucan.db :as db] [toucan.util.test :as tt])) @@ -128,8 +129,8 @@ ;; relative date -- last month (expect - (ga-query {:start-date (u/format-date "yyyy-MM-01" (u/relative-date :month -1)) - :end-date (u/format-date "yyyy-MM-01")}) + (ga-query {:start-date (du/format-date "yyyy-MM-01" (du/relative-date :month -1)) + :end-date (du/format-date "yyyy-MM-01")}) (mbql->native {:query {:filter {:filter-type := :field (ga-date-field :month) :value (qpi/map->RelativeDateTimeValue {:amount -1 @@ -138,8 +139,8 @@ ;; relative date -- this month (expect - (ga-query {:start-date (u/format-date "yyyy-MM-01") - :end-date (u/format-date "yyyy-MM-01" (u/relative-date :month 1))}) + (ga-query {:start-date (du/format-date "yyyy-MM-01") + :end-date (du/format-date "yyyy-MM-01" (du/relative-date :month 1))}) (mbql->native {:query {:filter {:filter-type := :field (ga-date-field :month) :value (qpi/map->RelativeDateTimeValue {:amount 0 @@ -148,8 +149,8 @@ ;; relative date -- next month (expect - (ga-query {:start-date (u/format-date "yyyy-MM-01" (u/relative-date :month 1)) - :end-date (u/format-date "yyyy-MM-01" (u/relative-date :month 2))}) + (ga-query {:start-date (du/format-date "yyyy-MM-01" (du/relative-date :month 1)) + :end-date (du/format-date "yyyy-MM-01" (du/relative-date :month 2))}) (mbql->native {:query {:filter {:filter-type := :field (ga-date-field :month) :value (qpi/map->RelativeDateTimeValue {:amount 1 @@ -158,8 +159,8 @@ ;; relative date -- 2 months from now (expect - (ga-query {:start-date (u/format-date "yyyy-MM-01" (u/relative-date :month 2)) - :end-date (u/format-date "yyyy-MM-01" (u/relative-date :month 3))}) + (ga-query {:start-date (du/format-date "yyyy-MM-01" (du/relative-date :month 2)) + :end-date (du/format-date "yyyy-MM-01" (du/relative-date :month 3))}) (mbql->native {:query {:filter {:filter-type := :field (ga-date-field :month) :value (qpi/map->RelativeDateTimeValue {:amount 2 @@ -168,8 +169,8 @@ ;; relative date -- last year (expect - (ga-query {:start-date (u/format-date "yyyy-01-01" (u/relative-date :year -1)) - :end-date (u/format-date "yyyy-01-01")}) + (ga-query {:start-date (du/format-date "yyyy-01-01" (du/relative-date :year -1)) + :end-date (du/format-date "yyyy-01-01")}) (mbql->native {:query {:filter {:filter-type := :field (ga-date-field :year) :value (qpi/map->RelativeDateTimeValue {:amount -1 diff --git a/test/metabase/driver/mysql_test.clj b/test/metabase/driver/mysql_test.clj index 7840327daf2..534f9418aac 100644 --- a/test/metabase/driver/mysql_test.clj +++ b/test/metabase/driver/mysql_test.clj @@ -16,7 +16,7 @@ [datasets :refer [expect-with-engine]] [interface :refer [def-database-definition]]] [metabase.test.util :as tu] - [metabase.util :as u] + [metabase.util.date :as du] [honeysql.core :as hsql] [toucan.db :as db] [toucan.util.test :as tt]) @@ -99,8 +99,8 @@ (tu/db-timezone-id))) -(def before-daylight-savings (u/str->date-time "2018-03-10 10:00:00")) -(def after-daylight-savings (u/str->date-time "2018-03-12 10:00:00")) +(def before-daylight-savings (du/str->date-time "2018-03-10 10:00:00")) +(def after-daylight-savings (du/str->date-time "2018-03-12 10:00:00")) (expect (#'mysql/timezone-id->offset-str "US/Pacific" before-daylight-savings) "-08:00") (expect (#'mysql/timezone-id->offset-str "US/Pacific" after-daylight-savings) "-07:00") @@ -114,18 +114,18 @@ ;; make sure DateTime types generate appropriate SQL... ;; ...with no report-timezone set (expect - ["?" (u/->Timestamp "2018-01-03")] + ["?" (du/->Timestamp "2018-01-03")] (tu/with-temporary-setting-values [report-timezone nil] - (hsql/format (sqlqp/->honeysql (MySQLDriver.) (u/->Timestamp "2018-01-03"))))) + (hsql/format (sqlqp/->honeysql (MySQLDriver.) (du/->Timestamp "2018-01-03"))))) ;; ...with a report-timezone set (expect ["convert_tz('2018-01-03T00:00:00.000', '+00:00', '-08:00')"] (tu/with-temporary-setting-values [report-timezone "US/Pacific"] - (hsql/format (sqlqp/->honeysql (MySQLDriver.) (u/->Timestamp "2018-01-03"))))) + (hsql/format (sqlqp/->honeysql (MySQLDriver.) (du/->Timestamp "2018-01-03"))))) ;; ...with a report-timezone set to the same as the system timezone (shouldn't need to do TZ conversion) (expect - ["?" (u/->Timestamp "2018-01-03")] + ["?" (du/->Timestamp #inst "2018-01-03")] (tu/with-temporary-setting-values [report-timezone "UTC"] - (hsql/format (sqlqp/->honeysql (MySQLDriver.) (u/->Timestamp "2018-01-03"))))) + (hsql/format (sqlqp/->honeysql (MySQLDriver.) (du/->Timestamp "2018-01-03"))))) diff --git a/test/metabase/http_client.clj b/test/metabase/http_client.clj index 9de0ef75217..81bb8c862b7 100644 --- a/test/metabase/http_client.clj +++ b/test/metabase/http_client.clj @@ -6,7 +6,8 @@ [clojure.tools.logging :as log] [metabase [config :as config] - [util :as u]])) + [util :as u]] + [metabase.util.date :as du])) ;;; build-url @@ -40,7 +41,7 @@ (map? response) (->> response (map (fn [[k v]] {k (cond - (contains? auto-deserialize-dates-keys k) (u/->Timestamp v) + (contains? auto-deserialize-dates-keys k) (du/->Timestamp v) (coll? v) (auto-deserialize-dates v) :else v)})) (into {})) diff --git a/test/metabase/middleware_test.clj b/test/metabase/middleware_test.clj index 1e3b37e33a2..3d3f21bdd5f 100644 --- a/test/metabase/middleware_test.clj +++ b/test/metabase/middleware_test.clj @@ -13,6 +13,7 @@ [metabase.models.session :refer [Session]] [metabase.test.data.users :refer :all] [metabase.test.async :refer [while-with-timeout]] + [metabase.util.date :as du] [ring.mock.request :as mock] [ring.util.response :as resp] [toucan.db :as db] @@ -75,7 +76,7 @@ (expect (user->id :rasta) (let [session-id (random-session-id)] - (db/simple-insert! Session, :id session-id, :user_id (user->id :rasta), :created_at (u/new-sql-timestamp)) + (db/simple-insert! Session, :id session-id, :user_id (user->id :rasta), :created_at (du/new-sql-timestamp)) (-> (auth-enforced-handler (request-with-session-id session-id)) :metabase-user-id))) @@ -96,7 +97,7 @@ ;; NOTE that :trashbird is our INACTIVE test user (expect response-unauthentic (let [session-id (random-session-id)] - (db/simple-insert! Session, :id session-id, :user_id (user->id :trashbird), :created_at (u/new-sql-timestamp)) + (db/simple-insert! Session, :id session-id, :user_id (user->id :trashbird), :created_at (du/new-sql-timestamp)) (auth-enforced-handler (request-with-session-id session-id)))) diff --git a/test/metabase/models/dependency_test.clj b/test/metabase/models/dependency_test.clj index 07eb81cb70c..661625c3498 100644 --- a/test/metabase/models/dependency_test.clj +++ b/test/metabase/models/dependency_test.clj @@ -3,6 +3,7 @@ [metabase.models.dependency :refer :all] [metabase.test.data :refer :all] [metabase.util :as u] + [metabase.util.date :as du] [toucan [db :as db] [models :as models]] @@ -49,12 +50,12 @@ :model_id 4 :dependent_on_model "test" :dependent_on_id 1 - :created_at (u/new-sql-timestamp)}] + :created_at (du/new-sql-timestamp)}] Dependency [_ {:model "Mock" :model_id 4 :dependent_on_model "foobar" :dependent_on_id 13 - :created_at (u/new-sql-timestamp)}]] + :created_at (du/new-sql-timestamp)}]] (format-dependencies (retrieve-dependencies Mock 4)))) @@ -101,6 +102,6 @@ :model_id 1 :dependent_on_model "test" :dependent_on_id 5 - :created_at (u/new-sql-timestamp)) + :created_at (du/new-sql-timestamp)) (update-dependencies! Mock 1 {:test [1 2]}) (format-dependencies (db/select Dependency, :model "Mock", :model_id 1)))) diff --git a/test/metabase/models/session_test.clj b/test/metabase/models/session_test.clj index 116d3755631..b77375357ef 100644 --- a/test/metabase/models/session_test.clj +++ b/test/metabase/models/session_test.clj @@ -5,6 +5,7 @@ [user :refer [User]]] [metabase.test.util :as tu] [metabase.util :as u] + [metabase.util.date :as du] [toucan.db :as db] [toucan.util.test :as tt])) @@ -18,17 +19,17 @@ (db/simple-insert-many! Session [{:id "the-greatest-day-ever" :user_id user-id - :created_at (u/->Timestamp "1980-10-19T05:05:05.000Z")} + :created_at (du/->Timestamp "1980-10-19T05:05:05.000Z")} {:id "even-more-greatness" :user_id user-id - :created_at (u/->Timestamp "1980-10-19T05:08:05.000Z")} + :created_at (du/->Timestamp "1980-10-19T05:08:05.000Z")} {:id "the-world-of-bi-changes-forever" :user_id user-id - :created_at (u/->Timestamp "2015-10-21")} + :created_at (du/->Timestamp "2015-10-21")} {:id "something-could-have-happened" :user_id user-id - :created_at (u/->Timestamp "1999-12-31")} + :created_at (du/->Timestamp "1999-12-31")} {:id "now" :user_id user-id - :created_at (u/new-sql-timestamp)}]) + :created_at (du/new-sql-timestamp)}]) (first-session-for-user user-id))) diff --git a/test/metabase/query_processor/expand_resolve_test.clj b/test/metabase/query_processor/expand_resolve_test.clj index 5bd8b51f0a0..4d6ae514890 100644 --- a/test/metabase/query_processor/expand_resolve_test.clj +++ b/test/metabase/query_processor/expand_resolve_test.clj @@ -12,7 +12,8 @@ [metabase.test [data :as data :refer :all] [util :as tu]] - [metabase.test.data.dataset-definitions :as defs])) + [metabase.test.data.dataset-definitions :as defs] + [metabase.util.date :as du])) ;; this is here because expectations has issues comparing and object w/ a map and most of the output ;; below has objects for the various place holders in the expanded/resolved query @@ -239,7 +240,7 @@ :type {:type/DateTime {:earliest "2014-01-01T00:00:00.000Z" :latest "2014-12-05T00:00:00.000Z"}}}}) :unit :year} - :value {:value (u/->Timestamp "1980-01-01") + :value {:value (du/->Timestamp "1980-01-01") :field {:field (merge field-defaults {:field-id (id :users :last_login) diff --git a/test/metabase/query_processor/middleware/fetch_source_query_test.clj b/test/metabase/query_processor/middleware/fetch_source_query_test.clj index 569716cf3be..0b9d4936ddb 100644 --- a/test/metabase/query_processor/middleware/fetch_source_query_test.clj +++ b/test/metabase/query_processor/middleware/fetch_source_query_test.clj @@ -10,6 +10,7 @@ [database :as database]] [metabase.query-processor.middleware.fetch-source-query :as fetch-source-query] [metabase.test.data :as data] + [metabase.util.date :as du] [toucan.util.test :as tt])) (def ^:private ^{:arglists '([query])} fetch-source-query (fetch-source-query/fetch-source-query identity)) @@ -79,9 +80,9 @@ {:source-query {:source-table {:schema "PUBLIC" :name "CHECKINS" :id (data/id :checkins)}, :join-tables nil} :filter {:filter-type :between, :field {:field-name "date", :base-type :type/Date}, - :min-val {:value (tcoerce/to-timestamp (u/str->date-time "2015-01-01")) + :min-val {:value (tcoerce/to-timestamp (du/str->date-time "2015-01-01")) :field {:field {:field-name "date", :base-type :type/Date}, :unit :default}}, - :max-val {:value (tcoerce/to-timestamp (u/str->date-time "2015-02-01")) + :max-val {:value (tcoerce/to-timestamp (du/str->date-time "2015-02-01")) :field {:field {:field-name "date", :base-type :type/Date}, :unit :default}}}}) (tt/with-temp Card [card {:dataset_query {:database (data/id) :type :query diff --git a/test/metabase/query_processor/middleware/parameters/mbql_test.clj b/test/metabase/query_processor/middleware/parameters/mbql_test.clj index 5193b192a54..9dfd8caa46f 100644 --- a/test/metabase/query_processor/middleware/parameters/mbql_test.clj +++ b/test/metabase/query_processor/middleware/parameters/mbql_test.clj @@ -8,7 +8,8 @@ [metabase.query-processor.middleware.expand :as ql] [metabase.query-processor.middleware.parameters.mbql :as mbql-params :refer :all] [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets])) + [metabase.test.data.datasets :as datasets] + [metabase.util.date :as du])) (defn- expand-parameters [query] (expand (dissoc query :parameters) (:parameters query))) @@ -232,10 +233,10 @@ {:query (str "SELECT count(*) AS \"count\" FROM \"PUBLIC\".\"CHECKINS\" " "WHERE (CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) BETWEEN CAST(? AS date) AND CAST(? AS date) " "OR CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) BETWEEN CAST(? AS date) AND CAST(? AS date))") - :params [(u/->Timestamp #inst "2014-06-01") - (u/->Timestamp #inst "2014-06-30") - (u/->Timestamp #inst "2015-06-01") - (u/->Timestamp #inst "2015-06-30")]} + :params [(du/->Timestamp #inst "2014-06-01") + (du/->Timestamp #inst "2014-06-30") + (du/->Timestamp #inst "2015-06-01") + (du/->Timestamp #inst "2015-06-30")]} (let [inner-query (data/query checkins (ql/aggregation (ql/count))) outer-query (-> (data/wrap-inner-query inner-query) diff --git a/test/metabase/sync/analyze/classify_test.clj b/test/metabase/sync/analyze/classify_test.clj index 3ba4aec617c..ba6e087a2e2 100644 --- a/test/metabase/sync/analyze/classify_test.clj +++ b/test/metabase/sync/analyze/classify_test.clj @@ -6,6 +6,7 @@ [metabase.sync.analyze.classify :as classify] [metabase.sync.interface :as i] [metabase.util :as u] + [metabase.util.date :as du] [toucan.util.test :as tt])) ;; Check that only the right Fields get classified @@ -21,7 +22,7 @@ Field [_ {:table_id (u/get-id table) :name "Current fingerprint, already analzed" :fingerprint_version Short/MAX_VALUE - :last_analyzed (u/->Timestamp "2017-08-09")}] + :last_analyzed (du/->Timestamp "2017-08-09")}] Field [_ {:table_id (u/get-id table) :name "Old fingerprint, not analyzed" :fingerprint_version (dec Short/MAX_VALUE) @@ -29,6 +30,6 @@ Field [_ {:table_id (u/get-id table) :name "Old fingerprint, already analzed" :fingerprint_version (dec Short/MAX_VALUE) - :last_analyzed (u/->Timestamp "2017-08-09")}]] + :last_analyzed (du/->Timestamp "2017-08-09")}]] (for [field (#'classify/fields-to-classify table)] (:name field))))) diff --git a/test/metabase/sync/analyze/fingerprint_test.clj b/test/metabase/sync/analyze/fingerprint_test.clj index caa8daaf8f6..0bef3751bce 100644 --- a/test/metabase/sync/analyze/fingerprint_test.clj +++ b/test/metabase/sync/analyze/fingerprint_test.clj @@ -10,6 +10,7 @@ [metabase.test.data :as data] [metabase.test.util] [metabase.util :as u] + [metabase.util.date :as du] [toucan.db :as db] [toucan.util.test :as tt])) @@ -212,7 +213,7 @@ :table_id (data/id :venues) :fingerprint nil :fingerprint_version 1 - :last_analyzed (u/->Timestamp "2017-08-09")}] + :last_analyzed (du/->Timestamp "2017-08-09")}] (with-redefs [i/latest-fingerprint-version 3 sample/sample-fields (constantly [[field [1 2 3 4 5]]]) fingerprint/fingerprint (constantly {:experimental {:fake-fingerprint? true}})] diff --git a/test/metabase/sync/analyze_test.clj b/test/metabase/sync/analyze_test.clj index 04b0ae514c8..df55f111cd0 100644 --- a/test/metabase/sync/analyze_test.clj +++ b/test/metabase/sync/analyze_test.clj @@ -10,11 +10,12 @@ [sync-metadata :as sync-metadata]] [metabase.test.data :as data] [metabase.util :as u] + [metabase.util.date :as du] [toucan.db :as db] [toucan.util.test :as tt])) (def ^:private fake-analysis-completion-date - (u/->Timestamp "2017-08-01")) + (du/->Timestamp "2017-08-01")) ;; Check that Fields do *not* get analyzed if they're not newly created and fingerprint version is current (expect @@ -61,7 +62,7 @@ (expect #{"Current fingerprint, not analyzed"} (with-redefs [i/latest-fingerprint-version Short/MAX_VALUE - u/new-sql-timestamp (constantly (u/->Timestamp "1999-01-01"))] + du/new-sql-timestamp (constantly (du/->Timestamp "1999-01-01"))] (tt/with-temp* [Table [table] Field [_ {:table_id (u/get-id table) :name "Current fingerprint, not analyzed" @@ -70,7 +71,7 @@ Field [_ {:table_id (u/get-id table) :name "Current fingerprint, already analzed" :fingerprint_version Short/MAX_VALUE - :last_analyzed (u/->Timestamp "2017-08-09")}] + :last_analyzed (du/->Timestamp "2017-08-09")}] Field [_ {:table_id (u/get-id table) :name "Old fingerprint, not analyzed" :fingerprint_version (dec Short/MAX_VALUE) @@ -78,6 +79,6 @@ Field [_ {:table_id (u/get-id table) :name "Old fingerprint, already analzed" :fingerprint_version (dec Short/MAX_VALUE) - :last_analyzed (u/->Timestamp "2017-08-09")}]] + :last_analyzed (du/->Timestamp "2017-08-09")}]] (#'analyze/update-fields-last-analyzed! table) - (db/select-field :name Field :last_analyzed (u/new-sql-timestamp))))) + (db/select-field :name Field :last_analyzed (du/new-sql-timestamp))))) diff --git a/test/metabase/task/sync_databases_test.clj b/test/metabase/task/sync_databases_test.clj index 10b7969357a..0f032d795b7 100644 --- a/test/metabase/task/sync_databases_test.clj +++ b/test/metabase/task/sync_databases_test.clj @@ -8,6 +8,7 @@ [metabase.task.sync-databases :as sync-db] [metabase.test.util :as tu] [metabase.util :as u] + [metabase.util.date :as du] [toucan.db :as db] [toucan.util.test :as tt]) (:import [metabase.task.sync_databases SyncAndAnalyzeDatabase UpdateFieldValues])) @@ -155,7 +156,7 @@ :ran-update-field-values? (not (zero? @update-field-values-counter))}))))) (defn- cron-schedule-for-next-year [] - (format "0 15 10 * * ? %d" (inc (u/date-extract :year)))) + (format "0 15 10 * * ? %d" (inc (du/date-extract :year)))) ;; Make sure that a database that *is* marked full sync *will* get analyzed (expect diff --git a/test/metabase/test/data/bigquery.clj b/test/metabase/test/data/bigquery.clj index b6318d19b17..aee81562e0d 100644 --- a/test/metabase/test/data/bigquery.clj +++ b/test/metabase/test/data/bigquery.clj @@ -11,6 +11,7 @@ [datasets :as datasets] [interface :as i]] [metabase.util :as u] + [metabase.util.date :as du] [metabase.util.schema :as su] [schema.core :as s]) (:import com.google.api.client.util.DateTime @@ -94,7 +95,7 @@ "Convert the HoneySQL form we normally use to wrap a `Timestamp` to a Google `DateTime`." [{[{s :literal}] :args}] {:pre [(string? s) (seq s)]} - (DateTime. (u/->Timestamp (str/replace s #"'" "")))) + (DateTime. (du/->Timestamp (str/replace s #"'" "")))) (defn- insert-data! [^String dataset-id, ^String table-id, row-maps] diff --git a/test/metabase/test/data/generic_sql.clj b/test/metabase/test/data/generic_sql.clj index b21330c2e07..e6c4522d770 100644 --- a/test/metabase/test/data/generic_sql.clj +++ b/test/metabase/test/data/generic_sql.clj @@ -11,7 +11,9 @@ [metabase.driver.generic-sql.query-processor :as sqlqp] [metabase.test.data.interface :as i] [metabase.util :as u] - [metabase.util.honeysql-extensions :as hx]) + [metabase.util + [date :as du] + [honeysql-extensions :as hx]]) (:import clojure.lang.Keyword java.sql.SQLException [metabase.test.data.interface DatabaseDefinition FieldDefinition TableDefinition])) @@ -173,7 +175,7 @@ (zipmap fields-for-insert (for [v row] (if (and (not (instance? java.sql.Time v)) (instance? java.util.Date v)) - (u/->Timestamp v) + (du/->Timestamp v) v)))))) (defn load-data-add-ids @@ -322,7 +324,7 @@ (execute-sql! driver :db dbdef (s/join ";\n" (map hx/unescape-dots @statements)))) ;; Now load the data for each Table (doseq [tabledef table-definitions] - (u/profile (format "load-data for %s %s %s" (name driver) (:database-name dbdef) (:table-name tabledef)) + (du/profile (format "load-data for %s %s %s" (name driver) (:database-name dbdef) (:table-name tabledef)) (load-data! driver dbdef tabledef)))) (def IDriverTestExtensionsMixin diff --git a/test/metabase/test/data/sqlite.clj b/test/metabase/test/data/sqlite.clj index 0c2f3839f44..c637165c413 100644 --- a/test/metabase/test/data/sqlite.clj +++ b/test/metabase/test/data/sqlite.clj @@ -4,7 +4,9 @@ [generic-sql :as generic] [interface :as i]] [metabase.util :as u] - [metabase.util.honeysql-extensions :as hx]) + [metabase.util + [date :as du] + [honeysql-extensions :as hx]]) (:import metabase.driver.sqlite.SQLiteDriver)) (defn- database->connection-details [context dbdef] @@ -29,10 +31,10 @@ (into {} (for [[k v] row] [k (cond (instance? java.sql.Time v) - (hsql/call :time (hx/literal (u/format-time v "UTC"))) + (hsql/call :time (hx/literal (du/format-time v "UTC"))) (instance? java.util.Date v) - (hsql/call :datetime (hx/literal (u/date->iso-8601 v))) + (hsql/call :datetime (hx/literal (du/date->iso-8601 v))) :else v)])))))) diff --git a/test/metabase/util/date_test.clj b/test/metabase/util/date_test.clj new file mode 100644 index 00000000000..2cacfd3af7d --- /dev/null +++ b/test/metabase/util/date_test.clj @@ -0,0 +1,85 @@ +(ns metabase.util.date-test + (:require [expectations :refer :all] + [metabase.util.date :refer :all])) + +;;; Date stuff + +(def ^:private ^:const saturday-the-31st #inst "2005-12-31T19:05:55") +(def ^:private ^:const sunday-the-1st #inst "2006-01-01T04:18:26") + +(expect false (is-temporal? nil)) +(expect false (is-temporal? 123)) +(expect false (is-temporal? "abc")) +(expect false (is-temporal? [1 2 3])) +(expect false (is-temporal? {:a "b"})) +(expect true (is-temporal? saturday-the-31st)) + +(expect saturday-the-31st (->Timestamp (->Date saturday-the-31st))) +(expect saturday-the-31st (->Timestamp (->Calendar saturday-the-31st))) +(expect saturday-the-31st (->Timestamp (->Calendar (.getTime saturday-the-31st)))) +(expect saturday-the-31st (->Timestamp (.getTime saturday-the-31st))) +(expect saturday-the-31st (->Timestamp "2005-12-31T19:05:55+00:00")) + +(expect nil (->iso-8601-datetime nil nil)) +(expect "2005-12-31T19:05:55.000Z" (->iso-8601-datetime saturday-the-31st nil)) +(expect "2005-12-31T11:05:55.000-08:00" (->iso-8601-datetime saturday-the-31st "US/Pacific")) +(expect "2006-01-01T04:05:55.000+09:00" (->iso-8601-datetime saturday-the-31st "Asia/Tokyo")) + + +(expect 5 (date-extract :minute-of-hour saturday-the-31st "UTC")) +(expect 19 (date-extract :hour-of-day saturday-the-31st "UTC")) +(expect 7 (date-extract :day-of-week saturday-the-31st "UTC")) +(expect 1 (date-extract :day-of-week sunday-the-1st "UTC")) +(expect 31 (date-extract :day-of-month saturday-the-31st "UTC")) +(expect 365 (date-extract :day-of-year saturday-the-31st "UTC")) +(expect 53 (date-extract :week-of-year saturday-the-31st "UTC")) +(expect 12 (date-extract :month-of-year saturday-the-31st "UTC")) +(expect 4 (date-extract :quarter-of-year saturday-the-31st "UTC")) +(expect 2005 (date-extract :year saturday-the-31st "UTC")) + +(expect 5 (date-extract :minute-of-hour saturday-the-31st "US/Pacific")) +(expect 11 (date-extract :hour-of-day saturday-the-31st "US/Pacific")) +(expect 7 (date-extract :day-of-week saturday-the-31st "US/Pacific")) +(expect 7 (date-extract :day-of-week sunday-the-1st "US/Pacific")) +(expect 31 (date-extract :day-of-month saturday-the-31st "US/Pacific")) +(expect 365 (date-extract :day-of-year saturday-the-31st "US/Pacific")) +(expect 53 (date-extract :week-of-year saturday-the-31st "US/Pacific")) +(expect 12 (date-extract :month-of-year saturday-the-31st "US/Pacific")) +(expect 4 (date-extract :quarter-of-year saturday-the-31st "US/Pacific")) +(expect 2005 (date-extract :year saturday-the-31st "US/Pacific")) + +(expect 5 (date-extract :minute-of-hour saturday-the-31st "Asia/Tokyo")) +(expect 4 (date-extract :hour-of-day saturday-the-31st "Asia/Tokyo")) +(expect 1 (date-extract :day-of-week saturday-the-31st "Asia/Tokyo")) +(expect 1 (date-extract :day-of-week sunday-the-1st "Asia/Tokyo")) +(expect 1 (date-extract :day-of-month saturday-the-31st "Asia/Tokyo")) +(expect 1 (date-extract :day-of-year saturday-the-31st "Asia/Tokyo")) +(expect 1 (date-extract :week-of-year saturday-the-31st "Asia/Tokyo")) +(expect 1 (date-extract :month-of-year saturday-the-31st "Asia/Tokyo")) +(expect 1 (date-extract :quarter-of-year saturday-the-31st "Asia/Tokyo")) +(expect 2006 (date-extract :year saturday-the-31st "Asia/Tokyo")) + + +(expect #inst "2005-12-31T19:05" (date-trunc :minute saturday-the-31st "UTC")) +(expect #inst "2005-12-31T19:00" (date-trunc :hour saturday-the-31st "UTC")) +(expect #inst "2005-12-31" (date-trunc :day saturday-the-31st "UTC")) +(expect #inst "2005-12-25" (date-trunc :week saturday-the-31st "UTC")) +(expect #inst "2006-01-01" (date-trunc :week sunday-the-1st "UTC")) +(expect #inst "2005-12-01" (date-trunc :month saturday-the-31st "UTC")) +(expect #inst "2005-10-01" (date-trunc :quarter saturday-the-31st "UTC")) + +(expect #inst "2005-12-31T19:05" (date-trunc :minute saturday-the-31st "Asia/Tokyo")) +(expect #inst "2005-12-31T19:00" (date-trunc :hour saturday-the-31st "Asia/Tokyo")) +(expect #inst "2006-01-01+09:00" (date-trunc :day saturday-the-31st "Asia/Tokyo")) +(expect #inst "2006-01-01+09:00" (date-trunc :week saturday-the-31st "Asia/Tokyo")) +(expect #inst "2006-01-01+09:00" (date-trunc :week sunday-the-1st "Asia/Tokyo")) +(expect #inst "2006-01-01+09:00" (date-trunc :month saturday-the-31st "Asia/Tokyo")) +(expect #inst "2006-01-01+09:00" (date-trunc :quarter saturday-the-31st "Asia/Tokyo")) + +(expect #inst "2005-12-31T19:05" (date-trunc :minute saturday-the-31st "US/Pacific")) +(expect #inst "2005-12-31T19:00" (date-trunc :hour saturday-the-31st "US/Pacific")) +(expect #inst "2005-12-31-08:00" (date-trunc :day saturday-the-31st "US/Pacific")) +(expect #inst "2005-12-25-08:00" (date-trunc :week saturday-the-31st "US/Pacific")) +(expect #inst "2005-12-25-08:00" (date-trunc :week sunday-the-1st "US/Pacific")) +(expect #inst "2005-12-01-08:00" (date-trunc :month saturday-the-31st "US/Pacific")) +(expect #inst "2005-10-01-08:00" (date-trunc :quarter saturday-the-31st "US/Pacific")) diff --git a/test/metabase/util_test.clj b/test/metabase/util_test.clj index 90647a34cc8..a618a245c91 100644 --- a/test/metabase/util_test.clj +++ b/test/metabase/util_test.clj @@ -3,89 +3,6 @@ (:require [expectations :refer :all] [metabase.util :refer :all])) - -;;; Date stuff - -(def ^:private ^:const saturday-the-31st #inst "2005-12-31T19:05:55") -(def ^:private ^:const sunday-the-1st #inst "2006-01-01T04:18:26") - -(expect false (is-temporal? nil)) -(expect false (is-temporal? 123)) -(expect false (is-temporal? "abc")) -(expect false (is-temporal? [1 2 3])) -(expect false (is-temporal? {:a "b"})) -(expect true (is-temporal? saturday-the-31st)) - -(expect saturday-the-31st (->Timestamp (->Date saturday-the-31st))) -(expect saturday-the-31st (->Timestamp (->Calendar saturday-the-31st))) -(expect saturday-the-31st (->Timestamp (->Calendar (.getTime saturday-the-31st)))) -(expect saturday-the-31st (->Timestamp (.getTime saturday-the-31st))) -(expect saturday-the-31st (->Timestamp "2005-12-31T19:05:55+00:00")) - -(expect nil (->iso-8601-datetime nil nil)) -(expect "2005-12-31T19:05:55.000Z" (->iso-8601-datetime saturday-the-31st nil)) -(expect "2005-12-31T11:05:55.000-08:00" (->iso-8601-datetime saturday-the-31st "US/Pacific")) -(expect "2006-01-01T04:05:55.000+09:00" (->iso-8601-datetime saturday-the-31st "Asia/Tokyo")) - - -(expect 5 (date-extract :minute-of-hour saturday-the-31st "UTC")) -(expect 19 (date-extract :hour-of-day saturday-the-31st "UTC")) -(expect 7 (date-extract :day-of-week saturday-the-31st "UTC")) -(expect 1 (date-extract :day-of-week sunday-the-1st "UTC")) -(expect 31 (date-extract :day-of-month saturday-the-31st "UTC")) -(expect 365 (date-extract :day-of-year saturday-the-31st "UTC")) -(expect 53 (date-extract :week-of-year saturday-the-31st "UTC")) -(expect 12 (date-extract :month-of-year saturday-the-31st "UTC")) -(expect 4 (date-extract :quarter-of-year saturday-the-31st "UTC")) -(expect 2005 (date-extract :year saturday-the-31st "UTC")) - -(expect 5 (date-extract :minute-of-hour saturday-the-31st "US/Pacific")) -(expect 11 (date-extract :hour-of-day saturday-the-31st "US/Pacific")) -(expect 7 (date-extract :day-of-week saturday-the-31st "US/Pacific")) -(expect 7 (date-extract :day-of-week sunday-the-1st "US/Pacific")) -(expect 31 (date-extract :day-of-month saturday-the-31st "US/Pacific")) -(expect 365 (date-extract :day-of-year saturday-the-31st "US/Pacific")) -(expect 53 (date-extract :week-of-year saturday-the-31st "US/Pacific")) -(expect 12 (date-extract :month-of-year saturday-the-31st "US/Pacific")) -(expect 4 (date-extract :quarter-of-year saturday-the-31st "US/Pacific")) -(expect 2005 (date-extract :year saturday-the-31st "US/Pacific")) - -(expect 5 (date-extract :minute-of-hour saturday-the-31st "Asia/Tokyo")) -(expect 4 (date-extract :hour-of-day saturday-the-31st "Asia/Tokyo")) -(expect 1 (date-extract :day-of-week saturday-the-31st "Asia/Tokyo")) -(expect 1 (date-extract :day-of-week sunday-the-1st "Asia/Tokyo")) -(expect 1 (date-extract :day-of-month saturday-the-31st "Asia/Tokyo")) -(expect 1 (date-extract :day-of-year saturday-the-31st "Asia/Tokyo")) -(expect 1 (date-extract :week-of-year saturday-the-31st "Asia/Tokyo")) -(expect 1 (date-extract :month-of-year saturday-the-31st "Asia/Tokyo")) -(expect 1 (date-extract :quarter-of-year saturday-the-31st "Asia/Tokyo")) -(expect 2006 (date-extract :year saturday-the-31st "Asia/Tokyo")) - - -(expect #inst "2005-12-31T19:05" (date-trunc :minute saturday-the-31st "UTC")) -(expect #inst "2005-12-31T19:00" (date-trunc :hour saturday-the-31st "UTC")) -(expect #inst "2005-12-31" (date-trunc :day saturday-the-31st "UTC")) -(expect #inst "2005-12-25" (date-trunc :week saturday-the-31st "UTC")) -(expect #inst "2006-01-01" (date-trunc :week sunday-the-1st "UTC")) -(expect #inst "2005-12-01" (date-trunc :month saturday-the-31st "UTC")) -(expect #inst "2005-10-01" (date-trunc :quarter saturday-the-31st "UTC")) - -(expect #inst "2005-12-31T19:05" (date-trunc :minute saturday-the-31st "Asia/Tokyo")) -(expect #inst "2005-12-31T19:00" (date-trunc :hour saturday-the-31st "Asia/Tokyo")) -(expect #inst "2006-01-01+09:00" (date-trunc :day saturday-the-31st "Asia/Tokyo")) -(expect #inst "2006-01-01+09:00" (date-trunc :week saturday-the-31st "Asia/Tokyo")) -(expect #inst "2006-01-01+09:00" (date-trunc :week sunday-the-1st "Asia/Tokyo")) -(expect #inst "2006-01-01+09:00" (date-trunc :month saturday-the-31st "Asia/Tokyo")) -(expect #inst "2006-01-01+09:00" (date-trunc :quarter saturday-the-31st "Asia/Tokyo")) - -(expect #inst "2005-12-31T19:05" (date-trunc :minute saturday-the-31st "US/Pacific")) -(expect #inst "2005-12-31T19:00" (date-trunc :hour saturday-the-31st "US/Pacific")) -(expect #inst "2005-12-31-08:00" (date-trunc :day saturday-the-31st "US/Pacific")) -(expect #inst "2005-12-25-08:00" (date-trunc :week saturday-the-31st "US/Pacific")) -(expect #inst "2005-12-25-08:00" (date-trunc :week sunday-the-1st "US/Pacific")) -(expect #inst "2005-12-01-08:00" (date-trunc :month saturday-the-31st "US/Pacific")) -(expect #inst "2005-10-01-08:00" (date-trunc :quarter saturday-the-31st "US/Pacific")) - ;;; ## tests for HOST-UP? (expect true -- GitLab