曜日を10秒で暗算しよう！


# 曜日を暗算でパっと計算したい
「来月の31日にスケジュール入れておいたから」
「水曜日ですね、わかりました(ｷﾘｯ」
↑
これがやりたい。
毎回式だけ作って忘れちゃうのでメモ。

# ツェラーの公式
曜日を求めたい日付を西暦$Y$年$m$月$d$日とすると、
曜日$h$(0 = 日曜日, ..., 6 = 土曜日)は、以下の式で表せられる。[^1][^2]
$${\displaystyle h=\left(d+\left\lfloor {\frac {13(m+1)}{5}}\right\rfloor +K+\left\lfloor {\frac {K}{4}}\right\rfloor +\left\lfloor {\frac {J}{4}}\right\rfloor -2J -1\right){\\%7}}$$
ただし:
$$\begin{align}K&={\displaystyle Y\\% 100,}\\\\
J&={\displaystyle\left\lfloor {\frac {Y}{100}}\right\rfloor,}\end{align}\\\\
m=1\mbox{ または }m=2\mbox{ のときは}\\\\
Y\mbox{ を }1\mbox{ だけ減じ、 }m\mbox{ をそれぞれ }13, 14\mbox{ として計算する.}$$

# 無理です
凡人には上式を暗算することなど無理でした。
なのでこの式を少しずつ暗算しやすいものに変形していきましょう。
暗算のしやすさの指標として以下の条件が重要なのではないかと思います。
1. 基本は加算と減算、1桁の乗算
1. 脳内スタックは1個まで
1. 重たい処理を最初に置く

「脳内スタックは1個まで」というのはこういうことです。
例えば、$2x - ax + 3$を計算するときに律義に$2x$を計算しておいてその値を覚えたまま$ax$を計算してその値からさっき覚えた値を引いて最後に3を足す、という作業は効率が悪くいかにもミスが起こりやすそうです。
なので次のように工夫してみるとしましょう。まず式を$(2-a)x+3$と見て、先に$2-a$を計算します。そしてその値にxを掛け、さらにその値に3を足し答えを得ます。このように覚える数字が常に1個で済むように極力頑張ります。
また、重たい処理は(私の場合は)最初にあった方がいいです。例えば $123 + 7^3$ を計算しようと思って $7^3$ を計算するタイミングで、「ええっと、シチシチシジュークだから、50×7-7で343か」と考えている間に123のことを忘れてしまいがちです。

# 要らないものは捨てる
上の式では律義に1582年以降の曜日を未来永劫正確に求められるようになっています。[^1]
そんな道具はいりません。これでは鶏の頭を落とすのに牛刀を使うようなものです。
~~ということで、まずは求める曜日を21世紀に(厳密には1年ずれてるけど)限定しちゃいましょう。~~
求める曜日を1900年3月1日～2100年2月28日に限定します。

:::note
2022年12月2日追記
元々は年を21世紀に限定していましたが、**たまたま**さらにその前100年であっても計算結果が一致したので年の有効範囲を広げました。これで誕生日の曜日の計算などにも使えますね！
:::

つまり、 $2000 \leq Y \lt 2100$ とします。
すると、$J={\displaystyle\left\lfloor {\frac {2000}{100}}\right\rfloor}=20$ となりガウス記号が1個消せますね。
$${\displaystyle h=\left(d+\left\lfloor {\frac {13(m+1)}{5}}\right\rfloor +K+\left\lfloor {\frac {K}{4}}\right\rfloor -1\right){\\%7}}$$
ただし:
$$K={\displaystyle Y\\% 100,}\\\\
m=1\mbox{ または }m=2\mbox{ のときは}\\\\Y\mbox{ を }1\mbox{ だけ減じ、 }m\mbox{ をそれぞれ }13, 14\mbox{ として計算する.}$$

なお、正確性を犠牲にするのはこのフェイズのみとなります。

# 欲しいものに寄せる。
どうせドヤ顔で曜日を答えたいのは今を基準に前後数年のものだと思うので、$ Y=2020+y $ としておきましょう。
$y$は正負どちらも取ります。すると、$K=20+y$ですので
$${\displaystyle h=\left(d+\left\lfloor {\frac {13(m+1)}{5}}\right\rfloor +y+\left\lfloor {\frac {y}{4}}\right\rfloor +3\right){\\%7}}$$
ただし:
$$m=1\mbox{ または }m=2\mbox{ のときは}\\\\y\mbox{ を }1\mbox{ だけ減じ、 }m\mbox{ をそれぞれ }13, 14\mbox{ として計算する.}$$

さあ、これで変数の値を極力小さくする工夫は済みました。

# 重たい処理を最初に置く
$${\displaystyle h=\left(\left\lfloor {\frac {13(m+1)}{5}}\right\rfloor +\left\lfloor {\frac {y}{4}}\right\rfloor +y+d+3\right){\\%7}}$$
ただし:
$$m=1\mbox{ または }m=2\mbox{ のときは}\\\\y\mbox{ を }1\mbox{ だけ減じ、 }m\mbox{ をそれぞれ }13, 14\mbox{ として計算する.}$$
項の順番を並び替えました。
式を見てみるとこの13を掛けて5でガウス割るフェイズが鬼門ですね。あと、但し書きも鬱陶しいです。これを何とかしましょう。
ガウス記号を使ってますが結局は12通りしかないので思い切って配列にしてみます。
$${\displaystyle h=\left(Array[m] +\left\lfloor {\frac {y}{4}}\right\rfloor +y+d+3\right){\\%7}}$$
ただし:[^3]
$$\begin{align}Array&=[1, 4, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5],\\\\m&=1または2のときはyを1だけ減ずる.\end{align}$$
もう一息です。
但し書きの $y$ の増減や末尾の $+3$ を今作った配列に吸収させちゃいましょう。
$${\displaystyle h=\left(\left\lfloor {\frac {\tilde{y}}{4}}\right\rfloor +y+Array[m]+d\right){\\%7}}$$
ただし:
$$\begin{align}Array&=[3, 6, 6, 2, 4, 0, 2, 5, 1, 3, 6, 1],\\\\
\tilde{y}&=\begin{cases}y- 1&\mbox{ if } m \leq 2\\\\y &\mbox{ otherwise.}\end{cases}\end{align}$$

# 完成！
できました。
表記はとっ散らかってますが、最初の式に比べればすこぶる簡単になっています。これならなんとか暗算できそうです。
$Array$ は生涯使えるので根性で覚えましょう。
実際に暗算するときの頭の中はこんな感じです。
2022年1月23日の曜日を求めましょう。
- mは2以下だから、(y=)2引く1を4でガウス割って0か。
- 0足す(y=)2で2。
- 2+(Array[1]=)3で5か。
- 5足す(d=23=)2で7 = 0 (mod 7)で日曜日だ！

# ベンチマーク
では本当に10秒で暗算できるのか確かめてみましょう。
不正解時のペナルティは自分が絶対に答えられる時間*1.5くらいがちょうどよかったです。
```ruby:my_zeller.rb
module MyZeller
  def self.random_day
    Time.new(2020, 1, 1) + rand(0..3650) * (60 * 60 * 24) * (-1)**rand(2)
  end

  def self.my_zeller(y, m, d)
    array = [3,6,6,2,4,0,2,5,1,3,6,1]
    y -= 2020
    y_tilde = m < 3 ? y - 1 : y
    (y_tilde / 4 + y + array[m - 1] + d) % 7
  end

  def self.benchmark(count: 10, penalty: 30)
    correct = []
    wrong = 0
    count.times do
      t = random_day
      st = Time.now
      puts 'What day does %p fall on? (0-6)' % t
      if my_zeller(t.year, t.month, t.day) == gets.to_i
        puts "Great!\nIt took %fsec" % (ct = Time.now - st)
        correct << ct
      else
        puts 'Too bad. It\'s %d.' % t.wday
        wrong += 1
      end
    end
    puts 'Ave %fsec. Rate: %f' % [
      (correct.sum + wrong * penalty) / count, 1.0 * correct.size / count
    ]
  end
end

MyZeller.benchmark
```

## 結果
```txt
$ ruby my_zeller.rb
What day does 2026-07-04 00:00:00 +0000 fall on?
6
Great!
It took 13.994682sec
What day does 2013-10-11 00:00:00 +0000 fall on?
5
Great!
It took 12.110008sec
What day does 2016-12-22 00:00:00 +0000 fall on?
4
Great!
It took 8.883201sec
What day does 2023-09-18 00:00:00 +0000 fall on?
1
Great!
It took 15.484423sec
What day does 2013-04-03 00:00:00 +0000 fall on?
3
Great!
It took 11.980344sec
What day does 2017-12-13 00:00:00 +0000 fall on?
3
Great!
It took 6.970080sec
What day does 2024-07-19 00:00:00 +0000 fall on?
5
Great!
It took 9.162382sec
What day does 2029-04-14 00:00:00 +0000 fall on?
6
Great!
It took 9.449800sec
What day does 2014-12-08 00:00:00 +0000 fall on?
1
Great!
It took 7.047777sec
What day does 2011-12-05 00:00:00 +0000 fall on?
1
Great!
It took 7.529152sec
Ave 10.261185sec. Rate: 1.000000
```
正答率100%、平均10.26秒！ まずまずですね。

# 課題
$[3,6,6,2,4,0,2,5,1,3,6,1]$ 覚えるの辛い。

## 語呂合わせをいただきました
![day.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726727/c4fdb0ce-925d-2e9e-fd98-9976f5b300ef.jpeg)
> <ruby>三室<rp>（</rp><rt>みむろ</rt><rp>）</rp></ruby>に司令　<ruby>似鯉<rp>（</rp><rt>にごい</rt><rp>）</rp></ruby>見ろい
>
> 三室は人名，似鯉はコイ目の淡水魚の名前です。

@scivolaさんありがとうございます！
12の要素が6-6で分かれているのもいいですね。
12月を思い出したいときはいきなり「似鯉～」から始めればよいわけです。
私はこれで覚えることにします。

## 2022年12月2日追記
1900年代の計算をする際に`y := 西暦 - 2000`で作った式があっても便利かと思ったので置いておきます。
とはいえ、本記事の式に`+3`しただけですが。
$${\displaystyle h=\left(\left\lfloor {\frac {\tilde{y}}{4}}\right\rfloor +y+Array[m]+d\right){\\%7}}$$
ただし:
$$\begin{align}Array&=[6, 2, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4],\\\\
\tilde{y}&=\begin{cases}y- 1&\mbox{ if } m \leq 2\\\\y &\mbox{ otherwise.}\end{cases}\end{align}$$


[^1]: 厳密にはグレゴリオ暦での曜日。
[^2]: 式は[Wikipedia](https://en.wikipedia.org/wiki/Zeller%27s_congruence)より。便宜上、式の一部を改変。
[^3]: Arrayは1-based indexing。