Clojure Cookbook: Generating Ranges of Dates & Times

  • clojure
  • cookbook

For this week’s Clojure Cookbook preview recipe we’ll be taking a dive into clj-time, one of my favorite libraries for dealing with time in Clojure.

Most commonly I use clj-time for its parsing and formatting capabilities. The library is a trove of goodies, however; it handles leap time, coercions, time zones and even predicates about time. One of my favorite ancillary function, though, is clj-time.periodic/periodic-seq.

This is a love story about that function…

Generating Ranges of Dates and Times

Problem

You need to generate a lazy sequence covering a range of dates and/or times.

Solution

This problem has no easy solution in Java, nor does it have one in Clojure–third-party libraries included. It is possible to use clj-time to get close, though. By composing clj-time’s Interval and periodic-seq functionality, you can create a function time-range that mimics range’s capabilities but for DateTimes:

(require '[clj-time.core :as time])
(require '[clj-time.periodic :as time-period])

(defn time-range
  "Return a lazy sequence of DateTime's from start to end, incremented
  by 'step' units of time."
  [start end step]
  (let [inf-range (time-period/periodic-seq start step)
        below-end? (fn [t] (time/within? (time/interval start end)
                                         t))]
    (take-while below-end? inf-range)))

This is how you can use the time-range function:

(def months-of-the-year (time-range (time/date-time 2012 01)
                                    (time/date-time 2013 01)
                                    (time/months 1)))

;; months-of-the-year is an unrealized lazy sequence
(realized? months-of-the-year)
;; -> false

(count months-of-the-year)
;; -> 12

;; now realized
(realized? months-of-the-year)
;; -> true

Discussion

While there is no ready-made, out-of-the-box time-range solution in Clojure, it is trivial to construct such a function with purely lazy semantics.

The basis for our lazy time-range function is an infinite sequences of values with a fixed starting time:

(defn time-range
  "Return a lazy sequence of DateTime's from start to end, incremented
  by 'step' units of time."
  [start end step]
  (let [inf-range (time-period/periodic-seq start step)            ; <1>
        below-end? (fn [t] (time/within? (time/interval start end) ; <2>
                                         t))]
    (take-while below-end? inf-range)))                            ; <3>
  1. Acquire a lazy infinite sequence.
  2. Create a predicate to terminate the sequence.
  3. Modify the infinite sequence to terminate when below-end? fails (lazily, of course).

Invoking periodic-seq with start and step returns an infinite lazy sequence of values beginning at start, each subsequent value one step later than the last.

Having a lazy sequence to infinite is one thing, but we need a lazy way to stop acquiring values when end is reached. The below-end? function created in let uses clj-time.core/interval to construct an interval from start to end and clj-time.core/within? to test if a time t falls within that interval. This function is passed as the predicate to take-while, which will lazily consume values until below-end? fails.

All together, time-range returns a lazy sequence of DateTime objects that stretches from a start time to an end time, stepped appropriately by the provided step value.

Imagine trying to build something similar in a language without first-class laziness.

See Also

Like this post? Subscribe to my newsletter.

Get fresh content on Clojure, Architecture and Software Development, each and every week.

comments powered by Disqus