これは何?
うるう年の計算を皆様がどう実装しているのかが気になったので調べてみた。
前提
現在の暦法は 1600年ごろから運用されている「グレゴリオ暦」で、うるう年の計算はそれに基づいている。
グレゴリオ暦は一年の長さを 365.2425日 とみなすことで、それまでのユリウス暦(一年の長さを 365.25日とみなす)より正確に太陽に追従する。
しかし、正確な一年の長さはどうも 365.24219日らしい。
暦法 | 一年の長さ | 正確な一年との差 |
---|---|---|
ユリウス暦 | 365.25日 | 1/128 日 |
グレゴリオ暦 | 365.2425日 | 1/3226 日 |
というわけで、グレゴリオ暦といえども 三千年ほどで一日ずれる。
……というかんたんな話ではない。
一日の長さは様々な摩擦によってどんどん長くなると考えられている。
また、地球の近傍を質量大きめの天体がスイングバイすると一日の長さ・一年の長さが多少変わることもあるだろう。
いずれにせよ、グレゴリオ暦が未来永劫続くということはありえないわけで、うるう年の計算はそのうち改変される。
破滅的な出来事に起因するのでない限り、最低でも 10年前に予告されるとは思うし、私が生きているうちに改変されるとは思っていないけれど、改変されることは間違いない。
さらに。
西暦1901年以降、2099年までにしか興味ない場合、「4で割り切れればうるう年」で正解となる。2100年以降に対する検査が必要ではない合理的な理由があるのであれば計算時間削減のためにそんな実装を選ぶのもひとつの正義だ。
実装側の悩みとしては、調査対象の年を制限するかどうかという問題がある。
グレゴリオ暦が存在しなかった時代の処理をどうするか。
グレゴリオ暦は存在するが、例えば日本国内では施行されていなかった時期はどうするのか。
グレゴリオ暦が到底存続指定なさそうなはるか未来(西暦百万年とか)をどうするのか。
というような話とはあまり関係なく、各実装の様子をピックアップしようと思う。
各実装
ruby
C 言語側で実装されている c_gregorian_leap_p(int y) にある。
return (MOD(y, 4) == 0 && y % 100 != 0) || MOD(y, 400) == 0;
Python
calendar の isleap が python で書かれている。
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
Go
うるう年かどうかを判定する関数は公式には出てない感じがするが、time.go 内に isLeap という関数
がある。
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
NodeJS(v8)
C++ で実装された関数 がある。
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
順当な感じ。
それとは別に GregorianCalendar::isLeapYear
という関数 もある。
return (year >= fGregorianCutoverYear ?
(((year&0x3) == 0) && ((year%100 != 0) || (year%400 == 0))) : // Gregorian
((year&0x3) == 0)); // Julian
名前に Gregorian
と入っているのにユリウス暦にも対応するらしい。
Java
他にもあるかもしれないけど、 OpenJDK で。
isGregorianLeapYear
というメソッド で定義されている。
return (((gregorianYear % 4) == 0)
&& (((gregorianYear % 100) != 0) || ((gregorianYear % 400) == 0)));
.NET Core
Syste.DateTime.IsLeapYear
の実装 は、わりと思いがけない内容になっている。
if (year < 1 || year > 9999)
{
ThrowHelper.ThrowArgumentOutOfRange_Year();
}
if ((year & 3) != 0) return false;
if ((year & 15) == 0) return true;
// return true/false not the test result https://github.com/dotnet/runtime/issues/4207
return (uint)year % 25 != 0 ? true : false;
year の下限は 1582 ぐらいがいいと思うんだけど大丈夫なのかなぁ。
最後にある (uint)
は計算速度上げるためかな?
? true : false;
は不要そうに見えるけど、どうなんだろ。
それはさておき、このわかりにくい実装は、たぶん計算が速い。
& 15
で 16 の倍数かどうかを見ている。
16 の倍数ならうるう年。400 の倍数かどうかのチェックに相当。
% 25
のチェックは、 100 の倍数かどうかのチェックに相当。
一方、 System.Data.SqlTypes.SqlDateTime
の IsLeapYear
は普通。
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
Linux
たくさんあるんだけど、中でも 面白いの をまずは紹介する。
/* 120 (2100 - 1980) isn't leap year */
#define YEAR_2100 120
#define IS_LEAP_YEAR(y) (!((y) & 3) && (y) != YEAR_2100)
潔い。西暦 2200 年には到達しないことがなにか別の条件でわかっているんだろう。
もうちょっと普通っぽいの もある。
is_leap_year = year_of_century != 0 ?
year_of_century % 4 == 0 : century % 4 == 0;
もっと普通な感じの もある。
leap_yr = ((!(yrs % 4) && (yrs % 100)) || !(yrs % 400));
不要な加算をしている例 もある。
leap_yr = ((!((yrs + 1900) % 4) && ((yrs + 1900) % 100)) ||
!((yrs + 1900) % 400));
不要な加算は、 % 4
の側は最適化器が殺してくれるかもしれないけど、 % 100
の方は厳しそう。
極めて普通なもの もある。
return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
FreeBSD
ふつうな実装 がある。
y += 1900;
return (y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0);
haiku
こちらも 普通の実装 となっている。
#define IS_LEAP_YEAR(y) ((((y) % 4) == 0) && (((y) % 100) || ((((y)) % 400) == 0)))
C++ なんだし、inline 関数にしたほうがいいよ、と思う。
まとめ
右端の列は、2022年について行われる計算を並べたもの。 %
は剰余、&
はビットAND、+
は二項演算の加算(など)である。
出どころ | 出てくる数字 | if を使う | 補足 | 2022の計算 |
---|---|---|---|---|
ruby | 4, 100, 400 | ❌ | %4, %400 | |
Python | 4, 100, 400 | ❌ | %4 | |
Go | 4, 100, 400 | ❌ | %4 | |
Node #1 | 4, 100, 400 | ❌ | %4 | |
Node #2 | 3, 100, 400 | ❌ | ユリウス暦対応 | &3 |
Java | 4, 100, 400 | ❌ | %4 | |
.NET Core #1 | 3, 15, 25 | ✅ | わかりにくく、たぶん速い | &3 |
.NET Core #2 | 4, 100, 400 | ❌ | %4 | |
Linux #1 | 3, 2100 | ❌ | 2200 年以降は正しくない | &3 |
Linux #2 | 4, 400 | ❌ | year of century を利用 | !=0, %4 |
Linux #3 | 4, 100, 400 | ❌ | %4, %400 | |
Linux #4 | 4, 100, 400 | ❌ | 不要な加算有り | +1900, %4, +1900, %400 |
Linux #5 | 4, 100, 400 | ❌ | %4 | |
FreeBSD | 4, 100, 400 | ❌ | +1900, %4 | |
haiku | 4, 100, 400 | ❌ | inline 関数にすればいいのに | %4 |
ということで、 if 文を使っているのは .NET Core の例だけだった。
上表の「出てくる数字」は、ソースコード上にある剰余等を意図する計算のための数字を登場順に並べたもの。
だいたいみんな 4, 100, 400 の順になっている。
当初
おそらく、最初に「4 で割り切れなければ false」という判断を入れると 75% の確率で 100 や 400 での除算をサボれるので計算時間への配慮でそうしてるんだと思う。唯一 if を使っている .NET Core でも
if ((year & 3) != 0) return false;
を最初(厳密には、範囲チェックの直後)にしている。
と書いていたが、
コメントの指摘 を受けて、2022年(つまり、4で割るだけでうるう年じゃないとわかる年)を示す値を入れた場合の計算がどうなるのかを見てみたところ、おもったより多くの実装でサボりきれてないことがわかった。
Linux #4 や FreeBSD の無駄に 1900 を足しているのはちょっと残念だね。
感想とか
もうちょっとバラけるかなと思っていたんだけど、案外同じ感のものが多かった。
文化の違いかぁ、みたいな話を書こうと思っていたのだけれど。
当初
.NET Core と、Linux #1 以外はわりと普通で、4で割った余りを剰余で取るか、ビット操作で取るかぐらい。
と書いていたが、Linux #2 の ?
:
を交えた「下二桁が0なら400との剰余を調べ、そうでなければ4との剰余を調べる」という作戦も珍しい。
他に「この言語 / OS のうるう年判定はこうだよ」という情報があったらコメントに書いていただけると楽しいと思う。