Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

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

こんにちは、hachi8833です。

先週のTechRacho記事「ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法 」の続きです。


先週の記事 のレビュー中に、弊社CTOのbaba さんからチェックが飛んできました。

160822_1049_4Fn2bK

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

end

max

しかしmax.....で値が異なります。
さらに小数点では、..の戻り値が小数点になり、...はエラーになります。

  (1..10).max             #=>  10
  (1...10).max            #=>  9
  (1..10.1).max           #=>  10.1
  (1...10.1).max          #=>  error

max

(1...10.1).maxが10.0にならずにエラーになるのは、終端値があたかも有理数であるような印象ですね(合ってるかな…)。

よく指摘される ことですが、終端値を含む..が短く(2文字)、終端値を含まない...が長い(3文字)というのは、割と紛らわしいですね。

関連記事

ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。