はじめに
この記事はUnity5.4.3p3(Mono)で発生した以下のエラーの原因と対処についての記事です。
System.ArgumentException: dateTime.Kind equals Local and offset does not equal the offset of the system's local time zone.
また、文中で出てくるMonoの実装とは、おそらくこう実装されているだろうという参考コードです。
DateTimeOffsetとは
System.DateTimeOffset
System.DateTimeに協定世界時(UTC)からのオフセット(System.TimeSpan)を加味することで
異なるタイムゾーンの時刻でも計算できるようにするものです。
問題の状況
今回の問題は以下のコードで比較的簡単に発生します。
DateTimeOffset.Parse("2017/03/11 12:00:00").AddDays(1)
ただし、これにはコード以外の前提条件が必要です。
- UnityにバンドルされているMono(mscorlib.dll)で動いている
- どうもEditorではシステムのmscorlib.dllが参照されているようで再現しませんでした
- 端末の時間がサマータイムが有効なタイムゾーンに設定されている
- 例えばアメリカ(EST)など
- ESTにおける2017年のサマータイムは3月12日(日)に始まり、11月5日(日)に終了します
- 例えばアメリカ(EST)など
- サマータイムが考慮されている端末である
- iOSや一部のAndroid端末でのみ発生を確認しています
発生している内容
DateTimeOffset
のコンストラクタに渡されたDateTime
のDateTime.Kind
がDateTimeKind.Local
である場合、
UTCからのオフセットの時間がTimeZone.CurrentTimeZoneと異なる場合に発生するエラーです。
UnityのMonoでは以下のように実装されていると思われます。
if (dateTime.Kind == DateTimeKind.Local && offset != TimeZone.CurrentTimeZone.GetUtcOffset(dateTime))
{
throw new ArgumentException("dateTime.Kind equals Local and offset does not equal the offset of the system's local time zone.");
}
前述のコードで発生しているロジックとしては以下の通りです。
-
DateTimeOffset.Parse("2017/03/11 12:00:00")
-
DateTimeOffset
が内部で保持しているDateTime
のDateTimeKind
はLocal
に設定されている - オフセットは
TimeZone.CurrentTimeZone.GetUtcOffset(dateTime);
であるため-5
が設定される
-
- AddDays(1) (内部で
Add
が呼ばれている)-
DateTime.Add(TimeSpan)
の結果("2017/03/12 12:00:00"
)、サマータイムではない時間からサマータイムの時間に切り替わる -
"2017/03/12 12:00:00"を指すDateTime
と内部で保持しているオフセット(-5
)がコンストラクタに渡される - **"2017/03/12 12:00:00"の時点でのオフセットは
-4
**であるため、 渡されたオフセット(-5
)と相違し、例外が発生する
-
UnityのMonoでは以下のように実装されていると思われます。
public DateTimeOffset Add(TimeSpan timeSpan)
{
return new DateTimeOffset(this.dt.Add(timeSpan), this.utc_offset);
}
つまり、サマータイムの境を超えるような計算をした場合に発生するということです。
もちろん、サマータイム中からサマータイムではない時間への変換も同様です。
類似の発生条件
結局のところ、TimeZone.CurrentTimeZone.GetUtcOffset(dateTime)
と違うオフセットを渡すとエラーになるため、以下のようなコードでも簡単に発生する。
DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(2.5)) // UTC+2.5のタイムゾーンは存在しないはず・・・
これはつまるところ次のコードでエラーが発生することを意味する
// 日本時間(JST)に変換する
DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(9)) // JST以外に設定されている場合にエラー
また、端末の設定によりタイムゾーンが自動で切り替わった場合にも同様に例外が発生するのではないかと思います(未検証)
var dto = DateTimeOffset.Now;
...
// 端末のタイムゾーンが切り替わる
...
dto + TimeSpan.FromHours(1);
回避方法
DateTimeOffsetが内部で保持しているDateTimeのDateTimeKindがLocalの場合にオフセットのチェックがされるため、
DateTimeKind
をDateTimeKind.Unspecified
にしてやれば問題は発生しません。
DateTimeKind
を使用しなくてもOffsetがあるため、挙動に影響はありません。
例えば次のようなコードを使用することで回避できます。
var local = System.DateTime.Now;
var offset = TimeZone.CurrentTimeZone.GetUtcOffset(local);
var unspecified = DateTime.SpecifyKind(local, DateTimeKind.Unspecified);
return new System.DateTimeOffset(unspecified, offset);
Unity以外では?
コンストラクタでのチェックは依然としてされているものの、
計算の直前でDateTimeKind.Unspecified
に変換されているので通常の利用時は問題は発生しません。
https://referencesource.microsoft.com/#mscorlib/system/datetimeoffset.cs,5cff8f26f5c7c095
Unity以外のMonoについては未検証です。