(注意:2024年1月15日更新→.NET5.0はサポート終了しました。サンプルコードは.NET8.0に更新しました)
WPFアプリとgRPCサービスで、日時を通信する処理を作成してみたところ、案外分かり難い点が多いので、ハマりやすい部分を重点的に解説したいと思う。
Protocol Buffers のC#ライブラリのTimestampと、.NETのDateTimeの仕様の違いから、単純な両者の変換処理では使用できない。
両者を正しく使用するにはUTC,UNIX時刻(Timestamp),タイムゾーン,DateTimeKindの知識が必要となる。
その点を踏まえて、Protocol Buffers の Timestamp と、.NETの DateTime・DateTimeOffsetを使用したクライアント・サーバー型アプリの使い方を解説する。
サンプルコードとして以下のGitHubへのリンクを掲載しておく。
二つの単項メソッドを追加している。
日時についての基礎知識
Protocol Buffers の Timestamp と、.NETの DateTime の扱い方を解説する前に、日付と日付型(時刻を含む)についての基礎知識の解説をする。
UTCとUNIX時刻(Timestamp)
Protocol Buffers の Timestamp は、その名に違わずUNIZタイムスタンプの値を持つ。
だから gRPCで日付型を扱う為にはUNIXタイムスタンプの知識が必要であり、更にUTCの知識も必要になる。
この認識がないと gRPCで日付型を扱う事ができない。
UNIX時刻(Timestamp)
UNIXタイムスタンプは1970年1月1日0時0分0秒からの経過秒数を整数で有する値である。
年月日時分の値は無い。秒数だけだ。
昔は32bitで表していたが、近年は64bit整数で表すものが多くなってきている。
Protocol Buffers の Timestamp も64bit整数で値を有する。
UTC(協定世界時)
UTとは国際的に定められた、英国の標準時である「グリニッジ標準時」を表す「世界時」の規格である。
UT1・UT2・UTC の三種類があり、一般市民が使用する「世界時」の規格が「UTC」である。
UTCは、1970年1月1日0時0分0秒からの経過秒数を整数で保持し、それを基に年月日時分秒に変換する規格を定めている。
多くのOSやコンピュータ言語やDBMSで、UNIXタイムスタンプからUTCに変換する機能を有し、世界各国の時差に対応する為に基準となる日付時刻になっている。
多くの場合、日本時間への対応もUTCから変換する事で行っている。
時差とタイムゾーン
地球は丸いので世界各国の標準時には時差がある。
各国にはそれぞれの「標準時」が存在する。
この中で東西方向の位置を基準に、南北に重なる国々の標準時は同じになる。
この同じ標準時ごとの分類をタイムゾーンと呼ぶ。
タイムゾーンはUTCとの時差で表す。
日本の場合は、UTCより9時間早いので「UTC+9:0:0」と表す。
台湾やシンガポールなら、UTCより8時間早いので「UTC+8:0:0」と表す。
ニューヨーク(米国東部時間)なら、UTCより5時間遅いので「UTC-5:0:0」と表す。
DateTimeとタイムゾーン
.NET で、国内専用の業務システムを開発していると意識しないが、DateTime型は自国とUTCを識別する情報を保有している。
タイムゾーンの情報は保有していないが、その日時がUTCであるか自国であるかを識別するフラグを有する。
この情報はコンストラクタで指定する事ができ、DateTimeKindをコンストラクタ引数に渡す事によって行う。
DateTimeKindは、Local, Unspecified, Utc の三つを区別する。
Protocol Buffers の Timestamp は DateTimeKind.Utc を有するDateTime型でなければ使用できない。
それ以外は変換エラーとなる。
DateTimeOffsetとタイムゾーン
DateTimeOffset型 はDateTime型の拡張で、DateTime型に加えて、内部にタイムゾーンの情報を有する。
日本のタイムゾーンを有するDateTimeOffset型は、日本標準時と時差「UTC+9:0:0」の情報を持つ。
UTCを参照する時はこの時差情報から、UTCに変換する。
これにより世界中のタイムゾーンの日付型を、一つのシステムに共存できる。
Protocol Buffers の Timestamp は DateTimeOffset型 に対応しており、Timestamp へ変換する時UTCへ変換する。
TimeSpan の使い方
TimeSpan は、日数、時間、分、秒、および秒の小数部の正または負の数として計測される時間間隔 (時間または経過時間) を表す。
異なる二つのDateTime型の日数時間差を表す時などに使用する。
DateTimeOffset型を生成する時にタイムゾーンをコンストラクタ引数に渡す器として使用する。
Protocol Buffers の異なる二つの Timestamp型の時間差を表す型に、Duration型 が有る。
これを .NETの型に変換する場合は、TimeSpan型のオブジェクトに変換する。
Timestamp型とTimeSpan型は、名前が似ているので最初は混乱するが、両者は全然違うものだ。
Timestamp と、.NETの 日付型との二つの扱い方
Protocol Buffers の Timestamp型は、UTC以外の標準時を受け付けない仕様の為、日本標準時などローカルの標準時を使用する場合は、工夫が必要になる。
Protocol Buffers の Timestamp型と、.NETの日付型を扱う方法は大きく二つになると思う。
一つは、全てDateTime型だけでシステムを開発する方法。
もう一つは、DateTimeOffset型を中心に使用し、UTCと日本標準時を明確に使い分ける扱い方だ。
日本国内のみDateTimeだけで扱う
全ての日付をDateTimeで扱うのなら、全ての日付型を「UTC」と解釈してシステム全体を開発する必要がある。
つまり、Protocol Buffers の Timestamp型を、日本標準時が「UTC」であると欺いて使用する事になる。
日本国内でのみ使用するシステムなら、この方がシンプルで良い。
特にペナルティーもない。
国際対応でDateTimeOffsetで扱う
正攻法で Protocol Buffers の Timestamp型を .NET で使用するなら、厳格にタイムゾーンを区別して日付型を扱う必要がある。
.NETの日付型には DateTimeOffset型を使用する。
初めから世界各国のタイムゾーンが共存できるように開発する。
gRPCサービス側はUTC中心に日付型を扱う
どちらの方法でも、シリアライズは全てUNIXタイムスタンプ(UTCと同じグリニッジ標準時)になるので、システム的には gRPCサービス側では「UTC」日付を中心に情報処理を行う事になる。
クライアント側はUNIXタイムスタンプを、ローカル標準時に変換して使用する。
gRPCサービスへローカル標準時を送る時は、UNIXタイムスタンプへ変換する。
gRPCサービス側は、常にUNIXタイムスタンプを受け取る。
日本国内のみDateTimeだけで扱うサンプル
日本国内のみ、C#のDateTimeだけで扱うgRPCサンプルコード
国際対応でDateTimeOffsetで扱うサンプル
国際対応で、C#のDateTimeOffsetで扱うgRPCサンプルコード
最初に国内のみか、国際対応するか、考える
gRPCサービスで日付型を使用するシステムを開発する時は、最初の設計段階から「国内のみで使用する」のか「国際的に使用できる」ようにするか、考えておくべきだ。
コーディング段階で考えてはいけない。
設計段階、できれば要件定義の段階で、どちらかに決めるべきだ。
コーディング段階で考えると、各担当者ごとにタイムゾーンの扱いがバラバラになり、「どうしよう、どうしよう」と散々議論と試行錯誤した挙げ句、結局「国際化対応するしかない」という結論に気がつき、膨大な手戻り作業を行う事になる。
前工程で決められる事は、決めておこう。
優柔不断と曖昧さは「敵」だ。
前工程こそ「コスト」にシビアであるべきだが、多くのリーダーやマネージャーは後工程でコストにシビアになり、赤字を膨らませる。
この悪習は排除すべきだと思う。