time unified time

Author: Chris Zheng  (z@caudate.me)
Date: 27 November 2018
Repository: https://github.com/zcaudate/hara
Version: 3.0.2

1    Introduction

hara.time is a unified framework for representating time on the JVM.

1.1    Installation

Add to project.clj dependencies:

[hara/time "3.0.2"]

All functions are in the hara.time namespace.

 (use (quote hara.time))

2    General



calendar ^

creates a calendar to be used by the base date classes

v 3.0
(defn calendar
  [^Date date ^TimeZone timezone]
  (doto (Calendar/getInstance timezone)
    (.setTime date)))
link
(-> ^Calendar (calendar (Date. 0) (TimeZone/getTimeZone "GMT")) (.getTime)) => #inst "1970-01-01T00:00:00.000-00:00"

default-type ^

accesses the default type for datetime

v 3.0
(defn default-type
  ([] *default-type*)
  ([cls]
   (alter-var-root #'*default-type*
                   (constantly cls))))
link
(default-type) ;; getter => clojure.lang.PersistentArrayMap (default-type Long) ;; setter => java.lang.Long

duration? ^

checks if an object implements the duration protocol

v 3.0
(defn duration?
  [obj]
  (satisfies? protocol.time/IDuration obj))
link
(t/duration? 0) => true (t/duration? {:weeks 1}) => true

epoch ^

returns the beginning of unix epoch

v 3.0
(defn epoch
  ([] (epoch {}))
  ([opts] (from-long 0 (merge {:type common/*default-type*}
                              opts))))
link
(t/epoch {:type Date}) => #inst "1970-01-01T00:00:00.000-00:00"

instant? ^

checks if an object implements the instant protocol

v 3.0
(defn instant?
  [obj]
  (satisfies? protocol.time/IInstant obj))
link
(t/instant? 0) => true (t/instant? (Date.)) => true

now ^

returns the current datetime

v 3.0
(defn now
  ([] (now {}))
  ([opts] (protocol.time/-now (merge {:type common/*default-type*}
                                     opts))))
link
(t/now) ;; => #(instance? (t/default-type) %) (t/now {:type Date}) => #(instance? Date %) (t/now {:type Calendar}) => #(instance? Calendar %)

representation? ^

checks if an object implements the representation protocol

v 3.0
(defn representation?
  [obj]
  (satisfies? protocol.time/IRepresentation obj))
link
(t/representation? 0) => false (t/representation? (common/calendar (Date. 0) (TimeZone/getTimeZone "GMT"))) => true

time-meta ^

retrieves the meta-data for the time object

v 3.0
(defn time-meta
  [cls]
  (protocol.time/-time-meta cls))
link
(t/time-meta TimeZone) => {:base :zone}

3    Timezone



default-timezone ^

accesses the default timezone as a string

v 3.0
(defn default-timezone
  ([]
   (or *default-timezone*
       (local-timezone)))
  ([tz]
   (alter-var-root #'*default-timezone*
                   (constantly (protocol.string/-to-string tz)))))
link
(default-timezone) ;; getter => "Asia/Ho_Chi_Minh" (default-timezone "GMT") ;; setter => "GMT"

get-timezone ^

returns the contained timezone if exists

v 3.0
(defn get-timezone
  [t]
  (protocol.time/-get-timezone t))
link
(t/get-timezone 0) => nil (t/get-timezone (common/calendar (Date. 0) (TimeZone/getTimeZone "EST"))) => "EST"

has-timezone? ^

checks if the instance contains a timezone

v 3.0
(defn has-timezone?
  [t]
  (protocol.time/-has-timezone? t))
link
(t/has-timezone? 0) => false (t/has-timezone? (common/calendar (Date. 0) (TimeZone/getDefault))) => true

local-timezone ^

returns the current timezone as a string

v 3.0
(defn local-timezone
  []
  (.getID (TimeZone/getDefault)))
link
(local-timezone) => "Asia/Ho_Chi_Minh"

with-timezone ^

returns the same instance in a different timezone

v 3.0
(defn with-timezone
  [t tz]
  (protocol.time/-with-timezone t tz))
link
(t/with-timezone 0 "EST") => 0

4    Access



day ^

accesses the day representated by the instant

v 3.0
(defn day
  ([t] (day t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-day) t opts)))
link
(t/day 0 {:timezone "GMT"}) => 1 (t/day (Date. 0) {:timezone "EST"}) => 31

day-of-week ^

accesses the day of week representated by the instant

v 3.0
(defn day-of-week
  ([t] (day-of-week t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-day-of-week) t opts)))
link
(t/day-of-week 0 {:timezone "GMT"}) => 4 (t/day-of-week (Date. 0) {:timezone "EST"}) => 3

hour ^

accesses the hour representated by the instant

v 3.0
(defn hour
  ([t] (hour t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-hour) t opts)))
link
(t/hour 0 {:timezone "GMT"}) => 0 (t/hour (Date. 0) {:timezone "Asia/Kolkata"}) => 5

millisecond ^

accesses the millisecond representated by the instant

v 3.0
(defn millisecond
  ([t] (millisecond t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-millisecond) t opts)))
link
(t/millisecond 1010 {:timezone "GMT"}) => 10

minute ^

accesses the minute representated by the instant

v 3.0
(defn minute
  ([t] (minute t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-minute) t opts)))
link
(t/minute 0 {:timezone "GMT"}) => 0 (t/minute (Date. 0) {:timezone "Asia/Kolkata"}) => 30

month ^

accesses the month representated by the instant

v 3.0
(defn month
  ([t] (month t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-month) t opts)))
link
(t/month 0 {:timezone "GMT"}) => 1

second ^

accesses the second representated by the instant

v 3.0
(defn second
  ([t] (second t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-second) t opts)))
link
(t/second 1000 {:timezone "GMT"}) => 1

year ^

accesses the year representated by the instant

v 3.0
(defn year
  ([t] (year t {}))
  ([t opts]
   ((wrap-proxy protocol.time/-year) t opts)))
link
(t/year 0 {:timezone "GMT"}) => 1970 (t/year (Date. 0) {:timezone "EST"}) => 1969

5    Convert



coerce ^

adjust fields of a particular time

v 3.0
(defn coerce
  [t {:keys [type timezone] :as opts}]
  (let [type (or type common/*default-type*)
        timezone (or timezone
                     (get-timezone t))]
    (-> (to-long t)
        (from-long {:type type :timezone timezone}))))
link
(t/coerce 0 {:type Date}) => #inst "1970-01-01T00:00:00.000-00:00" (t/coerce {:type clojure.lang.PersistentHashMap, :timezone "PST", :long 915148800000, :year 1999, :month 1, :day 1, :hour 0, :minute 0 :second 0, :millisecond 0} {:type Date}) => #inst "1999-01-01T08:00:00.000-00:00"

format ^

converts a date into a string

v 3.0
(defn format
  ([t pattern] (format t pattern {}))
  ([t pattern {:keys [cached] :as opts}]
   (let [tmeta (protocol.time/-time-meta (class t))
         ftype (-> tmeta :formatter :type)
         fmt   (cache +format-cache+
                      (fn [] (protocol.time/-formatter pattern (assoc opts :type ftype)))
                      [ftype pattern]
                      cached)]
     (protocol.time/-format fmt t opts))))
link
(f/format (Date. 0) "HH MM dd Z" {:timezone "GMT" :cached true}) => "00 01 01 +0000" (f/format (common/calendar (Date. 0) (TimeZone/getTimeZone "GMT")) "HH MM dd Z" {}) => "00 01 01 +0000" (f/format (Timestamp. 0) "HH MM dd Z" {:timezone "PST"}) => "16 12 31 -0800" (f/format (Date. 0) "HH MM dd Z") => string?

from-long ^

creates an instant from a long

v 3.0
(defn from-long
  ([t]
   (from-long t nil))
  ([t opts]
   (protocol.time/-from-long t (merge {:type common/*default-type*} opts))))
link
(-> (t/from-long 0 {:timezone "Asia/Kolkata" :type Calendar}) (t/to-map)) => {:type java.util.GregorianCalendar, :timezone "Asia/Kolkata", :long 0 :year 1970, :month 1, :day 1, :hour 5, :minute 30 :second 0, :millisecond 0}

from-map ^

creates an map from an instant

v 3.0
(defn from-map
  ([t] (from-map t {}))
  ([t opts] (from-map t opts common/+zero-values+))
  ([t {:keys [type timezone] :as opts} fill]
   (cond (#{PersistentArrayMap PersistentHashMap} type)
         (protocol.time/-with-timezone t timezone)

         :else
         (map/from-map t opts fill))))
link
(t/from-map {:type java.util.GregorianCalendar, :timezone "Asia/Kolkata", :long 0 :year 1970, :month 1, :day 1, :hour 5, :minute 30 :second 0, :millisecond 0} {:timezone "Asia/Kolkata" :type Date}) => #inst "1970-01-01T00:00:00.000-00:00"

parse ^

converts a string into a date

v 3.0
(defn parse
  ([s pattern] (parse s pattern {}))
  ([s pattern {:keys [cached] :as opts}]
   (let [opts   (merge {:type common/*default-type*} opts)
         type   (:type opts)
         tmeta  (protocol.time/-time-meta type)
         ptype  (-> tmeta :parser :type)
         parser (cache +parse-cache+
                       (fn [] (protocol.time/-parser pattern (assoc opts :type ptype)))
                       [ptype pattern]
                       cached)]
     (protocol.time/-parse parser s opts))))
link
(f/parse "00 00 01 01 01 1989 +0000" "ss mm HH dd MM yyyy Z" {:type Date :timezone "GMT"}) => #inst "1989-01-01T01:00:00.000-00:00" (-> (f/parse "00 00 01 01 01 1989 -0800" "ss mm HH dd MM yyyy Z" {:type Calendar}) (map/to-map {:timezone "GMT"} common/+default-keys+)) => {:type java.util.GregorianCalendar, :timezone "GMT", :long 599648400000, :year 1989, :month 1, :day 1, :hour 9, :minute 0, :second 0, :millisecond 0} (-> (f/parse "00 00 01 01 01 1989 +0000" "ss mm HH dd MM yyyy Z" {:type Timestamp}) (map/to-map {:timezone "Asia/Kolkata"} common/+default-keys+)) => {:type java.sql.Timestamp, :timezone "Asia/Kolkata", :long 599619600000, :year 1989, :month 1, :day 1, :hour 6, :minute 30, :second 0, :millisecond 0}

to-long ^

gets the long representation for the instant

v 3.0
(defn to-long
  [t]
  (protocol.time/-to-long t))
link
(t/to-long #inst "1970-01-01T00:00:10.000-00:00") => 10000

to-map ^

creates an map from an instant

v 3.0
(defn to-map
  ([t] (to-map t {}))
  ([t opts] (to-map t opts common/+default-keys+))
  ([t {:keys [timezone] :as opts} ks]
   (cond (map? t)
         (if timezone
           (protocol.time/-with-timezone t timezone)
           t)

         :else
         (map/to-map t opts ks))))
link
(-> (t/from-long 0 {:timezone "Asia/Kolkata" :type Date}) (t/to-map {:timezone "GMT"} [:year :month :day])) => {:type java.util.Date, :timezone "GMT", :long 0, :year 1970, :month 1, :day 1}

to-vector ^

converts an instant to an array representation

v 3.0
(defn to-vector
  [t {:keys [timezone] :as opts} ks]
  (cond (map? t)
        (if (or (nil? timezone)
                (= timezone (:timezone opts)))
          (mapv t ks)
          (-> (map/from-map t (assoc opts :type java.util.Calendar))
              (to-vector opts ks)))

        :else
        (let [tmeta (protocol.time/-time-meta (class t))
              [p pmeta] (if-let [{:keys [proxy via]} (-> tmeta :map :to)]
                          [(via t opts) (protocol.time/-time-meta proxy)]
                          [t tmeta])
              p         (if timezone
                          (protocol.time/-with-timezone p timezone)
                          p)
              ks   (cond (vector? ks) ks

                         (= :all ks)
                         (reverse common/+default-keys+)

                         (keyword? ks)
                         (->> common/+default-keys+
                              (drop-while #(not= % ks))
                              (reverse)))
              rep  (reduce (fn [out k]
                             (let [t-fn (get common/+default-fns+ k)]
                               (conj out (t-fn p opts))))
                           []
                           ks)]
          rep)))
link
(to-vector 0 {:timezone "GMT"} :all) => [1970 1 1 0 0 0 0] (to-vector (Date. 0) {:timezone "GMT"} :day) => [1970 1 1] (to-vector (common/calendar (Date. 0) (TimeZone/getTimeZone "EST")) {} [:month :day :year]) => [12 31 1969] (to-vector (common/calendar (Date. 0) (TimeZone/getTimeZone "EST")) {:timezone "GMT"} [:month :day :year]) => [1 1 1970]

6    Operation



adjust ^

adjust fields of a particular time

v 3.0
(defn adjust
  ([t rep]
   (adjust t rep {}))
  ([t rep opts]
   (let [m (-> (to-map t opts)
               (merge rep)
               (dissoc :long))]
     (from-long (from-map m {:type Long})
                (assoc m :type (class t))))))
link
(t/adjust (Date. 0) {:year 2000 :second 10} {:timezone "GMT"}) => #inst "2000-01-01T00:00:10.000-00:00"

after ^

compare dates, returns true if t1 is after t2, etc

v 3.0
(defn after
  ([t1 t2]
   (> (to-long t1) (to-long t2)))
  ([t1 t2 & more]
   (apply > (map to-long (cons t1 (cons t2 more))))))
link
(t/after 2 (Date. 1) (common/calendar (Date. 0) (TimeZone/getTimeZone "GMT"))) => true

before ^

compare dates, returns true if t1 is before t2, etc

v 3.0
(defn before
  ([t1 t2]
   (< (to-long t1) (to-long t2)))
  ([t1 t2 & more]
   (apply < (map to-long (cons t1 (cons t2 more))))))
link
(t/before 0 (Date. 1) (common/calendar (Date. 2) (TimeZone/getTimeZone "GMT"))) => true

earliest ^

returns the earliest date out of a range of inputs

v 3.0
(defn earliest
  [t1 t2 & more]
  (first (sort-by protocol.time/-to-long (apply vector t1 t2 more))))
link
(t/earliest (Date. 0) (Date. 1000) (Date. 20000)) => #inst "1970-01-01T00:00:00.000-00:00"

equal ^

compares dates, retruns true if all inputs are the same

v 3.0
(defn equal
  ([t1 t2]
   (= (to-long t2) (to-long t1)))
  ([t1 t2 & more]
   (apply = (map to-long (cons t1 (cons t2 more))))))
link
(t/equal 1 (Date. 1) (common/calendar (Date. 1) (TimeZone/getTimeZone "GMT"))) => true

latest ^

returns the latest date out of a range of inputs

v 3.0
(defn latest
  [t1 t2 & more]
  (last (sort-by protocol.time/-to-long (apply vector t1 t2 more))))
link
(t/latest (Date. 0) (Date. 1000) (Date. 20000)) => #inst "1970-01-01T00:00:20.000-00:00"

minus ^

substracts a duration from the time

v 3.0
(defn minus
  ([t duration]
   (minus t duration {}))
  ([t duration opts]
   (from-long (- (to-long t)
                 (to-length duration
                            (-> (to-map t opts [:day :month :year])
                                (assoc :backward true))))
              (assoc opts :type (class t)))))
link
(t/minus (Date. 0) {:years 1}) => #inst "1969-01-01T00:00:00.000-00:00" (-> (t/from-map {:type java.time.ZonedDateTime :timezone "GMT", :year 1970, :month 1, :day 1, :hour 0, :minute 0, :second 0, :millisecond 0}) (t/minus {:years 10 :months 1 :weeks 4 :days 2}) (t/to-map {:timezone "GMT"})) => {:type java.time.ZonedDateTime, :timezone "GMT", :long -320803200000 :year 1959, :month 11, :day 2, :hour 0, :minute 0, :second 0, :millisecond 0}

plus ^

adds a duration to the time

v 3.0
(defn plus
  ([t duration]
   (plus t duration {}))
  ([t duration opts]
   (from-long (+ (to-long t)
                 (to-length duration
                            (to-map t opts [:day :month :year])))
              (assoc opts :type (class t)))))
link
(t/plus (Date. 0) {:weeks 2}) => #inst "1970-01-15T00:00:00.000-00:00" (t/plus (Date. 0) 1000) => #inst "1970-01-01T00:00:01.000-00:00" (t/plus (java.util.Date. 0) {:years 10 :months 1 :weeks 4 :days 2}) => #inst "1980-03-02T00:00:00.000-00:00"

to-length ^

converts a object implementing IDuration to a long

v 3.0
(defn to-length
  ([t]
   (to-length t {:year 0 :month 1 :day 1}))
  ([t rep]
   (protocol.time/-to-length t rep)))
link
(t/to-length {:days 1}) => 86400000

truncate ^

truncates the time to a particular field

v 3.0
(defn truncate
  ([t col]
   (truncate t col {}))
  ([t col opts]
   (let [rep  (to-map t opts)
         trep (->> common/+default-keys+
                   (drop-while #(not= col %))
                   (concat [:type :timezone])
                   (select-keys rep))]
     (from-map (merge common/+zero-values+ (dissoc trep :long))
               (assoc opts :type (class t))))))
link
(t/truncate #inst "1989-12-28T12:34:00.000-00:00" :hour {:timezone "GMT"}) => #inst "1989-12-28T12:00:00.000-00:00" (t/truncate #inst "1989-12-28T12:34:00.000-00:00" :year {:timezone "GMT"}) => #inst "1989-01-01T00:00:00.000-00:00"

7    Walkthrough

We can start off with the easiest call:

(t/now)
;;=> {:day 4, :hour 14, :timezone "Asia/Kolkata",
;;    :long 1457081866919, :second 46, :month 3,
;;    :type java.util.Date, :year 2016, :millisecond 919, :minute 27}

Note that now returns a clojure map representing the current time. This is the default type, but we can also specify that we want a java.util.Date object

(t/now {:type java.util.Date})
;;=> #inst "2016-03-04T08:57:46.919-00:00"

If on Java version 1.8, the use of :type can set the returned object to be of type java.time.Instant.

(t/now {:type java.time.Instant})
;;=> #<Instant 2016-03-04T08:58:11.678Z>

The default timezone can also be accessed and modified through default-timezone

(t/default-timezone)
;;=> "Asia/Kolkata"

The default type can be accessed and modified through default-type:

(t/default-type)
;;=> clojure.lang.PersistentArrayMap

7.1    Supported Types

Currently hara.time supports the following time representations

  • java.lang.Long
  • java.util.Date
  • java.util.Calendar
  • java.sql.Timestamp
  • java.time.Instant
  • java.time.Clock
  • org.joda.time.DateTime (when required)

Changing the default-type to Calendar will immediately affect the now function to return a java.util.Calendar object

(t/default-type java.util.Calendar)

(t/now)
;;=> #inst "2016-03-04T14:28:39.481+05:30"

(type (t/now))
;;=> java.util.GregorianCalendar

And again, a change of type will result in another representation

(t/default-type java.time.ZonedDateTime)

(t/now)
;;=> #<ZonedDateTime 2016-03-04T15:41:17.901+05:30[Asia/Kolkata]>

(type (t/now))
;;=> java.time.ZonedDateTime

7.2    Date as Data

hara.time has two basic concepts of time:

  • time as an absolute value (long)
  • time as a representation in a given context (map)

These concepts can also be set as the default type, for example, we now set Long as the default type:

(t/default-type Long)

(t/now)
;;=> 1457086323250

As well as a map as the default type:

(t/default-type clojure.lang.PersistentArrayMap)

(t/now)
;;=> {:day 4, :hour 14, :timezone "Asia/Kolkata",
;;    :second 0, :day-of-week 6, :month 3,
;;    :year 2016, :millisecond 611, :minute 33}

A specific timezone can be passed in and this is the same for all supported time objects:

(t/now {:timezone "GMT"})
;;=> {:day 4, :hour 9, :timezone "GMT",
;;    :second 13, :day-of-week 6, :month 3,
;;    :year 2016, :millisecond 585, :minute 4}

7.3    Extensiblity

Because the API is based on protocols, it is very easy to extend. For an example of how other date libraries can be added to the framework, please see hara.time.joda for how joda-time was added.