For a recent problem, I was working with goog.DateTime objects and needed to send them along in JSON, specifically as #inst.

The docs for goog.DateTime say we can use them interchangeably with native js Date objects, but a quick check shows that cljs doesn’t automatically do what we want:

cljs.user=> (pr-str (js/Date. "October 13, 2015"))
"#inst \"2015-10-13T05:00:00.000-00:00\"" ;;what we want
cljs.user=> (pr-str (DateTime. (js/Date. "October 13, 2015")))
"#object[Object 20151013T000000]" ;;what we get :(

There are several ways to do this (e.g. convert the DateTimes to native Date objects), but really I just want DateTime objects to print like Date objects without having to think about it.

Here’s the implementation for printing native js/Date:

(defn- pr-writer-impl
  ;;details elided...

  (cond
    ;;more details...

    ;; Use the new, more efficient, IPrintWithWriter interface when possible.
    (implements? IPrintWithWriter obj)
    (-pr-writer ^not-native obj writer opts)

    ;;more details...

    (instance? js/Date obj)
    (let [normalize (fn [n len]
                      (loop [ns (str n)]
                        (if (< (count ns) len)
                          (recur (str "0" ns))
                          ns)))]
      (write-all writer
        "#inst \""
        (str (.getUTCFullYear obj))             "-"
        (normalize (inc (.getUTCMonth obj)) 2)  "-"
        (normalize (.getUTCDate obj) 2)         "T"
        (normalize (.getUTCHours obj) 2)        ":"
        (normalize (.getUTCMinutes obj) 2)      ":"
        (normalize (.getUTCSeconds obj) 2)      "."
        (normalize (.getUTCMilliseconds obj) 3) "-"
        "00:00\""))
    ;;details elided...
  ))

So we see the js/Date print implementation, which we’d like to use for DateTime. How to do that? Well, we see the check for implements? IPrintWithWriter, so if we implement that protocol, we can print however we want. (It’d be nice if that code was separated into it’s own function, but it’s not a big deal to just copy it.) We want to apply this for every DateTime object, so we can extend the DateTime object via extend-type:

(ns cljs.made-easy
  (:import [goog.date DateTime]))

(extend-type DateTime
  IPrintWithWriter
  (-pr-writer [obj writer _opts]
    (let [normalize (fn [n len]
                      (loop [ns (str n)]
                        (if (< (count ns) len)
                          (recur (str "0" ns))
                          ns)))]
      (write-all writer
                 "#inst \""
                 (str (.getUTCFullYear obj))             "-"
                 (normalize (inc (.getUTCMonth obj)) 2)  "-"
                 (normalize (.getUTCDate obj) 2)         "T"
                 (normalize (.getUTCHours obj) 2)        ":"
                 (normalize (.getUTCMinutes obj) 2)      ":"
                 (normalize (.getUTCSeconds obj) 2)      "."
                 (normalize (.getUTCMilliseconds obj) 3) "-"
                 "00:00\""))))

Now, we get what we want:

cljs.user=> (pr-str (DateTime. (js/Date. "October 13, 2015")))
"#inst \"2015-10-13T05:00:00.000-00:00\""

cljs.user=> (clj->js {:date (js/Date. "October 13, 2015")})
#js {:date #inst "2015-10-13T05:00:00.000-00:00"}

cljs.user=> (clj->js {:date (DateTime. (js/Date. "October 13, 2015"))})
#js {:date #inst "2015-10-13T05:00:00.000-00:00"} ;;aw yeah

Take-aways

With extend-type and IPrintWithWriter, we can simply control how various objects are printed. Even more generally, we can use extend-type and any protocol to add functionality to types/objects, without needing to break them open.

If we only cared about JSON, we could use IEncodeJS in a similar way. It’s useful to have consistent printable dates (e.g. for debugging) so IPrintWithWriter is worth using here.

Get access to new content

New posts are at bostonou.com. Go check it out, or you can just subscribe from here.

    Reminder: You're subscribing to bostonou.com