0〜9時にrspecが落ちるからおかしいなと思ったら、タイムゾーン以前に型が勝手に変換されていました。
since(◯.days)やago(◯.days)はDate型でもTime型に変換されます。
その上で色々ややこしく勉強になったので、メモを残しておきます。
0〜9時にrspecが落ちる(経緯)
どうやら、バリデーションが正しく評価されていないみたい。
このvalueが、1週間後以降の場合はOK, 6日以内だった場合はNGにしたい
(byebug) value Thu, 28 Apr 2022 (byebug) Date.current.since(6.days) Thu, 28 Apr 2022 00:00:00.000000000 JST +09:00 (byebug) value > Date.current.since(6.days) true
ファ!?? falseになってほしいんだが・・・
3つの盲点(原因)
(盲点1) Date型にsinceを使うとTime型に変換される
6日後の日付がほしかったのだが、
$ Date.current => Fri, 22 Apr 2022 $ Date.current.since(6.days) => Thu, 28 Apr 2022 00:00:00.000000000 JST +09:00
Time型になったのがおわかりいただけただろうか。
Date型に対して
- since(◯.days)
- ago(◯.days)
を使用すると、Time型になるようです。
しかも、時刻は0時0分0秒になっていることがわかります。
※ 最初から時刻(Time.current)で取得していれば、上記の現象は起きない。
$ Time.current.since(6.days)
=> Thu, 28 Apr 2022 07:54:01.082102900 JST +09:00
(ただ、今回は時刻が0時0分0秒になってしまうことは直接関係なかった。)
(盲点2) Date型にタイムゾーンの概念はない??
0~9時だけ落ちるというのは、だいたいタイムゾーンの問題ですよね。ということで、後日調べてみました。
<Date型の場合> $ d = Date.current => Tue, 26 Apr 2022 $ d.zone => undefined method `zone' for Tue, 26 Apr 2022:Date (NoMethodError) $ d.strftime("%Z") => "+00:00" <Time型の場合> $ t = Time.current => Tue, 26 Apr 2022 08:31:28.676181600 JST +09:00 $ t.zone => "JST" $ t.strftime("%Z") => "JST"
・・・???
どうやら、Date型にタイムゾーンなる概念はないようです。
(盲点3) Date型とTime型を比較するときのタイムゾーン
にしても、4月28日 > 4月28日0時0分0秒がtrueなのはおかしくない?
$ d => Tue, 26 Apr 2022 $ t => Tue, 26 Apr 2022 08:31:28.676181600 JST +09:00 $ d > t => true $ d == t => false $ d < t => false
そう、d > t がtrueになってしまう(エラーの再現)。他もfaultなので明らかに d > tです。
ただし、型を揃えるときちんとfalseになります。
$ d > t => true $ d.to_time > t => false $ d > t.to_date => false
そこで、9時を回ったので、比較してみましょう。先程のd, tに加えて9時以降の時刻t_after9を定義します。
$ t_after9 = Time.current => Tue, 26 Apr 2022 09:07:47.713042400 JST +09:00 $ d > t_after9 => false $ d == t_after9 => false $ d < t_after9 => true
先ほどと真逆の結果になりました! d < t_after9です。
つまり、t < d < t_after9なわけです。これに対して、2つの仮説が立ちました。
(仮説1) タイムゾーンが消滅して、全てUTCとして比較されている?
1つ目の仮説は、タイムゾーンが消滅して、全てUTCになったと考えると辻褄が合います。すなわち、タイムゾーンのない日付とタイムゾーン付の時刻を比較する際、
- t: 4月25日(月) 23:31:28 (JSTからUTCに変換された)
- d: 4月26日(火) 00:00:00 (タイムゾーンなし=UTC)
- t_after9: 4月26日(火) 00:07:47(JSTからUTCに変換された)
として比較されたのだとしたら、t < d < t_after9になるのも納得がいきます。
(仮説2) 日付の方が、時刻のタイムゾーンJSTに変換されている?
2つ目の仮説は、タイムゾーンの概念のない日付の方が、タイムゾーンの設定されている時刻のタイムゾーンに合わせて自動補正されている可能性です。つまり、
d: 4月26日(火) 00:00:00 (UTC)→ 4月26日(火) 09:00:00 (JST)
として比較されたのだとしたら、t < d < t_after9になるのも納得がいきます。
型を揃えよう(解決策)
Date型(日付)とTime型(時刻)を比べると、上記のようにおかしなことになるので、そもそもDate型に統一して、型を揃えてから比べましょう。さすればタイムゾーンのややこしい話をせずに済みます。
(解決策1) to_dateまたはDate.parseして型を揃えよう
(byebug) value Thu, 28 Apr 2022 (byebug) Date.current.since(6.days).to_date Thu, 28 Apr 2022 (byebug) value > Date.current.since(6.days).to_date false
OK!
文字列にしてパースしたもの、すなわち
Date.parse(Date.current.since(6.days).to_s)
でも同様の結果が得られます。
(解決策2) days_sinceやnext_dayなどの型が変わらないメソッドを使おう
そもそも、勝手にTime型になってしまうsenceメソッドを使うのをやめて、days_sinceやnext_dayなどの型が変わらないメソッドを使いましょう。(days_sinceは、Active Supportの導入が必要のようです。)
(byebug) Date.current => Fri, 22 Apr 2022 (byebug) 6.days.since # こちらは時刻を返します。 Thu, 28 Apr 2022 07:52:09.041650700 JST +09:00 (byebug) Date.current.since(6.days) # こちらはその日の始まりの時刻を返します。 Thu, 28 Apr 2022 00:00:00.000000000 JST +09:00 (byebug) Date.current.days_since(6) Thu, 28 Apr 2022 (byebug) Date.current.next_day(6) Thu, 28 Apr 2022
どちらでも変わらなそうなので、今回はActive Supportの不要なnext_dayを採用しました。
(byebug) value Thu, 28 Apr 2022 (byebug) Date.current.next_day(6) Thu, 28 Apr 2022 (byebug) value > Date.current.next_day(6) false
OK!見た目もスッキリして可読性も上がりました。
感想
比較するときは型を揃えようという、なんとも基本的なところで詰まってしまいました。これが動的型式言語のデメリットなのだと思います。それが経験できたのはとてもラッキー!
最後まで読んでくださってありがとうございました!
コメント