hara.time time as a clojure map

Author: Chris Zheng  (z@caudate.me)
Date: 11 February 2018
Repository: https://github.com/zcaudate/hara
Version: 2.8.2

1    Introduction

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

1.1    Installation

Add to project.clj dependencies:

[zcaudate/hara.time "2.8.2"]

All functionality is contained in the hara.time namespace.

(require '[hara.time :as t])

1.2    Motivation

hara.time provides a compact interface for dealing with the different representation of time available on the jvm. The library sticks to the following principles of how an interface around dates should be exposed:

  • it should be consistent, so that there may be a common language between all time implementions.
  • it should be extensible, so that new implemention can be added easily
  • it should be simple and clear, to have easy to use functions and for interfactions between time objects to be seamless

Currently there are a couple of implementions for time on the JVM:

  • the < jdk1.8 options for time: java.util.Date, java.util.Calendar, java.sql.Timestamp
  • the < jdk1.8 defacto standard: the joda-time package
  • the new jdk1.8 java.time library

Clojure libraries for time are:

hara.time comes at the problem by providing a core set of operations and representations of time, allowing for many different implementions of time to speak the same language.

2    Index

3    API

3.1    Representation

now ^

returns the current datetime

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

epoch ^

returns the beginning of unix epoch

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

default-type ^

accesses the default type for datetime

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

default-timezone ^

accesses the default timezone as a string

v 2.2
(defn default-timezone
   (or *default-timezone*
   (alter-var-root #'*default-timezone*
                   (constantly (string/-to-string tz)))))
(default-timezone) ;; getter => "Asia/Ho_Chi_Minh" (default-timezone "GMT") ;; setter => "GMT"

We can start off with the easiest call:

;;=> {: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

;;=> "Asia/Kolkata"

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

;;=> clojure.lang.PersistentArrayMap

3.2    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)

;;=> #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)

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

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

3.3    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)

;;=> 1457086323250

As well as a map as the default type:

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

;;=> {: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}

3.4    Coercion

Any of the dates can be coerced to and from each other. This is made possible by coerce. map and long representations of time provide the two most basic forms. from-map, to-map, from-long and to-long can be used to convert any datetime instance to a map/long as well as back again.

coerce ^

adjust fields of a particular time

v 2.2
(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}))))
(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"

to-long ^

gets the long representation for the instant

v 2.2
(defn to-long
  (time/-to-long t))
(t/to-long #inst "1970-01-01T00:00:10.000-00:00") => 10000

to-map ^

creates an map from an instant

v 2.2
(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
           (time/-with-timezone t timezone)

         (map/to-map t opts ks))))
(-> (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}

from-map ^

creates an map from an instant

v 2.2
(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)
         (time/-with-timezone t timezone)

         (map/from-map t opts fill))))
(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"

from-long ^

creates an instant from a long

v 2.2
(defn from-long
   (from-long t nil))
  ([t opts]
   (time/-from-long t (merge {:type common/*default-type*} opts))))
(-> (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}

3.5    Timezones

There are additional methods for dealing with timezones, as some datatime objects support timezones but others do not.

has-timezone? ^

checks if the instance contains a timezone

v 2.2
(defn has-timezone?
  (time/-has-timezone? t))
(t/has-timezone? 0) => false (t/has-timezone? (common/calendar (Date. 0) (TimeZone/getDefault))) => true

get-timezone ^

returns the contained timezone if exists

v 2.2
(defn get-timezone
  (time/-get-timezone t))
(t/get-timezone 0) => nil (t/get-timezone (common/calendar (Date. 0) (TimeZone/getTimeZone "EST"))) => "EST"

with-timezone ^

returns the same instance in a different timezone

v 2.2
(defn with-timezone
  [t tz]
  (time/-with-timezone t tz))
(t/with-timezone 0 "EST") => 0

3.6    Format and Parsing

Dates can be formatted and parsed using the following methods:

format ^

converts a date into a string

v 2.2
(defn format
  ([t pattern] (format t pattern {}))
  ([t pattern {:keys [cached] :as opts}]
   (let [tmeta (time/-time-meta (class t))
         ftype (-> tmeta :formatter :type)
         fmt   (cache +format-cache+
                      (fn [] (time/-formatter pattern (assoc opts :type ftype)))
                      [ftype pattern]
     (time/-format fmt t opts))))
(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?

parse ^

converts a string into a date

v 2.2
(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  (time/-time-meta type)
         ptype  (-> tmeta :parser :type)
         parser (cache +parse-cache+
                      (fn [] (time/-parser pattern (assoc opts :type ptype)))
                      [ptype pattern]
     (time/-parse parser s opts))))
(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}

3.7    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.

3.8    Accessors

Date accessors are provided to access singular values of time, as well vector representation for selected fields

year ^

accesses the year representated by the instant

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

month ^

accesses the month representated by the instant

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

day ^

accesses the day representated by the instant

v 2.2
(defn day
  ([t] (day t {}))
  ([t opts]
   ((wrap-proxy time/-day) t opts)))
(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 2.2
(defn day-of-week
  ([t] (day-of-week t {}))
  ([t opts]
   ((wrap-proxy time/-day-of-week) t opts)))
(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 2.2
(defn hour
  ([t] (hour t {}))
  ([t opts]
   ((wrap-proxy time/-hour) t opts)))
(t/hour 0 {:timezone "GMT"}) => 0 (t/hour (Date. 0) {:timezone "Asia/Kolkata"}) => 5

minute ^

accesses the minute representated by the instant

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

second ^

accesses the second representated by the instant

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

millisecond ^

accesses the millisecond representated by the instant

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

to-vector ^

converts an instant to an array representation

v 2.2
(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)))

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

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

                         (keyword? ks)
                         (->> common/+default-keys+
                              (drop-while #(not= % ks))
              rep  (reduce (fn [out k]
                             (let [t-fn (get common/+default-fns+ k)]
                               (conj out (t-fn p opts))))
(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]

3.9    Operations

Date can be compared and manipulated according to the following functions:

plus ^

adds a duration to the time

v 2.2
(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)))))
(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"

minus ^

substracts a duration from the time

v 2.2
(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)))))
(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}

equal ^

compares dates, retruns true if all inputs are the same

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

before ^

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

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

after ^

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

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

latest ^

returns the latest date out of a range of inputs

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

earliest ^

returns the earliest date out of a range of inputs

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

adjust ^

adjust fields of a particular time

v 2.2
(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))))))
(t/adjust (Date. 0) {:year 2000 :second 10} {:timezone "GMT"}) => #inst "2000-01-01T00:00:10.000-00:00"

truncate ^

truncates the time to a particular field

v 2.2
(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))))))
(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"