RubyのComparableモジュールで比較を実装

Effective Ruby

Effective Ruby

項目13 "<=>"とComparableモジュールで比較を実装しよう

例にでてくるのはバージョンの比較を実装する時の話。
例えば、"10.10.3"と"10.9.8"のような比較の場合どうするか?

Object#<=>

2つのオブジェクトが等しいかどうかをテストするだけの汎用メソッド。

class Version
  attr_reader(:major, :minor, :patch)

  def initialize(version)
    @major, @minor, @patch = version.split(".").map(&:to_i)
  end
end


>> %w(10.10.3 10.9.8).map { |v| Version.new(v) }.sort
ArgumentError (comparison of Version with Version failed)

# オブジェクトが等しくなければnilを返す
>> v1 = Version.new("10.10.3")
>> v2 = Version.new("10.9.8")
>> v1 <=> v2
=> nil

なので<=>を実装する

class Version
  attr_reader(:major, :minor, :patch)

  def initialize(version)
    @major, @minor, @patch = version.split(".").map(&:to_i)
  end

  # 追加
  def <=>(other)
    return nil unless other.is_a?(Version)

    # 各桁を比較し0でないものがあればその値を返す。全部0なら0を返す。
    [ major <=> other.major,
      minor <=> other.minor,
      patch <=> other.patch,
    ].detect { |n| !n.zero? } || 0
  end
end

>> Version.new("10.10.3") <=> "a"
=> nil

>> Version.new("10.10.3") <=> Version.new("10.11.3")
=> -1

>> Version.new("10.11.3") <=> Version.new("10.10.3")
=> 1

>> Version.new("10.10.3") <=> Version.new("10.10.3")
=> 0

ついでに比較演算子

"<", "<=", "==", ">", ">=" はComparableモジュールをincludeすればok。

class Version
  include Comparable  # これ追加
  attr_reader(:major, :minor, :patch)

  def initialize(version)
    @major, @minor, @patch = version.split(".").map(&:to_i)
  end

  def <=>(other)
    return nil unless other.is_a?(Version)

    [ major <=> other.major,
      minor <=> other.minor,
      patch <=> other.patch,
    ].detect { |n| !n.zero? } || 0
  end
end

>> v1 = Version.new("10.10.3")
>> v2 = Version.new("10.11.3")
>> [v1 < v2, v1 <= v2, v1 == v2, v1 >= v2, v1 > v2]
=> [true, true, false, false, false]

>> Version.new("10.10.10").between?(v1, v2)
>= true

覚えておくべき事項

  • オブジェクトの順序は、"<=>演算子を定義し、Comparableモジュールをインクルードして実装しよう
  • "<=>"演算子は、左日演算子が右日演算子と比較できないものならnilを返す。
  • クラスのために"<=>"を実装した場合、特にインスタンスをハッシュキーとして使うつもりなら、eql?を"=="の別名にすることを検討しよう。 別名にする場合には、hashメソッドもオーバーライドしなければならない。

最後のやつは、こんな感じに実装すると良い。

class Version
  alias_method(:eql?, :==)

  def hash
    [major, minor, patch].hash
  end
end