連載
» 2015年06月24日 05時00分 UPDATE

.NET TIPS:DateTimeとDateTimeOffsetの違いとは?[C#、VB]

.NET Frameworkが提供する二つの日付操作用クラス、DateTimeとDateTimeOffsetの違いとそれらの使い分け方を解説する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Platform Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 3.5以降
DateTimeOffset構造体が導入された.NET Frameworkのバージョンは、正確には.NET Framework 3.5と同時にリリースされた.NET Framework 2.0 SP 1からである。しかし、MSDNには.NET Framework 3.5の新機能として記載されているので、本稿ではそれに従った。
DateTime構造体は.NET 1.0から利用可能。


 日時処理のコーディングをしようとしたとき、現在の.NET Frameworkには利用できる構造体が2種類あって、どちらを使えばよいのか迷ってしまったことはないだろうか? .NET Frameworkの当初から利用できたDateTime構造体と、後から追加されたDateTimeOffset構造体である(いずれもSystem名前空間)。本稿では、その違いを解説する。

時差を扱うプログラムではDateTimeOffsetを使う

 使い分けの目安は、時差を扱う必要があるかどうかである。扱う必要がなければ、使い慣れたDateTime構造体でよい。時差を取り扱わねばならないときは、DateTimeOffset構造体を使用する。

  • DateTime構造体: 基本的には日時のデータだけを保持している。時差のデータを持っていないので、ある特定の時点を指定できない(例えば、「2015/6/24 5:00:00」というだけでは日本と米国では異なる時刻を指しており、ある瞬間を特定したことにはならない)。時差を問題にしなくてよいときは、コーディングが簡単である。
  • DateTimeOffset構造体: 内部にDateTime構造体とともに時差のデータも保持している。時差は、UTC(世界協定時刻)からのオフセットである(例えば、「2015/6/24 5:00:00 +9:00」という表現をしたとき、「+9:00」がUTCからのオフセット)。SQL Serverでもサポートされている(SQL Server 2008より)。

 なおDateTime構造体には.NET Framework 2.0でKindプロパティが追加され、「時刻の種類」(=UTC/ローカル時刻/不明の3通り)を表現できるようになっている。本稿ではそれも紹介するが、複雑になるばかりなので、今となっては実際に使うことはないだろう。DateTimeOffset構造体を使えばよいからだ。

.NET Frameworkで日時を扱う構造体の変遷 .NET Frameworkで日時を扱う構造体の変遷
最初に、日時だけを保持するDateTime構造体が提供された。
.NET Framework 2.0で、「時刻の種類」を示すKindプロパティがDateTime構造体に追加された。しかしそれでは不十分であった。時差を扱うプログラムは、あいかわらず時差データを別に持たねばならなかったのである。筆者には、かえって面倒になっただけのように感じられた。
.NET Framework 3.5で、DateTimeOffset構造体が導入された。時差データをOffsetプロパティに保持しているため、時差を扱うプログラムではこれを使えばよくなった。ただし、タイムゾーン(日本標準時/太平洋標準時など)のデータは持っていない(別途、TimeZoneInfoクラスを使って処理することになる)。
ちなみに、Windowsストアアプリ(Windows 10ではユニバーサルWindowsプラットフォームアプリ)で利用するWindowsランタイムでは、.NET FrameworkのDateTimeOffset構造体に相当するものだけが提供されている(WindowsランタイムのDateTime structureを参照)。

初期化するには?

 DateTimeOffset構造体を初期化するときには、必ず時差を指定する必要がある。

 まず、DateTime構造体の初期化方法から見ていこう(次のコード)。通常は、日時だけを与える。「時刻の種類」を指定することもできるが、DateTimeOffset構造体を利用できるならばそちらを使った方がよい。

// 日時のみを指定
DateTime dt1 = new DateTime(2015, 6, 24, 5, 0, 0);
Console.WriteLine("DateTime(日時のみ指定): {0}, DateTime.Kind={1}", dt1, dt1.Kind);
// 出力結果:
// DateTime(日時のみ指定): 2015/06/24 5:00:00, DateTime.Kind=Unspecified

// DateTimeKind列挙体を使って時刻の種類を指定できる
// DateTimeKind.Local値はPCの現地時刻という意味
DateTime dt2 = new DateTime(2015, 6, 24, 5, 0, 0, DateTimeKind.Local);
Console.WriteLine("DateTime(Kind=Local): {0}, DateTime.Kind={1}", dt2, dt2.Kind);
// 出力結果:
// DateTime(Kind=Local): 2015/06/24 5:00:00, DateTime.Kind=Local

// DateTimeKind.Utc値は世界協定時という意味
DateTime dt3 = new DateTime(2015, 6, 23, 20, 0, 0, DateTimeKind.Utc);
Console.WriteLine("DateTime(Kind=UTC): {0}, DateTime.Kind={1}", dt3, dt3.Kind);
// 出力結果:
// DateTime(Kind=UTC): 2015/06/23 20:00:00, DateTime.Kind=Utc

// ただし、比較するときにDateTime.Kindプロパティの値は無視される
Console.WriteLine("2番目と3番目は等しいか?:{0}", dt2 == dt3);
// dt2とdt3は同一の時刻を表しているので(Localが日本標準時を示す場合)、
// 等しいという結果になってほしい
// 出力結果:
// 2番目と3番目は等しいか?:False

// 現在時刻は、現地時刻(Local)または世界協定時(UTC)で取得できる
DateTime dtLocalNow = DateTime.Now;
Console.WriteLine("DateTime.Now: {0}, DateTime.Kind={1}", dtLocalNow, dtLocalNow.Kind);
DateTime dtUtcNow = DateTime.UtcNow;
Console.WriteLine("DateTime.UtcNow: {0}, DateTime.Kind={1}", dtUtcNow, dtUtcNow.Kind);
// 出力例:
// DateTime.Now: 2015/06/24 11:05:27, DateTime.Kind=Local
// DateTime.UtcNow: 2015/06/24 2:05:27, DateTime.Kind=Utc

' 日時のみを指定
Dim dt1 As DateTime = New DateTime(2015, 6, 24, 5, 0, 0)
Console.WriteLine("DateTime(日時のみ指定): {0}, DateTime.Kind={1}", dt1, dt1.Kind)
' 出力結果:
' DateTime(日時のみ指定): 2015/06/24 5:00:00, DateTime.Kind=Unspecified

' DateTimeKind列挙体を使って時刻の種類を指定できる
' DateTimeKind.Local値はPCの現地時刻という意味
Dim dt2 As DateTime = New DateTime(2015, 6, 24, 5, 0, 0, DateTimeKind.Local)
Console.WriteLine("DateTime(Kind=Local): {0}, DateTime.Kind={1}", dt2, dt2.Kind)
' 出力結果:
' DateTime(Kind=Local): 2015/06/24 5:00:00, DateTime.Kind=Local

' DateTimeKind.Utc値は世界協定時という意味
Dim dt3 As DateTime = New DateTime(2015, 6, 23, 20, 0, 0, DateTimeKind.Utc)
Console.WriteLine("DateTime(Kind=UTC): {0}, DateTime.Kind={1}", dt3, dt3.Kind)
' 出力結果:
' DateTime(Kind=UTC): 2015/06/23 20:00:00, DateTime.Kind=Utc

' ただし、比較するときにDateTime.Kindプロパティの値は無視される
Console.WriteLine("2番目と3番目は等しいか?:{0}", dt2 = dt3)
' dt2とdt3は同一の時刻を表しているので(Localが日本標準時を示す場合)、
' 等しいという結果になってほしい
' 出力結果:
' 2番目と3番目は等しいか?:False

' 現在時刻は、現地時刻(Local)または世界協定時(UTC)で取得できる
Dim dtLocalNow As DateTime = DateTime.Now
Console.WriteLine("DateTime.Now: {0}, DateTime.Kind={1}", dtLocalNow, dtLocalNow.Kind)
Dim dtUtcNow As DateTime = DateTime.UtcNow
Console.WriteLine("DateTime.UtcNow: {0}, DateTime.Kind={1}", dtUtcNow, dtUtcNow.Kind)
' 出力例:
' DateTime.Now: 2015/06/24 11:41:00, DateTime.Kind=Local
' DateTime.UtcNow: 2015/06/24 2:41:00, DateTime.Kind=Utc

DateTime構造体を初期化するコードの例(上:C#、下:VB)
DateTime構造体を初期化する際には、日時を与える(太字の部分)。
DateTimeKind列挙体を使って時刻の種類も指定できるが、単なる「覚え書き」にすぎない。比較や時間の加減算などでDateTime構造体のKindプロパティの値は利用されないのだ(ToLocalTimeメソッドやToUniversalTimeメソッドなどで使用されるが、そのルールは複雑だ)。
DateTime構造体のNowプロパティ/UctNowプロパティで取得した現在時刻には、Kindプロパティの値が設定されている。

 次に、DateTimeOffset構造体を初期化する方法である(次のコード)。前述したように、必ず時差も指定する必要がある。DateTime構造体を与えて初期化してもよい(その場合はDateTime構造体のKindプロパティから時差が決定される)。

// 日時と時差を指定
DateTimeOffset dto0 = new DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(9.0));
Console.WriteLine("DateTimeOffset(日時と時差を指定): {0}, DateTime.Kind={1}",
                  dto0, dto0.DateTime.Kind);
// 出力結果:
// DateTimeOffset(日時と時差を指定): 2015/06/24 5:00:00 +09:00, DateTime.Kind=Unspecified

// DateTime構造体を指定(Kind=Unspecified)
DateTimeOffset dto1 = new DateTimeOffset(dt1); // dt1は前述のコードを参照
Console.WriteLine("DateTimeOffset(Kind=UnspecifiedのDateTimeから): {0}, DateTime.Kind={1}",
                  dto1, dto1.DateTime.Kind);
// 出力結果:
// DateTimeOffset(Kind=UnspecifiedのDateTimeから): 2015/06/24 5:00:00 +09:00, DateTime.Kind=Unspecified

// DateTime構造体を指定(Kind=Local)
DateTimeOffset dto2 = new DateTimeOffset(dt2); // dt2は前述のコードを参照
Console.WriteLine("DateTimeOffset(Kind=LocalのDateTimeから): {0}, DateTime.Kind={1}",
                  dto2, dto2.DateTime.Kind);
// 出力結果:
// DateTimeOffset(Kind=LocalのDateTimeから): 2015/06/24 5:00:00 +09:00, DateTime.Kind=Unspecified

// DateTime構造体を指定(Kind=UTC)
DateTimeOffset dto3 = new DateTimeOffset(dt3); // dt3は前述のコードを参照
Console.WriteLine("DateTimeOffset(Kind=UTCのDateTimeから): {0}, DateTime.Kind={1}",
                  dto3, dto3.DateTime.Kind);
// 出力結果:
// DateTimeOffset(Kind=UTCのDateTimeから): 2015/06/23 20:00:00 +00:00, DateTime.Kind=Unspecified

// 比較するときに時差が考慮される
Console.WriteLine("2番目と3番目は等しいか?:{0}", dto2 == dto3);
// dto2とdto3は同一の時刻を表しているので(Localが日本標準時を示す場合)、
// 等しいという結果になってほしい
// 出力結果:
// 2番目と3番目は等しいか?:True

// 現在時刻は、現地時刻(Local)または世界協定時(UTC)で取得できる
DateTimeOffset dtoLocalNow = DateTimeOffset.Now;
Console.WriteLine("DateTimeOffset.Now: {0}, DateTime.Kind={1}",
                  dtoLocalNow, dtoLocalNow.DateTime.Kind);
DateTimeOffset dtoUtcNow = DateTimeOffset.UtcNow;
Console.WriteLine("DateTimeOffset.UtcNow: {0}, DateTime.Kind={1}",
                  dtoUtcNow, dtoUtcNow.DateTime.Kind);
// 出力例:
// DateTimeOffset.Now: 2015/06/24 11:17:31 +09:00, DateTime.Kind=Unspecified
// DateTimeOffset.UtcNow: 2015/06/24 2:17:31 +00:00, DateTime.Kind=Unspecified

' 日時と時差を指定
Dim dto0 As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(9.0))
Console.WriteLine("DateTimeOffset(日時と時差を指定): {0}, DateTime.Kind={1}",
                  dto0, dto0.DateTime.Kind)
' 出力結果:
' DateTimeOffset(日時と時差を指定): 2015/06/24 5:00:00 +09:00, DateTime.Kind=Unspecified

' DateTime構造体を指定(Kind=Unspecified)
Dim dto1 As DateTimeOffset = New DateTimeOffset(dt1) ' dt1は前述のコードを参照
Console.WriteLine("DateTimeOffset(Kind=UnspecifiedのDateTimeから): {0}, DateTime.Kind={1}",
                  dto1, dto1.DateTime.Kind)
' 出力結果:
' DateTimeOffset(Kind=UnspecifiedのDateTimeから): 2015/06/24 5:00:00 +09:00, DateTime.Kind=Unspecified

' DateTime構造体を指定(Kind=Local)
Dim dto2 As DateTimeOffset = New DateTimeOffset(dt2) ' dt2は前述のコードを参照
Console.WriteLine("DateTimeOffset(Kind=LocalのDateTimeから): {0}, DateTime.Kind={1}",
                  dto2, dto2.DateTime.Kind)
' 出力結果:
' DateTimeOffset(Kind=LocalのDateTimeから): 2015/06/24 5:00:00 +09:00, DateTime.Kind=Unspecified

' DateTime構造体を指定(Kind=UTC)
Dim dto3 As DateTimeOffset = New DateTimeOffset(dt3) ' dt3は前述のコードを参照
Console.WriteLine("DateTimeOffset(Kind=UTCのDateTimeから): {0}, DateTime.Kind={1}",
                  dto3, dto3.DateTime.Kind)
' 出力結果:
' DateTimeOffset(Kind=UTCのDateTimeから): 2015/06/23 20:00:00 +00:00, DateTime.Kind=Unspecified

' 比較するときに時差が考慮される
Console.WriteLine("2番目と3番目は等しいか?:{0}", dto2 = dto3)
' dto2とdto3は同一の時刻を表しているので(Localが日本標準時を示す場合)、
' 等しいという結果になってほしい
' 出力結果:
' 2番目と3番目は等しいか?:True

' 現在時刻は、現地時刻(Local)または世界協定時(UTC)で取得できる
Dim dtoLocalNow As DateTimeOffset = DateTimeOffset.Now
Console.WriteLine("DateTimeOffset.Now: {0}, DateTime.Kind={1}",
                  dtoLocalNow, dtoLocalNow.DateTime.Kind)
Dim dtoUtcNow As DateTimeOffset = DateTimeOffset.UtcNow
Console.WriteLine("DateTimeOffset.UtcNow: {0}, DateTime.Kind={1}",
                  dtoUtcNow, dtoUtcNow.DateTime.Kind)
' 出力例:
' DateTimeOffset.Now: 2015/06/24 11:41:00 +09:00, DateTime.Kind=Unspecified
' DateTimeOffset.UtcNow: 2015/06/24 2:41:00 +00:00, DateTime.Kind=Unspecified

DateTimeOffset構造体を初期化するコードの例(上:C#、下:VB)
DateTimeOffset構造体を初期化する際には、日時と時差を与えるのが基本となる(最初の太字の部分)。
DateTime構造体を与えて初期化してもよい(2番目の太字の部分)。
DateTimeOffset構造体のToStringメソッドを書式指定なしで呼び出すと、結果の文字列には時差も入る(前述のDateTime構造体のコードと見比べてほしい)。
DateTime構造体を与えて初期化する場合は、DateTime構造体のKindプロパティから時差が決定される。Kindプロパティが「Unspecified」のときは、Localと見なされる。初期化後のDateTimeOffset構造体に含まれるDateTime構造体のKindプロパティは、常に「Unspecified」になる。これらの挙動は少々複雑なので、DateTime構造体とDateTimeOffset構造体を混在させる場合はKindプロパティを利用すべきではないだろう(常に「Unspecified」にする)。
DateTimeOffset構造体を使用した場合、比較や時間の加減算では、きちんと時差が考慮された結果になる。
DateTimeOffset構造体のNowプロパティ/UctNowプロパティで取得した現在時刻は、当然ではあるが同一の時刻を指している。
なお、このVBのコードは、Visual Basic 2010から利用できるようになった「暗黙の行連結」を使用している。

書式指定に時差を入れるには?

 書式指定文字列を使って文字列に変換する際に、時差を入れ込むこともできる。

 まず、DateTime構造体では、時差の書式指定文字として「K」を使う(次のコード)。DateTime構造体のKindプロパティが「Local」のときは、実行中のPCに設定されている時差が使われる。「Utc」のときは、世界協定時を表す文字「Z」になる。そして、「Unspecified」のときは何も出力されない。

// 日時のみを指定
DateTime dt1 = new DateTime(2015, 6, 24, 5, 0, 0);
Console.WriteLine("DateTime(日時のみ指定):{0:yyyy/M/d H:mm:ssK}", dt1);
// 出力結果:
// DateTime(日時のみ指定):2015/6/24 5:00:00

// DateTimeKind.LocalはPCの現地時刻を表す
DateTime dt2 = new DateTime(2015, 6, 24, 5, 0, 0, DateTimeKind.Local);
Console.WriteLine("DateTime(Kind=Local):{0:yyyy/M/d H:mm:ssK}", dt2);
// 出力結果:
// DateTime(Kind=Local):2015/6/24 5:00:00+09:00

// DateTimeKind.Utcは世界協定時を表す
DateTime dt3 = new DateTime(2015, 6, 23, 20, 0, 0, DateTimeKind.Utc);
Console.WriteLine("DateTime(Kind=UTC):{0:yyyy/M/d H:mm:ssK}", dt3);
// 出力結果:
// DateTime(Kind=UTC):2015/6/23 20:00:00Z

' 日時のみを指定
Dim dt1 As DateTime = New DateTime(2015, 6, 24, 5, 0, 0)
Console.WriteLine("DateTime(日時のみ指定):{0:yyyy/M/d H:mm:ssK}", dt1)
' 出力結果:
' DateTime(日時のみ指定):2015/6/24 5:00:00

' DateTimeKind.LocalはPCの現地時刻を表す
Dim dt2 As DateTime = New DateTime(2015, 6, 24, 5, 0, 0, DateTimeKind.Local)
Console.WriteLine("DateTime(Kind=Local):{0:yyyy/M/d H:mm:ssK}", dt2)
' 出力結果:
' DateTime(Kind=Local):2015/6/24 5:00:00+09:00

' DateTimeKind.Utcは世界協定時を表す
Dim dt3 As DateTime = New DateTime(2015, 6, 23, 20, 0, 0, DateTimeKind.Utc)
Console.WriteLine("DateTime(Kind=UTC):{0:yyyy/M/d H:mm:ssK}", dt3)
' 出力結果:
' DateTime(Kind=UTC):2015/6/23 20:00:00Z

DateTime構造体を文字列に変換する際、時差を入れ込むコードの例(上:C#、下:VB)
書式指定文字として「K」を使う(太字の部分)。
DateTime構造体のKindプロパティが「Unspecified」のときは、結果に時差が出力されない。
Kindプロパティが「Local」のときは、時差が出力される。実行中のPCに設定されている時差が使われる。
Kindプロパティが「Utc」のときは、時差の代わりに文字「Z」が出力される。
これでは統一が取れていないので、実際には「K」を使わずに独自に時差の文字列を生成することになるだろう。

 次に、DateTimeOffset構造体の場合であるが、書式指定文字として「K」または「zzz」を使う(次のコード)。DateTime構造体とは異なり、常に時差が出力される。

DateTimeOffset dto1 = new DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(9.0));
Console.WriteLine("DateTimeOffset(時差+9時間):{0:yyyy/M/d H:mm:ss K}", dto1);
Console.WriteLine("DateTimeOffset(時差+9時間):{0:yyyy/M/d H:mm:ss zzz}", dto1);
// 出力結果:
// DateTimeOffset(時差+9時間):2015/6/24 5:00:00 +09:00
// DateTimeOffset(時差+9時間):2015/6/24 5:00:00 +09:00

DateTimeOffset dto2 = new DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(0.0));
Console.WriteLine("DateTimeOffset(時差0):{0:yyyy/M/d H:mm:ss K}", dto2);
Console.WriteLine("DateTimeOffset(時差0):{0:yyyy/M/d H:mm:ss zzz}", dto2);
// 出力結果:
// DateTimeOffset(時差0):2015/6/24 5:00:00 +00:00
// DateTimeOffset(時差0):2015/6/24 5:00:00 +00:00

DateTimeOffset dto3 = new DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(-7.0));
Console.WriteLine("DateTimeOffset(時差-7時間):{0:yyyy/M/d H:mm:ss K}", dto3);
Console.WriteLine("DateTimeOffset(時差-7時間):{0:yyyy/M/d H:mm:ss zzz}", dto3);
// 出力結果:
// DateTimeOffset(時差-7時間):2015/6/24 5:00:00 -07:00
// DateTimeOffset(時差-7時間):2015/6/24 5:00:00 -07:00

Dim dto1 As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(9.0))
Console.WriteLine("DateTimeOffset(時差+9時間):{0:yyyy/M/d H:mm:ss K}", dto1)
Console.WriteLine("DateTimeOffset(時差+9時間):{0:yyyy/M/d H:mm:ss zzz}", dto1)
' 出力結果:
' DateTimeOffset(時差+9時間):2015/6/24 5:00:00 +09:00
' DateTimeOffset(時差+9時間):2015/6/24 5:00:00 +09:00

Dim dto2 As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(0.0))
Console.WriteLine("DateTimeOffset(時差0):{0:yyyy/M/d H:mm:ss K}", dto2)
Console.WriteLine("DateTimeOffset(時差0):{0:yyyy/M/d H:mm:ss zzz}", dto2)
' 出力結果:
' DateTimeOffset(時差0):2015/6/24 5:00:00 +00:00
' DateTimeOffset(時差0):2015/6/24 5:00:00 +00:00

Dim dto3 As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 5, 0, 0, TimeSpan.FromHours(-7.0))
Console.WriteLine("DateTimeOffset(時差-7時間):{0:yyyy/M/d H:mm:ss K}", dto3)
Console.WriteLine("DateTimeOffset(時差-7時間):{0:yyyy/M/d H:mm:ss zzz}", dto3)
' 出力結果:
' DateTimeOffset(時差-7時間):2015/6/24 5:00:00 -07:00
' DateTimeOffset(時差-7時間):2015/6/24 5:00:00 -07:00

DateTimeOffset構造体を文字列に変換する際、時差を入れ込むコードの例(上:C#、下:VB)
書式指定文字として「K」または「zzz」を使う(「zz」/「z」もある)。常に、結果に時差が出力される。

DateTimeOffsetを現地時刻で出力するには?

 日時を文字列として表示するとき、多くは時差を付けずに現地時刻とするだろう。DateTime構造体は時差を持っていないので、そのまま表示すればよい(Kindプロパティを利用しているときはToLocalTimeメソッドを使う)。

 DateTimeOffset構造体では、ToLocalTimeメソッドを使って現地時刻に変換してから表示するのが基本になる(次のコード)。

DateTimeOffset dto1 = new DateTimeOffset(2015, 6, 24, 17, 0, 0, 0, TimeSpan.FromHours(9.0));
Console.WriteLine("dto1:{0}= {1:yyyy/M/d H:mm:ss}(現地時刻)", dto1, dto1.ToLocalTime());
// 出力結果:
// dto1:2015/06/24 17:00:00 +09:00= 2015/6/24 17:00:00(現地時刻)

DateTimeOffset dto2 = new DateTimeOffset(2015, 6, 24, 11, 0, 0, 0, TimeSpan.FromHours(-7.0));
Console.WriteLine("dto2:{0}= {1:yyyy/M/d H:mm:ss}(現地時刻)", dto2, dto2.ToLocalTime());
// 出力結果:
// dto2:2015/06/24 11:00:00 -07:00= 2015/6/25 3:00:00(現地時刻)

Dim dto1 As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 17, 0, 0, 0, TimeSpan.FromHours(9.0))
Console.WriteLine("dto1:{0}= {1:yyyy/M/d H:mm:ss}(現地時刻)", dto1, dto1.ToLocalTime())
' 出力結果:
' dto1:2015/06/24 17:00:00 +09:00= 2015/6/24 17:00:00(現地時刻)

Dim dto2 As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 11, 0, 0, 0, TimeSpan.FromHours(-7.0))
Console.WriteLine("dto2:{0}= {1:yyyy/M/d H:mm:ss}(現地時刻)", dto2, dto2.ToLocalTime())
' 出力結果:
' dto2:2015/06/24 11:00:00 -07:00= 2015/6/25 3:00:00(現地時刻)

DateTimeOffset構造体を現地時刻に変換して表示するコードの例(上:C#、下:VB)
DateTimeOffset構造体のToLocalTimeメソッドを呼び出すと、OffsetプロパティをPCの時差に合わせて調整した新しいDateTimeOffset構造体が返される。DateTimeOffset構造体を表示するときは、前述のように時差を付けて表示するか、このように現地時刻に変換して表示するかの、どちらかだと思ってよいくらいである(例外は、現地時刻だけを扱う場合)。
なお、現地時刻ではなく特定の時差の日時に変換したい場合は、ToOffsetメソッドを使う。

時差付きの文字列をパーズするには?

 それぞれの構造体のParse/ParseExact/TryParse/TryParseExactメソッドを使えばよい。ここでは、TryParseメソッドを使う例を紹介しよう。

 まずDateTime構造体であるが、現地時刻に変換される(次のコード)。文字列にあった時差情報は失われるのである。

DateTime dt = new DateTime();

// 時差の文字列なし
string s0 = "Wed, 24 Jun 2015 05:00:00";
DateTime.TryParse(s0, out dt);
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind);
// 出力結果:
// DateTime.TryParse:2015/06/24 5:00:00 (Unspecified)

// 現地時刻と等しい時差
string s1 = "Wed, 24 Jun 2015 05:00:00 +9:00";
DateTime.TryParse(s1, out dt);
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind);
// 出力結果:
// DateTime.TryParse:2015/06/24 5:00:00 (Local)

// 時差−2時間
string s2 = "Tue, 23 Jun 2015 18:00:00 -2:00";
DateTime.TryParse(s2, out dt);
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind);
// 出力結果:
// DateTime.TryParse:2015/06/24 5:00:00 (Local)

// 時差0(特別に「GMT」や「Z」を認識する)
string s3 = "Tue, 23 Jun 2015 20:00:00 GMT";
DateTime.TryParse(s3, out dt);
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind);
// 出力結果:
// DateTime.TryParse:2015/06/24 5:00:00 (Local)

Dim dt As DateTime = New DateTime()

' 時差の文字列なし
Dim s0 As String = "Wed, 24 Jun 2015 05:00:00"
DateTime.TryParse(s0, dt)
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind)
' 出力結果:
' DateTime.TryParse:2015/06/24 5:00:00 (Unspecified)

' 現地時刻と等しい時差
Dim s1 As String = "Wed, 24 Jun 2015 05:00:00 +9:00"
DateTime.TryParse(s1, dt)
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind)
' 出力結果:
' DateTime.TryParse:2015/06/24 5:00:00 (Local)

' 時差−2時間
Dim s2 As String = "Tue, 23 Jun 2015 18:00:00 -2:00"
DateTime.TryParse(s2, dt)
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind)
' 出力結果:
' DateTime.TryParse:2015/06/24 5:00:00 (Local)

' 時差0(特別に「GMT」や「Z」を認識する)
Dim s3 As String = "Tue, 23 Jun 2015 20:00:00 GMT"
DateTime.TryParse(s3, dt)
Console.WriteLine("DateTime.TryParse:{0} ({1})", dt, dt.Kind)
' 出力結果:
' DateTime.TryParse:2015/06/24 5:00:00 (Local)

DateTime構造体のTryParseメソッドを使うコードの例(上:C#、下:VB)
日時を表す4通りの文字列は、どれも日本での同じ時刻を表している。DateTime構造体のTryParseメソッドは、読み込む文字列中の時差を認識し、パーズした結果を現地時刻(Kindプロパティの値が「Local」なDateTime構造体)に変換して返す(文字列中に時差がないときはKindプロパティの値が「Unspecified」なDateTime構造体が作成される)。このとき、文字列にあった時差情報は失われてしまう。
なお、サンプルコードということで省略しているが、本来はTryParseメソッドの戻り値をチェックしてパーズに成功したかどうかを判定する必要がある。

 DateTimeOffset構造体では、文字列中の時差をそのまま保持する(次のコード)。文字列中に時差が書かれていないときは、現地時刻と解釈される。

DateTimeOffset dto = new DateTimeOffset();

// 時差の文字列なし(=現地時刻として解釈される)
string s0 = "Wed, 24 Jun 2015 05:00:00";
DateTimeOffset.TryParse(s0, out dto);
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto);
// 出力結果:
// DateTimeOffset.TryParse:2015/06/24 5:00:00 +09:00

// 現地時刻と等しい時差
string s1 = "Wed, 24 Jun 2015 05:00:00 +9:00";
DateTimeOffset.TryParse(s1, out dto);
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto);
// 出力結果:
// DateTimeOffset.TryParse:2015/06/24 5:00:00 +09:00

// 時差−2時間
string s2 = "Tue, 23 Jun 2015 18:00:00 -2:00";
DateTimeOffset.TryParse(s2, out dto);
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto);
// 出力結果:
// DateTimeOffset.TryParse:2015/06/23 18:00:00 -02:00

// 時差0(特別に「GMT」や「Z」を認識する)
string s3 = "Tue, 23 Jun 2015 20:00:00 GMT";
DateTimeOffset.TryParse(s3, out dto);
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto);
// 出力結果:
// DateTimeOffset.TryParse:2015/06/23 20:00:00 +00:00

Dim dto As DateTimeOffset = New DateTimeOffset()

' 時差の文字列なし(=現地時刻として解釈される)
Dim s0 As String = "Wed, 24 Jun 2015 05:00:00"
DateTimeOffset.TryParse(s0, dto)
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto)
' 出力結果:
' DateTimeOffset.TryParse:2015/06/24 5:00:00 +09:00

' 現地時刻と等しい時差
Dim s1 As String = "Wed, 24 Jun 2015 05:00:00 +9:00"
DateTimeOffset.TryParse(s1, dto)
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto)
' 出力結果:
' DateTimeOffset.TryParse:2015/06/24 5:00:00 +09:00

' 時差−2時間
Dim s2 As String = "Tue, 23 Jun 2015 18:00:00 -2:00"
DateTimeOffset.TryParse(s2, dto)
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto)
' 出力結果:
' DateTimeOffset.TryParse:2015/06/23 18:00:00 -02:00

' 時差0(特別に「GMT」や「Z」を認識する)
Dim s3 As String = "Tue, 23 Jun 2015 20:00:00 GMT"
DateTimeOffset.TryParse(s3, dto)
Console.WriteLine("DateTimeOffset.TryParse:{0}", dto)
' 出力結果:
' DateTimeOffset.TryParse:2015/06/23 20:00:00 +00:00

DateTimeOffset構造体のTryParseメソッドを使うコードの例(上:C#、下:VB)
日時を表す4通りの文字列は、どれも同じ時刻を表している。DateTimeOffset構造体のTryParseメソッドは、読み込む文字列中の時差を認識し、そのOffsetを持ったDateTimeOffset構造体として返す(文字列中に時差がないときは現地時刻と見なす)。文字列にあった時差情報はそのまま取り込まれるのである。
なお、サンプルコードということで省略しているが、本来はTryParseメソッドの戻り値をチェックしてパーズに成功したかどうかを判定する必要がある。

時差のある時間計算をするには?

 最後に、時差が異なる二つの日時の間の時間を求めてみよう。日時の引き算である。

 DateTime構造体では、単純に引き算をしてもうまくいかない。別に時差データを持っておかねばならないのだ(次のコード)。

// 例:飛行機の飛行時間
// 成田17:00発、ロサンゼルス同日11:00着(夏時間、UTC-7)

// DateTimeでは、単純に引き算をしてはダメ
DateTime dtDep = new DateTime(2015, 6, 24, 17, 0, 0, 0); // 17:00発
DateTime dtArr = new DateTime(2015, 6, 24, 11, 0, 0, 0); // 同日11:00着
TimeSpan dtFlightTime = dtArr.Subtract(dtDep);
Console.WriteLine("飛行時間は{0}時間(単純な引き算)", dtFlightTime.Hours);
// 出力結果:
// 飛行時間は−6時間(単純な引き算)

// 別の手段で保持しておいた時差を使ってUTCに変換してから引き算をする
DateTime dtDepUtc = dtDep.AddHours(-9.0);
DateTime dtArrUtc = dtArr.AddHours(+7.0);
TimeSpan dtFlightTimeUtc = dtArrUtc.Subtract(dtDepUtc);
Console.WriteLine("飛行時間は{0}時間(UTC換算後)", dtFlightTimeUtc.Hours);
// 出力結果:
// 飛行時間は10時間(UTC換算後)

' 例:飛行機の飛行時間
' 成田17:00発、ロサンゼルス同日11:00着(夏時間、UTC-7)

' DateTimeでは、単純に引き算をしてはダメ
Dim dtDep As DateTime = New DateTime(2015, 6, 24, 17, 0, 0, 0)
Dim dtArr As DateTime = New DateTime(2015, 6, 24, 11, 0, 0, 0)
Dim dtFlightTime As TimeSpan = dtArr.Subtract(dtDep)
Console.WriteLine("飛行時間は{0}時間(単純な引き算)", dtFlightTime.Hours)
' 出力結果:
' 飛行時間は−6時間(単純な引き算)

' 別の手段で保持しておいた時差を使ってUTCに変換してから引き算をする
Dim dtDepUtc As DateTime = dtDep.AddHours(-9.0)
Dim dtArrUtc As DateTime = dtArr.AddHours(+7.0)
Dim dtFlightTimeUtc As TimeSpan = dtArrUtc.Subtract(dtDepUtc)
Console.WriteLine("飛行時間は{0}時間(UTC換算後)", dtFlightTimeUtc.Hours)
' 出力結果:
' 飛行時間は10時間(UTC換算後)

DateTime構造体で時差のある時間計算をするコードの例(上:C#、下:VB)
この例では飛行機の飛行時間を計算している。成田を夕方の5時に離陸した飛行機が、夏時間のロサンゼルスに同日の午前11時に到着するならば、飛行時間は10時間である。DateTime構造体でそのまま引き算を実行すると、おかしな結果になってしまう。そこで、別の手段で保持しておいた時差を使い(このコードではサンプルということで定数を使っている)、UTCに換算してから引き算を行う。あるいは、単純な引き算の結果(ここでは−6時間)に時差の差(ここでは(+7)−(−9)=16時間)を加えてもよい。

 DateTimeOffset構造体では、単に引き算をすればよい(次のコード)。

// 例:飛行機の飛行時間
// 成田17:00発、ロサンゼルス同日11:00着(夏時間、UTC-7)

DateTimeOffset dtoDep = new DateTimeOffset(2015, 6, 24, 17, 0, 0, 0,
                                           TimeSpan.FromHours(9.0));
DateTimeOffset dtoArr = new DateTimeOffset(2015, 6, 24, 11, 0, 0, 0,
                                           TimeSpan.FromHours(-7.0));
TimeSpan dtoFlightTime = dtoArr.Subtract(dtoDep);
Console.WriteLine("飛行時間は{0}時間", dtoFlightTime.Hours);
// 出力結果:
// 飛行時間は10時間

' 例:飛行機の飛行時間
' 成田17:00発、ロサンゼルス同日11:00着(夏時間、UTC-7)

Dim dtoDep As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 17, 0, 0, 0,
                                                  TimeSpan.FromHours(9.0))
Dim dtoArr As DateTimeOffset = New DateTimeOffset(2015, 6, 24, 11, 0, 0, 0,
                                                  TimeSpan.FromHours(-7.0))
Dim dtoFlightTime As TimeSpan = dtoArr.Subtract(dtoDep)
Console.WriteLine("飛行時間は{0}時間", dtoFlightTime.Hours)
' 出力結果:
' 飛行時間は10時間

DateTimeOffset構造体で時差のある時間計算をするコードの例(上:C#、下:VB)
単に引き算を行えば、内部に保持している時差を使って正しく時間を計算してくれる。
なお、このVBのコードは、Visual Basic 2010から利用できるようになった「暗黙の行連結」を使用している。

まとめ

 .NET Framework 3.5以降を使っているとき、時差に関わる処理がある/ありそうならば、迷わずDateTimeOffset構造体を使っていただきたい。そしてDateTime構造体のKindプロパティのことは忘れてほしい。DateTimeKindに関する話を飛ばして本稿を読み直してもらえば、DateTimeKindによってどれだけ話が複雑になっているか分かるだろう。

 なお、夏時間を扱いたいときは、TimeZoneInfoクラスを使用する。そのコード例は「WinRT/Metro TIPS:夏時間を考慮して日時を変換するには?[Win 8/WP 8]」をご覧いただきたい。

利用可能バージョン: .NET Framework 3.5以降
カテゴリ: クラスライブラリ 処理対象:日付と時刻
使用ライブラリ: DateTime構造体(System名前空間)
使用ライブラリ: DateTimeOffset構造体(System名前空間)
使用ライブラリ: TimeSpan構造体(System名前空間)
関連TIPS: 日時や時間間隔の加減算を行うには?
関連TIPS:
UTC(世界協定時)を取得するには?[C#、VB]
関連TIPS: 日付や時刻を文字列に変換するには?
関連TIPS: 日付や時刻の文字列をDateTimeオブジェクトに変換するには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。