とりあえずコード書けよ

技術的なことの備忘録。

RubyのTimeの比較は同じ日付・時・分・秒でもfalseが返ることがある

まとめ

  • Timeの比較は1秒以下をみるので、パッと見同じ日付・時・分・秒でもfalseが返ることがある。
  • 同じ日付・時・分・秒をtrueにするには1秒以下をto_iで切り捨てる

本筋

Railsで時刻を比較する機会はなかなか多いと思う。
時刻の比較で、テストを書いていたときに同じケースを比較してtrueが返ってきてほしいところをfalseが返ってくるケースがあった。
最初ActiveSupport::TimeWithZoneと比較してる中で何かあるのかなと思った。

github.com
こういう挙動を調べるのにTraceLocationはとても手っ取り早くてとても助かる。

Generated by trace_location at 2019-11-03 16:48:00 +0900

~/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/time_with_zone.rb:225

ActiveSupport::TimeWithZone#<=>
def <=>(other)
  utc <=> other
end
# called from (pry):10

~/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/time_with_zone.rb:64

ActiveSupport::TimeWithZone#utc
def utc
  @utc ||= period.to_utc(@time)
end
# called from ~/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/time_with_zone.rb:226

~/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/core_ext/time/calculations.rb:292

Time#compare_with_coercion
def compare_with_coercion(other)
  # we're avoiding Time#to_datetime and Time#to_time because they're expensive
  if other.class == Time
    compare_without_coercion(other)
  elsif other.is_a?(Time)
    compare_without_coercion(other.to_time)
  else
    to_datetime <=> other
  end
end
alias_method :compare_without_coercion, :<=>
alias_method :<=>, :compare_with_coercion
# called from ~/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/time_with_zone.rb:226

alias_methodで最終的に<=>メソッドが走るので、要は最終的にActiveSupport::TimeWithZoneであれ、Timeに変換して比較しているだけであって、そこは関係ないと。

で、なのでTimeの比較を調べてみる。

docs.ruby-lang.org

丁寧に同一時刻の比較を書いてくれている。
Timeの比較は1秒以下の値を見てそこの大小を比較していることがわかる。
なので、パッと見同じ日付・時・分・秒でもfalseが返ることがあるということだ。

これをtrueにしたい場合、ミリ秒以下を切り捨てればよいので、to_iとかで比較してあげると良い。