TL;DR — 日付は
Doubleなので、myDate + 1は明日、myDate - 7は先週です — 素の足し算は 日数なら正しい。月と年では破綻します。ひと月は 30 日ではなく、1 年は 365 日ではないからです。DateAdd("m", 1, d)はカレンダーを知っています。DateDiff("d", a, b)は丸ごとの日数を数え、DateDiff("yyyy", a, b)は経過時間ではなく越えた年の境界の数を数えます — だから 12/31 → 1/1 が 「1 年」になるのです。
Sub DateMathDemo()
Debug.Print #2026-01-31# + 1 ' 2026-02-01 — 日数:素の + で足りる
Debug.Print DateAdd("m", 1, #2026-01-31#) ' 2026-02-28 — 月を理解し、丸め込む
Debug.Print DateDiff("d", #2026-01-01#, #2026-03-01#) ' 59 — 2 つの日付の間の丸ごとの日数
Debug.Print DateSerial(2026, 3, 0) ' 2026-02-28 — 2 月の最終日
End Sub
VBA の日付計算は、たった一つの事実に立っています。日付は日数を数える Double だ、と。だから日の
計算は自明です — + 1、- 7、Date + 30 はどれもそのまま動きます。1 日はちょうど 1 だからです。
厄介ごとが始まるのは、同じ手が月や年にもそのまま通じると思い込んだときです。月や年には一定の
長さがありません。DateAdd、DateDiff、DateSerial は、まさに素の + と - にはできない
カレンダー依存の部分を担うために存在します。
この記事で学べること
- 考え方の軸 — 素の
+/-は日数なら正しく、月と年では誤り DateAdd(interval, number, date)と、噛みついてくるインターバルコード("m"と"n"、"y"と"yyyy")DateAdd("m", 1, 1月31日)が 2 月 28 日を返す理由 — 素の+では得られない月末の知恵DateDiffは経過時間ではなく越えた境界を数える — 1 日から生まれる「1 年」DateSerialは地域差なく部品から日付を組み立てる、そして月末のイディオム- 素の足し算でよい場面と、関数が必須の場面
考え方の軸:素の計算でよいのは日数だけ
日付は日数の Double なので、日の計算には関数がまったく要りません。
tomorrow = Date + 1
lastWeek = Date - 7
Debug.Print #2026-02-28# + 1 ' 2026-03-01 — VBA が月をまたいでくれる
これは正真正銘に正しい — 丸ごとの日を足すとき、VBA は月末もうるう年も処理してくれます。ただ シリアル値に足しているだけだからです。破綻するのは、ひと月や 1 年を「日数」だと考えたときです。
nextMonth = Date + 30 ' 誤り — 30 日は「ひと月」ではない
nextYear = Date + 365 ' 誤り — うるう日を取りこぼし、毎年ずれていく
+ 30 は月によって着地する日が変わり、+ 365 は 2 月 29 日を含むどの期間でも 1 日ずれます。単位が
月や年になった瞬間、素の計算はバグであり、DateAdd が要ります。
DateAdd と、噛みついてくるインターバルコード
DateAdd(interval, number, date) は date に interval 単位を number 個足します。interval は
文字列コードで、2 つの衝突がほとんどの混乱を生みます。
| コード | 単位 | 注意 |
|---|---|---|
"yyyy" |
年 | "y" は年ではない — 年内通算日を意味する |
"q" |
四半期 | |
"m" |
月 | "m" は月。分は "n" |
"ww" |
週 | |
"d" |
日 | "y" と "w" も日として振る舞う |
"h" |
時 | |
"n" |
分 | "m" ではない |
"s" |
秒 |
DateAdd("m", 15, startTime) ' 15 か月を足す — たぶん意図と違う
DateAdd("n", 15, startTime) ' 15 分を足す — 本当にほしかった "n"
"m" と "n" の取り違えが目玉のバグです。「minute(分)」のつもりで "m" に手を伸ばすと、
タイムスタンプに 15 か月を無言で足してしまいます。コードを一度声に出しましょう — m は月、n は
分、yyyy は年。そうすればこの一族はもう間違えやすくなくなります。number は負にすれば減算になり、
結果はつねに正しい日付です。
DateAdd で 1 月 31 日に 1 か月足すと 2 月 28 日が返る理由
これは素の計算が決して与えてくれない見返りです。1 月 31 日に 1 か月足しても、着地すべき「2 月 31 日」は
存在しないので、DateAdd は対象月の有効な最終日に丸め込みます。
DateAdd("m", 1, #2026-01-31#) ' 2026-02-28 (2028 年なら 2028-02-29)
DateAdd("m", 1, #2026-03-31#) ' 2026-04-30 (4 月は 30 日まで)
DateAdd("yyyy", 1, #2028-02-29#) ' 2029-02-28 (翌年にうるう日はない)
DateAdd は各月の日数を知り、うるう年も自動で調整します — それはまさに + 30 が捨ててしまう知識
です。期限日、更新日、「翌月の同じ日」を計算するなら、この丸め込みこそがほしい挙動であり、月の計算を
手作りしてはいけない理由です。
DateDiff は経過時間ではなく越えた境界を数える
DateDiff(interval, date1, date2) は「2 つの日付の間の時間」に見え、"d"(日)ではそのとおりです。
しかし "m"、"yyyy"、"q"、"ww" では、もっと微妙で意外な意味になります。2 つの日付の間に
何個のインターバル境界があるかであって、実際にどれだけ時間が経ったかではありません。
DateDiff("yyyy", #2025-12-31#, #2026-01-01#) ' 1 — 年越しの境界は 1 つ、でも 1 日差
DateDiff("m", #2026-01-31#, #2026-02-01#) ' 1 — 月の境界 1 つ、1 日差
DateDiff("d", #2026-01-31#, #2026-02-01#) ' 1 — 実際に 1 日(これは文字どおり)
つまり DateDiff("yyyy", …) は暦年が変わった回数の数え上げであって、年齢でもなければ、
経過年数でもありません。本当の経過数を得るには、境界の差を計算してから調整します — 年齢なら、
今年の誕生日がまだ来ていなければ 1 を引きます。
age = DateDiff("yyyy", dob, Date)
If DateSerial(Year(Date), Month(dob), Day(dob)) > Date Then age = age - 1
DateDiff は date1 が date2 より後だと負の数も返すので、順序の判定に便利です。ルールは
こうです。丸ごとの日数には "d" を気軽に使い、"m"/"yyyy"/"q" は境界の数え上げとして扱い、
結果を期間と呼ぶ前に調整すること。
DateSerial は地域差なく部品から日付を組み立てる
年・月・日が別々の数値であるときは、それらを文字列に貼り合わせて CDate が
正しい順で読んでくれることを当てにしないでください — それはマシンの地域設定に依存し、国境を
越えると日と月を無言で入れ替えます。DateSerial(year, month, day) は、テキストもあいまいさもなく、
日付を直接組み立てます。
d = DateSerial(2026, 2, 1) ' どのマシンでもつねに 2026 年 2 月 1 日
さらに良いことに、DateSerial はあふれた値を正規化します。月 13 は翌年に繰り上がり、日 0 は
前月の最終日になります。ここから、日付コードで最も役立つ 2 つのイディオムが生まれます。
lastDayOfMonth = DateSerial(y, m + 1, 0) ' 日 0 = m 月の最終日
firstOfNextQ = DateSerial(y, m + 3, 1) ' 3 か月先の 1 日
DateSerial(y, m + 1, 0) は「今月の最終日」の定番形です — 月の日数の対応表も、うるう年の特別扱いも
要りません。部品取り出しのガイドの Year、Month、Day と組み合わせれば、
ある日付を基準にどんな日付でも組み立てられます。
率直な結論:日は手計算、それ以外は関数で
境界線ははっきりしていて、その正しい側にとどまるだけで、日付計算のバグはほとんど消えます。
- 日数 → 素の計算 →
date + n、date - n。正しくて速い。月やうるう年は VBA が繰り上げて くれます。 - 月/年 →
DateAdd→+ 30や+ 365は禁物。DateAdd("m", …)は実際の月の日数に丸め込み ます。"m"は月、"n"は分を忘れずに。 - 差 →
DateDiff、ただし境界として読む →"d"は文字どおりの日数、"m"/"yyyy"は境界の 越えた回数なので、期間と呼ぶ前に調整します。 - 部品から組み立て →
DateSerial→ 地域差がなく、DateSerial(y, m + 1, 0)が月末のイディオム です。
覚えておくべき一文はこれです。+ 演算子はカレンダーを知らず、DateDiff は時間を測らない。それぞれが
正しく扱える、ただ一つのことに使いましょう。
ExcelMaster の活用
高くつく日付計算のバグは、もっともらしく見えるものです。+ 30 で計算して月の長さの間でずれる
更新日、DateDiff("yyyy", …) で出した「年齢」が誕生日のあたりで 1 年ずれる、文字列から組み立てて
同僚の地域設定で日と月が入れ替わる期限日。どれも軽いテストでは正しく見え、端のケースで失敗します。
ExcelMaster は、カレンダーに
正しい版を書きます。計算を説明すれば — 「翌月の最初の営業日」、「45 日後が期限の請求」、「2 つの
日付の間の月数」 — 日数には素の計算を、月と年には正しいインターバルコードの DateAdd を、部品
からの組み立てには地域差のない DateSerial を選び、DateDiff の境界カウントを本物の期間へ調整
します。あなたは必要な日付を説明するだけ。月末やうるう年を乗り越える計算を書くのは、それがやります。
よくある質問
VBA で日付に日数を足すには?
ただ計算するだけです。日付は日数なので、newDate = myDate + 7 は 1 週間後、myDate - 1 は昨日です。
丸ごとの日を足すとき、VBA は月末もうるう年も繰り上げてくれます。DateAdd に手を伸ばすのは、単位が
月や年のときだけ。それらには一定の日数がないからです。
VBA の DateAdd と、ただ + を使うことの違いは?
素の + は日数を足し、日の計算では正しく動きます。DateAdd(interval, number, date) は
カレンダーを理解するので、DateAdd("m", 1, …) は本物のひと月を足し — 1月31日 はありえない
31 日ではなく 2月28日 になり — DateAdd("yyyy", 1, …) はうるう年を扱います。日数には +、月と年
には DateAdd を使いましょう。+ 30 や + 365 ではずれます。
VBA の DateDiff が年数を間違えるのはなぜ?
DateDiff("yyyy", …) が経過年数ではなく越えた年の境界を数えるからです。12 月 31 日から
1 月 1 日までは、1 日しか経っていなくても 1 を返します。年齢や本当の経過年数には、
DateDiff("yyyy", dob, Date) を計算し、今年の誕生日がまだなら 1 を引きます。文字どおりの日数が
ほしいときは "d" を使いましょう。
DateAdd のインターバルコードは何を意味しますか?
単位を表す文字列コードです。"yyyy" 年、"q" 四半期、"m" 月、"ww" 週、"d" 日、"h" 時、
"n" 分、"s" 秒。落とし穴は 2 つ。分は "n" で "m"(月)ではないこと、そして**"y" は年
ではなく年内通算日**(年は "yyyy")であることです。同じコードが DateDiff と DatePart でも
使えます。
VBA で月の最終日を取得するには?
翌月の日 0 を使う DateSerial です — DateSerial(Year(d), Month(d) + 1, 0)。日 0 は前月の最終日と
定義されているので、月の日数の表もうるう年の特別扱いもなしに、d の月の最終日を返します。
DateSerial は、文字列を解析するときの地域による日/月のあいまいさなしに、部品から日付を組み立て
ます。
検証環境
検証環境: Excel 365 (Windows 11), VBA 7.1 — 最終確認 2026-08-26。
関連ガイド: VBA Now・Date・Time — 時計を読む · VBA Weekday・DatePart — 日付の部品を取り出す · VBA CStr・CDate・Val — 文字列を数値・日付へ変換 · VBA Format — 日付・数値の書式設定 · VBA For ループ — 実務例
