日付計算の罠:閏年、月末、営業日、タイムゾーンの落とし穴

約5分

「3 月 31 日に 1 ヶ月足したら?」「2 月 29 日生まれの誕生日は?」など、日付計算は単純に見えて多くの罠があります。本記事では実装で遭遇する典型例を整理します。

月末日の処理:1 ヶ月足すと何日?

「3 月 31 日 + 1 ヶ月」の答えは:

  • 4 月 31 日(存在しない)
  • 4 月 30 日(一般的な解釈)
  • 5 月 1 日(あふれた分を翌月に)

JavaScript の Date は最後の解釈:

const d = new Date(2024, 2, 31); // 3月31日
d.setMonth(d.getMonth() + 1);
// 5月1日になる(4月は30日まで)

「同じ日付」を維持したい場合は別途処理が必要:

function addMonths(date, months) {
	const d = new Date(date);
	const targetMonth = d.getMonth() + months;
	d.setDate(1); // いったん 1 日にする
	d.setMonth(targetMonth);
	// 元の日付か月末のどちらか小さい方
	const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
	d.setDate(Math.min(date.getDate(), lastDay));
	return d;
}

閏年の判定

閏年のルール:

  1. 4 で割り切れる年は閏年
  2. ただし 100 で割り切れる年は平年
  3. ただし 400 で割り切れる年は閏年
function isLeap(year) {
	return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}

例:

  • 2000 年:閏年(400 で割り切れる)
  • 1900 年:平年(100 で割り切れるが 400 で割り切れない)
  • 2024 年:閏年
  • 2100 年:平年

2 月 29 日生まれの誕生日

法的・実用的な扱いは国によって違う:

  • 日本:2 月 28 日扱い(民法上は前日 24 時に年齢加算なので、平年は 2 月 28 日 24:00 に歳を取る)
  • 米国・英国:2 月 28 日または 3 月 1 日(州・州法による)
  • 台湾:3 月 1 日

実装で「年齢」を計算する場合、生年月日が 2 月 29 日で今年が平年のときの扱いを決めておく必要があります。

日付の差分:単純な引き算ではない

「2 つの日付の間の日数」:

const days = Math.floor((d2 - d1) / (1000 * 60 * 60 * 24));

これは大半のケースで動きますが、サマータイムの境界をまたぐ日は 23 時間や 25 時間になり、24 で割ると小数点以下が出ます。

UTC で計算するか、ライブラリ(date-fns の differenceInCalendarDays)を使うのが安全。

営業日計算

「3 営業日後」「翌月の最初の営業日」など、土日祝を除く計算:

function addBusinessDays(date, days) {
	const d = new Date(date);
	let added = 0;
	while (added < days) {
		d.setDate(d.getDate() + 1);
		const day = d.getDay();
		if (day !== 0 && day !== 6) added++; // 日曜・土曜を除く
	}
	return d;
}

ただし祝日は別途データが必要。日本の祝日は内閣府が公開(CSV)。動的に取得するか、ライブラリ(japanese-holidays など)を使う。

ISO 8601:標準形式

日付の標準的な表記:

  • 日付:2026-04-26
  • 日時:2026-04-26T12:00:00
  • タイムゾーン付き:2026-04-26T12:00:00+09:00
  • UTC:2026-04-26T03:00:00Z

メリット:

  • ソートが文字列比較で正しく動く
  • パーサーが多言語で揃っている
  • 一意で曖昧さがない

2026/04/2604/26/2026 のようなロケール依存表記は API・ログでは避ける。

週番号:ISO 8601 vs 日本式

週番号の計算ルールは複数あります:

  • ISO 8601:1 月 4 日を含む週が「第 1 週」、月曜開始
  • 米国式:1 月 1 日を含む週が「第 1 週」、日曜開始
  • 日本式:1 月 1 日を含む週が「第 1 週」、月曜開始

「2024 年第 1 週はいつか?」が方式で違うので、API で扱う際は明示する必要があります。

タイムゾーン依存の罠

const birthday = new Date('1990-01-01');
birthday.toString();
// "Mon Jan 01 1990 09:00:00 GMT+0900" (JSTで読むと9時)
// "Sun Dec 31 1989 16:00:00 GMT-0800" (PSTで読むと前日)

「1990-01-01」を ISO 形式で渡すと UTC 0時として解釈され、JST では 9 時、PST では前日の 16 時になります。日付のみを扱うならローカルタイムゾーンで作る:

const birthday = new Date(1990, 0, 1); // ローカルの1990年1月1日

年齢計算

function getAge(birthDate, today = new Date()) {
	let age = today.getFullYear() - birthDate.getFullYear();
	const monthDiff = today.getMonth() - birthDate.getMonth();
	if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
		age--;
	}
	return age;
}

「今年の誕生日が来たかどうか」を判定して 1 引く必要がある。月のみで比較すると同じ月の前後関係を見落とす。

ライブラリの選択肢

JavaScript で日付処理を本格的にやるなら:

  • date-fns:軽量、関数型、ツリーシェイクしやすい
  • dayjs:Moment.js 互換 API、軽量
  • Luxon:豊富な機能、Moment.js 後継
  • Temporal(提案中):将来のネイティブ API

日本語の祝日対応は @holiday-jp/holiday_jp などの専用ライブラリ。

まとめ

  • 月末日の足し算は実装によって挙動が違う
  • 閏年は 4/100/400 のルール
  • 2 月 29 日生まれの扱いは国・実装で異なる
  • 営業日計算は祝日データが必要
  • ISO 8601 形式を API のデフォルトに

2 つの日付の差や、N 日後の日付を求めたい場合、本サイトの日付計算機が使えます。