[C#] DateTimeKind の変換方法と意識すること

2024-01-22 (月)

DateTimeには Kind プロパティがあり、UtcLocal という状態を持つ場合があります。
基本的には Unspecified であり、Ticks の日時のみが処理されます。
しかし一部の処理では Kind を意識する必要があるため、変換方法や注意点をまとめます。

環境

  • .NET 8.0.100
  • C# 12.0
  • Visual Studio 2022 Version 17.8.5
  • Windows 11 Pro 22H2 22621.3007
  • 日本標準時 UTC+0900

結果

DateTimeKind内容
Unspecified0未定義
Utc1協定世界時 (UTC)
Local2ローカル時刻 (日本は UTC に対して +09:00)

DateTimeKind の変換

// コンストラクタで DateTimeKind を指定する
var dateTime = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc);

// Kind のみを変換する (Ticks 日時はそのまま)
DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);

// Kind を変換する (Ticks 日時も変わる)
dateTime.ToUniversalTime(); // Unspecified, Local は Utc に変換する
dateTime.ToLocalTime();     // Unspecified, Utc は Local に変換する

// Kind を変換する (Unspecified の場合、Ticks 日時はそのまま)
GetUtcDateTime(dateTime);   // Local は Utc に変換する
GetLocalDateTime(dateTime); // Utc は Local に変換する

public static DateTime GetUtcDateTime(DateTime dateTime)
    => dateTime.Kind == DateTimeKind.Unspecified
        ? DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)
        : dateTime.ToUniversalTime();

public static DateTime GetLocalDateTime(DateTime dateTime)
    => dateTime.Kind == DateTimeKind.Unspecified
        ? DateTime.SpecifyKind(dateTime, DateTimeKind.Local)
        : dateTime.ToLocalTime();

DateTimeKind の扱い

  • ==, Equlas(), CompareTo() などの比較は Kind を無視します。
  • Unspecified で取得されることが多く、Ticks のみで扱うことを基本とします。
  • Kind を必要とする場合は、UnspecifiedUtcLocal どちらの扱いにするか注意します。
  • 💡 Utc を前提とするプロパティ名には、Utc と命名に含めると明確です。
  • 💡 Utc を前提とする処理には、事前に Utc にしておくと明確です。
  • 💡 Local を前提とする処理には、事前に Local にしておくと明確です。

DateTimeKind を指定して DateTime を生成する

コンストラクタで DateTimeKind を指定します。

new DateTime(2024, 1, 2, 3, 4, 5); // 未指定の場合は Unspecified
new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc);
new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Local);

日本環境で確認してみます。

using System;
using System.Globalization;

Dump(new DateTime(2024, 1, 2, 3, 4, 5)); // 未指定の場合は Unspecified
Dump(new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc));
Dump(new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Local));

void Dump(DateTime value)
{
    Console.WriteLine(value.Kind);
    Console.WriteLine(value.ToString("yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture));
    Console.WriteLine(value.ToString("O"));
    Console.WriteLine();
}

結果

Unspecified
2024-01-02 03:04:05.0000000
2024-01-02T03:04:05.0000000

Utc
2024-01-02 03:04:05.0000000
2024-01-02T03:04:05.0000000Z

Local
2024-01-02 03:04:05.0000000
2024-01-02T03:04:05.0000000+09:00

DateTime の比較は DateTimeKind を無視する

DateTime==, Equlas(), CompareTo()Kind を無視して日時のみで比較されます。

ドキュメントにも記載があります。
https://learn.microsoft.com/ja-jp/dotnet/api/system.datetime.equals
https://learn.microsoft.com/ja-jp/dotnet/api/system.datetime.compareto

var value1 = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc);
var value2 = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Local);

Console.WriteLine(value1 == value2);         // True
Console.WriteLine(value1.CompareTo(value2)); // 0

参考に .NET 8.0.1 のソースコードも確認してみると、Ticks のみを比較しています。

コード抜粋

public bool Equals(DateTime value)
{
    return this == value;
}

public static bool operator ==(DateTime d1, DateTime d2) => ((d1._dateData ^ d2._dateData) << 2) == 0;

public int CompareTo(DateTime value)
{
    return Compare(this, value);
}

public static int Compare(DateTime t1, DateTime t2)
{
    long ticks1 = t1.Ticks;
    long ticks2 = t2.Ticks;
    if (ticks1 > ticks2) return 1;
    if (ticks1 < ticks2) return -1;
    return 0;
}

DateTimeKind を変換する

主な変換方法は、3 つあります。

項目内容Kind 変換Ticks 変換
ToUniversalTime()Utc 以外は Utc に変換する変換する変換する
ToLocalTime()Local 以外は Local に変換する変換する変換する
DateTime.SpecifyKind()Kind のみを変更する変換する変換しない
// 2024-01-02 09:00:00
var value = new DateTime(2024, 1, 2, 9, 0, 0, DateTimeKind.Unspecified);

// 2024-01-02 00:00:00 (-09:00 加算されて Utc に変換)
value.ToUniversalTime();

// 2024-01-02 18:00:00 (+09:00 加算されて Local に変換)
value.ToLocalTime();

// 2024-01-02 09:00:00 (日時そのままで Utc に変換)
DateTime.SpecifyKind(value, DateTimeKind.Utc);
メソッド変換前の Kind変換後の Kind変換前の Ticks変換後の TicksTicks 加算分
ToUniversalTime()UnspecifiedUtc2024-01-02 09:00:002024-01-02 00:00:00-09:00
ToUniversalTime()UtcUtc2024-01-02 09:00:002024-01-02 09:00:000
ToUniversalTime()LocalUtc2024-01-02 09:00:002024-01-02 00:00:00-09:00
ToLocalTime()UnspecifiedLocal2024-01-02 09:00:002024-01-02 18:00:00+09:00
ToLocalTime()UtcLocal2024-01-02 09:00:002024-01-02 18:00:00+09:00
ToLocalTime()LocalLocal2024-01-02 09:00:002024-01-02 09:00:000

DateTimeKind を変換する (Unspecified の Ticks は変換しない)

ToUniversalTime(), ToLocalTime() では、Unspecified の場合にも変換されてしまいます。
Unspecified の場合に日時はそのままで Kind のみを変換するには DateTime.SpecifyKind() を使用します。

public static DateTime GetUtcDateTime(DateTime dateTime)
    => dateTime.Kind == DateTimeKind.Unspecified
        ? DateTime.SpecifyKind(dateTime, DateTimeKind.Utc)
        : dateTime.ToUniversalTime();

public static DateTime GetLocalDateTime(DateTime dateTime)
    => dateTime.Kind == DateTimeKind.Unspecified
        ? DateTime.SpecifyKind(dateTime, DateTimeKind.Local)
        : dateTime.ToLocalTime();

例えば、DB 側の datetime 型が Kind 相当の TimeZoneOffset 情報を持たないカラムのケースです。

  • DB に INSERT する際は、Kind は無視されて日時のみが保存されます。
  • DB から SELECT する際は、Unspecified で取得されます。

DB に DateTime.UtcNow (Utc) で保存したら、その値を DB から取得した際に DateTimeKind.Utc にしたい場合は、ToUniversalTime() ではなく DateTime.SpecifyKind() を使用して自分で変換する必要があります。

後述のファイルの日時をセットするメソッドも、Unspecified の場合は変換されないようになっています。

DateTimeOffset への変換

UnspecifiedLocal と同じ扱いになります。

using System;
using System.Globalization;

Dump(new DateTime(2024, 1, 2, 9, 0, 0, DateTimeKind.Unspecified));
Dump(new DateTime(2024, 1, 2, 9, 0, 0, DateTimeKind.Utc));
Dump(new DateTime(2024, 1, 2, 9, 0, 0, DateTimeKind.Local));

void Dump(DateTime value)
{
    DateTimeOffset dateTimeOffset = value;

    Console.WriteLine($"{value.Kind} -> DateTimeOffset");
    Console.WriteLine(dateTimeOffset.Offset);
    Console.WriteLine(dateTimeOffset.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
    Console.WriteLine(dateTimeOffset.ToString("u"));
    Console.WriteLine();
}

結果

Unspecified -> DateTimeOffset
09:00:00
2024-01-02 09:00:00
2024-01-02 00:00:00Z

Utc -> DateTimeOffset
00:00:00
2024-01-02 09:00:00
2024-01-02 09:00:00Z

Local -> DateTimeOffset
09:00:00
2024-01-02 09:00:00
2024-01-02 00:00:00Z
変換前の Kind変換前の Ticks変換後の Ticks変換後の Offset
Unspecified2024-01-02 09:00:002024-01-02 09:00:00+09:00
Local2024-01-02 09:00:002024-01-02 09:00:00+09:00
Utc2024-01-02 09:00:002024-01-02 09:00:000

https://github.com/dotnet/runtime/blob/v8.0.1/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs#L80-L96

Utc と Local を意識する例

Utc または Local を返す代表的なメンバーは Now です。

  • DateTime.UtcNow (Utc)
  • DateTime.Now (Local)

ファイルの日時も、Utc または Local で取得するメソッドが用意されています。

  • File.GetLastWriteTimeUtc() (Utc)
  • File.GetLastWriteTime() (Local)

また、ファイルの日時をセットする場合は、Utc または Local で設定するメソッドが用意されています。

  • File.SetLastWriteTimeUtc()
  • File.SetLastWriteTime()

Set するメソッドについては注意しておく必要があり、UnspecifiedLocal または Utc と同じ扱いになります。

メソッド引数で指定した Kind引数で指定した TicksGetLastWriteTimeUtc() の結果
SetLastWriteTimeUtc()Unspecified2024-01-02 09:00:002024-01-02 09:00:00
SetLastWriteTimeUtc()Utc2024-01-02 09:00:002024-01-02 09:00:00
SetLastWriteTimeUtc()Local2024-01-02 09:00:002024-01-02 00:00:00
メソッド引数で指定した Kind引数で指定した TicksGetLastWriteTime() の結果
SetLastWriteTime()Unspecified2024-01-02 09:00:002024-01-02 09:00:00
SetLastWriteTime()Utc2024-01-02 09:00:002024-01-02 18:00:00
SetLastWriteTime()Local2024-01-02 09:00:002024-01-02 09:00:00
using System;
using System.Globalization;

Utc(DateTimeKind.Unspecified);
Utc(DateTimeKind.Utc);
Utc(DateTimeKind.Local);
Local(DateTimeKind.Unspecified);
Local(DateTimeKind.Utc);
Local(DateTimeKind.Local);

void Utc(DateTimeKind kind) => Set(kind, true);
void Local(DateTimeKind kind) => Set(kind, false);

void Set(DateTimeKind kind, bool useUtc)
{
    const string path = @"C:\_DateTimeTest.txt";

    var value = new DateTime(2024, 1, 2, 9, 0, 0, kind);
    DateTime result;
    var method = "";

    if (useUtc)
    {
        method = nameof(File.SetLastWriteTimeUtc);
        File.SetLastWriteTimeUtc(path, value);
        result = File.GetLastWriteTimeUtc(path);
    }
    else
    {
        method = nameof(File.SetLastWriteTime);
        File.SetLastWriteTime(path, value);
        result = File.GetLastWriteTime(path);
    }

    Console.WriteLine($"{method} ({value.Kind})");
    Console.WriteLine(result.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
    Console.WriteLine();
}

結果

SetLastWriteTimeUtc (Unspecified)
2024-01-02 09:00:00

SetLastWriteTimeUtc (Utc)
2024-01-02 09:00:00

SetLastWriteTimeUtc (Local)
2024-01-02 00:00:00

SetLastWriteTime (Unspecified)
2024-01-02 09:00:00

SetLastWriteTime (Utc)
2024-01-02 18:00:00

SetLastWriteTime (Local)
2024-01-02 09:00:00

感謝

公式

記事

2024-01-22 (月)