読者です 読者をやめる 読者になる 読者になる

曜日算出

日付が指定されたとき、その曜日を算出するにはどうするのが効率的だろう?
コンピュータ内部では基準日からの秒数で時刻を保持していることが多いので、日数に換算した上で 7 で割れば良いのだが、あくまで年月日の形で与えられた場合で考えることにする。
暦のルール通りに私なりにコードを書いてみるとこうなった。

(define (leap-year? year)
  (or (= (modulo year 400) 0)
      (and (= (modulo year 4) 0)
           (not (= (modulo year 100) 0)))))

(define (day-of-week year m d)
  (define md '#(0 0 3 3 6 1 4 6 2 5 0 3 5))
  (let* ((y (- year 1))
         (w (apply + y (map (cut quotient y <>) '(4 -100 400)))))
    (modulo (+ w d
               (vector-ref md m)
               (if (and (leap-year? year) (< 2 m)) 1 0))
            7)))

1 年の日数 365 日は一週間の日数 7 日で割ると余りが 1 となるので、毎年 1 日ずつ曜日はずれていくことになる。 閏年だと 2 日のズレが発生するので、閏年があった回数分だけ更にずらし、月ごとのずれは定数を与え、最後に日を足して mod 7 すれば曜日となる。 更に閏年を特別扱いして 3 月以降の曜日を 1 日ずらしている。
さて、よく知られた曜日算出の方法として「ツェラーの公式」というものがある。 Schemeツェラーの公式を実装した記事を紹介しておこう。
id:sirocco634:20090315:1237119679
さて、 Gauche には曜日の算出を行う関数は用意されていて、 srfi-19 に入っている date-week-day がそれだ。 更にその下請け関数である tm:week-day 関数が実際の算出を行っている。

(define (tm:week-day day month year)
  (let* ((a (quotient (- 14 month) 12))
	 (y (- year a))
	 (m (+ month (* 12 a) -2)))
    (modulo (+ day y (quotient y 4) (- (quotient y 100))
	       (quotient y 400) (quotient (* 31 m) 12))
	    7)))

ツェラーの公式と同じ理屈を使っているらしいことは漠然とわかるが、 Scheme で簡潔に書けるように変形してあるようだ。
ツェラーの公式では 1 月と 2 月は前年度の 13 月と 14 月として扱うというルールなのだが、条件分岐を使わずに (quotient (- 14 month) 12) という形で表現しているのは面白い。
さて、注目すべきは (quotient (* 31 m) 12) である。 月ごとのずれを定数で与えるかわりにこの式があるように見えるので、とりあえず実際にどのような値になるのか試してみよう。

(use srfi-1)
(map (lambda(m) (modulo (quotient (* 31 m) 12) 7)) (iota 12 1))
;;  => (2 5 0 3 5 1 4 6 2 4 0 3)

私が定数として書いたものと比較すると…

(0 0 3 3 6 1 4 6 2 5 0 3 5)
      (2 5 0 3 5 1 4 6 2 4 0 3)

計算の都合により 1 の違いになっているがぴったり一致する。
どうして 31/12 という定数からこのような結果が生じるのか興味深い。
Document ID: 95c57e203a4da6f70893969ca8b13e84