本記事は、サムザップ Advent Calendar 2021の12/7の記事です。
はじめに
C#の一部APIには、端末の設定によって結果が変わる挙動をするものがあります。
今回はそんな危うい挙動をする処理と、それを端末の設定に依存させないで行う方法を紹介したいと思います。
端末の言語設定に依存した処理
float n = float.Parse("1.5");
これは1.5
という文字列をパースしてfloat
へ変換する何の変哲もないコードですが、
端末が特定の言語に設定されている場合、以下の例外が投げられて処理は失敗してしまいます。
FormatException: Input string was not in a correct format.
これは小数点の表現がカルチャによって異なることと、デフォルトではカルチャは端末の言語設定を参照していることに依るものです。
[ws][sign] [integral-digits[,]]integral-digits[.[fractional-digits]][e[sign]exponential-digits][ws]
. A culture-specific decimal point symbol.
Single.Parse Method (System) | Microsoft Docs
例えば端末の言語がフランスに設定されている場合、小数点はカンマ(,
)で表現されるため、
1.5
を浮動小数点数として正しくパースすることが出来ません。
この言語設定への依存は、明示的にカルチャを指定することで解消出来ます。
以下のコードでは、float.Parse
メソッドの第2引数へ特定の国/地域に依存しないカルチャ1を指定しています。
float n = float.Parse("1.5", CultureInfo.InvariantCulture);
なお今回はfloat
を例に挙げましたが、同じ浮動小数点数のdouble
はもちろん、
DateTime
, DateTimeOffset
などのパースも端末の言語設定の影響を受けます。
Unity特有の事情
ちなみにUnityではAndroid向けにビルドしたアプリで、デフォルトのカルチャがInvariantCulture
となるバグ2がありました。
このバグは修正されており、Unity 2020.1.5f1以降ではちゃんと端末に設定された言語のCultureInfo
オブジェクトが返されます。
従って同じAndroid端末・同じ言語設定(例では日本語)で以下のコードを実行した場合、
Debug.Log(CultureInfo.CurrentCulture.DisplayName);
Unity 2019.4.15f1では、
Invariant Language (Invariant Country)
Unity 2020.3.14f1では、
Japanese (Japan)
と異なる結果が出力されます。
もしパース処理などでデフォルトのカルチャがInvariantCulture
であることに依存していたコードがある場合、
古いUnityからバージョンアップをする際には注意が必要でしょう。
端末のタイムゾーンに依存した処理
少々わざとらしいコードですが、以下の処理も端末のタイムゾーンによって結果が異なります。
// toの指す日付とfromの差を出力する
var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var delta = to.Date - from; // to.Dateでtoの日付部分を取得する
Debug.Log(delta.ToString());
例えば端末のタイムゾーンが日本標準時(+09:00)の場合は、想定した通りに2日と出力されますが、
2.00:00:00
中央ヨーロッパ時間(+01:00)に設定していた場合は、2日と8時間と出力されます。
2.08:00:00
何故この差異が生じるかというと、
DateTimeOffset
のDate
プロパティは元々のタイムゾーンが反映されないのと、
The value of the DateTime.Kind property of the returned DateTime object is always DateTimeKind.Unspecified. It is not affected by the value of the Offset property.
DateTimeOffset.Date Property (System) | Microsoft Docs
DateTime
とDateTimeOffset
の減算は、DateTime
がDateTimeOffset
に変換されてから行われますが、
その変換の際に端末に設定されたタイムゾーンが採用されてしまうことによります。
If the value of the DateTime.Kind property is DateTimeKind.Local or DateTimeKind.Unspecified, the date and time of the DateTimeOffset object is set equal to dateTime, and its Offset property is set equal to the offset of the local system's current time zone.
DateTimeOffset.Implicit(DateTime to DateTimeOffset) Operator (System) | Microsoft Docs
端末のタイムゾーンが中央ヨーロッパ時間(+01:00)のときの処理を一つ一つ追っていくと、
var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
DateTime a = to.Date; // 1. 2021-12-03 00:00:00, Kind=Unspecified
DateTimeOffset b = a; // 2. 2021-12-03 00:00:00+01:00
TimeSpan delta = b - from; // 3. 2021-12-03 00:00:00+01:00 - 2021-12-01 00:00:00+09:00
-
Date
プロパティで日付部分を表すDateTime
インスタンスを取得(DateTimeKind
はUnspecified
) -
DateTime
インスタンスのDateTimeOffset
への暗黙の変換、DateTimeKind
はUnspecified
なので、システムのタイムゾーン(+01:00)のオフセットが設定される - 差の計算の日時をUTCに換算すると、
2021-12-02 23:00:00 - 2021-11-30 15:00:00
となる
結果、差が2日と8時間になってしまっていることが分かります。
端末のタイムゾーン設定に依らず正しい結果を得たい場合は、
日付を取り出す際に元々のオフセットを指定して、明示的にDateTimeOffset
をインスタンス化する必要があります。
var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture);
var delta = new DateTimeOffset(to.Date, to.Offset) - from;
Debug.Log(delta.ToString());
これで意図した通りに2日と出力されます。
2.00:00:00
最後に
同じ環境で開発と動作確認を行っている場合、見逃されがちな端末の設定に依存した処理を紹介いたしました。
この記事が堅牢なシステムを構築するための助けになれば幸いです。
参考
- .NET のカルチャー依存 API 問題 | ++C++; // 未確認飛行 C ブログ
- .NET での数値文字列の解析 | Microsoft Docs
- DateTime と DateTimeOffset 間の変換 | Microsoft Docs
- CultureInfo.CurrentCulture always returning Invariant Language (Invariant Country) - Unity Forum