C Sharpens you up

http://qiita.com/yuba に移しつつあります

.Net FrameworkのDateTime型

日付時刻のデータタイプはだいたいどの処理系でも深い闇を湛えているものです。
.Net FrameworkのDateTime型はさてどんな闇でしょう。

DateTimeが表現するもの

時刻型データが表すものは大まかに3つのどれかに分類できます。

  • 世界時(≒グリニッジ時間)での時刻
  • ローカルタイムでの時刻
  • 地域を特定しない、とにかくとある時刻

前二者は絶対時刻、最後のものは特定の時刻を指していません。
特定の絶対時刻を指さない日付時刻データに意味はあるのか? たまにあるんですよ。たとえばボジョレー・ヌーボーの解禁日時を表現したりできます。
.NetのDateTimeは三種類とも表現可能です。デフォルトのコンストラクタやDateTime.Nowでは二番目のローカルタイムのものが作られます。

日付時刻値としては、グレゴリオ暦 0001-01-01 00:00:00 から100ナノ秒単位の通算カウント値で、うるう秒は含みません。うるう秒の発生した年には、.Netでは表現できない時間が1秒だけ出現します。

100ナノ秒単位ってのもくせ者ですね。大抵のRDBの時刻型と精度が合わないのでどこかで切り捨てが発生します。3ミリ秒単位という謎の精度を持つSQL Serverに比べればまだましなのか。

DateTimeの保持するデータ

よって、DateTimeは次の二つのデータを保持しています。

  • 種別(世界時Utc・ローカルLocalタイムゾーン無指定Unspecified
  • 通算カウント(64ビット符号付き整数)

通算カウントが符号付き整数なので紀元前の日付も扱えそうに思いますね? ところが扱えません。負の数を格納させようとすると例外が飛びます。

負の数を受け入れないならなぜ符号付き整数… この疑問に公式の回答は見つからないのですが、おそらく正解は時刻の引き算をシンプルにするためです。
ゼロ以上に限定した符号付き整数同士の引き算は絶対にオーバーフローしません。

DateTime型はイミュータブル、つまり作ったインスタンスの内容が書き換わることはありません。通算カウント値も、種別もです。Add, Subtract, SpecifyKindなどのメソッドは、変更結果を表す新しいインスタンスを返します。

ここでインスタンスという表現を使ってしまいましたがDateTime型は値型であり、ヒープにインスタンスが作られるのではなくスタックにデータが積まれるだけです。

種別の問題

DateTimeのCompareメソッドは、通算値の大小だけを比較します。種別を考慮しません。その結果、
Utc 2014-01-01 06:00Local(ここでは日本時間) 2014-01-01 09:00では、後者の方が遅い時刻だと判定されます。こんなクソメソッド絶対に使ってはいけません。そしてこの仕様を明記しないまま放置しているMSDNの文書化担当者は腹筋100回。

SpecifyKindメソッドは、種別を差し替えるだけのメソッドです。
Local(日本時間) 2014-01-01 06:00にSpecifyKind(DateTimeKind.Utc)を適用して得られる時刻はUtc 2014-01-01 06:00です。表現する絶対時刻が変わってしまいます。これはMSDNに明記してあるので担当者はセーフ。

絶対時刻を変えずに種別を変更するメソッドはToUniversalTime, ToLocalTimeメソッドです。Utcだった時刻にToUniversalTimeを適用しても何も変わらない、という動作は期待通りです。問題はタイムゾーン無指定の値にToUniversalTime, ToLocalTimeを適用した場合。これは、「逆の種類だったんだろう」と見なされて変換されます。闇ですね。

ToUniversalTime, ToLocalTimeは変換結果として表現範囲の上限・下限をはみ出てしまう可能性があります。こういう場合には例外が飛ぶのでも剰余が格納されるのでもなく、上限または下限に張り付きます。

まとめ

.Netの日付時刻もまた例外に漏れず闇です。
絶対時刻であるUtc, Localと概念時刻であるUnspecifiedを同じクラスで扱おうとしたあたりが闇の起源くさいですね。

いちおう、種別も判定した上で大小判定するユーティリティメソッドをGistに公開しておきました。