すっかり体調も良くなり、趣味のプログラミングも再開しようという気力がわいてきたところで、Clojureの勉強を再開しています。

試しに以前C言語で書いた日付を扱うプログラムをClojureで書き直してみようと思った所、Clojureで日付や時刻のデータを扱うのが案外面倒であることに気づきました。

数年前ならばJavaのJodaTimeライブラリの上に構築された、clj-timeを使っておけば間違いないような風潮があったと思いますが、clj-timeは既に DEPRECATED になっており、今から使うという選択肢は無いように思います。

周辺を調べてみると、JodaTimeが無ければイマイチだったJavaにおける日付時刻の取り扱いも、Java8で大きな改訂が入っていて、JVM上で日付時刻を扱う手段はかなり改善されているように思えます。

こうした背景を踏まえて「Clojureで日付時刻を扱うための現代的な選択肢は何だろうか?」と思って調べてみましたが、標準ライブラリとして提供されているような統一されたものはなく、数種類のライブラリがしのぎを削っている状況が見えてきました。

そんな訳で今回は、cljc.java-timtickclojure.java-timeを比較してみることにしました。

どう比べるか?

私が欲しい機能に注目して比較するため、下記の観点で比べてみることにします。
この記事での結論はあくまで私の目的を満たしているかとう観点に絞られていて、人に勧められるライブラリを探す目的ではないという点に注意してください。

各種の検証はWLS2環境上のUbuntu 20.04.2 LTSで、clojure 1.10.0/Leiningen 2.9.1 on Java 14.0.2 OpenJDK 64-Bit/VSCode 1.53.2 + Calva 2.0.173を用いて行いました。

比較の観点

  • 現在のロケールで、現在日時を得る
  • 現在のロケールで、指定した日付時刻を得る
  • 日付時刻の足し算/引き算(n日後、m時間後、i日前、j時間後等)
  • 日付時刻の比較(大小関係)
  • ライセンス

cljc.java-time

  • GitHubリポジトリ

  • 導入: [cljc.java-time "0.1.15"]を追加してlein depsしておきます
  • 以下、(require '[cljc.java-time.local-date-time :as ldt])としてライブラリを読み込んでいて、ldtの名前空間でcljc.java-timeが提供する日付時刻機能にアクセスできるようにしています。

  • 現在のロケールで、現在日時を得る
(ldt/now)
  • 現在のロケールで、指定した日付時刻を得る
(ldt/parse "2021-02-22T22:22:22.22")
  • 日付時刻の足し算/引き算(n日後、m時間後、i日前、j時間後等)
(def a-date (ldt/now)) ;基準になる日付(ある日)を適当にとる

(ldt/plus-years a-date 1)  ; 来年
(ldt/plus-years a-date -1) ; 昨年
(ldt/plus-months a-date 1) ; 来月
(ldt/plus-months a-date -1); 先月
(ldt/plus-days a-date 2)   ; 明後日
(ldt/plus-days a-date -2)  ; 一昨日
(ldt/plus-hours a-date 8)  ; 8時間後
(ldt/plus-hours a-date -2) ; 2時間前

plus-に対応する関数としてminus-も定義されているので、引数に負の値を与えずに、minus-系のメソッドを使う事でも日付時刻の加減算を行うことができました。

ちなみに、こんな演算も可能でした。

(ldt/plus-weeks a-date 4)  ; 4週間後
  • 日付時刻の比較(大小関係)
(def today (ldt/now))
(def tomorrow (ldt/plus-days (ldt/now) 1))
(def v1 (ldt/parse "2021-02-22T22:22:22.22"))
(def v2 (ldt/parse "2021-02-22T22:22:22.22"))

(ldt/equals v1 v2) ; 同じ日付を指しているか
; => true
; (= v1 v2)でも同じ

(ldt/is-after today tomorrow) ; todayはtomorrowの後か
; => false

(ldt/is-after tomorrow today) ; tomorrowはtodayの後か
; => true
  • ライセンス: EPL 1.0 (あるいはオプションでそれ以降のバージョンを選択)

ハイレベルな機能は特に提供されておらず、Javaで提供されている日付時刻ライブラリに対する非常に薄いラッパーライブラリであることが分かりました。
とはいえ実用するには十分で、ライブラリとしても薄い分、Java界隈の知見が活かせそうな感じがしました。

大小関係を見るのに、(JavaのLocalDateTime)オブジェクトに対するメソッドとして生えているis-after/is-beforeを使うあたりに、引数のどちらが前か後かと迷う所はありそうな気がしました。

tick

  • GitHubリポジトリ

  • 導入: [tick "0.4.30-alpha"]を追加してlein depsしておきます
  • 以下、(require '[tick.alpha.api :as tk])としてライブラリを読み込んでいて、tkの名前空間でtickが提供する日付時刻機能にアクセスできるようにしています。

  • 現在のロケールで、現在日時を得る
(tk/in (tk/now) (tk/zone))
  • 現在のロケールで、指定した日付時刻を得る
(tk/parse "2021-02-22T22:22:22.22")
;あるいはちゃんとロケール情報を付けてこうする
(tk/in (tk/parse "2021-02-22T22:22:22.22") (tk/zone))

ここでは日付時刻を扱いたいのでnowを使いましたが、日付だけであればtoday/tomorrow/yesterdayのような関数も用意されているので、よく使う操作で迷うことはなさそうかなと思いました。

  • 日付時刻の足し算/引き算(n日後、m時間後、i日前、j時間後等)
(def a-date (tk/in (tk/now) (tk/zone))) ;基準になる日付(ある日)を適当にとる

(tk/+ a-date (tk/new-period 1 :years))    ; 来年
(tk/+ a-date (tk/new-period -1 :years))   ; 昨年
(tk/+ a-date (tk/new-period 1 :months))   ; 来月
(tk/+ a-date (tk/new-period -1 :months))  ; 先月
(tk/+ a-date (tk/new-period 2 :days))     ; 明後日
(tk/+ a-date (tk/new-period -2 :days))    ; 一昨日
(tk/+ a-date (tk/new-duration 8 :hours))  ; 8時間後
(tk/+ a-date (tk/new-duration -2 :hours)) ; 2時間前

日付と時刻で扱う方を変えてやらない所は煩雑にも感じますが、考えてみれば日を扱いたいケースと時間を扱いたいケースは要件が異なっていることが多いと思うので、こういう限定した条件下だけで感じる手触り感なのかもしれません。
tickは、日付の加減算に演算子を使うことができて、そこが直感的で好きです。例では+演算子でごり押ししていますが、当然-演算子も用意されているので使いやすいほうを選ぶことができそうです。

  • 日付時刻の比較(大小関係)
(def my-today (tk/in (tk/now) (tk/zone)))
(def my-tomorrow (tk/+ my-today (tk/new-period 1 :days)))
(def v1 (tk/in (tk/parse "2021-02-22T22:22:22.22") (tk/zone)))
(def v2 (tk/in (tk/parse "2021-02-22T22:22:22.22") (tk/zone)))

; 同じ日付かを判別する関数は提供されていないようです
(and (tk/<= v1 v2) (not (tk/< v1 v2))) ; 例えばこうして代替できます
; => true

(tk/< my-tomorrow my-today) ; my-todayはmy-tomorrowの後か
; => false

(tk/< my-today my-tomorrow) ; my-tomorrowはmy-todayの後か
; => true

ここでも比較演算子を用いることができるので、直感的でとても触り心地が良いと感じました。
もちろん><=/>=といった演算子も使うことができるので、使っていく上では困らなさそうです。
ドキュメント的には「比較」の節はまだこれから整うようなので、いずれ等値の演算子も加わるかもしれません。

  • ライセンス: MIT

各種の演算子も定義されていますし、直感的に使える点が素晴らしいと思います。
ただ、アルファ版を公称しているライブラリなので、決して枯れているものではないという点には注意が必要かもしれません。
(とはいえ、Clojureのライブラリでアルファ版というのは結構安定している印象があるので、自分一人で使うには十分な気もしますが)

clojure.java-time

  • GitHubリポジトリ

  • 導入: [clojure.java-time "0.3.2"]を追加してlein depsしておきます
  • 以下、(require '[java-time :as jt])としてライブラリを読み込んでいて、jtの名前空間でtickが提供する日付時刻機能にアクセスできるようにしています。

  • 現在のロケールで、現在日時を得る
(jt/zoned-date-time)
  • 現在のロケールで、指定した日付時刻を得る
(jt/zoned-date-time "2021-02-22T22:22:22.22+09:00[Asia/Tokyo]")
  • 日付時刻の足し算/引き算(n日後、m時間後、i日前、j時間後等)
(def a-date (jt/local-date-time)) ;基準になる日付(ある日)を適当にとる

(jt/plus a-date (jt/years 1))   ; 来年
(jt/plus a-date (jt/years -1))  ; 昨年
(jt/plus a-date (jt/months 1))  ; 来月
(jt/plus a-date (jt/months -1)) ; 先月
(jt/plus a-date (jt/days 2))    ; 明後日
(jt/plus a-date (jt/days -2))   ; 一昨日
(jt/plus a-date (jt/hours 8))   ; 8時間後
(jt/plus a-date (jt/hours -2))  ; 2時間前

他と同じくminus関数も用意されているので、負の方向を扱うのがちょっと……という場合はそちらを選ぶ方法もあります。

日付の加減算について、個人的にはほかのライブラリに比べて抜群に手触り感が良く、気に入りました。

  • 日付時刻の比較(大小関係)
(def today (jt/local-date-time))
(def tomorrow (jt/plus today (jt/days 1)))
(def v1 (jt/local-date-time "2021-02-22T22:22:22.22"))
(def v2 (jt/local-date-time "2021-02-22T22:22:22.22"))

(= v1 v2) ; 同じ日付か調べる...のはこれで良いのだろうか
; => true

(jt/after? today tomorrow) ; todayはtomorrowの後か
; => false

(jt/after? tomorrow today) ; tomorrowはtodayの後か
; => true

比較演算子ではないものの、関数名の付け方が理解しやすかったので、書いていてほとんど違和感はありませんでした。
weekend?とかの週末判定関数なんかも用意されていて、使い心地としてはRuby on Railsでの日付時刻の扱いに近いような印象を覚えました。

  • ライセンス: MIT

まとめ

そんなわけで、3つの日付時刻のライブラリを比較してみました。
tickは初めて触った時の手軽さに感動がありましたが、振り返って比べてみるとclojure.java-timeの方がラッパーとして薄く、取り回しが良かった印象があるのでclojure.java-timeを使っていこうかなと思いました。
「指定した日付の次にくる最初の月曜日を求める」等のちょっと込み入った日付情報の扱い方をしようとすると、また違ったチョイスになるかもしれませんが、それはそういう用途が出てきたときに改めて検証していければ良いかなと思っています。