背景
「日時」というのは非常に馴染みがあり無意識に使いこなしていることでしょう。ですがシステムやアプリケーションで扱うと引っかかることがあります。時には頭を悩ませることもあるでしょう。
「日時なんて簡単じゃん。なんで難しく言うかなぁ?」と思うかもしれません。簡単に扱えているなら幸せな事です。私も簡単に扱いたくて「JSR-310とかややこしすぎるだろ。Calender、SimpleDateFormatで業務回せるよな?」と思っていたぐらいです。しかし実際痛い目を見て学び直していくうちに、日時そのものが難しいもので、さらにプログラミング言語上の実装が異なる点やプログラミングや設計の教育であまり触れられないことから、日時の扱いは難しくなってしまっているものだとおもいました。なので、素人なりに日時の考え方でブレないように扱いたいな、と思い書き起こしてみています。自分語りが長くてすみません。
記事を読んでいて「それは正しくない」「それでは業務に支障がある」と思うこともあるでしょう。「こういう時はどうすれば」という話もあるかもしれません。ぜひコメントで書いてください。勉強させてもらいます。
この記事の目的
以下のようになると嬉しいと思って書いてます
- 日時をシステムで自信をもって扱えるようになる
- 日時の型を選ぶ時に自信を持って選べるようになる
- 「とりあえずシステムのタイムゾーンを合わせる」といった付け焼刃な対応をしなくなる
要約
長くなったので...
「絶対時刻」と「浮動時刻」を区別しましょう
- 絶対時刻(Exact Time)とは時差情報があり「時点(瞬間)」を指し示せる時間。(
2022-01-02T12:34:56+09:00
など時差が明示されているもの)- 例えばUTC時間、時差付き日時、タイムゾーン付き日時
- 浮動時刻とは時差情報がない時間。(
2022-01-02T12:34:56
など時差が明示されていないもの)- 名前は定まっていません。wall-clock time, 浮動時刻(floating time), 概念時刻, clock time, local timeなどと呼ばれているようです
プログラミング言語上、データ上では常に「絶対時刻」で扱うことを考えましょう
- 可能な限り「絶対時刻」で扱いましょう
- 「浮動時刻」で連携されたら。なるべくすぐに「絶対時刻」に変換しましょう
- 「浮動時刻」は業務で扱いたい場面はほぼ出てきません。業務で扱いたいのは「絶対時刻」をタイムゾーン変換した日時のはずです
- もちろん浮動時刻で扱うほうが適切な場合もあると思いますが少ないと思っています
フォーマットを決めよう
- データ連携時のフォーマットは必ず決めましょう
- 文字列ならRFC3339かISO8601が良いです。が、結構緩いので、規格の名前を出した上で具体的な
%Y-%M-%DT%h:%m:%s%Z
と書式や例示をしましょう - 数値ならUnix Timeでミリ秒を扱うときは小数点を扱うか単位変換するかちゃんと決めましょう
- 表示の都合で整形したいときは、サーバサイドではなくクライアントサイドに渡して変換させましょう
区間の扱い
-
[開始, 終了)
の形に収めるように統一すると楽かもしれません(異論は認めます) - 表示の都合で整形したほうがいい時もあります
タイムゾーンはなるべく避けましょう
- タイムゾーンは国の気まぐれで変化します。サマータイム、国の都合…
- Timezone Databaseのアップデートが漏れたら問題になるかもしれません
- タイムゾーンの決定はユーザに委ねて、システム上では絶対時刻をタイムゾーン変換するだけにしましょう
- イベントの記録はタイムゾーンを含めて記録するのも手(必要ならタイムゾーンor時差の情報を別で送るようにしましょう)
参考
これらの記事の影響を大きく受けています。
つい先日、エムスリーの方がタイムゾーンに関しする日時の扱い方を書いた記事を分かりやすくまとめられております。
先を越された感がありますが、元々エムスリーさんは多くの日時に関する記事を出しておられます。もはや私の記事の価値なんてないかもしれません
また、次の日付と時刻にまつわる内容を図も交えてこれ1つで紹介してくださったMasaki Hara様の資料も素晴らしいです。業務で扱う場合はP.129の「第3部 コンピューティング」辺りを読むと良いと思います。また日時とは関係ないですが、最近この方が書かれた「状態設計から「なんとなく」を無くそう」も面白かったです。この方のまとめ力はなんなんだ
タイムゾーン周りでは歴史的な背景も含め、dmikurube様の記事が非常に読み応えがあります。歴史的な話も多いですが、一通り読めれば日時を扱う時に違和感とまともな思考を持てる事でしょう。
また、suikawikiの日時周りが異様に充実しており、非常にお世話になりました。結構昔からよく見かけてはお世話になってるwikiサイトなんですが一体何者なんだ…
他にも日時に関する記事は他にもたくさんあります。もっとたくさん読んでいますが、影響を受けた記事をここに記録しておきます。
日時を扱う上での用語の整理
本記事における用語を整理しておきます。私の思い込みの単語になっているかもしれませんので、一旦ここでまとめておきます。ずれているかもしれませんが許してください。
時点
まず「時間軸」があります。時間の流れです。図では左から右へという矢印なっていますが、これは過去から未来へと進んでいっている様子です。
そして時間軸にはある瞬間があります。これは時間軸の線においては点で表現され「時点、瞬間(Instant)」と呼んだりします。また「Timestamp」と呼ばれることもあります。
「日本時間の2023年12月25日12時に待ち合わせ!」という事はあるでしょう。こういったときは「時点」を指しています。多分日本時間であることや時点を普段はそこまで意識していないし厳密に表現することもないとは思います。しかし頭の中で補っているはずです。
間隔
時点と時点の間には「間隔(Period)」があります。時点の差とも言えるでしょう。「今から10分間休憩」といったらこの「10分」は間隔を指します。間隔は後で述べる区間の話で扱います。
広義の時刻・狭義の時刻
Masaki Hara様のスライドのP8,P9を参照ください。
緩く言えば、瞬間を指し示す時刻(広義の時刻)と、時・分・秒を示す時刻(狭義の時刻)の2つあるという感じです。
この記事では両方の意味で「時刻」と括って使っていますが、文脈で分かってもらえると思います。
日時・日付・時刻・時差
混同しがちなので区別しておきましょう。
「時差が+9時間の2022年12月25日12時34分56秒」をISO 8601に準拠した文字列で表現したときの、それぞれの呼び名です。
以下のような形になります。
- 日時(DateTime):
2022-12-25T12:34:56
,2022-12-25T12:34:56+09:00
- 日付(Date):
2022-12-25
- 時刻(Time):
12:34:56
, (12:34:56+09:00
と時差を含めて扱うことも) - 時差(Offset)
+09:00
なお、ISO 8601ではタイムゾーン(時間帯, 時刻帯, TimeZone)の扱いも規定されています。タイムゾーンに関しては後でちょっと触れます。
時差
日本は標準時より9時間進んでいるため時差は9時間です。
時差について調べると色々出てきますが、時計を作っている会社さんの記事も面白いです。
まず人の生活は太陽の位置を利用していました。日が昇り始めたら起きて活動し、日が沈んだら眠るように、朝、昼、晩と区別してきました。
しかし地球の自転により日が昇る時間は地域によって様々です。日本の朝7時の時点ではロンドンではまだ真っ暗でしょうから朝7時とは言えないでしょう。
そのため地球の経度を使って時差を決めました。経度0度は標準時とされました。円は360度ですから、これを24時間で割ると1時間あたり15度のズレとなります。なお、180度で区切られており、西半球、東半球となっています。日本は東半休に位置し東経135度なので135/15=9時間となります。西経なら時差はマイナスとなります。
時差(Offset)とタイムゾーン(時間帯, 時刻帯, TimeZone)
まず時差とタイムゾーンは別です。
タイムゾーンは地域によって異なる時差を表現します。国は15度単位で分割されていませんから、15度単位で1時間も変わってしまうのは色々都合が悪いのです。そのため国の都合で時差が違います。日本においては東経135度の地点を基準に「日本時間」と定められました。このおかげで北海道に居ても沖縄に居ても経度がズレていても時差は同じ「+9時間」と扱えます。
時差は一度決まれば不変です。+9時間がある日突然+10時間の意味になることはありません。
しかしタイムゾーンは不変ではありません。同じタイムゾーンでも時期によって時差が変わるときもあります。これは公転で日照時間は変わるのだから夏時間(サマータイム)を導入しようといった場合です。日本も2020年オリンピックに向けて夏時間を検討して中止したことがありましたね。また過去に日本にも夏時間はありました。それに国の都合で変わる場合があります。ベネズエラは夕方の電力削減を理由に30分進めたりしました。
なのでタイムゾーンの情報は固定ではありません。そのためタイムゾーンのデータベースがあり保守されています。つまりこのデータベースを更新しないと、古いタイムゾーンを参照することになります。これは時差で扱う時もタイムゾーンで扱う時も同様の問題が起きます。
UTC(協定世界時)
UTCはタイムゾーンです。雑に言えば時差は全てこのUTCからの時差となります。
なお、UTCでは1日=86400秒(TAIと呼ばれる時刻系)で扱いつつも、地球の自転に合わせた時刻系であるUT1(1日≒86400秒)に近づけるため、うるう秒が考慮されています。
Unix Time
Unix時間、Posix Timeとも呼ばれます。
これはUTC時間の1970年1月1日 0時0分0秒を起点(epoch)として経過秒を扱います。この起点をUnix epochと呼びます。
ミリ秒を扱えるようにしたり拡張されたりもしています。
注意点として、Unix Timeではうるう秒を扱わないといった割り切りがされています。ただUnix Time自体は日時を表現しないので問題はないはずです。例えばUnix TimeをUTCに変換するときはうるう秒分の加算が発生します。
「絶対時刻」と「浮動時刻」
ここで言いたいのは、時差情報がある日時を「絶対時刻」、そうでない日時を「浮動時刻」と呼び分けて、なるべく「絶対時刻」で扱っていきましょう、ということです。
「絶対時刻」と「浮動時刻」
浮動時刻というのは若干造語です。というのも、なぜか決まった日本語はもちろん、英語もないからです(要出典)
ですが、これは日時を扱う上で重要だと私は思いました。と言っても難しいことはありません
- 絶対時刻(Absolute time): タイムゾーンがある日時 or 時差がある日時 (時差があるので時点が決まる)
- 浮動時刻(Floating time): タイムゾーンがない日時 or 時差がない日時 (時差がわからないので時点が決められない)
絶対時刻(Absolute time)は時間の分野では一般的な用語です。深堀りできるほど時間について理解していないので、本記事ではそこまで触れませんが、とにかく時差情報がある日時なら絶対時刻と言えます。UTC時間も絶対時刻です。
浮動時刻(Floating time)はW3CのWikiに「Floating time」とかかれておりそこから流用して機械翻訳しました。
他にはTC39(ECMAScriptを策定しているコミュニティみたいなとこ)では「Exact time」と「Wall-clock time(またはlocal timeやclock time)」と呼んでいます。
なお、日本語で扱ったのは過去に「絶対時刻」と「概念時刻」と呼び分けていた三浦様かと思われます。ここから私の絶対時刻の旅が始まったと言っても過言ではありません。なお、最近の記事では「絶対時刻」と「不定時刻」と呼びわけています。決まってなさそう…
まだ一定の呼び方はまだなさそうですが、とにかくそんな感じです。
「絶対時刻」と「浮動時刻」の補足
絶対時刻と浮動時刻についてはなじみがないと思うので、具体例をあげながらもう少し補足しましょう。
例えば「日本における7時」と「ロンドンにおける7時」を考えてみましょう。
浮動時刻は、ただ壁掛け時計が刻んでる時間をみるような感じです。時計の針が「7時」を指していれば「7時」と解釈できます。しかしこの「7時」を日本時間で解釈するのか、ロンドン時間で解釈するのかで、時点の位置は変わってしまいます。
絶対時刻は、常に時間軸上のどこか1点で考えます。日本時間の16時とロンドンの7時は同じ時点を指し示すはずです。これらを同じ時点であると計算するためには必ず時差の情報が必要となります。その地域における浮動時刻とその地域の時差から絶対時刻を表すことができます。
絶対時刻から浮動時刻に変換するには時差を無視するだけで変換できますが、一度変換すると元の時差の情報がないと戻せません。
JavaのDate and Time APIではどう対応しているか補足してみます。
絶対時刻はタイムゾーンが有る時間です。文字列で示すと"2023-04-01T12:34:56+09:00"のように日時に加え、時差orタイムゾーンがついてきます。JavaではそれぞれOffsetDateTime,ZonedDateTimeが該当します。
浮動時刻はタイムゾーンが無い時間です。文字列で示すと"2023-04-01T12:34:56"のように日時だけで示します。JavaではLocalDateTimeが該当します。なお、値自体はシステムのタイムゾーン(PCの設定)を基に取得でき、時差情報を捨てているような形です。
浮動時刻は暗黙のタイムゾーンや時差を補完することで初めて絶対時刻になり意味を成すのです。日本で生活しているなら日本時間で考えているので時差情報がない日時を示されても問題なく過ごすことができています。たまたまみんな同じ日本時間で考えているから表面化しないだけです(LocalDateTimeのLocalもそういうニュアンスと言えるでしょう)
浮動時刻で扱いたいときだってあるかもしれない
「浮動時刻」を扱うケースもあります。例えばボジョレーヌーボーの解禁日時は分かりやすい例で「毎年11月の第3木曜日の午前0時」と決まっています。これにはなんとタイムゾーンや時差がありません。そのため日付変更線の都合もあり日本は比較的早く解禁日時を迎えることになります。このように地域によって解禁日時の「時点」が変わるケースがあります。こういった場合は浮動時刻で扱うべきでしょう。
ただ、結局は絶対時刻になります。実際にその時を迎えたかどうかを評価するときはタイムゾーンを考慮して「絶対時刻」に変換して扱うことになるからです。
あとは「言うほど浮動時刻で扱いたいケースってあるか?」という話です。私が思いついたのは「アラーム設定」や「スケジュール設定」ぐらいでした。スケジュール設定もプライベートなものは浮動時刻でいいけど、グローバルなものは絶対時刻じゃないとズレちゃうよね、と考えることはありそうです。私の視野が狭すぎるだけで色々あるのかもしれませんが、あったらそのように扱えばよいでしょう。
プログラミング言語上、データ上では「絶対時刻」で扱う
絶対時刻で扱いましょう
プログラミング言語でどう扱えばよいでしょうか。それは用意している日時型の仕様を確認しましょう。残念ながら言語毎に違うのが実情です。とはいえたいていは「絶対時刻」で扱う術が提供されています。それに則るのが良いでしょう。
言語には絶対時刻で扱えるライブラリが整っています。「時差情報を持ちまわる日時型」もしくは「UTC時間を扱う日時型」であるかどうかを調べてみてください。
JavaのJSR-310のDate and Time APIにおいては驚きの充実ぶりです。絶対時刻と浮動時刻は以下のように分類できます(DateTimeのみ抜粋)
- 絶対時刻
- OffsetDateTime(時差付き日時)
- ZonedDateTime (時間帯付き日時)
- 浮動時刻
浮動時刻のことを知っていれば「LocalDateTimeはなぜ変換にタイムゾーンやオフセットが要求されるのかというと、時点を指し示すためにはどれだけ時差があるかを補う必要があるから」ということが分かるかと思います。
他の言語でも絶対時刻が扱えますが、実装はまちまちです。
- C++: C++11からstd::chronoというかっこいい名前の標準ライブラリが加わってた時の
system_clock
が概ねの処理系でUTC時間を扱うようになっていました。明確にUTC時間を使う実装がC++20で入りました - C#: DateTimeOffsetで絶対時刻を表すことができます。前身にはDateTimeがありました。DateTimeにはKindという日時に3つの区分を持たせており、このうちUnspecifiedの場合だけ浮動時刻でそれ以外は絶対時刻です。ですが、DateTimeはKindの違いを区別しないため異なるKindを持つDateTimeの比較が紛れると解決が難しいバグになります。なのでまずはDateTimeOffsetを使うことをお勧めします。またJavaのDate and Time APIの元のJodaTimeを参考に実装されたNodaTimeを使ってもいいでしょう
- JavaScript: Dateクラスで扱えば絶対時刻(システム時間のタイムゾーン)で扱えるようになっています。機能は足りないですがフロントエンドに使う分には充分です。しかしフォーマット周りが貧弱すぎるのでしばしばライブラリを用いることになります
- Python: datetimeに絶対時刻をaware、浮動時刻をnativeと区別しています。(See: Aware オブジェクトと Naive オブジェクト)
言語とはちょっと違いますが、データストア上で日時を扱えるものがあり、その振る舞いを注目するのも大事です。
- PostgreSQLでは 浮動時刻の
timestamp without time zone
(timestamp
)と 絶対時刻のtimestamp with time zone
(timestamptz
)があります。後者のtimestamp with time zone
はUTC時間で持っているだけでタイムゾーン情報は持たないというのは有名で、名を体を示してないということで良い感情をお持ちでない方もいるかもしれません。しかし、絶対時刻で扱えているのであればタイムゾーン情報はさほど重要ではありません。変換すればよいだけです。一方、浮動時刻のtimestamp without time zone
は与えられた通りの日時を解釈しますが、どのタイムゾーンで扱われた日時かわかりません。さらに文字列をtimestamp without time zone
へ変換する場合、時差情報は無視します(エラーではない!)。なお、日時演算はシステムのタイムゾーン設定、セッションのタイムゾーン設定に影響を受けます。注意しましょう
と言った感じで、色々あります。
文字列表現、数値表現、システム間連携のフォーマットに関しては後述します。
データ連携で紛れる浮動時刻は絶対時刻に変換しましょう
しばしばレガシーなシステムでは2022/11/02 12:34:56
といった独自フォーマットで連携しようとしてくるところがあります。
言語によってどのようにパースされるかは様々です。システムのタイムゾーンで補って絶対時刻にすることもあれば、そのまま浮動時刻として扱うこともあります。
とにかく、絶対時刻で扱う中に浮動時刻が紛れるのはバグの元なので、入力のタイミング(例えばJSONをパースして日時型にマッピングするタイミング)で変換するように努めましょう。
なお、JavaのDate and Time APIのように絶対時刻と浮動時刻を明確に型で区別されている仕組みを使うと、この紛れはコンパイルエラーになるためしょうもないバグを減らせます。
特定のタイムゾーンを前提に浮動時刻で扱うのはやめましょう
よく日本時間を前提に日時を扱うという設計をするところを見かけました。これは守れれば実際うまく機能します。
が、クラウドプラットフォームが台頭した今の時代、もう古い考え方です。
AWSもGCPもAzureも提供されるVMのデフォルトのタイムゾーンはUTCですし、Amazon RDSと言ったデータストアも同様です。
Azure FunctionではTimerTriggerをCRON式で書けますが、UTC時間で動いているのでUTC時間の頭にして書かねばなりません。…これはちょっと違いますが、なんにしてもタイムゾーンを意識することは必ず出てくる時代です。
「適宜、適切にタイムゾーンを変更すればよいのではないか?」と思うかもしれません。それは構いませんが、うっかりミスを引き起こすと辛いことが待っています。しかもこういうのは即時気づけないので後で辛いです。ある程度動いてしまい翌日の朝に異変に気づきます。
また、UTC時間であることを前提で設計するという方法もあります。これも守れれば実際うまく機能します。
が、開発環境はUTC時間にできるでしょうか。言語でサポートしてくれているものもあるかもしれません。例えばLinux上の各言語ランタイムはTZ=UTC
とするとたいていはシステムのタイムゾーンを切り替えて動いてくれます。しかしWindows上でUTC時間で扱うことは私の知る限り簡単な方法はあまりありません。一度開発環境をWSLやVMにもっていくしかないでしょう。また運用時にタイムゾーンが9時間ズレたログを読まされたりいいことはありません。(最近はクラウドへ送り、表示時にタイムゾーンに合わせて変換するので、気にならないと思いますが。)
また、浮動時刻では日時の取り扱いで問題になる場合があります。日時をISO 8601っぽい文字列に変換してくれる関数は存在しますが、時差情報がわからないのでつけられないこともあります。想定通り変換されるでしょうか。
絶対時刻にすればたくさんある細かい悩みがかなり解決するはずです。
フォーマットを決めよう
クライアントやサービス間連携に使うフォーマットを決めましょう。
文字列なら RFC3339やISO8601に準拠した%Y-%M-%DT%h:%m:%s%Z
で絶対時刻で扱う
要は %Y-%M-%DT%h:%m:%s%Z
という感じの書式で時差付きの絶対時刻で扱いましょう。
- 各言語やライブラリのサポートが受けられやすい
- 9999年までと時差情報を統一すると文字列ソートが効く(データストア上は適切な型があるので文字列ではないほうがよいです)
この書式で扱うと言語やライブラリからのサポートが得られやすいです。プログラミング言語についてくる日時ライブラリでそのままパースできたり、SQLも大抵は文字列から日時型への変換をサポートしてくれています。
日時のフォーマットの規格としてISO8601や、それを取り入れたRFC3339があります。上記の書式は準拠しています。ではなぜ「ISO8601かRFC3339で」とは言わないのか。次を見てください。
一言で言ってもこれらの規格の書式は様々です。このようにRFC3339も区切り文字に自由度があったり時刻と日付の表記を含みますし、ISO8601はさらに間隔の表記なども含むため、日時の表記はこの規格で!というにはちょっと弱いです。ひねくれものがニヤニヤしてツッこんでくることでしょう。
とはいえ独自フォーマットと受け取られると、それはそれで面倒なので、「日時フォーマットはISO8601に準拠した形とします。例: 2022-01-02T03:45:06+00:00」とでも明示しておきましょう。まともな受け手はこれで理解できるはずです。
ここでも絶対時刻である点は重要です。絶対時刻で扱わないと時点を特定できず変換に困ってしまいます。別途タイムゾーンで分かるはずという場合でも時差情報は付けて「絶対時刻」で連携しましょう。
数値なら UNIX Timeをベースに必要な精度に応じて
おなじみのUNIX Timeです。エポック秒とも呼ばれますね。1970/01/01 00:00:00(UTC時間)からの経過秒を元にしますので絶対時刻です。うるう秒は考慮しませんが年月日で扱わないので気にする必要はありません。
よく小数点で1秒未満を表現したり、単位を秒からミリ秒に拡張することもあります。UNIX Timeには準拠しなくなりますが、ミリ秒ぐらいは欲しいので、ケースバイケースで選びましょう。
また文字列と比べて数値型は少ないデータサイズで表現できるのも特徴の一つです。データを大量にやり取りする場合はあえてUNIX Timeにして転送量の削減に努めても良いでしょう。
なお、ビット長と精度に起因する問題として有名なものに2038年問題というのがあります。これはエポック秒を符号付32bitで経過秒を扱っている場合は2038年1月19日3時14分7秒(UTC時間)を過ぎると算術オーバーフローを起こし様々な問題が起きるとされています。精度が高くても同様で符号付64bitで経過ミリ秒を扱っている場合(JavaのSystem.currentTimeMillis()
など)2億9227万8994年8月17日07時12分55秒807ミリ秒(UTC時間)を過ぎると同様の問題が起きるとされています。
とはいえ、符号付64bitで経過ミリ秒でもしばらくは充分かもしれません。64bitの先は未来の若者に任せましょう。
ユーザに見せる時はユーザに合わせて変換しよう
私は %Y-%M-%DT%h:%m:%s%Z
を日本時間で表示されているとそれだけで十分嬉しいのですが、普通のユーザは違うでしょう。
2022/11/01 12:34:56
という形式が良い人もいるでしょうし、現在日時からの差で1時間前
と出してほしい場合もあるでしょう。令和4年という和暦にしてほしいかもしれません。それにユーザは日本だけとは限りません。世界各国のユーザに最適な日時表現もあるでしょう。ユーザが人とは限りません。RSSの規格に合わせて出力しなければならないこともあるでしょう。
とはいえ、これらは表示の都合であるといえます。表示の都合に応えるには、変換で対応しましょう。変換元のデータが絶対時刻になっていればどうとでもなります。
表示の都合にシステムのデータ構造を巻き込む必要はありません。バックエンドは上記のフォーマットを採用した上で送るだけで良いはずです。ブラウザならブラウザがシステムのタイムゾーンに基づいて変換してくれますし、各自タイムゾーンに基づいて変換と言ったこともJavaScriptなら可能です。サーバサイドレンダリングは経験がないですが、ユーザからタイムゾーンの情報をもらっておいて、変換してやるぐらいがいいかもしれません。
なお、表示形式には他にも様々な形式はあります。suikawikiさんの日時形式が詳しいです。
その他(バイナリフォーマットなど)
MessagePackやProtocol bufferのように独自型の日時型に変換してシリアライズしてくれる仕組みもあります。
どちらも絶対時刻で扱ってくれますから紛れはありません。こういったのはデータサイズ的にも最適化されていますから乗っかっておきましょう。
区間の扱い
要約: 「開始と終了があること」と「それぞれ同値を含む(開)か含まない(閉)の組み合わせがあること」の2点を抑えておけばよいでしょう。
区間とは
時間を扱う時に区間で扱うことがあります。区間も時刻(毎日続く)、日付、日時のパターンがあるでしょう。
- 営業時間: 10時~22時
- セール期間: 12/24~12/25まで
- イベント期間: 2022/12/24 00:00~2022/12:25 23:59
さて、このように「開始」と「終了」の区間を表現することはよくあります。もちろん中間の値は「含まれる」と考えるでしょう。しかし、この時の開始や終了は厳密にどこまでを含めるべきor含めないべきでしょうか?
例えば「10:00~22:00」という表記を見て、現在が22:00だったとき、どう感じるでしょうか?
これには正確な答えはありません。どう扱うかは問題を見て考える必要があります。
数学における区間
この区間は数学における区間と捉えることができます。本来、集合の話における区間の話なんですが、時間軸も2つの点a,bが定まるとその間の無数の時点の集合として扱えますから適用できます(a,bは集合じゃないじゃん、って言われると何も言えないですけど)
とはいえ、表記としては分かれば完結で分かりやすいです。表記に悩んだらこれが良いと思います。
では、aとbをそれぞれある日時,日付,時刻として扱った時、同値はどうなるのか、というと、以下のようになります。また馴染み深いコードでx=aとx=bの場合に含まれるかどうかを判定式も書いてみました。
表記 | 読み方 | 時点xが区間に含まれる判定式 | x=a | x=b |
---|---|---|---|---|
[a, b] |
左開き右開き区間 | a <= x && x <= b |
含む | 含む |
[a, b) |
左開き右閉じ区間 | a <= x && x < b |
含む | 含まない |
(a, b] |
左閉じ右開き区間 | a < x && x <= b |
含まない | 含む |
(a, b) |
左閉じ右閉じ区間 | a < x && x < b |
含まない | 含まない |
読み方もう少しどうにかならんか?と言う感じですが、区間の考え方的に他の言い回しはなさそうです。頑張りましょう。
実際に当てはめて考えてみると分かる
実際にこんな感じで業務の用語を実際に出しながら当てはめるとすんなりまとまるはずです。
- 営業時間の「10:00~22:00まで」といった場合は「10:00:00」は含み、「22:00:00」は 含まない のが普通です。
[a, b)
として扱うとよいでしょう - セール期間の「12/24~12/25まで」といった場合は「12/24」は含み、「12/25」は 含む のが普通です。
[a, b]
として扱うとよいでしょう
ただ、セール期間のほうは判定処理を実装すると、この区間の表現を反する場合があります。
- 時分秒を扱わない形に変換し、
["2022-12-24", "2022-12-25"]
で判定 -
["2022-12-24T00:00:00+09:00", "2022-12-25T00:00:00+09:00" + 1日)
にして判定
どっちもそれっぽいですね。
しかし見てください。後者の方にすると、 [a, b)
という形になり、営業時間と同じような判定式に落とし込めます。しかしここは好みがあるかもしれません。「終日開催なら+1日足す」みたいな設定のルールが増えるのは微妙かもしれませんが、実装は一貫しやすくなるでしょう。
ともあれ、具体例を挙げて曖昧にせず明文化してみるのが良いです。その中でパターンを見つけて一貫性を通していく形になるかと思います。
アンチパターン: 精度の限界を攻める
「2022/12/25に終了だから2022-12-25T23:59:59+09:00
でデータを持たせよう」という設計はおススメしません
- 秒未満の精度の話を出された時に面倒です(じゃあミリ秒、マイクロ秒までを9で埋めれば?という話になる)
- 記入の難易度が上がります
- 文字列だとデータサイズも増えます
ここにも表記の都合があります
あるゲームのイベント開催期間で「2022/12/24 00:00~2022/12:25 23:59」という表記を見たことがあります。これをみて「2022/12:25 23:59:00」「2022/12:25 23:59:01」「2022/12:25 23:59:59.999999...」「2022/12:26 00:00:00」はどうなるんだろう?と思うことでしょう。
これを試した時、実際は「2022/12:25 23:59:5x.xxx」は含み、「2022/12:26 00:00:0x.xxx」は含まないという形の挙動になっていました(通信の関係上正確ではないが…)
これはユーザフレンドリーな設計と言えるかもしれません。
- 表記上、12/26という単語がないので誤認しにくい
- 12/25 23:59という表記から終日行うと想定できる
ではこれをデータ上はどう持っているかは恐らく、 2022-12-24T00:00:00+09:00
~2022-12-26T00:00:00+09:00
でしょう。つまり、判定は [a, b)
で行うということです。表記のときだけ終了日時を1分減算するということです。
なので「表記上こうしたいから、データの持たせ方も合わせて」という方向ではなく、「表記の都合だけなら表記するときに-1分してもよい」という方向で調整するのもアリだ、という話ですね。
もちろんケースバイケースで実際に合わせたほうが尤もらしい場合もあるかもしれません(私は思いつかないですが)
タイムゾーンを避けましょう
要件で出てきたらなるべく避けてください。それが幸せへの第一歩です…とはいえ、扱う場面もあります。
ユーザのタイムゾーンに合わせた業務はユーザにタイムゾーンを決めさせよう
朝にマーケティングでメールを送り付けたい場合などはユーザのタイムゾーンに合わせると良いでしょう。しかしサーバサイドではユーザのタイムゾーンは分かりません。
グローバルなサービスだと登録時やプロフィール画面でタイムゾーン設定を行えます。ユーザに決めさせるのが良いという考え方です。
イベントを記録するときは別でタイムゾーンをもって絶対時刻で
「Aさんが何時何分にこの商品を買った」といったようなイベントです。
イベントはしばしば「タイムスタンプ」で記録すると言われますが、要は絶対時刻で扱いましょうということです。同様に永続化時も絶対時刻を維持しましょう。
しかし分析として、どの時間帯(朝、昼、夜)に売れたかが重要な場合があります。その場合はイベントを記録する際にユーザのタイムゾーンか時差を記録するようにし、タイムゾーンか時差を別のカラムに追加して、分析時にどの時間帯か分類できるように記録しておきましょう。
RDBMSによってはタイムゾーンや時差の情報を持てる型も用意されています(例えばSQL Server)…が、あまり詳しくないのでよくわからないです…
書けてないこと
以下はアドベントカレンダーの投稿を間に合わせるために書けませんでした…
- 各言語の日時の扱い、ライブラリでの扱い
- データストア(RDBMS)での扱い、ORMapperライブラリでの扱い
- ツールでの扱い(主にEmbulkなどのETLツール)
- 業務の考慮(営業日, 月末, 締め日, 祝日カレンダー, 和暦, 集計業務…)
後半はちょっと適当な感じになってしまった。もっと良い形に書き直したい…