henryw374 / time-literals

Clojure(Script) tagged literals for jsr-310 ( java.time) entities
MIT License
65 stars 10 forks source link

Tests build

time-literals

#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

Rationale

What is #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.

The need for more Tagged Elements representing Dates in edn

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

Round-tripping at the REPL

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.

Related Libraries

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.

Usage

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"

ClojureScript

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"   

Clojure

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

Reading and Writing edn

 (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\"")

Alternatives

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.

Why use 'time' as namespace?

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.

License

Copyright © 2021 Widd Industries

Distributed under the MIT License