概要
2つの日付(例えば、自分の誕生日と今日)の間の経過日数を求めたくなったとします。 ぱっとは出てきませんね。 原因は主に、毎月の日数がばらばらなのと、うるう年のせいなんですが。
まあ、2つの日付の差というとちょっと面倒なんで、 とりあえず、グレゴリウス暦1年1月1日を基準にして、経過日数を求めることにします。 (グレゴリウス暦施行(早い国で1582年)より前の日付も、 形式的にグレゴリウス暦とみなして計算します。) この基準日からの経過日数が分かれば、その差を取ることで、2つの日付の差も分かります。
まあ、大筋だけ言うと、 1年1月1日からy年m月d日までの経過日数は、
-
dy: ( y −1 ) × 365
-
dl:y年までのうるう年の回数
-
dm: 1月1日からm月1日までの日数
-
d
という4つに分けて考えて、その和 dy + dl + dm + d −1 で求まります。
最初と最後の項については説明するまでもないと思うんで、 dl(うるう年)と dm(m 月までの日数)に関してを説明します。
うるう年
うるう年かどうかの判定は、
-
4の倍数の年はうるう年。
-
ただし、100の倍数の年はうるう年じゃない。
-
でも、やっぱり400の倍数の年はうるう年。
でできるので、 1年から y 年までのうるう年の回数は、
-
+ ⌊ y / 4 ⌋ ← 4年に1回、うるう年。
-
- ⌊ y / 100 ⌋ ← でも、100年に1回、うるう年でない年がある。
-
+ ⌊ y / 400 ⌋ ← でも、やっぱり400年に1回はうるう年。
(ただし、記号 ⌊x⌋ は、 x を超えない最大の整数) を足して、 ⌊ y / 4 ⌋ − ⌊ y / 100 ⌋ + ⌊ y / 400 ⌋ で計算可能です。 で、プログラム的には、整数同士の除算は、普通は余り切り捨てなので、
y / 4 - y / 100 + y / 400;
となります。 さらにちょっとプログラミング上の工夫をするなら、 除算を極力避けるために、 ÷4 をシフト演算で書き換えて、 以下のように書くことも可能。
int c = y / 100;
int dl = (y >> 2) - c + (c >> 2);
月ごとの日数
とりあえず、 「1月1日から m 月1日までの経過日数」とかいう長い言葉を何度も言いたくないので、 記号を定義しておきます。
-
d(m) :m月の日数
-
s(m) : 1月1日からm月1日までの経過日数 = 先月までの d(m) の和
まずは、 d(m) と s(m) の値の一覧を見てみましょう (表1)。 ここでは、うるう年は無視します。 14月まである理由は後述します。
月m | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
d(m) | 31 | 28 | 31 | 30 | 31 | 30 | 31 | 31 | 30 | 31 | 30 | 31 | 31 | 28 |
s(m) | 0 | 31 | 59 | 90 | 120 | 151 | 181 | 212 | 243 | 273 | 304 | 334 | 365 | 396 |
まあ、たった12個の整数ですし、 この s(m) を定数テーブルで持っておけば解決する話なんですけど、 それじゃ面白くないんで別の方法を紹介。 (無駄にテーブルを持ちたくないですし。)
まず、2月だけうるう年の問題があったり、 他の月と比べて極端に日数が少ないので、例外として扱いってしまいたいです。 そこで、1月と2月を「前年の13月と14月」とみなして、 3~14月にして考えます。
28日しかない2月を末尾に移したので、 s(m) の差 s(m)− s( m −1 ) は全て 30 か 31 のどちらかになります。 そうすると、 30 と 31 の出てくる順番は少々不規則ですが、 近似的になら直線で表せそうです。 (近似的に、というか、 格子点の隙間を通して、小数点以下切り捨てることで表すような感じ。)
すなわち、 s(m)=⌊ a m + b ⌋ となるような直線 a m + b を探してみることにします。 要は、m = 3~14 に対して、
という条件を満たすような実数 a, b を求めることになります。
まあ、 傾き a は、 30 と 31 の間を取って、30.5 前後の値になることは直感的に分かると思います。 頑張っていろいろ計算すると、 a の範囲が
214 |
7 |
245 |
8 |
のとき、上述の条件を満たすように出来ることが分かります。 ( 214/7 という値は、 2点 ( m, s(m) ) = ( 5, 120 ) と ( 12, 334 ) の傾きで、 245/8 の方は ( 6, 151 ) と ( 14, 396 ) の傾き。 )
例えば、きり良く a =30.6, b =−32.4 とかにして、
で計算したり、 あるいは、 除算を避けるために、 a =979/32 , b =−1033/32 にして、
で計算します。 プログラム的に書くなら、÷32 はシフト演算に置き換えられて、以下のようになります。
int dm = (m * 979 - 1033) >> 5;
完成品
結局、これまでに説明した内容をまとめると、 1年1月1日からの経過日数を求めるプログラムは以下のようになります。
/// <summary>
/// グレゴリウス暦1年1月1日からの経過日数を求める。
/// (グレゴリウス暦施行前の日付も、
/// 形式的にグレゴリウス暦と同じルールで計算。)
/// </summary>
/// <param name="y">年</param>
/// <param name="m">月</param>
/// <param name="d">日</param>
/// <returns>1年1月1日からの経過日数</returns>
static int GetDays(int y, int m, int d)
{
// 1・2月 → 前年の13・14月
if (m <= 2)
{
--y;
m += 12;
}
int dy = 365 * (y - 1); // 経過年数×365日
int c = y / 100;
int dl = (y >> 2) - c + (c >> 2); // うるう年分
int dm = (m * 979 - 1033) >> 5; // 1月1日から m 月1日までの日数
return dy + dl + dm + d - 1;
}
ちなみに、 経過日数の計算ができれば、曜日の判定も可能です。 有名な曜日判定法に、ツェラーの公式ってのがあるんですが、 この公式は、このページで説明した内容と同様にして導出できます。
参考までに、ツェラーの公式による曜日判定プログラムを書いておくと、以下の通りです。
/// <summary>
/// 曜日判定
/// </summary>
/// <param name="y">年</param>
/// <param name="m">月</param>
/// <param name="d">日</param>
/// <returns>0なら日曜、1: 月曜、…、6: 土曜</returns>
static int GetDayOfWeek(int y, int m, int d)
int c = y / 100;
y %= 100;
int dow = d + 26 * (m + 1) / 10 + y + y / 4 + c / 4 - 2 * c;
dow %= 7;
月 m の前に 26 という謎の定数が出てきますが、 これは、「月ごとの日数」で出てきた定数 306 を 70 = 7×10 で割った余りです。