.NETでサマータイムを扱うにはDateTimeOffset構造体を利用するが、その際に知っておくべきこと、日時の取得/生成などを行う方法を説明する。
サマータイム(夏時間)は英語ではDaylight Saving Time(DST)といい、欧米を中心として使われている制度である。DSTに対応したプログラミングは、海外向け、あるいは、海外と連携するアプリやWebサービスなどの開発者にはおなじみであろうが、初めて対応する開発者には多くの困難が待ち受けている。本稿では、その主なポイントを解説する。
特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。特記なきサンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。
using System;
using static System.Console;
Imports System.Console
現在の日本ではDSTを実施していないため*1、本稿ではDSTの実施例として太平洋標準時(PST)を用いる。
以降の解説やサンプルコードの出力を確認するには、WindowsのタイムゾーンをPSTに変更しておいていただきたい。次の画像にWindows 10での設定例を示す。
*1 日本でも数年間ではあるが、かつてサマータイムが実施されたことがある。手元のWindows 10を確認したところ、レジストリにその情報は登録されていなかった。UNIX系のOSが利用するtz databaseには登録されている。
DSTを扱う困難さは、次の2点に起因する。
1. UTC(協定世界時)からのオフセットが変化する
2. DST切り替え日は1日が24時間ではない
1.はよく知られているように、DSTでは時計を進めるというものだ。太平洋標準時(PST)では、標準のオフセットは-8時間であるが、DST期間中は-7時間になる。
従って、現在日時を取得/保存するときに、オフセットも一緒に取得/保存することになる。.NET Framework 3.5以降では、DateTimeOffset構造体(System名前空間)を使って日時を扱えばよい。別法として、日時を常にUTCで保存してもよい。なお、オフセットからタイムゾーンは一意に定まらないので、タイムゾーンの情報も必要なら、いずれにしてもタイムゾーンを別に保存しておく必要がある。
2.は、見落としがちだが、とても厄介な問題だ。DSTでオフセットを1時間ずらすとすると、DST開始日には1時間が消えてなくなり、1日が1時間短い23時間になる(次の図)。1日が24時間であることを前提にしているプログラムや、始業時刻までの時間が一定であることを前提にしているバッチ処理などは、見直しが必要だ。
逆に、DST終了日には同じ1時間が繰り返され、1日が1時間長い25時間になる。1日の間に同じ時刻が2回登場するのである。時刻の順序に依存するプログラムや、時刻をユニークなIDとして使っているプログラムなどは見直しが必要だ。また、時刻の入力には工夫が必要になるし、時刻の出力や印刷にもどちらの時刻か(DST終了前か後か)が分かるような工夫が必要になる。
.NET Framework 3.5以降では、TimeZoneInfoクラス(System名前空間)がタイムゾーンの情報とともにDSTの情報も管理している。DSTの開始日/終了日は、◯月の第◯週の◯曜日という形で保持されている(欧米のサマータイムのルールを表現するのに便利な形式)。
Windowsに設定されたタイムゾーンがPSTの場合、TimeZoneInfoクラスが保持している主な情報は次のコードのようになっている。
// Windowsに設定されているタイムゾーン情報
var currentTimeZone = TimeZoneInfo.Local;
WriteLine($"Id={currentTimeZone.Id}");
WriteLine($"DisplayName={currentTimeZone.DisplayName}");
WriteLine($"BaseUtcOffset={currentTimeZone.BaseUtcOffset.TotalHours:0.0}H");
WriteLine($"StandardName={currentTimeZone.StandardName}");
WriteLine($"SupportsDaylightSavingTime={currentTimeZone.SupportsDaylightSavingTime}");
WriteLine($"DaylightName={currentTimeZone.DaylightName}");
// 出力:
// Id=Pacific Standard Time
// DisplayName=(UTC-08:00) 太平洋標準時 (米国およびカナダ)
// BaseUtcOffset=-8.0H
// StandardName=太平洋標準時
// SupportsDaylightSavingTime=True
// DaylightName=太平洋夏時間
// DSTの調整ルール
TimeZoneInfo.AdjustmentRule[] rules = currentTimeZone.GetAdjustmentRules();
for(int i=0; i<rules.Length; i++)
{
WriteLine();
WriteLine($"AdjustmentRule[{i}]");
WriteLine($"DateStart→DateEnd={rules[i].DateStart:yyyy/MM/dd}→{rules[i].DateEnd:yyyy/MM/dd}");
WriteLine($"DaylightDelta={rules[i].DaylightDelta.TotalHours:0.0}H");
TimeZoneInfo.TransitionTime s = rules[i].DaylightTransitionStart;
WriteLine($"DaylightTransitionStart={s.Month}月 第{s.Week} {s.DayOfWeek} {s.TimeOfDay:HH:mm}");
TimeZoneInfo.TransitionTime e = rules[i].DaylightTransitionEnd;
WriteLine($"DaylightTransitionEnd={e.Month}月 第{e.Week} {e.DayOfWeek} {e.TimeOfDay:HH:mm}");
}
// 出力:
// AdjustmentRule[0]
// DateStart→DateEnd=0001/01/01→2006/12/31
// DaylightDelta=1.0H
// DaylightTransitionStart=4月 第1 Sunday 02:00
// DaylightTransitionEnd=10月 第5 Sunday 02:00
//
// AdjustmentRule[1]
// DateStart→DateEnd=2007/01/01→9999/12/31
// DaylightDelta=1.0H
// DaylightTransitionStart=3月 第2 Sunday 02:00
// DaylightTransitionEnd=11月 第1 Sunday 02:00
' Windowsに設定されているタイムゾーン情報
Dim currentTimeZone = TimeZoneInfo.Local
WriteLine($"Id={currentTimeZone.Id}")
WriteLine($"DisplayName={currentTimeZone.DisplayName}")
WriteLine($"BaseUtcOffset={currentTimeZone.BaseUtcOffset.TotalHours:0.0}H")
WriteLine($"StandardName={currentTimeZone.StandardName}")
WriteLine($"SupportsDaylightSavingTime={currentTimeZone.SupportsDaylightSavingTime}")
WriteLine($"DaylightName={currentTimeZone.DaylightName}")
' 出力:
' Id=Pacific Standard Time
' DisplayName=(UTC-08:00) 太平洋標準時 (米国およびカナダ)
' BaseUtcOffset=-8.0H
' StandardName=太平洋標準時
' SupportsDaylightSavingTime=True
' DaylightName=太平洋夏時間
' DSTの調整ルール
Dim rules As TimeZoneInfo.AdjustmentRule() = currentTimeZone.GetAdjustmentRules()
For i As Integer = 0 To (rules.Length - 1)
WriteLine()
WriteLine($"AdjustmentRule[{i}]")
WriteLine($"DateStart→DateEnd={rules(i).DateStart:yyyy/MM/dd}→{rules(i).DateEnd:yyyy/MM/dd}")
WriteLine($"DaylightDelta={rules(i).DaylightDelta.TotalHours:0.0}H")
Dim s As TimeZoneInfo.TransitionTime = rules(i).DaylightTransitionStart
WriteLine($"DaylightTransitionStart={s.Month}月 第{s.Week} {s.DayOfWeek} {s.TimeOfDay:HH:mm}")
Dim e As TimeZoneInfo.TransitionTime = rules(i).DaylightTransitionEnd
WriteLine($"DaylightTransitionEnd={e.Month}月 第{e.Week} {e.DayOfWeek} {e.TimeOfDay:HH:mm}")
Next
' 出力:
' AdjustmentRule[0]
' DateStart→DateEnd=0001/01/01→2006/12/31
' DaylightDelta=1.0H
' DaylightTransitionStart=4月 第1 Sunday 02:00
' DaylightTransitionEnd=10月 第5 Sunday 02:00
'
' AdjustmentRule[1]
' DateStart→DateEnd=2007/01/01→9999/12/31
' DaylightDelta=1.0H
' DaylightTransitionStart=3月 第2 Sunday 02:00
' DaylightTransitionEnd=11月 第1 Sunday 02:00
.NET Framework 3.5以降では、DateTimeOffset構造体のNowプロパティで現在日時を取得すればよい(次のコード)。TimeZoneInfoオブジェクトが持っているDSTの情報を利用して、自動的に正しいオフセットを設定してくれる。
var now = DateTimeOffset.Now;
WriteLine(now);
// 出力例:
// DSTのときに取得した場合
// 2018/08/23 2:04:46 -07:00
// DSTではないときに取得した場合
// 2018/11/23 2:06:05 -08:00
Dim Now = DateTimeOffset.Now
WriteLine(Now)
' 出力例:
' DSTときに取得した例
' 2018/08/23 2:04:46 -07:00
' DSTではないときに取得した例
' 2018/11/23 2:06:05 -08:00
取得した現在日時を保存するには、DateTimeOffsetオブジェクトをそのまま、あるいは文字列にシリアライズして保存するのが簡単だ。あるいは、UTCの日時に変換してから保存してもよいだろう。なお、DateTimeOffsetオブジェクトにタイムゾーンの情報は入っていないので、タイムゾーンの情報(例えば、タイムゾーンのID)も保存する必要があるときは、日時とは別に保存することになる。
タイムゾーンの違い(例えば日本標準時とPST)もDSTも、UTCからのオフセットが変化する。タイムゾーンを移動すれば、DSTの開始/終了と同じように時刻が飛んだり巻き戻ったりする。
両者はよく似ているが、.NET Frameworkでの扱いには違いがある。
アプリの実行中にタイムゾーンが変更されてもTimeZoneInfoオブジェクトは変化しないので、DateTimeOffset.Nowのオフセットも調整されない。アプリで明示的にTimeZoneInfoオブジェクトを再生成する必要がある(次のコード)。
Microsoft.Win32.SystemEvents.TimeChanged += (s, e)
=> TimeZoneInfo.ClearCachedData();
AddHandler Microsoft.Win32.SystemEvents.TimeChanged,
Sub(s, e)
TimeZoneInfo.ClearCachedData()
End Sub
任意のDateTimeOffsetオブジェクトを生成するとき、DSTがある場合は何時間のオフセットを設定するかが問題になる。PSTの場合は、-7時間とするか-8時間とするかを決めねばならない。TimeZoneInfoオブジェクトのGetUtcOffsetメソッドを使うと、ローカル時刻からその時刻に対応したオフセットを求められる。
ローカル時刻のDateTimeオブジェクト(System名前空間)を与えてDateTimeOffsetオブジェクトを生成するメソッドの例は、次のコードのようになる(ただし、後述するようにこれでは問題がある)。
static DateTimeOffset CreateDtoA(DateTime localDateTime)
{
TimeSpan offset = TimeZoneInfo.Local.GetUtcOffset(localDateTime);
return new DateTimeOffset(localDateTime, offset);
}
Function CreateDtoA(localDateTime As DateTime) As DateTimeOffset
Dim offset As TimeSpan = TimeZoneInfo.Local.GetUtcOffset(localDateTime)
Return New DateTimeOffset(localDateTime, offset)
End Function
ただし、上のメソッドでは、DST開始時の「失われた」時間を指定したときに、誤ったオフセットになってしまう(後掲の実行例を参照)。それに対処するには、DateTimeOffsetオブジェクトをTimeZoneInfoクラスのConvertTimeメソッドに渡して、正しいオフセットに補正する(次のコード)。
static DateTimeOffset CreateDtoB(DateTime localDateTime)
{
TimeSpan offset = TimeZoneInfo.Local.GetUtcOffset(localDateTime);
DateTimeOffset dto = new DateTimeOffset(localDateTime, offset);
return TimeZoneInfo.ConvertTime(dto, TimeZoneInfo.Local);
}
Function CreateDtoB(localDateTime As DateTime) As DateTimeOffset
Dim offset As TimeSpan = TimeZoneInfo.Local.GetUtcOffset(localDateTime)
Dim dto As DateTimeOffset = New DateTimeOffset(localDateTime, offset)
Return TimeZoneInfo.ConvertTime(dto, TimeZoneInfo.Local)
End Function
タイムゾーンがPSTのときに上記CreateDtoAメソッドを呼び出す例を、次のコードに示す。DST開始時以外はCreateDtoBメソッドの結果も同じになる。次のコードには、DST切り替え時のみCreateDtoBメソッドの呼び出しも載せてある。この実行例を見てもらうと、DST終了時は(2つある同一時刻のうち)1つ目の時刻を生成できていないことが分かる。その時間帯の時刻を生成するには、UTCでDateTimeOffsetオブジェクトを作ってから変換する、あるいは、午前0時のDateTimeOffsetオブジェクトを作ってから時刻を加算するといった工夫が必要になる。
// 標準時の例
var dt1 = new DateTime(2018, 1, 1, 7, 0, 0);
var dto1 = CreateDtoA(dt1);
WriteLine($"{dt1} → {dto1} (UTC {dto1.UtcDateTime:HH:mm:ss})");
// 出力:
// 2018/01/01 7:00:00 → 2018/01/01 7:00:00 -08:00 (UTC 15:00:00)
// DSTの例
var dt2 = new DateTime(2018, 7, 1, 7, 0, 0);
var dto2 = CreateDtoA(dt2);
WriteLine($"{dt2} → {dto2} (UTC {dto2.UtcDateTime:HH:mm:ss})");
// 出力:
// 2018/07/01 7:00:00 → 2018/07/01 7:00:00 -07:00 (UTC 14:00:00)
// DST開始をまたぐ例
var dt3 = new DateTime(2018, 3, 11, 1, 30, 0);
var dto3 = CreateDtoA(dt3);
WriteLine($"{dt3} → {dto3} (UTC {dto3.UtcDateTime:HH:mm:ss})");
var dt4 = new DateTime(2018, 3, 11, 2, 30, 0);
var dto4A = CreateDtoA(dt4);
WriteLine($"{dt4} → {dto4A} (UTC {dto4A.UtcDateTime:HH:mm:ss}) [A]");
var dto4B = CreateDtoB(dt4);
WriteLine($"{dt4} → {dto4B} (UTC {dto4B.UtcDateTime:HH:mm:ss}) [B]");
var dt5 = new DateTime(2018, 3, 11, 3, 30, 0);
var dto5 = CreateDtoA(dt5);
WriteLine($"{dt5} → {dto5} (UTC {dto5.UtcDateTime:HH:mm:ss})");
// 出力:
// 2018/03/11 1:30:00 → 2018/03/11 1:30:00 -08:00 (UTC 09:30:00)
// 2018/03/11 2:30:00 → 2018/03/11 2:30:00 -08:00 (UTC 10:30:00) [A]
// ※上の結果は誤り。2:00を過ぎているのだからオフセットは-7時間のはずである
// 2018/03/11 2:30:00 → 2018/03/11 3:30:00 -07:00 (UTC 10:30:00) [B]
// 2018/03/11 3:30:00 → 2018/03/11 3:30:00 -07:00 (UTC 10:30:00)
// DST終了をまたぐ例
var dt6 = new DateTime(2018, 11, 4, 0, 30, 0);
var dto6 = CreateDtoA(dt6);
WriteLine($"{dt6} → {dto6} (UTC {dto6.UtcDateTime:HH:mm:ss})");
var dt7 = new DateTime(2018, 11, 4, 1, 30, 0);
var dto7A = CreateDtoA(dt7);
WriteLine($"{dt7} → {dto7A} (UTC {dto7A.UtcDateTime:HH:mm:ss}) [A]");
var dto7B = CreateDtoB(dt7);
WriteLine($"{dt7} → {dto7B} (UTC {dto7A.UtcDateTime:HH:mm:ss}) [B]");
var dt8 = new DateTime(2018, 11, 4, 2, 30, 0);
var dto8 = CreateDtoA(dt8);
WriteLine($"{dt8} → {dto8} (UTC {dto8.UtcDateTime:HH:mm:ss})");
// 出力:
// 2018/11/04 0:30:00 → 2018/11/04 0:30:00 -07:00 (UTC 07:30:00)
// 2018/11/04 1:30:00 → 2018/11/04 1:30:00 -08:00 (UTC 09:30:00) [A]
// 2018/11/04 1:30:00 → 2018/11/04 1:30:00 -08:00 (UTC 09:30:00) [B]
// 2018/11/04 2:30:00 → 2018/11/04 2:30:00 -08:00 (UTC 10:30:00)
' 標準時の例
Dim dt1 = New DateTime(2018, 1, 1, 7, 0, 0)
Dim dto1 = CreateDtoA(dt1)
WriteLine($"{dt1} → {dto1} (UTC {dto1.UtcDateTime:HH:mm:ss})")
' 出力:
' 2018/01/01 7:00:00 → 2018/01/01 7:00:00 -08:00 (UTC 15:00:00)
' DSTの例
Dim dt2 = New DateTime(2018, 7, 1, 7, 0, 0)
Dim dto2 = CreateDtoA(dt2)
WriteLine($"{dt2} → {dto2} (UTC {dto2.UtcDateTime:HH:mm:ss})")
' 出力:
' 2018/07/01 7:00:00 → 2018/07/01 7:00:00 -07:00 (UTC 14:00:00)
' DST開始をまたぐ例
Dim dt3 = New DateTime(2018, 3, 11, 1, 30, 0)
Dim dto3 = CreateDtoA(dt3)
WriteLine($"{dt3} → {dto3} (UTC {dto3.UtcDateTime:HH:mm:ss})")
Dim dt4 = New DateTime(2018, 3, 11, 2, 30, 0)
Dim dto4A = CreateDtoA(dt4)
WriteLine($"{dt4} → {dto4A} (UTC {dto4A.UtcDateTime:HH:mm:ss}) [A]")
Dim dto4B = CreateDtoB(dt4)
WriteLine($"{dt4} → {dto4B} (UTC {dto4B.UtcDateTime:HH:mm:ss}) [B]")
Dim dt5 = New DateTime(2018, 3, 11, 3, 30, 0)
Dim dto5 = CreateDtoA(dt5)
WriteLine($"{dt5} → {dto5} (UTC {dto5.UtcDateTime:HH:mm:ss})")
' 出力:
' 2018/03/11 1:30:00 → 2018/03/11 1:30:00 -08:00 (UTC 09:30:00)
' 2018/03/11 2:30:00 → 2018/03/11 2:30:00 -08:00 (UTC 10:30:00) [A]
' ※上の結果は誤り。2:00を過ぎているのだからオフセットは-7時間のはずである
' 2018/03/11 2:30:00 → 2018/03/11 3:30:00 -07:00 (UTC 10:30:00) [B]
' 2018/03/11 3:30:00 → 2018/03/11 3:30:00 -07:00 (UTC 10:30:00)
' DST終了をまたぐ例
Dim dt6 = New DateTime(2018, 11, 4, 0, 30, 0)
Dim dto6 = CreateDtoA(dt6)
WriteLine($"{dt6} → {dto6} (UTC {dto6.UtcDateTime:HH:mm:ss})")
Dim dt7 = New DateTime(2018, 11, 4, 1, 30, 0)
Dim dto7A = CreateDtoA(dt7)
WriteLine($"{dt7} → {dto7A} (UTC {dto7A.UtcDateTime:HH:mm:ss}) [A]")
Dim dto7B = CreateDtoB(dt7)
WriteLine($"{dt7} → {dto7B} (UTC {dto7A.UtcDateTime:HH:mm:ss}) [B]")
Dim dt8 = New DateTime(2018, 11, 4, 2, 30, 0)
Dim dto8 = CreateDtoA(dt8)
WriteLine($"{dt8} → {dto8} (UTC {dto8.UtcDateTime:HH:mm:ss})")
' 出力:
' 2018/11/04 0:30:00 → 2018/11/04 0:30:00 -07:00 (UTC 07:30:00)
' 2018/11/04 1:30:00 → 2018/11/04 1:30:00 -08:00 (UTC 09:30:00) [A]
' 2018/11/04 1:30:00 → 2018/11/04 1:30:00 -08:00 (UTC 09:30:00) [B]
' 2018/11/04 2:30:00 → 2018/11/04 2:30:00 -08:00 (UTC 10:30:00)
TimeZoneInfoオブジェクトのIsDaylightSavingTimeメソッドを使う(次のコード)。
// 標準時の例
var dto1 = new DateTimeOffset(2018, 1, 1, 7, 0, 0, TimeSpan.FromHours(-8.0));
WriteLine($"{dto1}はDTSか?→{TimeZoneInfo.Local.IsDaylightSavingTime(dto1)}");
// 出力:2018/01/01 7:00:00 -08:00はDTSか?→False
// DSTの例
var dto2 = new DateTimeOffset(2018, 7, 1, 7, 0, 0, TimeSpan.FromHours(-7.0));
WriteLine($"{dto2}はDTSか?→{TimeZoneInfo.Local.IsDaylightSavingTime(dto2)}");
// 出力:2018/07/01 7:00:00 -07:00はDTSか?→True
var dto3 = new DateTimeOffset(2018, 7, 1, 0, 0, 0, TimeSpan.Zero);
WriteLine($"{dto3}はDTSか?→{TimeZoneInfo.Local.IsDaylightSavingTime(dto3)}");
// 出力:2018/07/01 0:00:00 +00:00はDTSか?→True
' 標準時の例
Dim dto1 = New DateTimeOffset(2018, 1, 1, 7, 0, 0, TimeSpan.FromHours(-8.0))
WriteLine($"{dto1}はDTSか?→{TimeZoneInfo.Local.IsDaylightSavingTime(dto1)}")
' 出力:2018/01/01 7:00:00 -08:00はDTSか?→False
' DSTの例
Dim dto2 = New DateTimeOffset(2018, 7, 1, 7, 0, 0, TimeSpan.FromHours(-7.0))
WriteLine($"{dto2}はDTSか?→{TimeZoneInfo.Local.IsDaylightSavingTime(dto2)}")
' 出力:2018/07/01 7:00:00 -07:00はDTSか?→True
Dim dto3 = New DateTimeOffset(2018, 7, 1, 0, 0, 0, TimeSpan.Zero)
WriteLine($"{dto3}はDTSか?→{TimeZoneInfo.Local.IsDaylightSavingTime(dto3)}")
' 出力:2018/07/01 0:00:00 +00:00はDTSか?→True
単純にDateTimeOffsetオブジェクト同士の引き算を行えばよい(次のコード)。オフセットの違いは適切に調整してくれる。
// 標準時間同士
var dto1 = new DateTimeOffset(2018, 2, 28, 7, 0, 0, TimeSpan.FromHours(-8.0));
var dto2 = new DateTimeOffset(2018, 3, 1, 7, 0, 0, TimeSpan.FromHours(-8.0));
TimeSpan ts1 = dto2 - dto1;
WriteLine($"({dto2})−({dto1})={ts1.TotalHours:0.0}H");
// 出力:(2018/03/01 7:00:00 -08:00)−(2018/02/28 7:00:00 -08:00)=24.0H
// DST同士
var dto3 = new DateTimeOffset(2018, 7, 31, 7, 0, 0, TimeSpan.FromHours(-7.0));
var dto4 = new DateTimeOffset(2018, 8, 1, 7, 0, 0, TimeSpan.FromHours(-7.0));
TimeSpan ts2 = dto4 - dto3;
WriteLine($"({dto4})−({dto3})={ts2.TotalHours:0.0}H");
// 出力:(2018/08/01 7:00:00 -07:00)−(2018/07/31 7:00:00 -07:00)=24.0H
// DST開始時刻またぎ
var dto5 = new DateTimeOffset(2018, 3, 10, 7, 0, 0, TimeSpan.FromHours(-8.0));
var dto6 = new DateTimeOffset(2018, 3, 11, 7, 0, 0, TimeSpan.FromHours(-7.0));
TimeSpan ts3 = dto6 - dto5;
WriteLine($"({dto6})−({dto5})={ts3.TotalHours:0.0}H");
// 出力:(2018/03/11 7:00:00 -07:00)−(2018/03/10 7:00:00 -08:00)=23.0H
// DST終了時刻またぎ
var dto7 = new DateTimeOffset(2018, 11, 3, 7, 0, 0, TimeSpan.FromHours(-7.0));
var dto8 = new DateTimeOffset(2018, 11, 4, 7, 0, 0, TimeSpan.FromHours(-8.0));
TimeSpan ts4 = dto8 - dto7;
WriteLine($"({dto8})−({dto7})={ts4.TotalHours:0.0}H");
// 出力:(2018/11/04 7:00:00 -08:00)−(2018/11/03 7:00:00 -07:00)=25.0H
' 標準時間同士
Dim dto1 = New DateTimeOffset(2018, 2, 28, 7, 0, 0, TimeSpan.FromHours(-8.0))
Dim dto2 = New DateTimeOffset(2018, 3, 1, 7, 0, 0, TimeSpan.FromHours(-8.0))
Dim ts1 As TimeSpan = dto2 - dto1
WriteLine($"({dto2})−({dto1})={ts1.TotalHours:0.0}H")
' 出力:(2018/03/01 7:00:00 -08:00)−(2018/02/28 7:00:00 -08:00)=24.0H
' DST同士
Dim dto3 = New DateTimeOffset(2018, 7, 31, 7, 0, 0, TimeSpan.FromHours(-7.0))
Dim dto4 = New DateTimeOffset(2018, 8, 1, 7, 0, 0, TimeSpan.FromHours(-7.0))
Dim ts2 As TimeSpan = dto4 - dto3
WriteLine($"({dto4})−({dto3})={ts2.TotalHours:0.0}H")
' 出力:(2018/08/01 7:00:00 -07:00)−(2018/07/31 7:00:00 -07:00)=24.0H
' DST開始時刻またぎ
Dim dto5 = New DateTimeOffset(2018, 3, 10, 7, 0, 0, TimeSpan.FromHours(-8.0))
Dim dto6 = New DateTimeOffset(2018, 3, 11, 7, 0, 0, TimeSpan.FromHours(-7.0))
Dim ts3 As TimeSpan = dto6 - dto5
WriteLine($"({dto6})−({dto5})={ts3.TotalHours:0.0}H")
' 出力:(2018/03/11 7:00:00 -07:00)−(2018/03/10 7:00:00 -08:00)=23.0H
' DST終了時刻またぎ
Dim dto7 = New DateTimeOffset(2018, 11, 3, 7, 0, 0, TimeSpan.FromHours(-7.0))
Dim dto8 = New DateTimeOffset(2018, 11, 4, 7, 0, 0, TimeSpan.FromHours(-8.0))
Dim ts4 As TimeSpan = dto8 - dto7
WriteLine($"({dto8})−({dto7})={ts4.TotalHours:0.0}H")
' 出力:(2018/11/04 7:00:00 -08:00)−(2018/11/03 7:00:00 -07:00)=25.0H
DateTimeOffsetオブジェクトにTimeSpanオブジェクトを加算すれば、正しい時刻が得られる。ただし、加算したことでDSTの開始時刻/終了時刻をまたぐと、時刻としては正しくてもオフセットが正しくなくなってしまう。そこで、「任意の日時を生成するには?」で説明したように、TimeZoneInfoクラスのConvertTimeメソッドでオフセットを補正する必要がある(次のコード)。
// DST開始時刻をまたぐ例
var dto1 = new DateTimeOffset(2018, 3, 10, 7, 0, 0, TimeSpan.FromHours(-8.0));
var dto2 = dto1 + TimeSpan.FromHours(24.0);
WriteLine($"({dto1})+24.0H=({dto2}) ←誤り");
// 出力:(2018/03/10 7:00:00 -08:00)+24.0H=(2018/03/11 7:00:00 -08:00) ←誤り
// 次のようにして常に正しいオフセットになるようにする
var dto3 = TimeZoneInfo.ConvertTime(dto2, TimeZoneInfo.Local);
WriteLine($"({dto1})+24.0H=({dto3}) ←正しい");
// 出力:(2018/03/10 7:00:00 -08:00)+24.0H=(2018/03/11 8:00:00 -07:00) ←正しい
// DST終了時刻をまたぐ例
var dto4 = new DateTimeOffset(2018, 11, 3, 7, 0, 0, TimeSpan.FromHours(-7.0));
var dto5 = TimeZoneInfo.ConvertTime(dto4 + TimeSpan.FromHours(24.0), TimeZoneInfo.Local);
WriteLine($"({dto4})+24.0H=({dto5})");
// 出力:(2018/11/03 7:00:00 -07:00)+24.0H=(2018/11/04 6:00:00 -08:00)
' DST開始時刻をまたぐ例
Dim dto1 = New DateTimeOffset(2018, 3, 10, 7, 0, 0, TimeSpan.FromHours(-8.0))
Dim dto2 = dto1 + TimeSpan.FromHours(24.0)
WriteLine($"({dto1})+24.0H=({dto2}) ←誤り")
' 出力:(2018/03/10 7:00:00 -08:00)+24.0H=(2018/03/11 7:00:00 -08:00) ←誤り
' 次のようにして常に正しいオフセットになるようにする
Dim dto3 = TimeZoneInfo.ConvertTime(dto2, TimeZoneInfo.Local)
WriteLine($"({dto1})+24.0H=({dto3}) ←正しい")
' 出力:(2018/03/10 7:00:00 -08:00)+24.0H=(2018/03/11 8:00:00 -07:00) ←正しい
' DST終了時刻をまたぐ例
Dim dto4 = New DateTimeOffset(2018, 11, 3, 7, 0, 0, TimeSpan.FromHours(-7.0))
Dim dto5 = TimeZoneInfo.ConvertTime(dto4 + TimeSpan.FromHours(24.0), TimeZoneInfo.Local)
WriteLine($"({dto4})+24.0H=({dto5})")
' 出力:(2018/11/03 7:00:00 -07:00)+24.0H=(2018/11/04 6:00:00 -08:00)
サマータイム(DST)を扱う仕組みとして、.NET Framework 3.5からはDateTimeOffset構造体とTimeZoneInfoクラスが提供されている。DSTの開始時刻/終了時刻をまたぐ処理は、時刻が飛んだり巻き戻ったり、あるいは、1日の長さが24時間ではなくなったりするので、業務設計もプログラミングも注意が必要だ。また、本稿では扱わなかったが、終了日に2回現れる同じ時刻を区別してエンドユーザーに指定してもらうには、専用のUIを作る必要があるだろう。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:クラスライブラリ 処理対象:日付と時刻
使用ライブラリ:DateTimeOffset構造体(System名前空間)
使用ライブラリ:TimeZoneInfoクラス(System名前空間)
関連TIPS:タイムゾーンから時差を求めるには?[C#、VB]
関連TIPS:DateTimeとDateTimeOffsetの違いとは?[C#、VB]
関連TIPS:日時や時間間隔の加減算を行うには?
関連TIPS:日付や時刻を文字列に変換するには?
関連TIPS:日付や時刻の文字列をDateTimeオブジェクトに変換するには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.