「9 時から 18 時」といった時刻の範囲に、ある時刻(例えば 12 時や 21 時)が入っているかどうかという判定をタイムゾーン込みで行う必要に駆られたのですが意外と難しかったので以下のように考えてみました。
時刻 hh:mm:ss+tz を、そのタイムゾーンでの 0 時からの経過秒数と、そのタイムゾーンの UTC からのオフセットの秒数の組 (x, tz) で表すことにします。例を挙げると:
-
UTC の 0 時は
(0, 0) -
UTC の 9 時は
(9*60*60, 0) -
JST の 0 時は
(0, 9*60*60)
簡単のため (hh:mm(:ss), aa:bb(:cc)) という表記を許容し、これは ((hh * 60) + mm) * 60 + ss, (aa * 60 + bb) * 60 + cc) を表すことにします。この表記を使うと上記の例は以下のように書けます:
-
UTC の 0 時は
(00:00, 00:00) -
UTC の 9 時は
(09:00, 00:00) -
JST の 0 時は
(00:00, 09:00)
この表記を使って冒頭の問題を書くと、2 つの時刻 (x1, tz1)・(x3, tz3) が与えられたときに、ある時刻 (t2, tz2) が (x1, tz1) と (x3, tz3) との「間にある」か、つまり (x1, tz1) から時刻を進めていったときにいずれ (x3, tz3) に到達するが、そこまでの間に (x2, tz2) が存在するかどうかを判定したいということになります。
時刻 (t2, tz2) が (x1, tz1) と (x3, tz3) との「間にある」ことを (x1, tz1) <= (x2, tz2) <= (x3, tz3) と書くことにします。ここで、この関係は三項関係で、二項関係を定義しているわけではない((x1, tz1) <= (x2, tz2) を定めていない)ことに注意してください。
例を挙げると以下の「間にある」関係は成り立ちます。
(00:00, 00:00) <= (01:00, 00:00) <= (02:00, 00:00)
(23:00, 00:00) <= (00:00, 00:00) <= (01:00, 00:00)
(23:00, 00:00) <= (09:00, 09:00) <= (01:00, 00:00)
// 含まれないのは 01:00 UTC 〜 03:00 UTC
(03:00, 00:00) <= (00:00, 00:00) <= (01:00, 00:00)
(12:00, 09:00) <= (09:00, 09:00) <= (10:00, 09:00)
一方で以下は成り立ちません。
(03:00, 00:00) <= (02:00, 00:00) <= (01:00, 00:00)
肝心のこの関係の判定方法ですが、時刻を UTC に直したうえで、最左の時刻との差分を 00:00〜23:59 に変換し、通常の意味で大小比較すればよいです[1]。OCaml で書くと以下のような形です。 ptime を使っています。またテストは ppx_inline_test で書いています。
type t = { start : Ptime.time; end_ : Ptime.time } [@@deriving make]
let is_in { start; end_ } time =
let mod86400 a =
(* 左辺が負数でも結果が [0, 86400) に収まる mod *)
let b = 86400 in
let r = a mod b in
if r < 0 then r + b else r
in
let time_to_seconds ((h, m, s), tz) = mod86400 ((((h * 60) + m) * 60) + s - tz) in
let u0, u2 = (time_to_seconds start, time_to_seconds end_) in
let u1 = time_to_seconds time in
let u1_rel, u2_rel = (mod86400 (u1 - u0), mod86400 (u2 - u0)) in
u1_rel <= u2_rel
let%test_module _ =
(module struct
let test ?(not = false) t0 t1 t2 =
let conv (h, tz) = ((h, 0, 0), tz * 3600) in
assert (not <> is_in (make ~start:(conv t0) ~end_:(conv t2)) (conv t1))
let%test_unit "( 0, 0) <= ( 1, 0) <= ( 2, 0)" = test (0, 0) (1, 0) (2, 0)
let%test_unit "(23, 0) <= ( 0, 0) <= ( 1, 0)" = test (23, 0) (0, 0) (1, 0)
let%test_unit "(23, 0) <= ( 9, 9) <= ( 1, 0)" = test (23, 0) (9, 9) (1, 0)
let%test_unit "( 3, 0) <= ( 0, 0) <= ( 1, 0)" = test (3, 0) (0, 0) (1, 0)
let%test_unit "(12, 9) <= ( 9, 9) <= (10, 9)" = test (12, 9) (9, 9) (10, 9)
let%test_unit "( 3, 0) <= (23, 0) <= ( 1, 0)" = test (3, 0) (23, 0) (1, 0)
let%test_unit "( 0, 0) <= ( 0, 0) <= ( 1, 0)" = test (0, 0) (0, 0) (1, 0)
let%test_unit "( 0, 0) <= ( 1, 0) <= ( 1, 0)" = test (0, 0) (1, 9) (1, 9)
let%test_unit "(03, 0) <= ( 2, 0) <= ( 1, 0)" = test ~not:true (3, 0) (2, 0) (1, 0)
end)
注釈
-
白状すると、ここまで書いたうえでさっぱり分からなかったので、定義を LLM に投げつけて教えてもらいました。ちゃんとした証明も無いですがテストが動いているのでまぁいいかなと思っています(最悪)。多分、時刻が の形で書けて、各 と がだんだん大きくなる、かつ「間にある」の定義から は高々 1 しか離れていないと言えるので、そこから差分の判定に繋げられる気はします。 ↩