🗓️

JSで西暦1年1月1日が何曜日か求める

2021/12/21に公開3

ということで果たして需要があるのかは謎ですが、古い日付の曜日を正しく取得する方法について考えていきます。

突然ですが、西暦1年1月1日が何曜日かJSで求めるにはどうしたら良いでしょうか?

忙しい人のためのまとめ

西暦1年1月1日は日曜日です。
ポイントは

  • 改暦を考慮してユリウス暦で求める
  • ツェラーの公式を使うと簡単に求められる
  • さらに、閏年のミスを考慮に入れる
    ...

getDay

まずはシンプルにDate.prototype.getDay()を使って求めてみます。

const firstDay = new Date(1,0,1);
const day = firstDay.getDay();
console.log(day); // 2 つまり火曜日

果たしてこれは正しいのでしょうか?
検証してみます。

Dateの仕様

まず、通常のコンストラクタの呼び出しでは第一引数のyearは 0-99を渡した場合、1900-1999に自動でマッピングされてしまいます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#two_digit_years_map_to_1900_-_1999
つまり上記では1を渡しているので、マッピングされた結果1901年の曜日が出力されます。

マッピングを回避するにはDate.prototype.setFullYear()メソッドを使えば良いようです。

const firstDay = new Date(1,0,1);
firstDay.setFullYear(1);
const day = firstDay.getDay(); 
console.log(day); // 1 つまり月曜日

ではこのgetDayがどのような仕様になっているかを確認します。

The weekday for a particular time value t is defined as

WeekDay(t) = 𝔽(ℝ(Day(t) + 4𝔽) modulo 7)
A weekday value of +0𝔽 specifies Sunday; 1𝔽 specifies Monday; 2𝔽 specifies Tuesday; 3𝔽 specifies Wednesday; 4𝔽 specifies Thursday; 5𝔽 specifies Friday; and 6𝔽 specifies Saturday. Note that WeekDay(+0𝔽) = 4𝔽, corresponding to Thursday, 1 January 1970.

https://tc39.es/ecma262/#sec-week-day」

ポイントは Day(t)の部分です。引数として渡されているtはtime Valueで、1970年からのmsのカウントとなっています。Day関数はそのカウントを元に、1970年から何日立ったか?を元に曜日を算出しているようです。 4𝔽プラスしているのは1970年1月1日が木曜日であったことを表します。

では、ミリ秒のカウントが仮に存在しなかった場合、どうやって曜日を導き出せるでしょうか?

閏年

先程も少し触れましたが、忘れてはならないのは閏年という存在です。
閏年の定義は、

1.西暦年が4で割り切れる年は(原則として)閏年。
2.ただし、西暦年が100で割り切れる年は(原則として)平年。
3.ただし、西暦年が400で割り切れる年は必ず閏年。

となっています。
wikipedia 閏年

ECMA-262でも、上記法則の通りに定義されています。

DaysInYear(y)
= 365𝔽 if (ℝ(y) modulo 4) ≠ 0
= 366𝔽 if (ℝ(y) modulo 4) = 0 and (ℝ(y) modulo 100) ≠ 0
= 365𝔽 if (ℝ(y) modulo 100) = 0 and (ℝ(y) modulo 400) ≠ 0
= 366𝔽 if (ℝ(y) modulo 400) = 0

この計算式に従って各年を計算していけば、曜日が求められそうですね。

ツェラーの公式

とはいえ各年を計算していくのは途方もない話です。
公式に当てはめて曜日を算出できるツェラーの公式というものがあります。

y年m月d日の曜日を求める場合は

ただし

C=⌊y/100⌋
Y=y mod 100
Γ=-2C + ⌊Y/4⌋

また注意点として、1月2月は前年の13月,14月として計算します。
深夜バイトの25:00みたいな扱いですね。

計算したhの0~6を土曜日~金曜日に対応づけると求めることができます。
式の詳しい解説は下記の記事が参考になります。
【面白い数学】誕生日の曜日を計算で求める方法~ツェラーの公式~

これで初めの質問「西暦1年1月1日は何曜日か」に答えることができそうです。
ですがもう一点、考慮すべき事項があります。

グレゴリオ歴とユリウス歴

ポイントは1582年に暦がユリウス歴からグレゴリオ歴に変わっているという点です。
ECMAの仕様ではユリウス歴は考慮されていない為ズレが出てきます。

グレゴリオ歴とユリウス歴では閏年の頻度が異なっています。ユリウス歴で追加しすぎてずれてしまった閏年を調整するため、ユリウス歴の1582年10月4日の次の日を、グレゴリオ歴の1582年10月15日と定めました。
これを配慮して計算しなければ、ユリウス歴の頃の曜日を正確に出すことはできません。
幸いツェラーの公式にはユリウス歴での計算式も用意されています。

Γ(ガンマ)は暦によって値が変わります。

となります。
ですが、JSの場合、負数の剰余の計算は第一オペランドが正か負かによって返る値が変わってしまいます。

-3 % 2 // -1
3 % -2 // 1

なので、プログラミングで求める場合Γは

  • ユリウス歴では6C + 5
  • グリゴリオ歴では-5C+⌊C/4⌋

と調整してあげましょう。

ユリウス歴の閏年の誤り

さらに困ったことに、ユリウス歴では閏年が誤って入れられたことで不規則になっている期間があります。

詳細

紀元前45年にカエサルがこの暦法を導入した際に閏年は4年に1回と決められたが、直後の紀元前44年にカエサルが暗殺された後、誤って3年に1回ずつ閏日が挿入された。この誤りを修正するため、ローマ皇帝アウグストゥスは、紀元前6年から紀元後7年までの13年間にわたって、3回分(紀元前5年、紀元前1年、紀元4年)の閏年を停止した。紀元8年からは正しく4年ごとに閏日を挿入している。

紀元前45年から紀元8年までの間に、どの年に閏年が置かれていたのかについては、詳しい記録が残っておらず、何度か論議になった。紀元前45年から3年ごとという学者もいれば、紀元前44年から3年ごとという学者もいた。1999年にローマ暦とエジプト暦の両方の日付が記載された紀元前24年当時の暦が発見され、それを基にした最新の説によると、紀元前45年から紀元16年までの閏年の置かれ方は次のとおりである。

紀元前44年・紀元前41年・紀元前38年・紀元前35年・紀元前32年・紀元前29年・紀元前26年・紀元前23年・紀元前20年・紀元前17年・紀元前14年・紀元前11年・紀元前8年・(この間は閏年を置かず)・紀元8年・紀元12年・紀元16年(以後、4年ごと)。

そのため西暦4年以前はツェラーの公式で求めることができません...
西暦1~4年間の曜日だけを求めたいのであれば、単純に西暦4年2月28日から以前の曜日を+1すれば良いので応急処置としてこちらで対応してみます。この辺りが限界です。

ちなみに歴史的文献などを参照する際は、グレゴリオ暦が導入された時期が地域によってまちまちなので注意が必要です。
詳しくはwikipediaのグレゴリオ暦 各国・各地域における導入を確認してください。

結局西暦1年1月1日は何曜日?

さて、これまでの調査を踏まえ計算してみましょう。
1月は前年の13月とする為、
ツェラーの公式に則ると、
h = {1 + 36 + 0 + 0 + 5} mod 7
= 42 mod 7
= 0

そして西暦4年2月28日から以前のため +1 すると 6(日曜日) となります。
(ツェラーの公式では0~6は土曜~金曜で対応づけられています。)
なかなか大変な道のりでした。

ライブラリ

せっかくなのでzellerで曜日を求めるライブラリをさくっと作りました。
https://www.npmjs.com/package/@iricocco/zeller
https://github.com/takanokana/zeller

下記のように使用できます。

import zeller from '@iricocco/zeller'

zeller(1995, 6, 1)  
// 1995年6月1日の曜日である "Thu"(木曜日)が返る

zeller(1, 1, 1)
// 1年1月1日の曜日である "Sun"(日曜日)が返る

Discussion

hoshinotsuyoshihoshinotsuyoshi

昔の暦難しいので興味深く読みました。
1点、 最後6が出力されてますが、6は土曜日だと思ってました... JSでは日曜日なのでしょうか

iricoirico

@honio hoshino
ここはツェラーの公式の約束事として

計算したhの0~6を土曜日~金曜日に対応づけると求めることができます。
となっています。

JSでの曜日と数字の対応と異なるのでちょっとわかりづらかったかと思うので、
近くにその旨追記しておきます、コメントありがとうございます!