サマータイムに対応したかった（Windows+C#編）

# はじめに

サマータイムの制度自体の良し悪しはともかくとして、サマータイムの技術面、というかプログラムについて、何を考慮して何を対応する必要があるのかという話をしようかと思います。OSや言語を限定しないと一般論（OSによる、とか言語による、とか）になりそうなので、WindowsとC#に限定した話にしたいと思います。

# Windowsはどの程度夏時間に対応している？

## Windowsにおける夏時間の設定

Windowsの夏時間の設定は、コントロールパネルのタイムゾーンから行うのですが、下絵のように、「自動的に夏時間の調整をする」にチェックすれば、夏時間が設定できます。（下絵はWindows7ですが、Windows10でも見た目は違いますが、同じ設定があります）

![2018-08-22_143233.png](https://qiita-image-store.s3.amazonaws.com/0/208281/e75a0b7f-fa3d-e7f0-ff08-98f1b00d0aff.png)

しかし、夏時間が設定できるのはタイムゾーンによるらしく、日本では夏時間が無いため、設定ができません。（Windows10の場合はグレーアウトされていて、入力できないようになっています）

![2018-08-22_143619.png](https://qiita-image-store.s3.amazonaws.com/0/208281/dc837820-b6cd-5e8d-43c6-c55f4e4862f2.png)

つまり、OSで夏時間を設定して対応完了！　ということはできなさそう、ということです。

## Microsoftは夏時間の対応をしてくれるのか

日本で本当に夏時間が導入されてしまったら、MicrosoftはWindows Updateで、日本のタイムゾーンでも夏時間の設定ができるようしてくれるかもしれません。私個人的な見解で根拠は無いのですが、おそらくMicrosoftは対応すると思っています。ただ問題は、Microsoftは「ちゃんと」夏時間の設定ができるようにしてくれるのか、ということです。

「ちゃんと」というのは、日本で導入しようとしている夏時間は、通常の１時間ずらすのではなく、２時間ずらす、二重夏時間(BDST - British Double Summer Time)だからです。かつて二重夏時間を採用したことがあるイギリスのタイムゾーン：ロンドンを見ても、二重夏時間を設定できるような箇所はありません。（ただし、イギリスが二重夏時間を導入していた時期は、コンピューターがある時代よりずっと前の時代であり、コンピュータが使われるようになった時代では、二重夏時間を導入している国がないからかもしれません）

![2018-08-22_145605.png](https://qiita-image-store.s3.amazonaws.com/0/208281/4d50abf7-2e04-02b8-0f3f-f6811f807ddb.png)

Microsoftは「日本でも夏時間をWindowsに対応しました」と言いつつ、普通の１時間ずらす夏時間しか設定できない、ということが、非常にありそうだということです。

# C#で夏時間を扱う

Windowsが夏時間に対応してくれない場合（あるいは残念な対応しかしてくれない場合）、プログラムでなんとかするしかありません。幸い、C#には（夏時間を表現できる） `TimeZoneInfo` を作ることが出来ます。

## C#で日時はどうプログラムするんだっけ？

`TimeZoneInfo`の前に、まず復習しておきます。C#での日時の扱い方は、入門書や入門サイトで何度も何度も何度も見ていると思いますので、説明は簡単にしておきます。

まず、**`DateTime`はオワコン**なので、存在自体を忘れてください。
`DateTimeOffset`は「日時」と「UTCからの時差」を保持したクラスです。このクラスは夏時間を考慮していません。
夏時間（とUTCとの時差）の情報は`TimeZoneInfo`から取得します。

以上から、日時は、時間を表す`DateTimeOffset`とUTCとの時差を表す`TimeZoneInfo`の２つを使っていくことになります。しかし、通常日時を処理する場合、次のように`TimeZoneInfo`はほとんど意識していない思います。（蛇足ですが、特定の日時の`DateTimeOffset`を生成するとき、最後の引数を `TimeSpan.FromHours(9)` というように、ハードコーディングしていませんよね？）

```csharp
// 現在日時
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`を作る方法もあります。

```csharp
// (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()` メソッドで作ることが出来ます。引数の文字列の部分(４箇所)は表示上の名前です。「日本二重夏時間」の正式名称がないので、勝手に適当に命名しました。第２引数は、標準時間の場合のUTCとの時差なので、ここは `+09:00` を指定します。最後の引数(adjustmentRules)で、夏時間の定義をします。
(2) `TimeZoneInfo.AdjustmentRule` は、`TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule()` メソッドで作ることができます。第１・第２引数は、夏時間が運用される開始と終了年月日を指定します。夏時間の開始日と終了日ではありません。上のコード片では、2018年だけ夏時間が適用される例となります。第３引数は夏時間でずらす時間を指定します。二重夏時間なので、`+2` を指定しました。第４・第５引数は、夏時間の期間を `TimeZoneInfo.TransitionTime` 指定します。
(3) `TimeZoneInfo.TransitionTime` は 夏時間の開始/終了日が固定の場合は `CreateFixedDateRule()`、「第２月曜日から」のように動的な場合は `CreateFloatingDateRule()` メソッドを使います。上のコード片では、8/1 1:00～9/31 1:00という固定日の例です。 第１引数は、適用する**時分秒**を指定します。なぜか`DateTime`を要求していますが、実際に使われるのは時分秒の部分だけです。そのため、上のコード片では、（使われないので）年月日を1/1/1に指定しています。第２・第３引数は夏時間の開始/終了月日を指定します。

(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の１秒後が3:00:01になるのか、0:59:59の１秒後が3:00:00になるのかは分かりませんが）下絵のように1:00=3:00になり、**1:00～3:00が存在しない時間**になります。１時だと思ったら３時だった…　という状況です。この時間帯のことを、Windows界では Invalid Time と呼ぶそうです。

- 日本時間視点
 
![2018-08-30_164130.png](https://qiita-image-store.s3.amazonaws.com/0/208281/880e4e13-da5c-2fe8-7d16-b2abcf804ef9.png)

- UTC視点

![2018-08-30_164209.png](https://qiita-image-store.s3.amazonaws.com/0/208281/d81b2724-e55a-27f6-8c18-cb0b55028480.png)

### 重複する時間

逆に夏時間→通常に切り替えのときは、同じ時間が繰り返されます。例えば 3:00 に切り替えがおきたとすると、下絵のように、1:00～3:00が２回発生します。Windows界では、この時間帯を Ambiguous Time と呼びます。

- 日本時間視点

![2018-08-30_165317.png](https://qiita-image-store.s3.amazonaws.com/0/208281/54b32431-c220-4196-9caa-d54f8f4babe5.png)

- UTC視点

![2018-08-30_165340.png](https://qiita-image-store.s3.amazonaws.com/0/208281/74467e16-09b6-b478-4438-8baa1faef527.png)

### 問題点は？

日時の入力で特定の値を指定された場合、夏時間か通常の時間かの判定を自前でやらなければならない、と書きましたが、それだけではなく、Skipされている時間(Invalid Time)なのか重複する時間なのか(Ambiguous Time)の判定も必要になり、さらにエラー処理も増えるということです。

以下、私がとっさに思いついた問題が起こりそうな箇所を上げてみたいと思います。

## 時間で検索

例えばユーザーが自分のアクセス履歴を見るために、0:30-1:30という時間の範囲指定で検索をしたとしましょう。夏時間→通常の切り替えが 1:00 にある場合、この「1:30」が夏時間中の「1:30(UTC+11:00)」なのか、通常の「1:30(UTC+09:00)」なのか区別が付きません。これに対応するには、画面に「サマータイム？」のチェックボックスを用意するしかないでしょう。つまり、画面の修正と入力項目が１つ増えることになります。

時間入力のエラーチェックも増えます。通常→夏時間の切り替えが 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にするかしかなさそうです。

## 「１時間後」の処理

0:30の１時間後が1:30ではななく3:30の場合があるので、ただ「足す」だけでは問題が起こる場合があります。もし、アプリケーション内部では時間はすべてUTCで扱っているなら、何の問題もありません。文字通り「足せ」ばいいだけです。`DateTimeOffset` は日時とUTCとの時差を持っているため、通常/夏時間の切り替わりは考えなくても大丈夫です。

```csharp
// 現在時刻の１時間後(夏時間考慮込み)
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の時間を２時間進める/遅らせるだけ、という対応はやめたほうがいいでしょう。アプリケーションが乗っているサーバーは、大抵NTPで時刻同期をしているでしょうから…

# 参考文献・サイト

- [日時の問題とC#での扱い方の話](https://www.slideshare.net/atsushikambara/handling-globaldatetime-27589220)

時差の話がメインのようですが、夏時間の話もあります。C#の話に特化しているため、C#のプログラマーなら一読の価値はあります。スライドショーなので30分程度で全部読めますし。

- [Microsoftのドキュメント](https://msdn.microsoft.com/ja-jp/library/system.timezoneinfo(v=vs.110).aspx)

`TimeZoneInfo`の使い方なんて、慣れている人はいないでしょうから、一度は見ておいたほうがいいでしょう。