Easter

It is simple but tedious to calculate the date of Easter in the gregorian calendar. The calculation below comes from the book Astronomical Algorithms by Jean Meeus via en.wikipedia.org/wiki/computus.

(define (easter year)
  (let* ((a (modulo year 19))
         (b (quotient year 100))
         (c (modulo year 100))
         (d (quotient b 4))
         (e (modulo b 4))
         (f (quotient (+ b 8) 25))
         (g (quotient (+ (- b f) 1) 3))
         (h (modulo (- (+ (* 19 a) b 15) d g) 30))
         (i (quotient c 4))
         (k (modulo c 4))
         (l (modulo (- (+ 32 (* 2 e) (* 2 i)) h k) 7))
         (m (quotient (+ a (* 11 h) (* 22 l)) 451))
         (month (quotient (- (+ h l 114) (* 7 m)) 31))
         (day (+ (modulo (- (+ h l 114) (* 7 m)) 31) 1)))
    (values month day)))

Here is an example showing the use of easter:

| (let loop ((year 2000))
|   (call-with-values
|     (lambda () (easter year))
|     (lambda (month day)
|       (for-each display
|         `(,year ": " ,(if (= month 3) "March" "April") " " ,day))
|         (newline)))
|   (if (< year 2020) (loop (+ year 1))))
|  
| 2000: April 23
| 2001: April 15
| 2002: March 31
| 2003: April 20
| 2004: April 11
| 2005: March 27
| 2006: April 16
| 2007: April 8
| 2008: March 23
| 2009: April 12
| 2010: April 4
| 2011: April 24
| 2012: April 8
| 2013: March 31
| 2014: April 20
| 2015: April 5
| 2016: March 27
| 2017: April 16
| 2018: April 1
| 2019: April 21
| 2020: April 12

Various other dates are based on Easter. All can be calculated using the easter-add and easter-sub functions:

(define (easter-add year days)
  (call-with-values
    (lambda () (easter year))
    (lambda (m d)
      (let loop ((m m) (d (+ d days)))
        (cond ((and (= m 3) (> d 31)) (loop (+ m 1) (- d 31)))
              ((and (= m 4) (> d 30)) (loop (+ m 1) (- d 30)))
              ((and (= m 5) (> d 31)) (loop (+ m 1) (- d 31)))
              ((and (= m 6) (> d 30)) (loop (+ m 1) (- d 30)))
              (else (values m d)))))))

(define (easter-sub year days)
  (call-with-values
    (lambda () (easter year))
    (lambda (m d)
      (let ((feb (if (zero? (modulo year 4)) 29 28)))
        (let loop ((m m) (d (- d days)))
          (cond ((and (= m 4) (< d 1)) (loop (- m 1) (+ d 31)))
                ((and (= m 3) (< d 1)) (loop (- m 1) (+ d feb)))
                ((and (= m 2) (< d 1)) (loop (- m 1) (+ d 31)))
                (else (values m d))))))))

Easter-sub uses an invalid test for leap year, but it will work until after I die, so I don't care.

(define (mardi-gras     year) (easter-sub year 47))
(define (ash-wednesday  year) (easter-sub year 46))
(define (palm-sunday    year) (easter-sub year  7))
(define (holy-thursday  year) (easter-sub year  3))
(define (good-friday    year) (easter-sub year  2))
(define (holy-saturday  year) (easter-sub year  1))
(define (easter-vigil   year) (easter-sub year  1))
(define (divine-mercy   year) (easter-add year  7))
(define (ascension      year) (easter-add year 39))
(define (pentecost      year) (easter-add year 49))
(define (corpus-christi year) (easter-add year 63))