鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

.NET で時刻を表現する方法

.NET で時刻を表現する方法は複数あります。本記事ではそれらをまとめてみます。

DateTime(Offset)

最もよく使うのは、DateTime 構造体だと思います。
これらは、年、月、日、時、分、秒といった個別の時刻要素に対応するプロパティを持っています。
内部的には Ticks プロパティに相当する単一のフィールドを持っており、各種プロパティはここから計算されます。
DateTimeOffset 構造体の場合、それに加えて UTC との時差を持っています。

Ticks

これは DateTime 構造体における時刻の最小解像度による表現です。Ticks は整数値であり、100ナノ秒を1とする表現です。10 で1マイクロ秒、10,000で1ミリ秒、10,000,000で1秒です。
DateTime の起点は、グレゴリオ暦(いわゆる西暦)における、0001-01-01T00:00:00 ですから、例えば Ticks が 10,000,000 であれば、0001-01-01T00:00:01 という時刻を表します。
TicksDateTimeOffset 構造体や TimeSpan 構造体TimeOnly 構造体の内部表現としても使われます。DateOnly 構造体は日単位であるため、Ticks ではなく日数で保持しています。

タイムスタンプ

Stopwatch.GetTimestamp メソッドTimeProvider.GetTimestamp メソッドによって得られる整数値です。
これは、他の形式のような絶対時刻ではなく、コンピュータの起動時点からの相対時刻です。そのため、データベースに記録したり、Web API を通じて受け渡しするのに適当な形式ではありません。TimeProviderインスタンスが異なれば互換性がないものと考えましょう。

精度もプラットフォームによって異なり、Stopwatch.Frequency フィールドや、TimeProvider.TimestampFrequency プロパティによって得られます。
ある Windows 環境では 10,000,000 でしたが、同じマシン上の Linux (WSL) 環境では 1,000,000,000 となりました。2桁違いますね。

これらは Frequency、つまり周波数ですから、1秒間でどれだけ値が増加するかを表します。10,000,000カウントが1秒ということは、1は100ナノ秒で、これは Ticks と同じ精度です。Linux ではナノ秒精度ですね。
タイムスタンプ値(の差分)を周波数で割ることで経過秒数が得られます。

相対時刻であるため、起点時刻情報を補わなければ DateTime に変換することができません。起点時刻情報を origin とするのであれば、以下のようなコードで DateTime が得られるでしょう。

var timeProvider = TimeProvider.System;
var timestamp = timeProvider.GetTimeStamp();
var dateTime = origin.AddSeconds((double)timestamp / timeProvider.TimestampFrequency)

Unix 時刻

.NET でよく使われる表現ではありませんが、他言語や Web API とのやり取りではよく使われる方法です。
これは 1970-01-01T00:00:00 を起点として、秒、マイクロ秒、ナノ秒などの単位で表される整数値です(閏秒については考慮されません)。

.NET との相互運用では、DateTimeOffset 構造体の FromUnixTimeSeconds メソッドFromUnixTimeMilliseconds メソッドToUnixTimeSeconds メソッドToUnixTimeMilliseconds メソッドで相互変換が可能です。

FILETIME

Win32 API でよく使われる形式で、1601-01-01T00:00:00 を起点とする100ナノ秒精度、64bit幅の整数値です。起点は違いますが精度は Ticks と同じですね。
DateTime.FromFileTime メソッドDateTime.ToFileTime メソッド等で相互変換できます。

OLE オートメーション時刻

だいぶマイナーな形式です。DateTime.FromOADate メソッドDateTime.ToOADate メソッドで相互変換できます。
1899-12-30T00:00:00 を起点とし、1日を1として、1日未満の時間は少数で表す浮動小数点形式です。
実は、ExcelNOW 関数等が扱う「シリアル値」というのはこれです。
OLE オートメーションについての解説はここでは扱いません。

なお、起点が変な日付なのは、Excel との互換性のためであると考えられます。
まず Excel では 1900-01-01T00:00:001 として起点としています。セルに 1 と入力して、書式を日付にすると、1900/1/1 になります(0 を日付として書式設定すると 1989/12/31 ではなく 1900/1/0 という不正な日付になってしまうので、1 が有効な最小値と言えます)。
さらに、Excel には Lotus 1-2-3 との互換性とのために故意に仕込まれたバグがあります。これは西暦1900年を閏年として扱うというもので、このため、本来存在しない 1900/2/29 が存在しており、1日多くなっています。
こうした問題から、1900/3/1 以降において、FromOADate メソッドとシリアル値の整合性を取るために、1899-12-30T00:00:00 という妙な日時が起点になっているのだと思われます。

ついでに言うと、VBADateSerial 関数ToOADate メソッドと同じ挙動をします。

おわりに

最近 TimeProvider をよく使うようになって、タイムスタンプとは何ぞやというのが気になったのでまとめてみました。お役に立てましたら幸いです。