🤨

なぜRailsのall_dayなどが返すRangeは「以上・未満」ではなく「以上・以下」なのか?

2022/07/11に公開

Railsに限らず、期間を指定したいケースはたくさんある。

Rubyでは、期間を示す Range クラスが用意されており
Railsでは、 Range を利用して期間を取り扱いやすいように、その日の期間を示す all_day だったり、その月の期間を示す all_month だったり、標準で用意されている。

今回は、それらが、なぜ「以上・未満」ではなく、「以上・以下」で定義されているのか?
気になったので調べてみた。

個人的に、期間を示す際は「以上・未満」でなければ、 際どいタイミング(23時59分59秒999999..)で登録されたデータが漏れる可能性がある と思っていたのがキッカケ。

結論

RailsがDBの日時の精度を認識した上で、クエリを発行するので「以上・以下」でも、データの漏れは発生しない

本題

その日の期間を示す all_day を見てみると、たしかに「以上・以下」で定義されている
https://github.com/rails/rails/blob/7-0-stable/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb#L299-L302

補足:
(..) double-dot 「以上・以下」
(...) triple-dot 「以上・未満」

実際に、クエリを見てみる。

※以下、MySQLを想定
◆ 精度が0(長さ0で、秒以下の小数点は丸められる)場合
start_at: datetime(0)

# target_at = Time.zone.parse('2022-07-07')
# Hoge.where(start_at: target_at.all_day)
.. `start_at` BETWEEN '2022-07-06 15:00:00' AND '2022-07-07 14:59:59

=> 2022-07-07 14:59:592022-07-07 15:00:00 の間にはデータは入り得ない

◆ 精度が6(小数点以下6桁以下は丸められる)場合
created_at: datetime(6)

# target_at = Time.zone.parse('2022-07-07')
# Hoge.where(created_at: target_at.all_day)
.. `created_at` BETWEEN '2022-07-06 15:00:00' AND '2022-07-07 14:59:59.999999'

=> 2022-07-07 14:59:59.9999992022-07-07 15:00:00.000000 の間にはデータは入り得ない

つまり、RailsがRangeを利用したクエリを発行する際は、その対象となるカラムの精度を考慮した上で、クエリを用意してくれるので、「以上・以下」でも問題ない。

⚠️ とはいえ、人間がクエリを書く際には精度を気にして書きたくないので「以上・未満」で。

余談

DBとは関係なしに、Rails上で表現できる日時の最大精度は小数点以下9桁までのようだ。

https://github.com/rails/rails/blob/4cd5709e2fc8899c24fc1b205d6f5bbdddfc29e1/activesupport/lib/active_support/core_ext/date_time/calculations.rb#L135-L138

9桁を超えて表現しようと思うと ArgumentError が発生する

irb(main):017:0> target_at.change(usec: Rational(999999999, 1000))
=> Thu, 07 Jul 2022 00:00:00.999999999 JST +09:00

irb(main):018:0> target_at.change(usec: Rational(9999999999, 1000))
/usr/local/bundle/gems/activesupport-7.0.3/lib/active_support/core_ext/time/calculations.rb:154:in `change': argument out of range (ArgumentError)

Discussion