3/31の1ヶ月後が4/30になったり5/1になったりするメモ。

日付計算で月を加算 (減算) して、加算結果の日付が無効になったときの動きのメモ。

書くこと

  • 加算結果が無効な日付になる例
  • 各言語での例
  • 仕様
  • まとめ

加算結果が無効な日付になる例

日付計算で月を加算 (減算) したとき、加算結果の日付が無効になることがある *1
たとえば、3/31に1ヶ月を足すと4/31という無効な日付になるが、プログラミング言語により無効な日付の扱いが変わってくる。
いくつかの言語での例を書いてみる。

各言語での例

# Java
$ jshell
jshell> import java.time.LocalDate;
jshell> LocalDate.of(2018, 3, 31).plusMonths(1);
$2 ==> 2018-04-30

# Ruby
$ irb
irb(main):001:0> require "date"
=> true
irb(main):002:0> Date.new(2018, 3, 31).next_month
=> #<Date: 2018-04-30 ((2458239j,0s,0n),+0s,2299161j)>

# Golang
$ gore -autoimport
gore version 0.3.0  :help for help
gore> time.Date(2018, 3, 31, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0)
2018-05-01 00:00:00 Local

JavaRuby では4月の最後の日である4/30が返されるが、Go では翌月の5/1が返されている。

仕様

各言語の仕様を見てみる。

Java

LocalDate

This method adds the specified amount to the months field in three steps:
1. Add the input months to the month-of-year field
2. Check if the resulting date would be invalid
3. Adjust the day-of-month to the last valid day if necessary
For example, 2007-03-31 plus one month would result in the invalid date 2007-04-31. Instead of returning an invalid result, the last valid day of the month, 2007-04-30, is selected instead.

加算結果の日付が無効になるときは、月の日を有効な最後の日に調整するようだ。

Ruby

instance method Date#

対応する月に同じ日が存在しない時は、代わりにその月の末日が使われます。

Go

Time.AddDate

AddDate normalizes its result in the same way that Date does, so, for example, adding one month to October 31 yields December 1, the normalized form for November 31.

AddDateは、Dateと同じ方法で結果を正規化します。たとえば、10月31日に1か月を追加すると、11月31日の正規化された形式である12月1日が得られます。

まとめ

個人的には、Go のように3/31の1ヶ月後が5/1になるというのは変な感じがするが、4/31という無効な日付は4/30の1日後であるから、4/30の有効な1日後 = 5/1が返されるというのは納得できる。
むしろ、言語やライブラリの場合は仕様にはっきりと書かれているので使う側が気をつければいいが、お客さんや同僚と話しているときにこのあたりの "自然な加算結果" が違っていると、少し苦労しそう。

*1:うるう年が絡むと年の加算などでも同じことが起こる