Posted at

UnityのDateTimeOffsetでArgumentExceptionがでてしまう件

More than 1 year has passed since last update.


はじめに

この記事は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日(日)に終了します





  • サマータイムが考慮されている端末である


    • iOSや一部のAndroid端末でのみ発生を確認しています




発生している内容

DateTimeOffsetのコンストラクタに渡されたDateTimeDateTime.KindDateTimeKind.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が内部で保持しているDateTimeDateTimeKindLocalに設定されている

    • オフセットは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の場合にオフセットのチェックがされるため、

DateTimeKindDateTimeKind.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については未検証です。