国際対応で、C#のDateTimeOffsetで扱うgRPCサンプルコード

「C#によるgRPC通信のサンプルコード」に戻る

引き続き、gRPCサービスと通信するWPFアプリの例を解説する。

前回は、国内のみで使用するシステムの例で、DateTimeと日本標準時だけを使用して作成したサンプルコードを解説した。

今回は、国際的に使用するシステムを想定し、DateTimeOffset を使用し、UTC経由でのタイムゾーンの変換を前提としたサンプルコードを解説する。

(注意:2024年1月15日更新→.NET5.0はサポート終了しました。サンプルコードは.NET8.0に更新しました)

既に説明している事は、繰り返し説明しない。

前回の記事を読んでから、この記事を読んで欲しい。

DateTimeOffset とタイムゾーン

DateTimeでは、その日付がUTCであるか、Localタイムゾーンであるか、どちらでも無いかを示す DateTimeKind を有している。

DateTime 構造体 (System)
特定の時点を表します。通常、日時形式で表されます。
DateTime.Kind Property (System)
Gets a value that indicates whether the time represented by this instance is based on local time, Coordinated Universal ...

DateTImeKind はタイムゾーンの情報は持たないが、その日付がUTCかLocalかを区別して、日付解釈ミスを防ぐ為にある。

DateTimeOffset は DateTime の持つ情報を全て持ち、加えてその日付のタイムゾーン情報を有する。

その為、日付を別のタイムゾーンに変換する事もできる。

DateTimeOffset 構造体 (System)
特定の時点を表します。通常、世界協定時刻 (UTC) を基準とする相対的な日時として表されます。

タイムゾーンの中心は 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 構造体

TimeSpan 構造体 (System)
時間間隔を表します。

日本標準時の日付を ToString() で表示すると、「+09:00」とタイムゾーンの時差も表示する。

DateTimeOffset は UtcDateTime を有していて、これを表示するとこの日本時間のUTCグリニッジ標準時を表示できる。

全ての DateTimeOffset の日付は UTC を有している。

タイムゾーンの変換もこれを基準に行う。

タイムゾーンを変換してみる。


TimeZoneInfo timeZoneInfoNewYork = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

TimeZoneInfo はタイムゾーンに関する情報を持つクラスで、目的のタイムゾーンに関する情報を得たいときは、これを使用する。

TimeZoneInfo クラス

TimeZoneInfo クラス (System)
世界中のいずれかのタイム ゾーンを表します。

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 の時とさほど変わらない。

Google.Protobuf.WellKnownTypes.Timestamp Class Reference

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 に登録している。

Release v1.7.1.0 · motoi-tsushima/Sample_GrpcService
.NET 8.0 により、リビルドしました。
Release v1.7.1.0 · motoi-tsushima/Sample_gRPC_WpfApp
.NET 8.0 により、リビルドしました。

今回、解説するのは「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 による日時の扱い方についての解説を完了します。

お役に立てば幸いだ。

「C#によるgRPC通信のサンプルコード」に戻る

タイトルとURLをコピーしました