はじめに
サマータイムの制度自体の良し悪しはともかくとして、サマータイムの技術面、というかプログラムについて、何を考慮して何を対応する必要があるのかという話をしようかと思います。OSや言語を限定しないと一般論(OSによる、とか言語による、とか)になりそうなので、WindowsとC#に限定した話にしたいと思います。
Windowsはどの程度夏時間に対応している?
Windowsにおける夏時間の設定
Windowsの夏時間の設定は、コントロールパネルのタイムゾーンから行うのですが、下絵のように、「自動的に夏時間の調整をする」にチェックすれば、夏時間が設定できます。(下絵はWindows7ですが、Windows10でも見た目は違いますが、同じ設定があります)
しかし、夏時間が設定できるのはタイムゾーンによるらしく、日本では夏時間が無いため、設定ができません。(Windows10の場合はグレーアウトされていて、入力できないようになっています)
つまり、OSで夏時間を設定して対応完了! ということはできなさそう、ということです。
Microsoftは夏時間の対応をしてくれるのか
日本で本当に夏時間が導入されてしまったら、MicrosoftはWindows Updateで、日本のタイムゾーンでも夏時間の設定ができるようしてくれるかもしれません。私個人的な見解で根拠は無いのですが、おそらくMicrosoftは対応すると思っています。ただ問題は、Microsoftは「ちゃんと」夏時間の設定ができるようにしてくれるのか、ということです。
「ちゃんと」というのは、日本で導入しようとしている夏時間は、通常の1時間ずらすのではなく、2時間ずらす、二重夏時間(BDST - British Double Summer Time)だからです。かつて二重夏時間を採用したことがあるイギリスのタイムゾーン:ロンドンを見ても、二重夏時間を設定できるような箇所はありません。(ただし、イギリスが二重夏時間を導入していた時期は、コンピューターがある時代よりずっと前の時代であり、コンピュータが使われるようになった時代では、二重夏時間を導入している国がないからかもしれません)
Microsoftは「日本でも夏時間をWindowsに対応しました」と言いつつ、普通の1時間ずらす夏時間しか設定できない、ということが、非常にありそうだということです。
C#で夏時間を扱う
Windowsが夏時間に対応してくれない場合(あるいは残念な対応しかしてくれない場合)、プログラムでなんとかするしかありません。幸い、C#には(夏時間を表現できる) TimeZoneInfo
を作ることが出来ます。
C#で日時はどうプログラムするんだっけ?
TimeZoneInfo
の前に、まず復習しておきます。C#での日時の扱い方は、入門書や入門サイトで何度も何度も何度も見ていると思いますので、説明は簡単にしておきます。
まず、DateTime
はオワコンなので、存在自体を忘れてください。
DateTimeOffset
は「日時」と「UTCからの時差」を保持したクラスです。このクラスは夏時間を考慮していません。
夏時間(とUTCとの時差)の情報はTimeZoneInfo
から取得します。
以上から、日時は、時間を表すDateTimeOffset
とUTCとの時差を表すTimeZoneInfo
の2つを使っていくことになります。しかし、通常日時を処理する場合、次のようにTimeZoneInfo
はほとんど意識していない思います。(蛇足ですが、特定の日時のDateTimeOffset
を生成するとき、最後の引数を TimeSpan.FromHours(9)
というように、ハードコーディングしていませんよね?)
// 現在日時
var now = DateTimeOffset.Now; // -> 2018/08/31 11:44:04 +09:00
// 特定の日時
var dt = new DateTimeOffset(2018, 1, 2, 3, 4, 5, TimeZoneInfo.Local.BaseUtcOffset);
// -> 2018/01/02 03:04:05 +09:00
現在日時や特定の日時の出力結果を見て分かるように、使われているTimeZoneInfo
はOSのタイムゾーンになっています。つまり、Windowsが日本二重夏時間(仮称)に対応してくれれば、プログラムではほとんど考慮しなくて良くなります。(実際には、ユーザーへの画面入出力とった、日本時間自体が重要な箇所においては、単にTimeZoneInfo
だけ考慮すれば良い、という訳にはいかない)
日本二重夏時間(仮称)TimeZoneInfoを作る
Windows(Microsoft)が日本二重夏時間(仮称)に対応してくれない場合、自分でTimeZoneInfo
を作る方法もあります。
// (3)
var transitionStart = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 1, 0, 0), 8, 1);
var transitionEnd = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 1, 0, 0), 9, 31);
// (2)
var adjustment = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(
new DateTime(2018, 1, 1),
new DateTime(2018, 12, 31),
TimeSpan.FromHours(2),
transitionStart,
transitionEnd);
// (1)
var jddtTimeZone = TimeZoneInfo.CreateCustomTimeZone(
"Japan Double Daylight Time",
TimeSpan.FromHours(9),
"(UTC+09:00) Osaka, Sapporo, Tokyo",
"Japan Standard Time",
"Japan Double Daylight Time",
new TimeZoneInfo.AdjustmentRule[] { adjustment });
// (4-1) 現在日時の場合
var now = DateTimeOffset.Now;
var ddt = TimeZoneInfo.ConvertTime(now, jddtTimeZone); // -> 2018/08/31 13:44:05 +11:00
// (4-2) 特定日時の場合
var dt1 = new DateTimeOffset(2018, 9, 1, 12, 34, 56,
jddtTimeZone.BaseUtcOffset + jddtTimeZone.GetAdjustmentRules()[0].DaylightDelta);
// -> 2018/09/01 12:34:56 +11:00
var dt2 = new DateTime(2018, 9, 1, 12, 34, 56);
//var dt2 = new DateTime(2018, 10, 1, 12, 34, 56);
if (jddtTimeZone.IsDaylightSavingTime(dt2))
{
var dt3 = new DateTimeOffset(dt2,
jddtTimeZone.BaseUtcOffset + jddtTimeZone.GetAdjustmentRules()[0].DaylightDelta);
// -> 2018/09/01 12:34:56 +11:00
} else
{
var dt3 = new DateTimeOffset(dt2, jddtTimeZone.BaseUtcOffset);
// -> 2018/10/01 12:34:56 +09:00
}
手順は、いつものように複雑です。
(1) 自作TimeZoneInfo
は TimeZoneInfo.CreateCustomTimeZone()
メソッドで作ることが出来ます。引数の文字列の部分(4箇所)は表示上の名前です。「日本二重夏時間」の正式名称がないので、勝手に適当に命名しました。第2引数は、標準時間の場合のUTCとの時差なので、ここは +09:00
を指定します。最後の引数(adjustmentRules)で、夏時間の定義をします。
(2) TimeZoneInfo.AdjustmentRule
は、TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule()
メソッドで作ることができます。第1・第2引数は、夏時間が運用される開始と終了年月日を指定します。夏時間の開始日と終了日ではありません。上のコード片では、2018年だけ夏時間が適用される例となります。第3引数は夏時間でずらす時間を指定します。二重夏時間なので、+2
を指定しました。第4・第5引数は、夏時間の期間を TimeZoneInfo.TransitionTime
指定します。
(3) TimeZoneInfo.TransitionTime
は 夏時間の開始/終了日が固定の場合は CreateFixedDateRule()
、「第2月曜日から」のように動的な場合は CreateFloatingDateRule()
メソッドを使います。上のコード片では、8/1 1:00~9/31 1:00という固定日の例です。 第1引数は、適用する時分秒を指定します。なぜかDateTime
を要求していますが、実際に使われるのは時分秒の部分だけです。そのため、上のコード片では、(使われないので)年月日を1/1/1に指定しています。第2・第3引数は夏時間の開始/終了月日を指定します。
(4-1) TimeZoneInfo
を作ったので、現在時刻を取得してみます。DateTimeOffset.Now
では、夏時間の無い日本時間(というか、OSのタイムゾーン)になってしまうので、これを TimeZoneInfo.ConvertTime()
で変換する必要があります。変換した結果は、+11:00
となっているので、二重夏時間が適用されています。
(4-2) 特定の日時を指定する場合、TimeZoneInfo#BaseUtcOffset
は標準時間とUTCとの時差だけの値であるため(TimeZoneInfo.AdjustmentRule
を考慮していない値であるため)、自分で夏時間であるかの判定を行い、TimeZoneInfo.AdjustmentRule
の DaylightDelta
を加えるしかありません。つまり入力の場合、一度DateTime
で時差情報のない日時オブジェクトを作り、夏時間/通常の判定をしたあと、DateTimeOffset
をインスタンス化するとき、適切な TimeSpan
を指定しなければならないため、とても面倒なことになります。ちなみに出力の場合は、TimeZoneInfo.ConvertTime()
で変換すればいいので面倒ではありません。
TimeZoneInfo
だけでは対処できなさそうなこと
先ほどちらっと「ユーザーへの画面入(出)力といった、日本時間自体が重要な箇所においては対処できない」と書きました。
夏時間の切り替えで発生する、存在しない時間と重複する時間
問題点の話の前に、夏時間の切り替え前後で何が起こるのか、の話を書いておきます。
存在しない時間
例えば、1:00に通常→夏時間の切り替えが発生したとすると、(1:00の1秒後が3:00:01になるのか、0:59:59の1秒後が3:00:00になるのかは分かりませんが)下絵のように1:00=3:00になり、1:00~3:00が存在しない時間になります。1時だと思ったら3時だった… という状況です。この時間帯のことを、Windows界では Invalid Time と呼ぶそうです。
- 日本時間視点
- UTC視点
重複する時間
逆に夏時間→通常に切り替えのときは、同じ時間が繰り返されます。例えば 3:00 に切り替えがおきたとすると、下絵のように、1:00~3:00が2回発生します。Windows界では、この時間帯を Ambiguous Time と呼びます。
- 日本時間視点
- UTC視点
問題点は?
日時の入力で特定の値を指定された場合、夏時間か通常の時間かの判定を自前でやらなければならない、と書きましたが、それだけではなく、Skipされている時間(Invalid Time)なのか重複する時間なのか(Ambiguous Time)の判定も必要になり、さらにエラー処理も増えるということです。
以下、私がとっさに思いついた問題が起こりそうな箇所を上げてみたいと思います。
時間で検索
例えばユーザーが自分のアクセス履歴を見るために、0:30-1:30という時間の範囲指定で検索をしたとしましょう。夏時間→通常の切り替えが 1:00 にある場合、この「1:30」が夏時間中の「1:30(UTC+11:00)」なのか、通常の「1:30(UTC+09:00)」なのか区別が付きません。これに対応するには、画面に「サマータイム?」のチェックボックスを用意するしかないでしょう。つまり、画面の修正と入力項目が1つ増えることになります。
時間入力のエラーチェックも増えます。通常→夏時間の切り替えが 1:00 にある場合、1:00=3:00になるので、つまり1:01~2:59は存在しない時間になるので、0:30-1:30という検索条件は、時間不正のエラーにする必要があります。0:00~23:59以外で時間の入力値チェックなんてしていないでしょうから、時間入力のある画面/APIにチェックを増やしていく作業が発生します。
いちおうC#には、日時がサマータイムでスキップされている時間であるか(TimeZoneInfo#IsInvalidTime
)、重複している時間であるか(TimeZoneInfo#IsAmbiguousTime
)を調べるメソッドがあります。しかしスキップされる時間範囲と重複する時間範囲を取得するメソッドはありません(簡単に作れますが)。
データベースの時間
時間を格納している型がUTCか、UTCからの時差も含めて格納しているなら、問題なさそうです。しかし、"20190102030405"にように、日本時間を文字列で入れてしまっている場合、問題です。日付型に変換するか、文字列のままにするがUTCにするかしかなさそうです。
「1時間後」の処理
0:30の1時間後が1:30ではななく3:30の場合があるので、ただ「足す」だけでは問題が起こる場合があります。もし、アプリケーション内部では時間はすべてUTCで扱っているなら、何の問題もありません。文字通り「足せ」ばいいだけです。DateTimeOffset
は日時とUTCとの時差を持っているため、通常/夏時間の切り替わりは考えなくても大丈夫です。
// 現在時刻の1時間後(夏時間考慮込み)
var now1 = DateTimeOffset.Now.AddHours(1);
// 結局出力は(自作した)TimeZoneInfoで変換するので、DateTimeOffsetなら問題ない
jddstTimeZone.ConvertTime(now1);
まとめ
- サマータイムの設定はOS(Windows)に行われるのであり、プログラムやアプリケーションにするものではない。
- 現状(2018年8月時点)では、Windowsに日本のタイムゾーンはサマータイムの設定はない。しかし、Microsoftはサマータイムの設定をできるようにしてくれるかもしれない。
- C#のプログラムでは、
DateTimeOffset
&TimeZoneInfo
を使えば、とりあえずサマータイムの対処はできるようになる。逆にDateTime
を使っていると、酷いことになる。 - Windowsでサマータイムの設定ができるようにならない場合は、自分で「日本二重夏時間」を表す
TimeZoneInfo
を作ることが出来る。 - それでも外部からの入力値がある場合、自分で夏時間/通常の判定をしなければならないし、不正な時間であるかの判定とエラー処理も加える必要がある。
- データベースや時間指定の検索など、日時を使うシステムは、内部ではUTCにしておくのが無難。
システム内部で(サマータイムの無い)UTCで時間を扱うなら、サマータイムなんていらないじゃん
最後に、OSの時間を2時間進める/遅らせるだけ、という対応はやめたほうがいいでしょう。アプリケーションが乗っているサーバーは、大抵NTPで時刻同期をしているでしょうから…
参考文献・サイト
時差の話がメインのようですが、夏時間の話もあります。C#の話に特化しているため、C#のプログラマーなら一読の価値はあります。スライドショーなので30分程度で全部読めますし。
TimeZoneInfo
の使い方なんて、慣れている人はいないでしょうから、一度は見ておいたほうがいいでしょう。