タイムゾーン対応の罠　意外な難しさとその乗り越え方

## はじめに
プロダクト開発において、複数の国や地域で時間に関する情報が正しく扱えるように、タイムゾーンの情報を用いることが一般的です。これは一見簡単そうですが、実際に手を動かしてみると、複雑さに直面することがあります。
本記事では、タイムゾーン対応における課題の整理と、その解決方法について考えてみます。

## まとめ
長くなってしまったので、最初にまとめです。

タイムゾーン対応とは、
1. 時間情報を正規化する処理
1. 時間情報を指定されたタイムゾーンで表示する処理

とがある

また、時間情報の形式として、大まかに下記の３分類がある
1. 絶対時刻 （例： **`2024年12月04日09時00分00秒`** 形式）
1. 時刻がついているが、絶対時刻ではない時間情報（例： **`毎月15日09:00`** 形式）
1. 時刻情報のない、曖昧な時間情報（例： **`毎月15日`** ）

それぞれのデータごとのタイムゾーン対応は、下記の方針で行う
- 絶対時刻
  - 正規化：ISO8601フォーマットなどを用いる
  - 表示処理：ユーザーに現在の表示がどのタイムゾーンかを正しく認識してもらえるようなUIを作る
- 時刻がついているが、絶対時刻ではない時間情報
  - 正規化：システム内の適切なデータリソースにタイムゾーン情報を保持する
  - 表示処理：ユーザーに現在の表示がどのタイムゾーンかを正しく認識してもらえるようなUIを作る
- 時刻情報のない、曖昧な時間情報
  - 正規化
      - システム内の適切なデータリソースにタイムゾーン情報を保持する
      - 時刻情報を保持するようにデータ構造を変更することを検討する
        - データ構造を変更しない場合、タイムゾーンによる表記揺れが許容できないので、システムのタイムゾーンでのみデータを更新する
  - 表示処理
      - ユーザーに現在の表示がどのタイムゾーンかを正しく認識してもらえるようなUIを作る
      - 正規化でデータ構造を変更しない選択をした場合、システムのタイムゾーンで表示する

時刻情報がないような曖昧な時間情報は、極力なくすのが最適解

です。

では、以下詳細です。

## タイムゾーン対応とは
時間が関係する機能は、非常に身近なものかと思います。友達に予定を共有する機能や、設定した時刻にリマインドメールを送信する機能、その日のスケジュールを表示する機能など、種々様々なアプリで提供されています。
異なるタイムゾーンを持つユーザーがこれらの機能を使うとき、 **ユーザー体験やデータの整合性に問題が発生しないように、タイムゾーンを考慮して時間情報を管理する必要があります。** これがタイムゾーン対応です。

## タイムゾーン対応で直面する課題
タイムゾーン対応にあたって解決する必要のある課題は、大きく下記の２つがあります。
- データの持ち方：時間にまつわるデータをどうやって管理・計算するのか
- 表示方法：時間にまつわるデータをどのタイムゾーンを使ってユーザーに表示するのか

まずは一番簡単な例について、見ていきましょう。

## 絶対時刻のタイムゾーン対応
### 絶対時刻とは
絶対時刻を、暦の上で一意に特定できる時刻、すなわち **`xx年xx月xx日xx時xx分xx秒`** で表すことができる時刻とします。

### データの持ち方
地球には時差があるので、たとえば **`2024/12/04の午前9時`** は、単一の時刻を表しません。日本時間では朝の９時でも、中国ではまだ朝の８時だったりするからです。
これを、 **`2024/12/04の午前9時（日本時間）`** のように表記すると、時刻情報として一意に表現でき、比較するなどの計算が容易になります。
このように、データを統一的な基準で整えて扱いやすくすることを正規化といいます。

正規化については、弊社[@megmogmog1965](https://qiita.com/megmogmog1965)が書いた[記事](https://qiita.com/megmogmog1965/items/907f2a12b21900195610)もありますので、よろしければご参考ください。

#### 絶対時刻情報の正規化
**`2024/12/04の午前9時（日本時間）`** のような絶対時刻情報は、たとえば国際標準のISO8601形式で下記のように正規化できます。
```txt:ISO8601のフォーマット
2024-12-04T09:00:00+09:00（日本時間）
2024-12-04T00:00:00+00:00（協定世界時 UTC）
```

したがって、絶対時刻情報は、この形式でシステムに保持できるように設計されていれば、問題ありません。

### 表示方法
#### 表示方法の取りうる方針
時間に関する情報の表示方法は、下記の２通りが考えられます。
- ユーザーのローカルタイムゾーンで表示する
    - ユーザーの端末のタイムゾーン情報（≒ユーザーがいる場所のタイムゾーン情報）で時間情報を表示する
- システムが指定したタイムゾーンで表示する
    - 何かしらの方法でシステムがタイムゾーン情報を指定し、そのタイムゾーンで時間情報を表示する


#### どちらのタイムゾーンで表示すべきか？
たとえば、 **「任意の時刻にリマインドメールを送信できるプロダクト」** を考えます。
いま、ユーザーが日本で、 **`2024-12-11T09:00:00+09:00にリマインドメールを自分に送信する`** と設定しました。このアプリの設定を見た時、リマインドメールが送信される時刻は、**`2024-12-11 09:00:00`** と **タイムゾーン情報がない状態で表示されているとします。**

このとき、ユーザーが海外出張でハワイ（日本との時差は19時間）に行き、このアプリの設定を見た時、リマインドメールが送信される時刻は、どう書かれているのがわかりやすいでしょうか？
- ユーザーのローカルタイムゾーンで表示
    - **`2024-12-10 14:00:00`** と表示される
- システムが指定したタイムゾーンで表示する（ここでは日本時間とします）
    - **`2024-12-11 09:00:00`** と表示される

結論としては、プロダクトの目的やユーザーのニーズに応じてどちらを採用するかを検討すればよいです。ただし、 **表示されているタイムゾーンがユーザーに認識されている必要があります。**

たとえば、表示されている内容が日本と変わっている場合、「表示時刻が日本いるときと変わっているのは、いま画面に表示されているタイムゾーンがハワイだからだ」と認識している必要があります。
表示されている内容が日本と変わっていない場合も同様です。画面に表示されているタイムゾーンが日本と同じであることを認識できなければなりません。

タイムゾーンに応じた表示に関する具体的な実装についても、[Day.js](https://day.js.org/)のようなユーティリティライブラリがあるので、特に手間もかかりません。

## ISO8601形式で表現できない時間情報について
さて、ここからが本題です。
プロダクトを作っていると、ISO8601形式で直接表現できない時間情報に出会うことがあります。
たとえば、さきほどの「任意の時刻にリマインドメールを送信できるプロダクト」で、月に１回、ユーザーが設定した時間に自動でメールを送信する機能があったとします。

ここで、この機能の仕様を、下記２つのパターンで考えます。
1. 時刻情報までユーザーに設定させる
1. ユーザーには日付まで指定させて、送る時刻はランダムにシステム側が決める

それぞれの仕様に応じて、システムでは下記の情報が保持されているとします。
1. **`毎月15日09:00`** という情報をシステムが保持しているパターン
1. **`毎月15日`** という情報をシステムが保持しているパターン

それぞれについてのタイムゾーン対応を考えます。

## 毎月15日15:00という情報を保持しているパターン
### データの持ち方
#### タイムゾーン情報をシステムのどこかに保持する
システムがメールを送信するには、 **`毎月15日15:00`** というデータから絶対時刻を計算する必要があるため、タイムゾーン情報がないと時刻が割り出せません。
したがって、システムのどこかにタイムゾーン情報を保存しておく必要があります。

タイムゾーン情報を、システム上のどこで保持するべきかは、機能の要件やシステムの特性によって異なります。
たとえば、ユーザーのテーブルにタイムゾーン情報を持たせて、その情報を元に計算するのが妥当な場合もありますし、システムで一意のタイムゾーン情報で十分、というケースも考えられます。
このように、機能の要件に合わせて、どういったデータリソース単位でタイムゾーン情報を持たせるべきか？の検討を行う必要があります。

この対応によって、**`毎月15日09:00（日本時間）`** がデータとして表すことができるようになり、正規化としては問題ありません。ハワイ時間では、 **`毎月14日14:00（ハワイ時間）`** となり、タイムゾーンに合わせて自由に変換も可能です。

### 表示方法
先程の絶対時刻の場合と同様、表示されているデータのタイムゾーンがユーザーに認知されていれば、どちらのタイムゾーンで表示しても問題ありません。

## 毎月15日という情報を保持しているパターン
「毎月15日」にメールを送信するという仕様の場合、 **タイムゾーン対応とは関係なく、システム側が内部仕様として何らかの方法で時刻情報を決めている** はずです。
たとえば、日本時間15:00に送信する、であったり、15日のランダムな時間に送信する、という風にしているかもしれません。
ここでは、日本時間15日の0:00-23:59の間にランダムに送信する、という仕様の場合を想定します。
そして、 **この曖昧さが、タイムゾーン対応を行う段階で問題として顕在化します。**

### データの持ち方
#### タイムゾーン情報をシステムのどこかに保持する
少なくともタイムゾーン情報を持たないと、絶対時刻は割り出せないので、タイムゾーン情報を保持する必要があります。
そうすることで、**`毎月15日（日本時間）`** というデータの形式となります。 **ただし、データの外で、暗黙的に日本時間の0:00-23:59である、というのを仮定してしまっています。**

#### 他のタイムゾーンの表記に変換できない
**`毎月15日（日本時間）`** を、ハワイ時間に変換するにはどうすればよいのでしょうか？
単純に考えると、**`毎月14日（ハワイ時間）`** と表現する形になりそうです。
しかし、システムは、暗黙的に **`日本時間の0:00-23:59`** を仮定してしまっているので、時刻情報を込みで表現すると **`毎月14日5:00~15日4:59（ハワイ時間）`** という意味を持つ、データです。
これを、**`毎月14日（ハワイ時間）`** と表現するのは、ちょっと無理がありそうです。

したがって、このデータは他のタイムゾーンの表記に変換することができません。
ただし、少し不便ですが、**`毎月14日（日本時間）`** のように、日本時間であることを前提とすれば、 **不整合を起こさずにデータの変更自体は可能です**。

したがって、対応方針は、下記の２つが考えられます。
1. システムのタイムゾーン情報を保持するが、データ構造は変更しない。ただし、データ変更は、システムタイムゾーンと同じ場合のみ許容される
1. システムのタイムゾーン情報を保持する＆データ構造を変更して、時刻情報を合わせて保持するように仕様を変更し、他のタイムゾーンの値に変換できるようにする

#### どちらのタイムゾーンで表示すべきか？
２つ目の方針のときは、これまでと同様、どちらのタイムゾーンでも問題ありません。
１つ目の方針のときは、システムのタイムゾーンでないと、表示が難しくなります。 **`毎月14日（ハワイ時間）`** というような表記ができないからです。
なんとか頑張って、表記の変更をしていくという手もなくはなさそうですが、 **ローカルタイムゾーンでの表示が必須の場合は、仕様の変更から検討していくほうが素直でしょう。**

#### なぜこのようなことになるのか？
なぜ **`毎月15日`** のときは、問題が起きたのでしょうか？
直接的な原因は、 **システムが暗黙的に時刻情報を仮定してしまっており、データに含まれていなかったから** です。 
また、人間の認知として「毎月15日」というデータが24時間の幅があるものと捉えるのが難しく **曖昧であるという認識がなかった**、というのも考えられそうです。
こういったデータは、プロダクトが単一のタイムゾーンのみで利用されている間は、何の問題も起きません。 **タイムゾーン対応をする段になって初めて仕様の曖昧さに気がつく** ことになるため、事前に発見するのが難しい構造もあるように思えます。

データの曖昧性が問題の根源なので、曖昧なデータはなるべく持たないように心がける、というのが最善策になろうかと思います。

## 終わりに
いかがだったでしょうか？
思ったより考えることがあるな、と思っていただければ、記事を書いた意味があったなと思います。
タイムゾーン対応に悩む方々の一助となれば幸いです。

ありがとうございました。