From 88eb6abdd68d8fc7dcdaf9f13cebb55f4bfaaf84 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 15 Apr 2021 17:03:36 +0200 Subject: [PATCH] :sparkles: Improve time related functions (frontend). --- frontend/package.json | 4 +- frontend/src/app/util/time.cljs | 226 +++++++++++++++++++++++++---- frontend/src/app/util/transit.cljs | 21 +++ frontend/yarn.lock | 18 +-- 4 files changed, 227 insertions(+), 42 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 31728cfb9..efb975945 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,11 +35,11 @@ "shadow-cljs": "^2.11.20" }, "dependencies": { - "date-fns": "^2.19.0", + "date-fns": "^2.21.1", "draft-js": "^0.11.7", "highlight.js": "^10.6.0", - "humanize-duration": "~3.25.0", "js-beautify": "^1.13.5", + "luxon": "^1.26.0", "mousetrap": "^1.6.5", "randomcolor": "^0.6.2", "react": "~17.0.1", diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs index 70668984c..968036558 100644 --- a/frontend/src/app/util/time.cljs +++ b/frontend/src/app/util/time.cljs @@ -6,47 +6,211 @@ (ns app.util.time (:require - ["date-fns/parseISO" :as dateFnsParseISO] - ["date-fns/formatISO" :as dateFnsFormatISO] - ["date-fns/format" :as dateFnsFormat] - ["date-fns/formatDistanceToNowStrict" :as dateFnsFormatDistanceToNowStrict] - ["date-fns/locale/fr" :as dateFnsLocalesFr] - ["date-fns/locale/en-US" :as dateFnsLocalesEnUs] - ["date-fns/locale/zh-CN" :as dateFnsLocalesZhCn] - ["date-fns/locale/es" :as dateFnsLocalesEs] - ["date-fns/locale/ru" :as dateFnsLocalesRu] + [cuerdas.core :as str] + ["luxon" :as lxn] + ["date-fns/formatDistanceToNowStrict" :default dateFnsFormatDistanceToNowStrict] + ["date-fns/locale/fr" :default dateFnsLocalesFr] + ["date-fns/locale/ca" :default dateFnsLocalesCa] + ["date-fns/locale/en-US" :default dateFnsLocalesEnUs] + ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn] + ["date-fns/locale/es" :default dateFnsLocalesEs] + ["date-fns/locale/tr" :default dateFnsLocalesTr] + ["date-fns/locale/ru" :default dateFnsLocalesRu] [app.util.object :as obj])) +(def DateTime lxn/DateTime) +(def Duration lxn/Duration) + +(defprotocol ITimeMath + (plus [_ o]) + (minus [_ o])) + +(defprotocol ITimeFormat + (format [_ fmt])) + +(defn duration? + [o] + (instance? Duration o)) + +(defn datetime? + [o] + (instance? DateTime o)) + +(defn duration + [o] + (cond + (integer? o) (.fromMillis Duration o) + (duration? o) o + (string? o) (.fromISO Duration o) + (map? o) (.fromObject Duration (clj->js o)) + :else (throw (js/Error. "unexpected arguments")))) + +(defn datetime + ([s] (datetime s nil)) + ([s {:keys [zone force-zone] :or {zone "local" force-zone false}}] + (cond + (integer? s) + (.fromMillis ^js DateTime s #js {:zone zone :setZone force-zone}) + + (map? s) + (.fromObject ^js DateTime (-> (clj->js s) + (obj/set! "zone" zone) + (obj/set! "setZone" force-zone))) + + :else + (throw (js/Error. "invalid arguments"))))) + +(defn epoch->datetime + ([seconds] (epoch->datetime seconds nil)) + ([seconds {:keys [zone force-zone] :or {zone "local" force-zone false}}] + (.fromSeconds ^js DateTime seconds #js {:zone zone :setZone force-zone}))) + +(defn iso->datetime + "A faster option for transit date parsing." + [s] + (.fromISO ^js DateTime s #js {:zone "local"})) + +(defn parse-datetime + ([s] (parse-datetime s :iso nil)) + ([s fmt] (parse-datetime s fmt nil)) + ([s fmt {:keys [zone force-zone] :or {zone "local" force-zone false}}] + (if (string? fmt) + (.fromFormat ^js DateTime s fmt #js {:zone zone :setZone force-zone}) + (case fmt + :iso (.fromISO ^js DateTime s #js {:zone zone :setZone force-zone}) + :rfc2822 (.fromRFC2822 ^js DateTime s #js {:zone zone :setZone force-zone}) + :http (.fromHTTP ^js DateTime s #js {:zone zone :setZone force-zone}))))) + +(defn now + [] + (.local ^js DateTime)) + +(defn utc-now + [] + (.utc ^js DateTime)) + +(defn ->utc + [dt] + (.toUTC ^js dt)) + +(defn diff + [dt1 dt2] + (.diff ^js dt1 dt2)) + +(extend-protocol IEquiv + DateTime + (-equiv [it other] + (.equals it other)) + + Duration + (-equiv [it other] + (.equals it other))) + +(extend-protocol Inst + DateTime + (inst-ms* [inst] (.toMillis ^js inst)) + + Duration + (inst-ms* [inst] (.toMillis ^js inst))) + +(extend-protocol IComparable + DateTime + (-compare [it other] + (if ^boolean (.equals it other) + 0 + (if (< (inst-ms it) (inst-ms other)) -1 1))) + + Duration + (-compare [it other] + (if ^boolean (.equals it other) + 0 + (if (< (inst-ms it) (inst-ms other)) -1 1)))) + +(extend-protocol ITimeMath + DateTime + (plus [it o] + (if (map? o) + (.plus ^js it (clj->js o)) + (.plus ^js it o))) + + (minus [it o] + (if (map? o) + (.minus ^js it (clj->js o)) + (.minus ^js it o))) + + Duration + (plus [it o] + (if (map? o) + (.plus ^js it (clj->js o)) + (.plus ^js it o))) + + (minus [it o] + (if (map? o) + (.minus ^js it (clj->js o)) + (.minus ^js it o)))) + +(extend-protocol IPrintWithWriter + DateTime + (-pr-writer [p writer opts] + (-write writer (str/fmt "#stks/datetime \"%s\"" (format p :iso)))) + + Duration + (-pr-writer [p writer opts] + (-write writer (str/fmt "#stks/duration \"%s\"" (format p :iso))))) + +(defn- resolve-format + [v] + (case v + :time-24-simple (.-TIME_24_SIMPLE ^js DateTime) + :datetime-short (.-DATETIME_SHORT ^js DateTime) + :datetime-med (.-DATETIME_MED ^js DateTime) + :datetime-full (.-DATETIME_FULL ^js DateTime) + :date-full (.-DATE_FULL ^js DateTime) + :date-med-with-weekday (.-DATE_MED_WITH_WEEKDAY ^js DateTime) + v)) + +(defn- format-datetime + [dt fmt] + (case fmt + :iso (.toISO ^js dt) + :rfc2822 (.toRFC2822 ^js dt) + :http (.toHTTP ^js dt) + :json (.toJSON ^js dt) + :date (.toJSDate ^js dt) + :epoch (js/Math.floor (.toSeconds ^js dt)) + :millis (.toMillis ^js dt) + (let [f (resolve-format fmt)] + (if (string? f) + (.toFormat ^js dt f) + (.toLocaleString ^js dt f))))) + +(extend-protocol ITimeFormat + DateTime + (format [it fmt] + (format-datetime it fmt)) + + Duration + (format [it fmt] + (case fmt + :iso (.toISO it) + :json (.toJSON it) + (.toFormat ^js it fmt)))) + (def ^:private locales #js {:en dateFnsLocalesEnUs :fr dateFnsLocalesFr + :tr dateFnsLocalesTr :es dateFnsLocalesEs + :ca dateFnsLocalesCa :ru dateFnsLocalesRu :zh_cn dateFnsLocalesZhCn}) -(defn now - "Return the current Instant." - [] - (js/Date.)) - -(defn parse - [v] - (^js dateFnsParseISO v)) - -(defn format-iso - [v] - (^js dateFnsFormatISO v)) - -(defn format - ([v fmt] (format v fmt nil)) - ([v fmt {:keys [locale] :or {locale "en"}}] - (dateFnsFormat v fmt #js {:locale (obj/get locales locale)}))) - (defn timeago ([v] (timeago v nil)) ([v {:keys [locale] :or {locale "en"}}] (when v - (->> #js {:includeSeconds true - :addSuffix true - :locale (obj/get locales locale)} - (dateFnsFormatDistanceToNowStrict v))))) + (let [v (if (datetime? v) (format v :date) v)] + (->> #js {:includeSeconds true + :addSuffix true + :locale (obj/get locales locale)} + (dateFnsFormatDistanceToNowStrict v)))))) diff --git a/frontend/src/app/util/transit.cljs b/frontend/src/app/util/transit.cljs index 49ebf8152..24d385176 100644 --- a/frontend/src/app/util/transit.cljs +++ b/frontend/src/app/util/transit.cljs @@ -10,6 +10,7 @@ [cognitect.transit :as t] [linked.core :as lk] [linked.set :as lks] + [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.matrix :as gmt] [app.util.time :as dt])) @@ -67,6 +68,22 @@ (def ordered-set-read-handler (t/read-handler #(into (lk/set) %))) +(def date-read-handler + (t/read-handler (fn [value] (-> value (js/parseInt 10) (dt/datetime))))) + +(def duration-read-handler + (t/read-handler (fn [value] (dt/duration value)))) + +(def date-write-handler + (t/write-handler + (constantly "m") + (fn [v] (str (inst-ms v))))) + +(def duration-write-handler + (t/write-handler + (constantly "duration") + (fn [v] (inst-ms v)))) + ;; --- Transit Handlers (def ^:privare +read-handlers+ @@ -75,11 +92,15 @@ "ordered-set" ordered-set-read-handler "jsonblob" blob-read-handler "matrix" matrix-read-handler + "m" date-read-handler + "duration" duration-read-handler "point" point-read-handler}) (def ^:privare +write-handlers+ {gmt/Matrix matrix-write-handler Blob blob-write-handler + dt/DateTime date-write-handler + dt/Duration duration-write-handler lks/LinkedSet ordered-set-write-handler gpt/Point point-write-handler}) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 119d530e1..1d05ca47f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1189,10 +1189,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1" - integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg== +date-fns@^2.21.1: + version "2.21.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0" + integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA== dateformat@^3.0.3: version "3.0.3" @@ -2368,11 +2368,6 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -humanize-duration@~3.25.0: - version "3.25.1" - resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.25.1.tgz#50e12bf4b3f515ec91106107ee981e8cfe955d6f" - integrity sha512-P+dRo48gpLgc2R9tMRgiDRNULPKCmqFYgguwqOO2C0fjO35TgdURDQDANSR1Nt92iHlbHGMxOTnsB8H8xnMa2Q== - iconv-lite@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" @@ -3129,6 +3124,11 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" +luxon@^1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.26.0.tgz#d3692361fda51473948252061d0f8561df02b578" + integrity sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A== + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6"