[comp.lang.scheme] Calendrical calculations.

oz@nexus.YorkU.CA (Ozan Yigit) (10/11/90)

This is a scheme version of the "Calendrical Calculations" code
from SP&E Vol.20 #9. Note the macro "sum" that needs to be changed
according to your implementation. Also note that I have changed some
of the procedure names [ref: the article] into the scheme style.

enjoy... oz

---- snip here: calendar.scm ----
;
; The original Lisp code is from ``Calendrical Calculations'' by Nachum
; Dershowitz and Edward M. Reingold, Software---Practice & Experience,
; vol. 20, no. 9 (September, 1990), pp. 899--928.
;
; This code is in the public domain, but any use of it should
; acknowledge its source.
;
; scheme conversion (psi) and hacks by Ozan Yigit
;

(define (nth n lst)
  (list-ref lst n))

; date (month day year) accessors

(define extract-month car)   ; month field
(define extract-day cadr)    ; day field
(define extract-year caddr)  ; year field

; Sum expression for index = initial and successive integers,
; as long as condition holds.

(macro (sum expression index initial condition)
  (let* ((temp (gensym)))
    `(do ((,temp 0 (+ ,temp ,expression))
          (,index ,initial (1+ ,index)))
         ((not ,condition) ,temp))))

; Last day in Gregorian month during year.

(define (last-day-of-gregorian-month month year)
  (if (and (= month 2)			; February in a leap year
	   (= (modulo year 4) 0)
	   (not (member (modulo year 400) (list 100 200 300))))
      29
      (nth (1- month)
	   (list 31 28 31 30 31 30 31 31 30 31 30 31))))

; Absolute date equivalent to the Gregorian date.

(define (gregorian->absolute date)
  (let* ((month (extract-month date))
         (year (extract-year date)))
    (+ (extract-day date)		; days so far this month.
       (sum				; Days in prior months this year.
        (last-day-of-gregorian-month m year) m 1 (< m month))
       (* 365 (1- year))		; Days in prior years.
       (quotient (1- year) 4)		; Julian leap days in prior years...
       (-				; ...minus prior century years...
        (quotient (1- year) 100))
       (quotient			; ...plus prior years divisible...
        (1- year) 400))))		; ...by 400.

; Gregorian (month day year) corresponding absolute date.

(define (absolute->gregorian date)
  (let* ((approx (quotient date 366))	; Approximation from below.
         (year				; Search forward from the approximation
          (+ approx
             (sum 1 y approx
                  (>= date
                      (gregorian->absolute
                       (list 1 1 (1+ y)))))))
         (month				; Search forward from January.
          (1+ (sum 1 m 1
                   (> date
                      (gregorian->absolute
                       (list m
                             (last-day-of-gregorian-month m year)
                             year))))))
         (day				; Calculate the day by subtraction.
          (- date (1- (gregorian->absolute
                       (list month 1 year))))))
    (list month day year)))

; Absolute date of the k-day on or before date.
; k=0 means Sunday, k=1 means Monday, and so on.

(define (k-day-on-or-before date k)
  (- date (modulo (- date k) 7)))

; Absolute date equivalent to ISO date = (week day year).

(define (iso->absolute date)
  (let* ((week (car date))
         (day (cadr date))
         (year (caddr date)))
    (+ (k-day-on-or-before
        (gregorian->absolute (list 1 4 year))
        1)				; Days in prior years.
       (* 7 (1- week))			; Days in prior weeks this year.
       (1- day))))			; Prior days this week.

; ISO (week day year) corresponding to the absolute date.

(define (absolute->iso date)
  (let* ((approx
          (extract-year (absolute->gregorian (- date 3))))
         (year (if (>= date
                       (iso->absolute (list 1 1 (1+ approx))))
                   (1+ approx)
                   approx))
         (week (1+ (quotient
                    (- date (iso->absolute (list 1 1 year)))
                    7)))
         (day (if (= 0 (modulo date 7))
                  7
                  (modulo date 7))))
    (list week day year)))

; Last day in Julian month during year.

(define (last-day-of-julian-month month year)
  (if (and (= month 2)
	   (= (modulo year 4) 0))	; February in a leap year
      29
      (nth (1- month) (list 31 28 31 30 31 30 31 31 30 31 30 31))))

; Absolute date equivalent to Julian date.

(define (julian->absolute date)
  (let* ((month (extract-month date))
         (year (extract-year date)))
    (+ (extract-day date)		; Days so far this month.
       (sum				; Days in prior months this year.
        (last-day-of-julian-month m year) m 1 (< m month))
       (* 365 (1- year))		; Days in prior years.
       (quotient (1- year) 4)		; Leap days in prior years.
       -2)))				; Days elapsed before absolute date 1.

; Julian (month day year) corresponding to absolute date.

(define (absolute->julian date)
  (let*
      ((approx				; Approximation from below.
        (quotient (+ date 2) 366))
       (year				; Search forward from the approximation
        (+ approx
           (sum 1 y approx
                (>= date
                    (julian->absolute (list 1 1 (1+ y)))))))
       (month				; Search forward from January.
        (1+ (sum 1 m 1
                 (> date
                    (julian->absolute
                     (list m
                           (last-day-of-julian-month m year)
                           year))))))
       (day				; Calculate the day by subtraction.
        (- date (1- (julian->absolute (list month 1 year))))))
    (list month day year)))

; true if year is an Islamic leap year.

(define (islamic-leap-year? year)
  (< (modulo (+ 14 (* 11 year)) 30) 11))

; Last day in month during year on the Islamic calendar.

(define (last-day-of-islamic-month month year)
  (if (or (odd? month)
          (and (= month 12)
	       (islamic-leap-year? year)))
      30
      29))

; Absolute date equivalent to Islamic date.

(define (islamic->absolute date)
  (let* ((month (extract-month date))
         (year (extract-year date)))
    (+ (extract-day date)		; Days so far this month.
       (* 29 (1- month))		; Days so far...
       (quotient month 2)		;           ...this year.
       (* (1- year) 354)		; Non-leap days in prior years.
       (quotient			; Leap days in prior years.
        (+ 3 (* 11 year)) 30)
       227014)))			; Days before start of calendar.

; Islamic date (month day year) corresponding to absolute date.

(define (absolute->islamic date)
  (if (<= date 227014)			; Pre-Islamic date.
      (list 0 0 0)
      (let* ((approx			; Approximation from below.
	      (quotient (- date 227014) 355))
	     (year			; Search forward from the approximation
	      (+ approx
		 (sum 1 y approx
		      (>= date
			  (islamic->absolute
			   (list 1 1 (1+ y)))))))
	     (month			; Search forward from Muharram.
	      (1+ (sum 1 m 1
		       (> date
			  (islamic->absolute
			   (list m
				 (last-day-of-islamic-month m year)
				 year))))))
	     (day			; Calculate the day by subtraction.
	      (- date (1- (islamic->absolute
			   (list month 1 year))))))
        (list month day year))))

; true if year is a leap year.

(define (hebrew-leap-year? year)
  (< (modulo (1+ (* 7 year)) 19) 7))

; Last month of Hebrew year.

(define (last-month-of-hebrew-year year)
  (if (hebrew-leap-year? year)
      13
      12))

; Last day of month in Hebrew year.

(define (last-day-of-hebrew-month month year)
  (if (or (member month (list 2 4 6 10 13))
          (and (= month 12)
	       (not (hebrew-leap-year? year)))
          (and (= month 8)
	       (not (long-heshvan year)))
          (and (= month 9)
	       (short-kislev year)))
      29
      30))

; Number of days elapsed from the Sunday prior to the start of the
; Hebrew calendar to the mean conjunction of Tishri of Hebrew year.

(define (hebrew-calendar-elapsed-days year)
  (let*
      ((months-elapsed
        (+
         (* 235				; Months in complete cycles so far.
            (quotient (1- year) 19))
         (* 12				; Regular months in this cycle.
            (modulo (1- year) 19))
         (quotient			; Leap months this cycle
          (1+ (* 7 (modulo (1- year) 19)))
          19)))
;;
;;    (parts-elapsed (+ 5604 (* 13753 months-elapsed)))
;;    (day                                    ; Conjunction day
;;     (+ 1 (* 29 months-elapsed) (quotient parts-elapsed 25920)))
;;    (parts (modulo parts-elapsed 25920))       ; Conjunction parts
;;
;; The above lines of code are correct, but can have intermediate
;; values that are too large for a 32-bit machine.  The following
;; lines of code that replace them are equivalent, but avoid the
;; problem.
;;
       (parts-elapsed
        (+ 204
           (* 793 (modulo months-elapsed 1080))))
       (hours-elapsed
        (+ 5
           (* 12 months-elapsed)
           (* 793 (quotient months-elapsed 1080))
           (quotient parts-elapsed 1080)))
       (day				; Conjunction day
        (+ 1
           (* 29 months-elapsed)
           (quotient hours-elapsed 24)))
       (parts				; Conjunction parts
        (+ (* 1080 (modulo hours-elapsed 24))
           (modulo parts-elapsed 1080)))
       (alternative-day
        (if (or
             (>= parts 19440)		; If new moon is at or after midday,
             (and
	      (= (modulo day 7) 2)	; ...or is on a Tuesday...
	      (>= parts 9924)		; at 9 hours, 204 parts or later...
	      (not (hebrew-leap-year? year))) ; of a common year,
             (and
	      (= (modulo day 7) 1)	; ...or is on a Monday at...
	      (>= parts 16789)		; 15 hours, 589 parts or later...
	      (hebrew-leap-year?		; at the end of a leap year
	       (1- year))))
            (1+ day)			; Then postpone Rosh HaShanah one day
            day)))
; If Rosh HaShanah would occur on Sunday, Wednesday, or Friday
    (if (member (modulo alternative-day 7) (list 0 3 5))
        (1+ alternative-day)		; Then postpone it one (more) day
	alternative-day)))

; Number of days in Hebrew year.

(define (days-in-hebrew-year year)
  (- (hebrew-calendar-elapsed-days (1+ year))
     (hebrew-calendar-elapsed-days year)))

; true if Heshvan is long in Hebrew year.

(define (long-heshvan? year)
  (= (modulo (days-in-hebrew-year year) 10) 5))

; true if Kislev is short in Hebrew year.

(define (short-kislev? year)
  (= (modulo (days-in-hebrew-year year) 10) 3))

; absolute date of Hebrew date.

(define (hebrew->absolute date)
  (let* ((month (extract-month date))
         (day (extract-day date))
         (year (extract-year date)))

    (+ day				; Days so far this month.
       (if (< month 7)			; before Tishri
; add days in prior months this year before and after Nisan.
           (+ (sum (last-day-of-hebrew-month m year)
                   m 7 (<= m (last-month-of-hebrew-year year)))
              (sum (last-day-of-hebrew-month m year)
                   m 1 (< m month)))
; add days in prior months this year
	   (sum (last-day-of-hebrew-month m year) m 7 (< m month)))
       (hebrew-calendar-elapsed-days year) ; Days in prior years.
       -1373429)))			; Days elapsed before absolute date 1.

; hebrew (month day year) corresponding to absolute date.

(define (absolute->hebrew date)
  (let* ((approx			; Approximation from below.
          (quotient (+ date 1373429) 366))
         (year				; Search forward from the approximation
          (+ approx (sum 1 y approx
                         (>= date
                             (hebrew->absolute
                              (list 7 1 (1+ y)))))))
         (start				; Starting month for search for month.
          (if (< date (hebrew->absolute (list 1 1 year)))
	      7				; start at Tishri
              
	      1))			; start at Nisan
	 (month			; Search forward from either Tishri or Nisan.
          (+ start
             (sum 1 m start
                  (> date
                     (hebrew->absolute
                      (list m
                            (last-day-of-hebrew-month m year)
                            year))))))
         (day				; Calculate the day by subtraction.
          (- date (1- (hebrew->absolute (list month 1 year))))))
    (list month day year)))

; Absolute date of American Independence Day in Gregorian year.

(define (independence-day year)
  (gregorian->absolute (list 7 4 year)))

; Absolute date of the n-th k-day in Gregorian month, year.
; If n<0, the n-th k-day from the end of month is returned
; (that is, -1 is the last k-day, -2 is the penultimate k-day,
; and so on).  k=0 means Sunday, k=1 means Monday, and so on.

(define (nth-k-day n k month year)
  (if (> n 0)
      (+ (k-day-on-or-before		; First k-day in month.
          (gregorian->absolute
           (list month 7 year)) k)
         (* 7 (1- n)))                  ; Advance n-1 k-days.
      (+ (k-day-on-or-before		; Last k-day in month.
          (gregorian->absolute
           (list month
                 (last-day-of-gregorian-month month year)
                 year))
          k)
         (* 7 (1+ n)))))                ; Go back -n-1 k-days.

; Absolute date of American Labor Day in Gregorian year.

(define (labor-day year)
  (nth-k-day 1 1 9 year))		; First Monday in September.

; Absolute date of American Memorial Day in Gregorian year.

(define (memorial-day year)
  (nth-k-day -1 1 5 year))		; Last Monday in May.

; Absolute date of the start of American daylight savings time
; in Gregorian year.

(define (daylight-savings-start year)
  (nth-k-day 1 0 4 year))		; First Sunday in April.

; Absolute date of the end of American daylight savings time
; in Gregorian year.

(define (daylight-savings-end year)
  (nth-k-day -1 0 10 year))		; Last Sunday in October.

; Absolute date of Christmas in Gregorian year.

(define (christmas year)
  (gregorian->absolute (list 12 25 year)))

; Absolute date of Advent in Gregorian year.

(define (advent year)
  (k-day-on-or-before (gregorian->absolute (list 12 3 year)) 0))

; Absolute date of Epiphany in Gregorian year.

(define (epiphany year)
  (+ 12 (christmas year)))

; List of zero or one absolute dates of Eastern Orthodox
; Christmas in Gregorian year.

(define (eastern-orthodox-christmas year)
  (let* ((jan1 (gregorian->absolute (list 1 1 year)))
         (dec31 (gregorian->absolute (list 12 31 year)))
         (y (extract-year (absolute->julian jan1)))
         (c1 (julian->absolute (list 12 25 y)))
         (c2 (julian->absolute (list 12 25 (1+ y)))))
    (append
     (if (<= jan1 c1 dec31)		; c1 occurs in current year
	 (list c1)			; that date; otherwise, none
	 nil)
     (if (<= jan1 c2 dec31)		; c2 occurs in current year
	 (list c2)			; that date; otherwise, none
	 nil))))

; Absolute date of Easter in Julian year, according to the rule
; of the Council of Nicaea.

(define (nicaean-rule-easter year)
  (let* ((shifted-epact			; Age of moon for April 5.
          (modulo (+ 14
		     (* 11 (modulo year 19)))
		  30))
         (paschal-moon			; Day after full moon on or after March
 21.
          (- (julian->absolute (list 4 19 year))
             shifted-epact)))

; Return the Sunday following the Paschal moon

    (k-day-on-or-before (+ paschal-moon 7) 0)))

; Absolute date of Easter in Gregorian year.

(define (easter year)
  (let* ((century (1+ (quotient year 100)))
         (shifted-epact			; Age of moon for April 5...
          (modulo
           (+ 14 (* 11 (modulo year 19));    ...by Nicaean rule
              (-		; ...corrected for the Gregorian century rule
               (quotient (* 3 century) 4))
              (quotient		; ...corrected for Metonic cycle inaccuracy.
               (+ 5 (* 8 century)) 25)
              (* 30 century))	; Keeps value positive.
           30))
         (adjusted-epact	; Adjust for 29.5 day month..
          (if (or (= shifted-epact 0)
                  (and (= shifted-epact 1)
		       (< 10 (modulo year 19))))
              (1+ shifted-epact)
	      shifted-epact))
         (paschal-moon		; Day after full moon on or after March 21.
          (- (gregorian->absolute (list 4 19 year))
             adjusted-epact)))

; Return the Sunday following the Paschal moon.

    (k-day-on-or-before (+ paschal-moon 7) 0)))

; Absolute date of Pentecost in Gregorian year.

(define (pentecost year)
  (+ 49 (easter year)))

; List of the absolute dates of Islamic month, day
; that occur in Gregorian year.

(define (islamic-date month day year)
  (let* ((jan1 (gregorian->absolute (list 1 1 year)))
         (dec31 (gregorian->absolute (list 12 31 year)))
         (y (extract-year (absolute->islamic jan1)))

	 ; The possible occurrences in one year are

         (date1 (islamic->absolute (list month day y)))
         (date2 (islamic->absolute (list month day (1+ y))))
         (date3 (islamic->absolute (list month day (+ 2 y)))))

    ; Combine in one list those that occur in current year

    (append 
     (if (<= jan1 date1 dec31)
	 (list date1) nil)
     (if (<= jan1 date2 dec31)
	 (list date2) nil)
     (if (<= jan1 date3 dec31)
	 (list date3) nil))))        

; List of absolute dates of Mulad-al-Nabi occurring in
; Gregorian year.

(define (mulad-al-nabi year)
  (islamic-date 3 12 year))

; Absolute date of Yom Kippur occurring in Gregorian year.

(define (yom-kippur year)
  (hebrew->absolute (list 7 10 (+ year 3761))))

; Absolute date of Passover occurring in Gregorian year.

(define (passover year)
  (hebrew->absolute (list 1 15 (+ year 3760))))

; Absolute date of Purim occurring in Gregorian year.

(define (purim year)
  (hebrew->absolute
   (list
    (last-month-of-hebrew-year (+ year 3760)) ; Adar or Adar II
    14
    (+ year 3760))))

; Absolute date of Ta'anith Esther occurring in Gregorian year.

(define (ta-anit-esther year)
  (let* ((purim-date (purim year)))	; absolute date of Purim
    (if (= (modulo purim-date 7) 0)	; Purim is on Sunday
        (- purim-date 3)		; return prior Thursday
	(1- purim-date))))		; return previous day

; Absolute date of Tisha B'Av occurring in Gregorian year.

(define (tisha-b-av year)
  (let* ((ninth-of-av
          (hebrew->absolute (list 5 9 (+ year 3760)))))
    (if 
     (= (modulo ninth-of-av 7) 6)	; Ninth of Av is Saturday
     (1+ ninth-of-av)			; return the next day
     ninth-of-av)))

; Absolute date of the anniversary of Hebrew birthdate
; occurring in Hebrew year.

(define (hebrew-birthday birthdate year)
  (let* ((birth-day (extract-day birthdate))
         (birth-month (extract-month birthdate))
         (birth-year (extract-year birthdate)))
; It's Adar in a normal year or Adar II in a leap year
; then use the same day in last month of year.
    (if (= birth-month (last-month-of-hebrew-year birth-year))
        (hebrew->absolute
         (list (last-month-of-hebrew-year year) birth-day year))
; else use the normal anniversary of the birth date,
; or the corresponding day in years without that date
	(hebrew->absolute (list birth-month birth-day year)))))

; Absolute date of the anniversary of Hebrew death-date
; occurring in Hebrew year.

(define (yahrzeit death-date year)
  (let* ((death-day (extract-day death-date))
         (death-month (extract-month death-date))
         (death-year (extract-year death-date)))
    (cond
; If it's Heshvan 30 it depends on the first anniversary.
; If that was not Heshvan 30, use the day before Kislev 1.
     ((and (= death-month 8)
           (= death-day 30)
           (not (long-heshvan? (1+ death-year))))
      (1- (hebrew->absolute (list 9 1 year))))
; If it's Kislev 30 it depends on the first anniversary.
; If that was not Kislev 30, use the day before Teveth 1.
     ((and (= death-month 9)
           (= death-day 30)
           (short-kislev? (1+ death-year)))
      (1- (hebrew->absolute (list 10 1 year))))
; If it's Adar II, use the same day in last month of
; year (Adar or Adar II).
     ((= death-month 13)
      (hebrew->absolute
       (list (last-month-of-hebrew-year year) death-day year)))
; If it's the 30th in Adar I and year is not a leap year
; (so Adar has only 29 days), use the last day in Shevat.
     ((and (= death-day 30)
           (= death-month 12)
           (not (hebrew-leap-year? death-year)))
      (hebrew->absolute (list 11 30 year)))
; In all other cases, use the normal anniversary of the
; date of death.
     (t (hebrew->absolute
         (list death-month death-day year))))))