Clojure Cookbook: Generating Ranges of Dates & Times
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>
- Acquire a lazy infinite sequence.
- Create a predicate to terminate the sequence.
- 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
- Comparing Dates
- Calculating the Length of a Time Interval
- Generating Ranges of Dates and Times using Native Java Types for an alternative that uses only native types
- Retrieving Dates Relative to One Another
Like this post? Subscribe to my newsletter.
Get fresh content on Clojure, Architecture and Software Development, each and every week.