#time/date "2039-01-01"
A Clojure(Script) library which provides tagged literals for date-time objects
There are two artifacts:
This talk provides some more background.
Note : To use this from Clojurescript, you must have at least version 1.11.51. If using shadow-cljs, it must be at least version 2.19.3
#inst
?Reader Literals were a headline new feature in Clojure 1.4 and with that came built-in support for the #inst tag. The #inst
tag is a part of the edn spec, where it is defined as representing an instant in time, which means a point in time relative to UTC that is given to (at least) millisecond precision.
In Clojure(script), #inst
is read as a legacy platform Date
object by default, but as is made clear by the edn spec and by this talk from Rich Hickey the default implementation is just that: #inst
may be read to whatever internal representation is useful to a consuming program. For example a program running on the jvm could read #inst
tags to java.time.Instant (or java.time.OffsetDateTime if wishing to preserve the UTC offset information). It seems to me unfortunate that Clojure(script) provided defaults for #inst
because users may not realise it is 'just a default', but that's just my opinion. My guess is that Clojure is trying to be both simple and easy in this case.
When conveying data using edn format, built-in tagged elements are preferred to user defined elements.
There are many kinds of things relating to date and time that are not an instant in time
, so #inst
would not be an appropriate way to tag them. For example the month of a particular year such as 'January 1990' or a calendar date such as 'the first of June, 3030'. There are no built-in edn tags for these.
Note that the default Clojure reader behaviour is to accept partially specified instants, such as #inst "2020"
(and read that to a Date with millisecond precision) - but this is specific to the Clojure implementation and not valid edn (AFAIK).
Clojure provides two mechanisms for printing objects - abstract and concrete as this code printing the same object shows:
(let [h (java.util.HashMap.)]
{:abstract (pr-str h)
:concrete (binding [*print-dup* true]
(pr-str h))})
=> {:abstract "{}", :concrete "#=(java.util.HashMap. {})"}
When at the REPL and using say, persistent datastructures, the concrete representation is rarely useful to know, but when dealing with date-time objects it is always useful. Also, the string output can be passed back to the reader to recreate the same internal representation again, which is known as round-tripping
.
The default readers and printers of platform date objects don't allow round-tripping, the reason for which is unknown.
This is relevant to the java.time types which logically correspond to #inst
(java.time.Instant and java.time.OffsetDateTime). This library contains specific readers and printers for those objects so that they do round-trip. When conveying these objects out of process in edn format, they should be tagged as #inst
of course. To do that, simply provide your own implementation of clojure.core/print-method
for those types. With *print-dup*
true, the concrete type will still show.
cljc.java-time is a one for one mapping of the classes and methods from java.time into a Clojure(Script) library
The tick library is an intuitive Clojure(Script) library for dealing with time, intended as a replacement for clj-time. It bundles this library and enables time-literals
printing
by default.
Note: IMHO one should avoid putting tag literals in source code because a tag can be bound to different readers in different contexts, but code will be expecting some specific API. Additionally one has to add a side-effecting require of the tag-reader-namespace to make sure the reader function (ie the one you hope is bound to the tag) exists. tl;dr - it is too magical.
The library includes the magic file data_readers.cljc
which Clojure and the Clojurescript
compilers will look for.
In order to modify the printer to print these literals, run:
(time-literals.read-write/print-time-literals-clj!)
(time-literals.read-write/print-time-literals-cljs!)
Example literals:
#time/month "JUNE"
#time/period "P1D"
#time/date "2039-01-01"
#time/date-time "2018-07-25T08:08:44.026"
#time/zoned-date-time "2018-07-25T08:09:11.227+01:00[Europe/London]"
#time/offset-date-time "2018-07-25T08:11:54.453+01:00"
#time/instant "2018-07-25T07:10:05.861Z"
#time/time "08:12:13.366"
#time/duration "PT1S"
#time/year "3030"
#time/year-month "3030-01"
#time/zone "Europe/London"
#time/day-of-week "TUESDAY"
For example, in a Clojure repl:
;In a cljs repl
(require '[java.time])
(println #time/duration "PT1S")
; => #object[Duration PT1S]
; Now, include printing and edn reading
(require '[time-literals.read-write])
(time-literals.read-write/print-time-literals-cljs!)
(println #time/duration "PT1S")
; => #time/duration "PT1S"
As with any non-core tagged literal, the tag reader functions referred to from a data_readers file must be loaded before the forms can be read.
(require '[time-literals.read-write]) ;; For printing/writing
(time-literals.read-write/print-time-literals-clj!)
(println #time/duration "PT1S")
(require '[time-literals.read-write])
(time-literals.read-write/print-time-literals-clj!)
Printing will now automatically change, for example re run the println above
Read edn like this:
(clojure.edn/read-string {:readers time-literals.read-write/tags} "#time/date \"2011-01-01\"")
If you only need Instant
from java.time/jsr-310, you could just rebind the tag readers and printer fns for
#inst
. Note
however that Clojure's inst
format is based on RFC3339 and so is actually closer to the default format
for java.time.OffsetDateTime
(for example to
read an inst
tag, OffsetDateTime/parse
will work ok, but Instant/parse will not). But... I think in most use
cases Instant is preferred over OffsetDateTime as a representation of an absolute point in time. Rebinding inst
reader and printer might
also lead
to problems where a programmer needs to work with both java.util.Date (or js/Date) and java.time.Instant objects - for example if using
Datomic - it only works with java.util.Date objects.
There is a similar library, java-time-literals but this currently only works
on the jvm, and also doesn't provide a way to read edn with the literals (via clojure.edn/read-string or cljs.reader). The naming of tags
in this library (time-literals
) follows the tick convention, for example
#time/date
for LocalDate, instead of #time/ld
as in java-time-literals
.
TL;DR it is sufficiently ambiguous.
This library reads/writes java.time objects. It would be feasible to use the same set of tags with a different time library, either on the jvm or other elsewhere. If the namespace were 'jsr310' or 'java.time' that would be too implementaion specific.
A set of literals for the ISO-8601 specification would probably be the ideal for date interchange, with literals such as `#iso8601/ordinal-date"1981-095"``
However, although the Java.time domain overlaps significantly with concepts in ISO-8601, there are differences. For example, the ISO 'Duration' is roughly a combination of java.time.Duration and java.time.Period, and the IANA time zone names (such are you see in the literal representation of ZonedDateTime) are not part of ISO.
Copyright © 2021 Widd Industries
Distributed under the MIT License