Clojure Cookbook: Time and Dates

Day of Week, Month, or Year

Problem

How do I determine the day of week for a given date?

Solution

Given the year, the month, and the day of the month we can use Zeller's Congruence to compute the day of the week:
J is the century
K is the year within the century
m is the month
q is the day of the month

The day of the week h is calculated by this formula:
h = (q + 26(m+1)/10 + K + K/4 + J/4 + 5J) mod 7
where all divisions are truncated.

h will be a number between 0 (Saturday) and 6 (Friday).

The formula considers January and February to be months 13 and 14 of the previous year. Consequently we compute a modified month value m and a modified year value y. We implement the formula as follows:

(defn zeller [year month day]
  (let [m (+ (mod (+ month 9) 12) 3)
        y (- year (quot (- m month) 12))
        J (quot y 100)
        K (mod y 100)
        q day]
    (mod (+ q
            (quot (* 26 (inc m)) 10)
            K
            (quot K 4)
            (quot J 4)
            (* 5 J))
         7)))

(zeller 1999 7 16) => 6
(zeller 2010 9 15) => 4

Of course, we may not be satisfied with this form of output. So let's first shift the range 0-6 (Saturday-Friday) to the range 1-7 (Sunday-Saturday).

The general solution for n elements 0, …, n-1 (7 elements in our case) where we want to shift m elements (1 here) is:

(defn shift0 [m n i]
  (+ (mod (+ i (- n m)) n) m))

For example, suppose we want to map morning military time to a 12-hour clock, in other words the range 0-11 becomes 1-12. We have 12 elements and want to shift 1:
(shift0 1 12 0) => 12
(shift0 1 12 1) => 1
(shift0 1 12 2) => 2

This works for us here too:
(shift0 1 7 0) => 7
(shift0 1 7 1) => 1
(shift0 1 7 6) => 6

But let's use a more specialized form for this particular case:

(defn shift0 [i]
  (+ (mod (+ i 6) 7) 1))

This is similar to what we did in the zeller function above shifting January and February to months 13 and 14. In effect, we took the range 1-12 and mapped it to the range 3-14:

(+ (mod (+ month 9) 12) 3)

The constants come out a little different though since that range started with 1 and our weekday range started with 0.

Now our simplified version works this way:
(zeller 2010 9 4) => 0
(shift0 (zeller 2010 9 4)) => 7
(zeller 2010 9 5) => 1
(shift0 (zeller 2010 9 5)) => 1

So Saturday comes out as 7 and Sunday as 1. Finally, let's turn those into strings. We can get the names of the days of the week from Java:
(seq (.getWeekdays (java.text.DateFormatSymbols.))) => ("" "Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday")

Putting it all together we get:

(defn day-of-week [year month day]
  (nth (.getWeekdays (java.text.DateFormatSymbols.))
       (shift0 (zeller year month day))))

(day-of-week 1999 7 16) => "Friday"
(day-of-week 2010 9 15) => "Wednesday"

Determining Leap Years

Problem

How do I tell whether or not a year is a leap year?

Solution

Under the Julian calendar, a leap year was designated every four years. As a result, the average length of the year was 365.25 days. This is close but not exactly right. The Gregorian calendar attempts to fix this by modifying the above rule. Now we designate every fourth year as a leap year unless the year is also a multiple of one hundred. However, this rule also has an exception. If the year is a multiple of four hundred, then it is still designated as a leap year. Thus, 1900 was not a leap year, but 2000 was.

This means that every 400 years rather than having 100 leap years we have only 97. Consequently the average length of the year is 365.2425 days:
(/ (+ (* 303 365) (* 97 366)) 400.0) => 365.2425

Putting all of this information together we can test whether or not a year is a leap year:

(defn leap-year? [year]
  (cond (zero? (mod year 400)) true
    (zero? (mod year 100)) false
    :else (zero? (mod year 4))))

(leap-year? 2010) => false
(leap-year? 2000) => true
(leap-year? 1900) => false
(leap-year? 2012) => true

Back to Clojure Cookbook: Table of Contents


Comments

Add a New Comment
or Sign in as Wikidot user
(will not be published)
- +

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License