こんにちは、hachi8833です。
先週のTechRacho記事「ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法 」の続きです。
先週の記事 のレビュー中に、弊社CTOのbaba さんからチェックが飛んできました。
Range#exclude_end? は、RailsではなくRubyのコア機能です。
また、Webチーム筆頭エンジニア morimorihoge さんから「ここを見るといいよ」と、Active Record Rangeオブジェクトで文字どおり範囲を扱っているRangeHandlerクラス のパスを投げてもらいました。
Range#exclude_end?
まずはRange#exclude_end?
の公式ドキュメントを参照してみます。範囲の終端値を含む場合にtrue
を返します。
Returns true if the range excludes its end value.
Rubyのドキュメントはつい日本語版 を参照していましたが、英語版はコードサンプルに加えて、クリックするとソースも表示されて具合がいいですね。
以下はドキュメントのサンプルそのままですが、こんな感じで使われます。
(1..5).exclude_end? #=> false
(1...5).exclude_end? #=> true
RangeHandler::call
続いてActiveRecordのRangeHandlerクラスのcall
メソッドを見てみます(ActiveRecord v5.0.0.1)。
module ActiveRecord
class PredicateBuilder
class RangeHandler # :nodoc:
RangeWithBinds = Struct.new(:begin, :end, :exclude_end?)
def initialize(predicate_builder)
@predicate_builder = predicate_builder
end
def call(attribute, value)
if value.begin.respond_to?(:infinite?) && value.begin.infinite?
if value.end.respond_to?(:infinite?) && value.end.infinite?
attribute.not_in([])
elsif value.exclude_end?
attribute.lt(value.end)
else
attribute.lteq(value.end)
end
elsif value.end.respond_to?(:infinite?) && value.end.infinite?
attribute.gteq(value.begin)
elsif value.exclude_end?
attribute.gteq(value.begin).and(attribute.lt(value.end))
else
attribute.between(value)
end
end
protected
attr_reader :predicate_builder
end
end
end
範囲に関するクエリをここで処理しています。
call
メソッドでこのexclude_end
を使って、以下のように終端値の有無を判定しています。先週の記事 の..
と...
の挙動と一致しています。
elsif value.exclude_end?
attribute.lt(value.end)
else
attribute.lteq(value.end)
end
elsif value.exclude_end?
attribute.gteq(value.begin).and(attribute.lt(value.end))
else
attribute.between(value)
end
なお、attribute.lteq
などのメソッドはrails/arel (v6.0.3)由来です。
def lt right
Nodes::LessThan.new self, quoted_node(right)
end
#(省略)
def lteq right
Nodes::LessThanOrEqual.new self, quoted_node(right)
end
infinite?
というメソッドも気になりますね。
範囲関連メソッドの挙動
以下は、範囲関連のメソッドでbabaさんが見つけた興味深い挙動です。字面だけでこれらのメソッドを使うとはまりそうで、冷やっとしました。
end
end
は、範囲演算子..
と...
どちらの場合も同じ値を返します。exclude_end?
と挙動が異なっている点にご注目ください。
(1..10).end #=> 10
(1...10).end #=> 10
(1..10).exclude_end? #=> false
(1...10).exclude_end? #=> true
max
しかしmax
は..
と...
で値が異なります。
さらに小数点では、..
の戻り値が小数点になり、...
はエラーになります。
(1..10).max #=> 10
(1...10).max #=> 9
(1..10.1).max #=> 10.1
(1...10.1).max #=> error
(1...10.1).max
が10.0にならずにエラーになるのは、終端値があたかも有理数であるような印象ですね(合ってるかな…)。
よく指摘される ことですが、終端値を含む..
が短く(2文字)、終端値を含まない...
が長い(3文字)というのは、割と紛らわしいですね。