引き続き、gRPCサービスと通信するWPFアプリの例を解説する。
前回は、国内のみで使用するシステムの例で、DateTimeと日本標準時だけを使用して作成したサンプルコードを解説した。
今回は、国際的に使用するシステムを想定し、DateTimeOffset を使用し、UTC経由でのタイムゾーンの変換を前提としたサンプルコードを解説する。
(注意:2024年1月15日更新→.NET5.0はサポート終了しました。サンプルコードは.NET8.0に更新しました)
既に説明している事は、繰り返し説明しない。
前回の記事を読んでから、この記事を読んで欲しい。
DateTimeOffset とタイムゾーン
DateTimeでは、その日付がUTCであるか、Localタイムゾーンであるか、どちらでも無いかを示す DateTimeKind を有している。
DateTImeKind はタイムゾーンの情報は持たないが、その日付がUTCかLocalかを区別して、日付解釈ミスを防ぐ為にある。
DateTimeOffset は DateTime の持つ情報を全て持ち、加えてその日付のタイムゾーン情報を有する。
その為、日付を別のタイムゾーンに変換する事もできる。
タイムゾーンの中心は UTC
全てのタイムゾーンの情報は、グリニッジ標準時の UTC を基準に、そこから何時間異なるかの情報を持つ事で、タイムゾーンを識別し変換する。
よって、例えば日本標準時から米国東部時間へタイムゾーンを変換する場合は、日本時間を一度 UTC に変換してから、米国東部時間へ変換する事になる。
コードの記述形式のショートカットで、手順を省略する場合もあるかも知れないが、考え方としてタイムゾーンの変換や比較は、全て UTC を基準に行っている。
タイムゾーン変換のコンソールサンプル
.NET Core コンソールアプリで、簡単に DateTimeOffset のタイムゾーンを変換してみる。
日本標準時のある日付の DateTimeOffset を作成し、それを米国東部時間(New York の時刻)に変換する。
DateTimeOffset は、内部に「UtcDateTime」というUTC日時を有しているので、それも表示する。
static void Main(string[] args)
{
//日付型を宣言する(日本標準時)
TimeSpan timeZoneJapan = new TimeSpan(9, 0, 0);
DateTimeOffset dateTime = new DateTimeOffset(2021, 2, 15, 14, 21, 11, timeZoneJapan);
Console.WriteLine("DateTimeOffset(Japan) = {0}", dateTime);
Console.WriteLine("DateTimeOffset(UTC) = {0}", dateTime.UtcDateTime);
//New York のタイムゾーンを取得する。
TimeZoneInfo timeZoneInfoNewYork = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
TimeSpan timeZoneNewYork = timeZoneInfoNewYork.BaseUtcOffset;
//New York 時に変換する。
DateTimeOffset newYorkDateToOffset = dateTime.ToOffset(timeZoneNewYork);
//DateTimeOffset newYorkDateToOffset = dateTime.UtcDateTime.ToOffset(timeZoneNewYork);
Console.WriteLine("resultDateToOffset(New York) = {0}", newYorkDateToOffset);
Console.WriteLine("resultDateToOffset(UTC) = {0}", newYorkDateToOffset.UtcDateTime);
}
実行結果>
DateTimeOffset(Japan) = 2021/02/15 14:21:11 +09:00
DateTimeOffset(UTC) = 2021/02/15 5:21:11
resultDateToOffset(New York) = 2021/02/15 0:21:11 -05:00
resultDateToOffset(UTC) = 2021/02/15 5:21:11
タイムゾーンは DateTimeOffset のコンストラクタで、TimeSpan により「時差」で指定する。
日本標準時は UTC より9時間早いので、「new TimeSpan(9, 0, 0)」で定義する。
TimeSpan 構造体
日本標準時の日付を ToString() で表示すると、「+09:00」とタイムゾーンの時差も表示する。
DateTimeOffset は UtcDateTime を有していて、これを表示するとこの日本時間のUTCグリニッジ標準時を表示できる。
全ての DateTimeOffset の日付は UTC を有している。
タイムゾーンの変換もこれを基準に行う。
タイムゾーンを変換してみる。
TimeZoneInfo timeZoneInfoNewYork = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
TimeZoneInfo はタイムゾーンに関する情報を持つクラスで、目的のタイムゾーンに関する情報を得たいときは、これを使用する。
TimeZoneInfo クラス
TimeZoneInfo.FindSystemTimeZoneById(String) メソッド により、米国東部標準時の情報を取得している。
“Eastern Standard Time” というのは タイム ゾーン ID だ。
それぞれのタイムゾーンごとにIDがある。
タイム ゾーン ID の取得方法はあとで掲載する。
つぎに BaseUtcOffset で、米国東部標準時の UTC との時差を取得する。
TimeSpan timeZoneNewYork = timeZoneInfoNewYork.BaseUtcOffset;
この時差を使用して、日本標準時の DateTimeOffset 日付 のタイムゾーンを、米国東部標準時へ変換する。
タイムゾーンの変換は、DateTimeOffset の ToOffset() メソッドに米国東部標準時の時差を渡して行う。
//New York 時に変換する。
DateTimeOffset newYorkDateToOffset = dateTime.ToOffset(timeZoneNewYork);
返り値の newYorkDateToOffset の日付は米国東部標準時になっている。
ちなみに、
dateTime.UtcDateTime.ToOffset(timeZoneNewYork);
のように UTC に対して、ToOffset でタイムゾーンの変換をする事もできる。
元々、内部の UTC からタイムゾーンの変換をしているので、同じ事だ。
タイム ゾーン ID の取得方法
そのシステムで使用可能なタイムゾーンの全一覧は「TimeZoneInfo.GetSystemTimeZones()」メソッドで取得できる。
var sysTimeZones = TimeZoneInfo.GetSystemTimeZones();
foreach(TimeZoneInfo timeZone in sysTimeZones)
{
Console.WriteLine("{0},{1}",timeZone.Id,timeZone.DisplayName);
}
timeZone.Id の値を、TimeZoneInfo.FindSystemTimeZoneById(timeZone.Id) に渡して呼び出す事で、目的の TimeZoneInfo を得る事ができる。
Protobuf の Timestamp と DateTimeOffset 間の変換
以前も説明したが、Protobuf の Timestamp と DateTimeOffset の双方向の変換は、DateTime の時とさほど変わらない。
DateTimeOffset と Timestamp が互いに変換する為には、Google.Protobuf.WellKnownTypes.Timestamp の中で定義されている関数を使用する。
DateTimeOffsetからTimestampへ変換するなら、FromDateTimeOffset(DateTimeOffset dateTimeOffset) メソッドを使用する。
Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeOffset dateTimeOffset)
TimestampからDateTimeOffset へ変換するなら、ToDateTimeOffset() メソッドを使用する。
Google.Protobuf.WellKnownTypes.Timestamp.ToDateTimeOffset()
.NET Core コンソールアプリで Protobuf を使用するには、以下の手順で設定を行う。
Visual Studio 2019 の「新しいプロジェクトの作成」を選び、「コンソールアプリ(.NET Core)」で新規コンソールアプリのプロジェクトを作成する。
ソリューションエクスプローラーから、プロジェクトを右クリックして「NuGetパッケージの管理」をクリックして開く。
NuGetパッケージマネージャーから、「参照」タブを選択して「Google.Protobuf」を検索して選択し、画面右の「インストール」をクリックしてインストールすれば、利用可能になる。
Timestamp と DateTimeOffset 双方の変換処理のサンプルコードを以下に掲載する。
static void Main(string[] args)
{
//日付型を宣言する(日本標準時)
TimeSpan timeZoneJapan = new TimeSpan(9, 0, 0);
DateTimeOffset dateTime = new DateTimeOffset(2021, 2, 15, 14, 21, 11, timeZoneJapan);
Console.WriteLine("DateTimeOffset = {0}", dateTime);
Google.Protobuf.WellKnownTypes.Timestamp timestamp;
timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(dateTime);
Console.WriteLine("Timestamp = {0}", timestamp.ToDiagnosticString());
DateTimeOffset resultDateOffset = timestamp.ToDateTimeOffset();
Console.WriteLine("resultDate = {0}", resultDateOffset);
Console.WriteLine("resultDate(UTC) = {0}", resultDateOffset.UtcDateTime);
//日本標準時に変換する。
DateTimeOffset resultDateToOffset = resultDateOffset.ToOffset(timeZoneJapan);
Console.WriteLine("resultDateToOffset(Japan) = {0}", resultDateToOffset);
Console.WriteLine("resultDateToOffset(UTC) = {0}", resultDateToOffset.UtcDateTime);
}
実行結果>
DateTimeOffset = 2021/02/15 14:21:11 +09:00
Timestamp = "2021-02-15T05:21:11Z"
resultDate = 2021/02/15 5:21:11 +00:00
resultDate(UTC) = 2021/02/15 5:21:11
resultDateToOffset(Japan) = 2021/02/15 14:21:11 +09:00
resultDateToOffset(UTC) = 2021/02/15 5:21:11
最初に「DateTimeOffset dateTime」に、日本標準時の適当な日付の日付型を作成する。
次に、Protobuf の Timestamp.FromDateTimeOffset(dateTime) により、UNIXタイムスタンプに変換する。
UNIXタイムスタンプは「UTC」基準の「秒数」なので、グリニッジ標準時になっている。
この時点で、日本標準時からグリニッジ標準時(英国標準時)にタイムゾーンが変換されている。
UNIXタイムスタンプは以下のメソッドで日付にして表示できる。
Console.WriteLine("Timestamp = {0}", timestamp.ToDiagnosticString());
DateTimeOffset と Protobuf を使用する上で注意しなければならない要点がここにある。
DateTimeOffset を Protobuf でシリアライズして gRPCサービスへ送った時点で、そのタイムゾーンは「UTC」になっている。
これは避けられない。
だから、このルールに従ってシステムを開発する必要がある。
使用する日付が全て「日本標準時」でも、 gRPCサービスとの通信の度にタイムゾーンは「UTC」に変換される。
仮に gRPCサービスから Timestamp が返されたとして、その日付のタイムゾーンは「UTC」になっている。
Timestamp は、
DateTimeOffset resultDateOffset = timestamp.ToDateTimeOffset();
により DateTimeOffset に変換できるが、この時点でのタイムゾーンは UTC である。
resultDate = 2021/02/15 5:21:11 +00:00
gRPCサービスから受け取った UTC日付は日本標準時に変換して使用する事になる。
以下のステップがそれをやっている。
//日本標準時に変換する。
DateTimeOffset resultDateToOffset = resultDateOffset.ToOffset(timeZoneJapan);
resultDateToOffset(Japan) = 2021/02/15 14:21:11 +09:00
この点に注意すれば、あとは DateTime と同様に使用できる。
以上で、DateTimeOffset と Protobuf の使用法の基礎的解説を終わる。
次に、gRPCサービスと WPFアプリで、実際にDateTImeOffset日付を通信するサンプルコードを解説する。
DateTimeOffset を gRPCサービスで送受信する
WPFアプリと gRPCサービスの通信サンプルとして、以下のソースコードを公開する。
前回から使用しているgRPCサービスとWPFアプリの Visual Studio プロジェクトである。
それぞれ別のソリューションとして GitHub に登録している。
今回、解説するのは「Reserve」メソッドである。
このサンプルの画面レイアウトは、このようになる。
この内、「Reserve」はこの部分である。
gRPCサービスのサンプルコード
Sample_GrpcService プロジェクトの「greet.proto」ファイルに以下の message と rpc を追加している。
service Greeter {
// Reserve function
rpc Reserve (ReservationTime) returns (ReservationTime);
}
// The request and response for
message ReservationTime {
string subject = 1;
google.protobuf.Timestamp time = 2;
google.protobuf.Duration duration = 3;
google.protobuf.Duration timeZone = 4;
google.protobuf.Duration countryTimeZone = 5;
}
同 Sample_GrpcService プロジェクトの「GreeterService.cs」ファイルにも「Reserve」の実装を追加している。
/// <summary>
/// 施設予約
/// </summary>
/// <param name="request"></param>
/// <param name="context"></param>
/// <returns></returns>
public override Task<ReservationTime> Reserve(ReservationTime request, ServerCallContext context)
{
//リクエストパラメータを取得する
DateTimeOffset requestDateTimeUtc = request.Time.ToDateTimeOffset();
//TimeSpan requestTimeZone = request.TimeZone.ToTimeSpan();
TimeSpan requestTimeZone = TimeSpan.Zero;
TimeSpan requestCountryTimeZone = request.CountryTimeZone.ToTimeSpan();
DateTimeOffset requestDateTimeOffset =
new DateTimeOffset(requestDateTimeUtc.DateTime, requestTimeZone);
DateTimeOffset requestCountryDateTimeOffset =
new DateTimeOffset(requestDateTimeUtc.ToOffset(requestCountryTimeZone).DateTime, requestCountryTimeZone);
TimeSpan requestTimeSpan = request.Duration.ToTimeSpan();
//日程調整
requestCountryDateTimeOffset = ScheduleAdjustment(requestCountryDateTimeOffset, requestTimeSpan);
//予約日時を設定する。
ReservationTime reservation = new ReservationTime();
reservation.Subject = request.Subject;
//reservation.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(requestDateTimeOffset);
reservation.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(requestCountryDateTimeOffset);
reservation.Duration = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(requestTimeSpan);
reservation.TimeZone = request.TimeZone;
reservation.CountryTimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(requestCountryTimeZone);
//予約日時を返す。
return Task.FromResult(reservation);
}
/// <summary>
/// 日程調整
/// </summary>
/// <param name="requestCountryDateTimeOffset">施設側タイムゾーンの日時</param>
/// <param name="requestTimeSpan">予約時間</param>
/// <returns></returns>
private DateTimeOffset ScheduleAdjustment(DateTimeOffset requestCountryDateTimeOffset, TimeSpan requestTimeSpan)
{
//開業時間と曜日
List<OpeningDays> openingDays = new List<OpeningDays> {
new OpeningDays( 9, 17, DayOfWeek.Monday )
,new OpeningDays( 9, 17, DayOfWeek.Tuesday )
,new OpeningDays( 9, 12, DayOfWeek.Wednesday )
,new OpeningDays( 9, 17, DayOfWeek.Thursday )
,new OpeningDays( 13, 20, DayOfWeek.Friday )
};
int whereCount;
OpeningDays selectedDays = openingDays.FirstOrDefault();
bool addDayProcess = false;
//リクエスト日より後の開業曜日を検索する。
do
{
List<OpeningDays> openings =
openingDays.Where(d => d.OpeningWeek == requestCountryDateTimeOffset.DayOfWeek)
.ToList<OpeningDays>();
whereCount = openings.Count();
if (whereCount == 0)
{
//開業曜日に含まれない場合、一日追加する。
requestCountryDateTimeOffset = requestCountryDateTimeOffset.AddDays(1.0);
}
else if (addDayProcess == true)
{
selectedDays = openings.FirstOrDefault();
//リクエスト日の終了時間が遅すぎる場合、営業時間の最も遅い時間に、時間を早める。
TimeSpan subtractionTime = new TimeSpan(
selectedDays.ClosingTime - requestCountryDateTimeOffset.Hour - requestTimeSpan.Hours,
(requestTimeSpan.Minutes + requestCountryDateTimeOffset.Minute) * (-1) , 0);
requestCountryDateTimeOffset = requestCountryDateTimeOffset.Add(subtractionTime);
addDayProcess = false;
}
else
{
//開業曜日に含まれる場合、該当の開業時間と曜日を返す。
selectedDays = openings.FirstOrDefault();
//リクエスト日の終了時間が遅すぎる場合、時間を早めて、一日追加する。
double countryMinutes = (double)requestCountryDateTimeOffset.Minute / 60.0;
double durationMinutes = ((double)requestTimeSpan.Hours * 60.0 + (double)requestTimeSpan.Minutes) / 60.0;
if ((double)selectedDays.ClosingTime <
((double)requestCountryDateTimeOffset.Hour + countryMinutes + durationMinutes)
)
{
requestCountryDateTimeOffset = requestCountryDateTimeOffset.AddDays(1.0);
addDayProcess = true;
whereCount = 0;
}
}
}
while (whereCount == 0);
//リクエスト日の開始時間が早すぎる場合、開始時間に変更する。
if (requestCountryDateTimeOffset.Hour < selectedDays.OpeningTime)
{
requestCountryDateTimeOffset = requestCountryDateTimeOffset.AddHours(selectedDays.OpeningTime - requestCountryDateTimeOffset.Hour);
requestCountryDateTimeOffset = requestCountryDateTimeOffset.AddMinutes(requestCountryDateTimeOffset.Minute * -1);
}
return requestCountryDateTimeOffset;
}
Reserve の機能
「Reserve」メソッドの機能は、
施設の予約が可能な日時を、提案するという機能である。
予約するユーザーの国と、施設のある国は異なるタイムゾーンである。
画面からユーザーのタイムゾーンと、施設のタイムゾーンを「Reserve」メソッドに渡す。
施設の予約日時を「Reserve」メソッドに渡すと、その時間以降で施設の営業時間内の予約可能日時を提案する。
返される日時は、相手施設の予約可能日時である。
ローカルタイムゾーンの google.protobuf.Timestamp time で与えられた UNIXタイムスタンプを、
google.protobuf.Duration countryTimeZone で指定した施設のタイムゾーンに変換して、
gRPCサービス側 (サーバー側) が保持している「施設営業時間」と照合して、施設営業時間内の日時を提案する。
提案される日時はUNIXタイムスタンプであり、クライアント側でユーザーのタイムゾーンに変換され表示する。
google.protobuf.Duration timeZone が、ユーザー側のタイムゾーン、
google.protobuf.Duration duration は、施設の予約時間帯(分単位)である。
会議室の予約や、病院の診察時間の予約などを想像してもらうと良い。
WPFアプリのサンプルコード
WPFアプリの側「Sample_gRPC_WpfApp」の側も、「greet.proto」ファイルを同様に変更している。
プロジェクトは「Sample_gRPC_ClassLibrary」になる。
gRPCサービスと同じ変更なので、ここには掲載しない。
XAMLコード
画面レイアウトには、以下の XAML を追加している。
<StackPanel Orientation="Vertical" Grid.Row="8" Height="30" VerticalAlignment="Top" Background="Aqua">
<StackPanel Orientation="Horizontal" Height="25" VerticalAlignment="Center" Background="Aqua">
<DatePicker x:Name="xDatePicker" Width="120" Height="25" />
<TextBox Margin="20,0,0,0" x:Name="xHourTextBox" Width="50" TextAlignment="Left"/>
<TextBlock Text="時刻(24時間制)" Width="100" TextAlignment="Left" />
<TextBox x:Name="xMinuteTextBox" Width="50" TextAlignment="Left"/>
<TextBlock Text="分" Width="30" TextAlignment="Left" />
<TextBox x:Name="xSpanTextBox" Width="50" TextAlignment="Left"/>
<TextBlock Text="時間(分)" Width="60" TextAlignment="Left" />
<TextBlock Text="タイムゾーン" Width="60" TextAlignment="Right" />
<ComboBox x:Name="xTimeSpanComboBox" ItemsSource="{Binding TimeZone}" Width="180" />
<TextBlock Text="予約施設タイムゾーン" Width="120" TextAlignment="Right" />
<ComboBox x:Name="xCountryTimeSpanComboBox" ItemsSource="{Binding TimeZone}" Width="180" />
<Button x:Name="ReserveButton" Content="予約" Width="100" Margin="30,0,0,0" Click="ReserveButton_Click"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Row="9" Height="30" VerticalAlignment="Top" Background="Aqua">
<StackPanel Orientation="Horizontal" Height="25" VerticalAlignment="Top" Background="Aqua">
<TextBlock Text="予約候補日時" Width="125" TextAlignment="Left" />
<TextBox x:Name="xReserveTextBox" Width="450" TextAlignment="Left"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Row="10" Height="30" VerticalAlignment="Top" Background="Aqua">
<StackPanel Orientation="Horizontal" Height="25" VerticalAlignment="Top" Background="Aqua">
<TextBlock Text="予約先日時" Width="125" TextAlignment="Left" />
<TextBox x:Name="xCountryTextBox" Width="450" TextAlignment="Left"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Row="11" Height="30" VerticalAlignment="Top" Background="Aqua">
<StackPanel Orientation="Horizontal" Height="25" VerticalAlignment="Top" Background="Aqua">
<TextBlock Text="予約候補グリニッジ日時" Width="125" TextAlignment="Left" />
<TextBox x:Name="xReserveUtcTextBox" Width="450" TextAlignment="Left"/>
</StackPanel>
</StackPanel>
xDatePickerで日付を入力し、
xHourTextBoxと xMinuteTextBoxに「時分の値」を入力する。
xTimeSpanComboBoxでユーザーのタイムゾーンを選択し、
xCountryTimeSpanComboBoxで施設のタイムゾーンを選択する。
ReserveButtonで、gRPCサービスをリクエストする。
リプライの予約日時の提案は xReserveTextBox(予約候補日時)へ表示する。
xCountryTextBox(予約先日時) には、その日時の施設側タイムゾーンを表示する。
xReserveUtcTextBox(予約候補グリニッジ日時)には、その日時のUTCを表示する。
コードビハインド
ReserveButton のイベントを追加して、gRPCサービスを呼び出している。
private void ReserveButton_Click(object sender, RoutedEventArgs e)
{
//選択していない場合は無視する。
if (this.xDatePicker.SelectedDate == null)
return;
if (this.xTimeSpanComboBox.SelectedValue == null)
return;
if (this.xCountryTimeSpanComboBox.SelectedValue == null)
return;
//時間を文字列から数値に変換する。
int hour, minute, span;
if(int.TryParse(this.xHourTextBox.Text, out hour) == false) return;
if (hour >= 24)
{
this.xHourTextBox.Text = "×24超";
return;
}
if (int.TryParse(this.xMinuteTextBox.Text, out minute) == false) return;
if (minute >= 60)
{
this.xMinuteTextBox.Text = "×60超";
return;
}
if (int.TryParse(this.xSpanTextBox.Text, out span) == false) return;
//クライアント側のタイムゾーンを設定
DateTime dateTime = this.xDatePicker.SelectedDate.Value;
string timeZoneText = ((KeyValuePair<string, TimeSpan>)this.xTimeSpanComboBox.SelectedValue).Key;
TimeSpan timeZone = ((KeyValuePair<string, TimeSpan>)this.xTimeSpanComboBox.SelectedValue).Value;
//予約先施設側のタイムゾーンを設定s
string countryTimeZoneText = ((KeyValuePair<string, TimeSpan>)this.xCountryTimeSpanComboBox.SelectedValue).Key;
TimeSpan countryTimeZone = ((KeyValuePair<string, TimeSpan>)this.xCountryTimeSpanComboBox.SelectedValue).Value;
//予約時間
TimeSpan duration = new TimeSpan(0, span, 0);
//クライアント側のタイムゾーンを反映した日付型を作成する。
DateTimeOffset dateTimeOffset = new DateTimeOffset(dateTime.Year, dateTime.Month, dateTime.Day, hour, minute, 0, 0, timeZone);
//ReservationTime 作成
ReservationTime reservationTime = new ReservationTime();
reservationTime.Subject = "打合せ予約";
reservationTime.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(dateTimeOffset);
reservationTime.Duration = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(duration);
reservationTime.TimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(timeZone);
reservationTime.CountryTimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(countryTimeZone);
// gRPC サービスを呼び出す。
var reply = this.grpcClient.GreeterClient.Reserve(reservationTime);
// リプライ日付をクライアント側のタイムゾーンに変換する。
DateTimeOffset replyDateTime = reply.Time.ToDateTimeOffset().ToOffset(timeZone);
DateTimeOffset replyCountryDateTime = reply.Time.ToDateTimeOffset().ToOffset(countryTimeZone);
// 予約日を表示する。
this.xReserveTextBox.Text = replyDateTime.ToString("yyyy年MM月dd日 H時m分") + " / 時間 = " + reply.Duration.ToTimeSpan().ToString() + " / TimeZone = " + reply.TimeZone.ToTimeSpan().ToString();
this.xCountryTextBox.Text = replyCountryDateTime.DateTime.ToString("yyyy年MM月dd日 H時m分") + " / TimeZone = " + countryTimeZone.ToString();
this.xReserveUtcTextBox.Text = reply.Time.ToDateTimeOffset().ToString("yyyy年MM月dd日 H時m分");
}
また、タイムゾーンをコンボボックスで選択する為に、タイムゾーンのコレクションの実体を宣言している。
このソースは ChangeTZButton のイベントと共有している。
private Dictionary<string, TimeSpan> _timeZone = null;
public Dictionary<string, TimeSpan> TimeZone { get { return this._timeZone; } }
private void InitTimeZone()
{
this._timeZone = new Dictionary<string, TimeSpan>();
var sysTimeZones = TimeZoneInfo.GetSystemTimeZones();
foreach (TimeZoneInfo timeZone in sysTimeZones)
{
string timeZoneId = timeZone.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
TimeSpan offset = timeZoneInfo.BaseUtcOffset;
this._timeZone.Add(timeZoneId, offset);
}
}
TimeZoneInfo はタイムゾーンを管理するクラスである。
GetSystemTimeZones() メソッドはシステムで管理するタイムゾーンの一覧を返す。
foreachでタイムゾーンの一覧をDictionaryに登録し、後でこれをComboBoxにバインドする。
ReserveButton_Click の中で、最初の方の処理は画面からパラメータ値を取り出しているだけである。
主要な日付型処理は、呼び出しの前処理が、以下になる。
//クライアント側のタイムゾーンを設定
DateTime dateTime = this.xDatePicker.SelectedDate.Value;
string timeZoneText = ((KeyValuePair<string, TimeSpan>)this.xTimeSpanComboBox.SelectedValue).Key;
TimeSpan timeZone = ((KeyValuePair<string, TimeSpan>)this.xTimeSpanComboBox.SelectedValue).Value;
//予約先施設側のタイムゾーンを設定s
string countryTimeZoneText = ((KeyValuePair<string, TimeSpan>)this.xCountryTimeSpanComboBox.SelectedValue).Key;
TimeSpan countryTimeZone = ((KeyValuePair<string, TimeSpan>)this.xCountryTimeSpanComboBox.SelectedValue).Value;
//予約時間
TimeSpan duration = new TimeSpan(0, span, 0);
//クライアント側のタイムゾーンを反映した日付型を作成する。
DateTimeOffset dateTimeOffset = new DateTimeOffset(dateTime.Year, dateTime.Month, dateTime.Day, hour, minute, 0, 0, timeZone);
//ReservationTime 作成
ReservationTime reservationTime = new ReservationTime();
reservationTime.Subject = "打合せ予約";
reservationTime.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(dateTimeOffset);
reservationTime.Duration = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(duration);
reservationTime.TimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(timeZone);
reservationTime.CountryTimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(countryTimeZone);
最初に二つのコンボボックスから、ユーザー側と施設側のタイムゾーンを取得している。
予約時間を TimeSpan duration にインスタンス化する。
ユーザー側のタイムゾーンを反映したDateTimeOffset日付型を作成する。ここでユーザー側タイムゾーンを指定する。
Protobufシリアライズ用の ReservationTime に、DateTimeOffset や TimeSpan の値を変換して、インスタンス化する。
次に gRPCコールをする。
// gRPC サービスを呼び出す。
var reply = this.grpcClient.GreeterClient.Reserve(reservationTime);
var reply に、結果が返ってくる。
リプライしたUNIXタイムスタンプをユーザー側と施設側のタイムゾーンに変換する。
// リプライ日付をクライアント側のタイムゾーンに変換する。
DateTimeOffset replyDateTime = reply.Time.ToDateTimeOffset().ToOffset(timeZone);
DateTimeOffset replyCountryDateTime = reply.Time.ToDateTimeOffset().ToOffset(countryTimeZone);
その結果を、三つのTextBoxへ表示する。
// 予約日を表示する。
this.xReserveTextBox.Text = replyDateTime.ToString("yyyy年MM月dd日 H時m分") + " / 時間 = " + reply.Duration.ToTimeSpan().ToString() + " / TimeZone = " + reply.TimeZone.ToTimeSpan().ToString();
this.xCountryTextBox.Text = replyCountryDateTime.DateTime.ToString("yyyy年MM月dd日 H時m分") + " / TimeZone = " + countryTimeZone.ToString();
this.xReserveUtcTextBox.Text = reply.Time.ToDateTimeOffset().ToString("yyyy年MM月dd日 H時m分");
Duration から、TimeSpan への相互変換の方法は、以前説明したので省略する。
このサンプルは先に解説したように、施設の営業時間内の予約日時を提案する。
施設の営業時間は、gRPCサービス側で、ScheduleAdjustment というメソッドの中で定義しており、その宣言は以下のようになっている。
//開業時間と曜日
List<OpeningDays> openingDays = new List<OpeningDays> {
new OpeningDays( 9, 17, DayOfWeek.Monday )
,new OpeningDays( 9, 17, DayOfWeek.Tuesday )
,new OpeningDays( 9, 12, DayOfWeek.Wednesday )
,new OpeningDays( 9, 17, DayOfWeek.Thursday )
,new OpeningDays( 13, 20, DayOfWeek.Friday )
};
土曜日曜は休業日である。
営業時間は曜日によって異なる。
このやり方が正攻法である
サンプルなのでコードが汚いかも知れないが、コードの美しさや合理性やオブジェクト指向的な話は別にして、Protobuf によりシリアライズして、gRPC を使用するなら、DateTimeOffset を使用して日時を扱う、このサンプルのやり方が正攻法である。
DateTime だけで日時を扱う方法は邪道である。
極力、このやり方を参考に日時を扱って欲しい。
Protobuf と gRPC による日時の扱い方についての解説を完了します。
お役に立てば幸いだ。