目次

概要

2つの日付(例えば、自分の誕生日と今日)の間の経過日数を求めたくなったとします。 ぱっとは出てきませんね。 原因は主に、毎月の日数がばらばらなのと、うるう年のせいなんですが。

まあ、2つの日付の差というとちょっと面倒なんで、 とりあえず、グレゴリウス暦1年1月1日を基準にして、経過日数を求めることにします。 (グレゴリウス暦施行(早い国で1582年)より前の日付も、 形式的にグレゴリウス暦とみなして計算します。) この基準日からの経過日数が分かれば、その差を取ることで、2つの日付の差も分かります。

まあ、大筋だけ言うと、 1年1月1日からymd日までの経過日数は、

  • dy: ( y 1 ) × 365

  • dl:y年までのうるう年の回数

  • dm: 1月1日からm月1日までの日数

  • d

という4つに分けて考えて、その和 dy + dl + dm + d 1 で求まります。

最初と最後の項については説明するまでもないと思うんで、 dl(うるう年)と dmm 月までの日数)に関してを説明します。

うるう年

うるう年かどうかの判定は、

  • 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月まである理由は後述します。

d(m), s(m)
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 に対して、

s(m) a m + b < s(m)

という条件を満たすような実数 a, b を求めることになります。

まあ、 傾き a は、 30 と 31 の間を取って、30.5 前後の値になることは直感的に分かると思います。 頑張っていろいろ計算すると、 a の範囲が

30.57143
214
7
a <
245
8
=30.625

のとき、上述の条件を満たすように出来ることが分かります。 ( 214/7 という値は、 2点 ( m, s(m) ) = ( 5, 120 ) ( 12, 334 ) の傾きで、 245/8 の方は ( 6, 151 ) ( 14, 396 ) の傾き。 )

例えば、きり良く a =30.6, b =32.4 とかにして、

dm = ( 306 m 324 ) / 10

で計算したり、 あるいは、 除算を避けるために、 a =979/32 , b =1033/32 にして、

dm = ( 979 m 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 で割った余りです。

更新履歴

ブログ