Skip to content
A scheduling library for humans.
Pull request Compare This branch is 11 commits behind aredington:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
resources/leiningen
src/monotony
test/monotony/test
.gitignore
LICENSE
README.md
design.md
project.clj

README.md

Monotony

Monotony is a solution to the problem of how to schedule things in a way that humans find intuitive.

Cron strings are not very intuitive, but saying something like "The third friday of every month at 6pm" specifies a predictable and regular but complex pattern of times.

The core concepts of monotony are periods and cycles. A period is a stretch of time with a start and an end. We can represent the year of 2011 as:

[#<Date Sat Jan 01 00:00:00 EST 2011> #<Date Sat Dec 31 23:59:59 EST 2011>]

A cycle is a description of a period as an abstract concept, e.g. an hour, a day, a week. The cycles monotony comprehends are contained in the cycles map.

Computing times

Monotony is based on concepts of regular generation. As an example, let's reason about generating an infinite sequence of periods representing the hour of 6PM to 7PM on the third Wednesday of every month. (Coincidentally, this is when TriClojure meets at Relevance HQ!)

I like to use cake to develop on monotony, so let's take a test drive with cake

cake repl
=> (require ['monotony.core :as 'm])

Monotony is dealing with time, and for it to be generally useful to other Clojure developers it's been designed around an API of pure functions. While it would be possible to use the local calendar and current system time by default, this would make all of the API's return results contextually sensitive to the environment in which it is run, both in locality and in time. Consequently, much of monotony's API requires a configuration that is significantly small. It consists of two elements, :calendar and :seed. :calendar is a function that returns a new Calendar instance (which provides time formatting and time offset information), while :seed is function which returns a new UNIX time from which all calculations should be based. We will create a new config with local time and calendar settings.

=> (def conf (m/local-config))
#'user/conf

This is an event that occurs every month. Monotony can generate infinite series of periods from some starting point in time with the periods fn. Passed :month as a keyword, it generates month long periods, including the period of the present month.

=> (def months (m/periods conf :month))
#'user/months

Let's peek at the first 12 months of this seq:

=> (take 12 months)
([#<Date Tue Nov 01 00:00:00 EDT 2011> #<Date Wed Nov 30 23:59:59
  EST 2011>] [#<Date Thu Dec 01 00:00:00 EST 2011> #<Date Sat Dec
  31 23:59:59 EST 2011>] [#<Date Sun Jan 01 00:00:00 EST 2012>
  #<Date Tue Jan 31 23:59:59 EST 2012>] [#<Date Wed Feb 01
  00:00:00 EST 2012> #<Date Wed Feb 29 23:59:59 EST 2012>]
  [#<Date Thu Mar 01 00:00:00 EST 2012> #<Date Sat Mar 31
  23:59:59 EDT 2012>] [#<Date Sun Apr 01 00:00:00 EDT 2012>
  #<Date Mon Apr 30 23:59:59 EDT 2012>] [#<Date Tue May 01
  00:00:00 EDT 2012> #<Date Thu May 31 23:59:59 EDT 2012>]
  [#<Date Fri Jun 01 00:00:00 EDT 2012> #<Date Sat Jun 30
  23:59:59 EDT 2012>] [#<Date Sun Jul 01 00:00:00 EDT 2012>
  #<Date Tue Jul 31 23:59:59 EDT 2012>] [#<Date Wed Aug 01
  00:00:00 EDT 2012> #<Date Fri Aug 31 23:59:59 EDT 2012>]
  [#<Date Sat Sep 01 00:00:00 EDT 2012> #<Date Sun Sep 30
  23:59:59 EDT 2012>] [#<Date Mon Oct 01 00:00:00 EDT 2012>
  #<Date Wed Oct 31 23:59:59 EDT 2012>])

Now we can start slicing and dicing up these periods into smaller cycles. We have two tools for this, cycles-in which divides up a period by the cycle length, and bounded-cycles-in which divides up a period so that the cuts align with the boundaries of the cycle passed as an argument, according to the calendar provided by the configuration.

=> (def nov (first months))
#'user/nov

=> nov
[#<Date Tue Nov 01 00:00:00 EDT 2011> #<Date Wed Nov 30 23:59:59 EST 2011>]

=> (m/bounded-cycles-in conf nov :day)
([#<Date Tue Nov 01 00:00:00 EDT 2011> #<Date Tue Nov 01 23:59:59
  EDT 2011>] [#<Date Wed Nov 02 00:00:00 EDT 2011> #<Date Wed Nov
  02 23:59:59 EDT 2011>] [#<Date Thu Nov 03 00:00:00 EDT 2011>
  #<Date Thu Nov 03 23:59:59 EDT 2011>] [#<Date Fri Nov 04
  00:00:00 EDT 2011> #<Date Fri Nov 04 23:59:59 EDT 2011>]
  [#<Date Sat Nov 05 00:00:00 EDT 2011> #<Date Sat Nov 05
  23:59:59 EDT 2011>] [#<Date Sun Nov 06 00:00:00 EDT 2011>
  #<Date Sun Nov 06 23:59:59 EST 2011>] [#<Date Mon Nov 07
  00:00:00 EST 2011> #<Date Mon Nov 07 23:59:59 EST 2011>]
  [#<Date Tue Nov 08 00:00:00 EST 2011> #<Date Tue Nov 08
  23:59:59 EST 2011>] [#<Date Wed Nov 09 00:00:00 EST 2011>
  #<Date Wed Nov 09 23:59:59 EST 2011>] [#<Date Thu Nov 10
  00:00:00 EST 2011> #<Date Thu Nov 10 23:59:59 EST 2011>]
  [#<Date Fri Nov 11 00:00:00 EST 2011> #<Date Fri Nov 11
  23:59:59 EST 2011>] [#<Date Sat Nov 12 00:00:00 EST 2011>
  #<Date Sat Nov 12 23:59:59 EST 2011>] [#<Date Sun Nov 13
  00:00:00 EST 2011> #<Date Sun Nov 13 23:59:59 EST 2011>]
  [#<Date Mon Nov 14 00:00:00 EST 2011> #<Date Mon Nov 14
  23:59:59 EST 2011>] [#<Date Tue Nov 15 00:00:00 EST 2011>
  #<Date Tue Nov 15 23:59:59 EST 2011>] [#<Date Wed Nov 16
  00:00:00 EST 2011> #<Date Wed Nov 16 23:59:59 EST 2011>]
  [#<Date Thu Nov 17 00:00:00 EST 2011> #<Date Thu Nov 17
  23:59:59 EST 2011>] [#<Date Fri Nov 18 00:00:00 EST 2011>
  #<Date Fri Nov 18 23:59:59 EST 2011>] [#<Date Sat Nov 19
  00:00:00 EST 2011> #<Date Sat Nov 19 23:59:59 EST 2011>]
  [#<Date Sun Nov 20 00:00:00 EST 2011> #<Date Sun Nov 20
  23:59:59 EST 2011>] [#<Date Mon Nov 21 00:00:00 EST 2011>
  #<Date Mon Nov 21 23:59:59 EST 2011>] [#<Date Tue Nov 22
  00:00:00 EST 2011> #<Date Tue Nov 22 23:59:59 EST 2011>]
  [#<Date Wed Nov 23 00:00:00 EST 2011> #<Date Wed Nov 23
  23:59:59 EST 2011>] [#<Date Thu Nov 24 00:00:00 EST 2011>
  #<Date Thu Nov 24 23:59:59 EST 2011>] [#<Date Fri Nov 25
  00:00:00 EST 2011> #<Date Fri Nov 25 23:59:59 EST 2011>]
  [#<Date Sat Nov 26 00:00:00 EST 2011> #<Date Sat Nov 26
  23:59:59 EST 2011>] [#<Date Sun Nov 27 00:00:00 EST 2011>
  #<Date Sun Nov 27 23:59:59 EST 2011>] [#<Date Mon Nov 28
  00:00:00 EST 2011> #<Date Mon Nov 28 23:59:59 EST 2011>]
  [#<Date Tue Nov 29 00:00:00 EST 2011> #<Date Tue Nov 29
  23:59:59 EST 2011>] [#<Date Wed Nov 30 00:00:00 EST 2011>
  #<Date Wed Nov 30 23:59:59 EST 2011>])

bounded-cycles-in accepts a period and returns us a seq, breaking down the input period to the resolution passed in. Now we have a day granularity scale of one month. We can use the period-named? predicate to find only the wednesdays in one month:

=> (filter #(m/period-named? conf % :wednesday) (m/bounded-cycles-in conf nov :day))
([#<Date Wed Nov 02 00:00:00 EDT 2011> #<Date Wed Nov 02 23:59:59
  #EDT 2011>] [#<Date Wed Nov 09 00:00:00 EST 2011> #<Date Wed Nov
  #09 23:59:59 EST 2011>] [#<Date Wed Nov 16 00:00:00 EST 2011>
  ##<Date Wed Nov 16 23:59:59 EST 2011>] [#<Date Wed Nov 23
  #00:00:00 EST 2011> #<Date Wed Nov 23 23:59:59 EST 2011>]
  #[#<Date Wed Nov 30 00:00:00 EST 2011> #<Date Wed Nov 30
  #23:59:59 EST 2011>])

Now we can make some specific functions for plucking sub-periods of time out of a bigger period, compose them, and generate our desired time sequence. First let's pull the third wednesday out of a month.

=> (defn third-wednesday [month] (nth (filter #(m/period-named? conf % :wednesday) (m/bounded-cycles-in conf month :day)) 2))
#'user/third-wednesday

Let's make another function to pluck 6PM out of a day long period:

=> (defn six-pm [day] (nth (m/bounded-cycles-in conf day :hour) 18))
#'user/six-pm

If we apply this to a month long period, we'll get out the triclojure meeting for that month:

=> ((comp six-pm third-wednesday) (first months))
[#<Date Wed Nov 16 18:00:00 EST 2011> #<Date Wed Nov 16 18:59:59
 EST 2011>]

Now that we can chain these together, if we generate a sequence of months, and apply our function to find the triclojure meeting in one month, we have the infinite sequence of all triclojure meetings.

=> (def triclojure-meetings (map (comp six-pm third-wednesday) months))
#'user/triclojure-meetings

=> (take 3 triclojure-meetings)
([#<Date Wed Nov 16 18:00:00 EST 2011> #<Date Wed Nov 16 18:59:59
  EST 2011>] [#<Date Wed Dec 21 18:00:00 EST 2011> #<Date Wed Dec
  21 18:59:59 EST 2011>] [#<Date Wed Jan 18 18:00:00 EST 2012>
  #<Date Wed Jan 18 18:59:59 EST 2012>])

Configuration Niceties

"Passing around that configuration object is a pain!" I hear you. Monotony makes use of fidjet, which means that we can get nice config-wrapper blocks for our code by using the monotony.configured namespace. Let's do the prior example in monotony.configured:

(require ['monotony.configured :as 'm])
(def conf (m/local-config))
(defn third-wednesday [month] (nth (filter #(m/period-named? % :wednesday) (m/bounded-cycles-in month :day)) 2))
(defn days [month] (m/bounded-cycles-in month :day))
(defn six-pm [day] (nth (m/bounded-cycles-in day :hour) 18))
(m/with-config conf
  (def months (m/periods :month))
  (def triclojure-meetings (map (comp six-pm third-wednesday) months))
  (doall (take 3 triclojure-meetings)))

Note that ALL of the evaluation of monotony.configured functions has to occur in the with-config block, and that the monotony.core functions are lazy. Thusly, we use doall to realize the sequence in its entirety and return it.

Acknowledgements

Monotony would not be possible without the following people:

  • Alex Redington (core maintainer)
  • Alan Dipert (co-design)
  • David Nolen (for the excellent core.logic library and for contributing to monotony.logic)
Something went wrong with that request. Please try again.