ActiveRecordのRangeHandlerクラスとRubyの範囲メソッドRange#exclude_end?

こんにちは、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. (class Range – Documentation for Ruby 2.3.0) 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文字)というのは、割と紛らわしいですね。 関連記事 ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法