(ns cljs-time.format "### Utilities for parsing and unparsing DateTimes as Strings. Parsing and printing are controlled by formatters. You can either use one of the built in ISO 8601 and a single RFC 822 formatters or define your own, e.g.: (def built-in-formatter (formatters :basic-date-time)) (def custom-formatter (formatter \"yyyyMMdd\")) To see a list of available built-in formatters and an example of a date-time printed in their format: (show-formatters) Once you have a formatter, parsing and printing are strait-forward: => (parse custom-formatter \"20100311\") # => (unparse custom-formatter (date-time 2010 10 3)) \"20101003\" By default the parse function always returns a DateTime instance with a UTC time zone, and the unparse function always represents a given DateTime instance in UTC. A formatter can be modified to different timezones, locales, etc with the functions with-zone, with-locale, with-chronology, and with-pivot-year." (:require [cljs-time.internal.core :refer [index-of valid-date? format zero-pad]] [cljs-time.core :as time] [clojure.set :refer [difference]] [clojure.string :as string] [goog.date :as date] [goog.date.duration :as duration] [goog.string :as gstring] [goog.string.format])) (def months ["January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December"]) (def days ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"]) (defn abbreviate [n s] (subs s 0 n)) (def ^{:doc "**Note: not all formatters have been implemented yet.** The pattern syntax is mostly compatible with java.text.SimpleDateFormat - time zone names cannot be parsed and a few more symbols are supported. All ASCII letters are reserved as pattern letters, which are defined as follows: Symbol Meaning Presentation Examples ------ ------- ------------ ------- G era text AD C century of era (>=0) number 20 Y year of era (>=0) year 1996 x weekyear year 1996 w week of weekyear number 27 e day of week number 2 E day of week text Tuesday; Tue y year year 1996 D day of year number 189 M month of year month July; Jul; 07 d day of month number 10 a halfday of day text PM K hour of halfday (0~11) number 0 h clockhour of halfday (1~12) number 12 H hour of day (0~23) number 0 k clockhour of day (1~24) number 24 m minute of hour number 30 s second of minute number 55 S fraction of second number 978 a meridiem text am; pm A meridiem text AM; PM z time zone text Pacific Standard Time; PST Z time zone offset/id zone -0800; -08:00; America/Los_Angeles ' escape for text delimiter '' single quote literal ' The count of pattern letters determine the format. **Text:** If the number of pattern letters is 4 or more, the full form is used; otherwise a short or abbreviated form is used if available. **Number:** The minimum number of digits. Shorter numbers are zero-padded to this amount. **Year:** Numeric presentation for year and weekyear fields are handled specially. For example, if the count of 'y' is 2, the year will be displayed as the zero-based year of the century, which is two digits. **Month:** 3 or over, use text, otherwise use number. **Zone:** 'Z' outputs offset without a colon, 'ZZ' outputs the offset with a colon, 'ZZZ' or more outputs the zone id. **Zone names:** Time zone names ('z') cannot be parsed. Any characters in the pattern that are not in the ranges of ['a'..'z'] and ['A'..'Z'] will be treated as quoted text. For instance, characters like ':', '.', ' ', '#' and '?' will appear in the resulting time text even they are not embraced within single quotes."} date-formatters (let [d #(.getDate %) M #(inc (.getMonth %)) y #(.getYear %) h #(let [hr (mod (.getHours %) 12)] (if (zero? hr) 12 hr)) a #(if (< (.getHours %) 12) "am" "pm") A #(if (< (.getHours %) 12) "AM" "PM") H #(.getHours %) m #(.getMinutes %) s #(.getSeconds %) S #(.getMilliseconds %) Z #(.getTimezoneOffsetString %) doy #(.getDayOfYear %) dow #(.getDay %)] {"d" d "dd" #(zero-pad (d %)) "dth" #(let [d (d %)] (str d (case d 1 "st" 2 "nd" 3 "rd" 21 "st" 22 "nd" 23 "rd" 31 "st" "th"))) "dow" #(days (dow %)) "D" doy "DD" doy "DDD" doy "EEE" #(abbreviate 3 (days (dow %))) "EEEE" #(days (dow %)) "M" M "MM" #(zero-pad (M %)) "MMM" #(abbreviate 3 (months (dec (M %)))) "MMMM" #(months (dec (M %))) "yyyy" y "YYYY" y "yy" #(mod (y %) 100) "YY" #(mod (y %) 100) "xxxx" y "a" a "A" A "h" h "H" H "m" m "s" s "S" S "hh" #(zero-pad (h %)) "HH" #(zero-pad (H %)) "mm" #(zero-pad (m %)) "ss" #(zero-pad (s %)) "SSS" #(zero-pad (S %) 3) "Z" Z "ZZ" Z "ww" #(zero-pad (.getWeekNumber %)) "e" dow})) (defn timezone-adjustment [d timezone-string] (let [[_ sign hh mm] (string/split timezone-string #"Z|(?:([-+])(\d{2})(?::?(\d{2}))?)$")] (when (and sign hh mm) (let [sign (cond (= sign "-") time/plus (= sign "+") time/minus) [hh mm] (map #(js/parseInt % 10) [hh mm]) adjusted (-> d (sign (time/hours hh)) (sign (time/minutes mm)))] (.setTime d (.getTime adjusted)))) d)) (def date-parsers (let [parse-int #(js/parseInt % 10) assoc-fn (fn [kw] #(assoc %1 kw (parse-int %2))) y (assoc-fn :years) d (assoc-fn :days) M #(assoc %1 :months (dec (parse-int %2))) h #(assoc %1 :hours (mod (parse-int %2) 12)) a (fn [{:keys [hours] :as date} x] (if (#{"pm" "p"} (string/lower-case x)) (assoc date :hours (let [hours (+ 12 hours)] (if (= hours 24) 0 hours))) date)) H (assoc-fn :hours) m (assoc-fn :minutes) s (assoc-fn :seconds) S (assoc-fn :millis) MMM #(let [full (first (filter (fn [m] (re-seq (re-pattern (str "^" %2)) m)) months))] (M %1 (str (inc (index-of months full))))) MMMM #(M %1 (str (inc (index-of months %2)))) skip (fn [x & args] x) tz #(assoc %1 :time-zone %2)] {"d" ["(\\d{1,2})" d] "dd" ["(\\d{2})" d] "D" ["(\\d{1,3})" d] "DD" ["(\\d{2,3})" d] "DDD" ["(\\d{3})" d] "dth" ["(\\d{1,2})(?:st|nd|rd|th)" d] "M" ["(\\d{1,2})" M] "MM" ["((?:\\d{2})|(?:\\b\\d{1,2}\\b))" M] "y" ["(\\d{1,4})" y] "yy" ["(\\d{2,4})" y] "yyyy" ["(\\d{4})" y] "Y" ["(\\d{1,4})" y] "YY" ["(\\d{2,4})" y] "YYYY" ["(\\d{4})" y] "MMM" [(str \( (string/join \| (map (partial abbreviate 3) months)) \)) MMM] "MMMM" [(str \( (string/join \| months) \)) MMMM] "E" [(str \( (string/join \| (map (partial abbreviate 3) days)) \)) skip] "EEE" [(str \( (string/join \| (map (partial abbreviate 3) days)) \)) skip] "EEEE" [(str \( (string/join \| days) \)) skip] "dow" [(str \( (string/join \| days) \)) skip] "a" ["(am|pm|a|p|AM|PM|A|P)" a] "A" ["(am|pm|a|p|AM|PM|A|P)" a] "m" ["(\\d{1,2})" m] "s" ["(\\d{1,2})" s] "S" ["(\\d{1,2})" S] "h" ["(\\d{1,2})" h] "H" ["(\\d{1,2})" H] "hh" ["(\\d{2})" h] "HH" ["(\\d{2})" H] "mm" ["(\\d{2})" m] "ss" ["(\\d{2})" s] "SSS" ["(\\d{3})" S] "Z" ["((?:(?:\\+|-)\\d{2}:?\\d{2})|Z+)" tz] "ZZ" ["((?:(?:\\+|-)\\d{2}:\\d{2})|Z+)" tz]})) (def date-setters {:years #(.setYear %1 %2) :months #(.setMonth %1 %2) :days #(.setDate %1 %2) :hours #(.setHours %1 %2) :minutes #(.setMinutes %1 %2) :seconds #(.setSeconds %1 %2) :millis #(.setMilliseconds %1 %2) :time-zone timezone-adjustment}) (defn parser-sort-order-pred [parser] (index-of ["YYYY" "YY" "Y" "yyyy" "yy" "y" "d" "dd" "D" "DD" "DDD" "dth" "M" "MM" "MMM" "MMMM" "dow" "h" "H" "m" "s" "S" "hh" "HH" "mm" "ss" "a" "A" "SSS" "Z" "ZZ"] parser)) (def date-format-pattern (re-pattern (str "(" (string/join ")|(" (reverse (sort-by count (keys date-formatters)))) ")"))) (defn old-string-replace [s match replacement] (.replace s (js/RegExp. (.-source match) "g") replacement)) (defn date-parse-pattern [formatter] (-> formatter (old-string-replace #"'([^']+)'" "$1") (old-string-replace date-format-pattern #(first (date-parsers %))) re-pattern)) (defn- parser-fn [fmts] (fn [s] (->> (interleave (nfirst (re-seq (date-parse-pattern fmts) s)) (map first (re-seq date-format-pattern fmts))) (partition 2) (sort-by (comp parser-sort-order-pred second))))) (defn- formatter-fn [fmts formatters] (fn [date & [formatter-overrides]] (let [a (atom {:c 0})] [(old-string-replace fmts #"'([^']+)'" (fn [x s] (if (and (seq s) (= \' (first x)) (= \' (last x))) (let [{:keys [c]} @a k (str "&&&&" c)] (swap! a assoc-in [:replace k] (constantly s)) (swap! a update-in [:c] inc) k) x))) (-> (.-source date-format-pattern) (cond->> (:replace @a) (str "(" (string/join ")|(" (keys (:replace @a))) ")|")) (re-pattern)) #(((merge formatters formatter-overrides (:replace @a)) %) date)]))) (defn formatter ([fmts] (formatter fmts time/utc)) ([fmts dtz] (with-meta {:format-str fmts :formatters date-formatters} {:type ::formatter}))) (defn formatter-local [fmts] (with-meta {:format-str fmts :formatters (assoc date-formatters "Z" (constantly "") "ZZ" (constantly ""))} {:type ::formatter})) (defn not-implemented [sym] #(throw (clj->js {:name :not-implemented :message (format "%s not implemented yet" (name sym))}))) (defn with-default-year "Return a copy of a formatter that uses the given default year." [f default-year] (assoc f :default-year default-year)) (def ^{:doc "Map of ISO 8601 and a single RFC 822 formatters that can be used for parsing and, in most cases, printing. Note: due to current implementation limitations, timezone information cannot be kept. Although the correct offset will be applied to UTC time if supplied."} formatters {:basic-date (formatter "yyyyMMdd") :basic-date-time (formatter "yyyyMMdd'T'HHmmss.SSSZ") :basic-date-time-no-ms (formatter "yyyyMMdd'T'HHmmssZ") :basic-ordinal-date (formatter "yyyyDDD") :basic-ordinal-date-time (formatter "yyyyDDD'T'HHmmss.SSSZ") :basic-ordinal-date-time-no-ms (formatter "yyyyDDD'T'HHmmssZ") :basic-time (formatter "HHmmss.SSSZ") :basic-time-no-ms (formatter "HHmmssZ") :basic-t-time (formatter "'T'HHmmss.SSSZ") :basic-t-time-no-ms (formatter "'T'HHmmssZ") :basic-week-date (formatter "xxxx'W'wwe") :basic-week-date-time (formatter "xxxx'W'wwe'T'HHmmss.SSSZ") :basic-week-date-time-no-ms (formatter "xxxx'W'wwe'T'HHmmssZ") :date (formatter "yyyy-MM-dd") :date-element-parser (not-implemented 'dateElementParser) :date-hour (formatter "yyyy-MM-dd'T'HH") :date-hour-minute (formatter "yyyy-MM-dd'T'HH:mm") :date-hour-minute-second (formatter "yyyy-MM-dd'T'HH:mm:ss") :date-hour-minute-second-fraction (formatter "yyyy-MM-dd'T'HH:mm:ss.SSS") :date-hour-minute-second-ms (formatter "yyyy-MM-dd'T'HH:mm:ss.SSS") :date-opt-time (not-implemented 'dateOptionalTimeParser) :date-parser (not-implemented 'dateParser) :date-time (formatter "yyyy-MM-dd'T'HH:mm:ss.SSSZZ") :date-time-no-ms (formatter "yyyy-MM-dd'T'HH:mm:ssZZ") :date-time-parser (not-implemented 'dateTimeParser) :hour (formatter "HH") :hour-minute (formatter "HH:mm") :hour-minute-second (formatter "HH:mm:ss") :hour-minute-second-fraction (formatter "HH:mm:ss.SSS") :hour-minute-second-ms (formatter "HH:mm:ss.SSS") :local-date-opt-time (not-implemented 'localDateOptionalTimeParser) :local-date (not-implemented 'localDateParser) :local-time (not-implemented 'localTimeParser) :ordinal-date (formatter "yyyy-DDD") :ordinal-date-time (formatter "yyyy-DDD'T'HH:mm:ss.SSSZZ") :ordinal-date-time-no-ms (formatter "yyyy-DDD'T'HH:mm:ssZZ") :time (formatter "HH:mm:ss.SSSZZ") :time-element-parser (not-implemented 'timeElementParser) :time-no-ms (formatter "HH:mm:ssZZ") :time-parser (formatter 'timeParser) :t-time (formatter "'T'HH:mm:ss.SSSZZ") :t-time-no-ms (formatter "'T'HH:mm:ssZZ") :week-date (formatter "xxxx-'W'ww-e") :week-date-time (formatter "xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ") :week-date-time-no-ms (formatter "xxxx-'W'ww-e'T'HH:mm:ssZZ") :weekyear (formatter "xxxx") :weekyear-week (formatter "xxxx-'W'ww") :weekyear-week-day (formatter "xxxx-'W'ww-e") :year (formatter "yyyy") :year-month (formatter "yyyy-MM") :year-month-day (formatter "yyyy-MM-dd") :rfc822 (formatter "EEE, dd MMM yyyy HH:mm:ss Z") :mysql (formatter "yyyy-MM-dd HH:mm:ss")}) (def ^{:private true} parsers #{:date-element-parser :date-opt-time :date-parser :date-time-parser :local-date-opt-time :local-date :local-time :time-element-parser :time-parser}) (def ^{:private true} printers (difference (set (keys formatters)) parsers)) (def part-splitter-regex #"(?:(?!(?:\+|-)\d{2}):(?!\d{2}$))|[^\w:]+|.[TW]|'[^']+'") (defprotocol IDateMap (date-map [date])) (extend-protocol IDateMap goog.date.Date (date-map [date] {:years 0 :months 0 :days 1}) goog.date.DateTime (date-map [date] {:years 0 :months 0 :days 1 :hours 0 :minutes 0 :seconds 0 :millis 0}) goog.date.UtcDateTime (date-map [date] {:years 0 :months 0 :days 1 :hours 0 :minutes 0 :seconds 0 :millis 0 :time-zone nil})) (defn parse* [constructor {:keys [format-str default-year] :as fmt} s] {:pre [(seq s)]} (let [min-parts (count (string/split s part-splitter-regex))] (let [parse-fn (parser-fn format-str) parse-seq (seq (map (fn [[a b]] [a (second (date-parsers b))]) (parse-fn s)))] (if (>= (count parse-seq) min-parts) (let [d (new constructor 0 0 0 0 0 0 0) empty (assoc (date-map d) :years (or default-year 0)) setters (select-keys date-setters (keys empty))] (->> parse-seq (reduce (fn [date [part do-parse]] (do-parse date part)) empty) valid-date? (merge-with #(%1 d %2) setters)) d) (throw (ex-info "The parser could not match the input string." {:type :parser-no-match})))))) (defn parse "Returns a DateTime instance in the UTC time zone obtained by parsing the given string according to the given formatter." ([fmt s] (parse* goog.date.UtcDateTime fmt s)) ([s] (first (for [f (vals formatters) :let [d (try (parse f s) (catch :default _))] :when d] d)))) (defn parse-local "Returns a local DateTime instance obtained by parsing the given string according to the given formatter." ([fmt s] (parse* goog.date.DateTime fmt s)) ([s] (first (for [f (vals formatters) :let [d (try (parse-local f s) (catch js/Error _ nil))] :when d] d)))) (defn parse-local-date "Returns a local Date instance obtained by parsing the given string according to the given formatter." ([fmt s] (parse* goog.date.Date fmt s)) ([s] (first (for [f (vals formatters) :let [d (try (parse-local-date f s) (catch js/Error _ nil))] :when d] d)))) (defn unparse "Returns a string representing the given DateTime instance in UTC and in the form determined by the given formatter." [{:keys [format-str formatters]} dt] {:pre [(not (nil? dt)) (instance? goog.date.DateTime dt)]} (apply old-string-replace ((formatter-fn format-str formatters) dt))) (defn unparse-local "Returns a string representing the given local DateTime instance in the form determined by the given formatter." [{:keys [format-str formatters] :as fmt} dt] {:pre [(not (nil? dt)) (instance? goog.date.DateTime dt)]} (apply old-string-replace ((formatter-fn format-str formatters) dt (assoc date-formatters "Z" (constantly "") "ZZ" (constantly ""))))) (defn unparse-local-date "Returns a string representing the given local Date instance in the form determined by the given formatter." [{:keys [format-str formatters] :as fmt} dt] {:pre [(not (nil? dt)) (instance? goog.date.Date dt)]} (apply old-string-replace ((formatter-fn format-str formatters) dt (assoc date-formatters "Z" (constantly "") "ZZ" (constantly ""))))) (defn show-formatters "Shows how a given DateTime, or by default the current time, would be formatted with each of the available printing formatters." ([] (show-formatters (time/now))) ([dt] (doseq [p (sort printers)] (let [fmt (formatters p)] (print (format "%-40s%s\n" p (unparse fmt dt))))))) (defprotocol Mappable (instant->map [instant] "Returns a map representation of the given instant. It will contain the following keys: :years, :months, :days, :hours, :minutes and :seconds.")) (defn unparse-duration "Accepts a Period or Interval and outputs an absolute duration time in form of \"1 day\", \"2 hours\", \"20 minutes\", \"2 days 1 hour 15 minutes\" etc." [duration] (-> duration time/in-millis duration/format)) (defn- to-map [years months days hours minutes seconds millis] {:years years :months months :days days :hours hours :minutes minutes :seconds seconds :millis millis}) (extend-protocol Mappable goog.date.UtcDateTime (instant->map [dt] (to-map (.getYear dt) (inc (.getMonth dt)) (.getDate dt) (.getHours dt) (.getMinutes dt) (.getSeconds dt) (.getMilliseconds dt))) cljs-time.core.Period (instant->map [m] (time/->period m)) cljs-time.core.Interval (instant->map [m] (time/->period m)) cljs.core/PersistentArrayMap (instant->map [m] (case (:type (meta m)) :cljs-time.core/period m :cljs-time.core/interval (time/->period m))))