投稿者:Tetsuo Ajima | 投稿日:2011年4月04日(月) 05:29

3月30日の1カ月前は3月1日?

下記のApexコードを実行すると、デバッグログにはどのように出力されるでしょうか?

Datetime dt = Datetime.newInstance(2011, 03, 30); // (1)
dt = dt.addMonths(-1); // (2)
String result = dt.format('yyyy/MM/dd'); // (3)
System.debug(result);

このコードでは、2011年3月30日の1カ月前の日付を"yyyy/MM/dd"形式で求めようとしています。
しかし、実行結果は"2011/03/01" になります。


どうしてこのようなことが起こるのか?

上記のコードは、次のようにして動作しています。

(1)
実行ユーザーのローカル時刻で2011年3月30日0時0分0秒を指すDatetime型のインスタンスが作成される
(2)
1カ月前の日時が計算される
(3)
実行ユーザーのローカル時刻でフォーマットされた文字列が返る

注意しなければいけない点は、上記(2)の処理です。(2)の計算の結果、存在しない値(2月30日など)となった場合、存在しうる値(うるう年でなければ2月28日など)に丸められますが、ローカルタイムゾーンは考慮されず、GMTベースで丸め処理が行われます。


処理の流れをもう少し詳細に記述すると下記のようになります。

(1)
実行ユーザーのローカル時刻(日本時間)で2011年3月30日0時0分0秒 (GMTでは2011年3月29日15時0分0秒) を指すDatetime型のインスタンスが作成される。
(2)
1カ月前の日時が計算される。2011年3月29日15時0分0秒(GMT)の1カ月前は2011年2月29日15時0分0秒(GMT)となり、うるう年でなければ2011年2月28日15時0分0秒(GMT)に丸められる。
(3)
実行ユーザーのローカル時刻(日本時間)でフォーマットされた文字列が返る。2011年2月28日15時0分0秒(GMT)を日本時間に換算すると、2011年3月1日0時0分0秒となり、"2011/03/01"が返る。


GMT以外のタイムゾーンを利用するすべてのユーザーが同じ影響を受けます。
日本時間では、addMonths()の結果が下記の日時になる場合に注意が必要です。
02/29 00:00:00 - 08:59:59 (うるう年を除く)
02/30 00:00:00 - 08:59:59
02/31 00:00:00 - 08:59:59
04/31 00:00:00 - 08:59:59
06/31 00:00:00 - 08:59:59
09/31 00:00:00 - 08:59:59
11/31 00:00:00 - 08:59:59

また、addYears()メソッドにも類似の注意点が存在します。


回避策

「指定したある日付の1カ月前を求める」という要件であれば、すべての値をGMTで揃えるのが最も安全な方法です。

Datetime dt = Datetime.newInstanceGmt(2011, 03, 30); // (1)
dt = dt.addMonths(-1); // (2)
String result = dt.formatGmt('yyyy/MM/dd'); // (3)
System.debug(result);

このコードでは、2011年3月30日(GMT)の1カ月前の日付を"yyyy/MM/dd"形式で求めています。
Datetime#newInstance()の代わりにDatetime#newInstanceGmt()を使うと、引数に指定した値をGMTとして解釈してDatetime型のインスタンスが作成されます。
また、Datetime#format()の代わりにDatetime#formatGmt()を使うと、GMT換算でフォーマットされた文字列が返ります。


別のアプローチとして、Datetime型ではなくDate型を使う方法もあります。Date型にはタイムゾーンという概念が無いため、気にせずにaddMonths()を使うことができます。
ただし、Date型ではformat()に書式を指定できないため、year()、month()、daty()の各メソッドを使います。

Date d = Date.newInstance(2011, 3, 30);
d = d.addMonths(-1);
String result = d.year() + '/' + d.month() + '/' + d.day();
System.debug(result);