無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

Objectの比較メソッドを実装する。

Object#eql?などObjectの比較を行うAPIを実装してみました。オブジェクトの型とすべての属性が完全に一致する場合、"同一"とみなします。たいていのモデルクラスはこの仕様でOKかと。

実装

リファレンスによると以下のようなルールになっているようなので、これに従ってmoduleとして実装。

  • eql?,==,===はオブジェクトごとにカスタマイズ可能となっているのでこれをオーバーライドする。
    • ==はifで、===はcase, eql?はハッシュでの同一性判定に使われるらしい。
  • eql?を変更したらhashも変更する必要がある。これはJavaと同じ。
  • equals?はオーバーライドしてはいけない。
#
#== オブジェクトの比較メソッドの実装。
#
#Object#eal?(other), Object#hash, Object#==(other) の基本実装です。
#オブジェクトの型とすべての属性が完全に一致する場合、"同一"とみなします。
#
module Equals
  def ==(other)
    _eql?(other) { |a,b| a == b }
  end
  def ===(other)
    _eql?(other) { |a,b| a === b }
  end
  def eql?(other)
    _eql?(other) { |a,b| a.eql? b }
  end
  def hash
    hash = 0
    values.each {|v|
      hash = v.hash + 31 * hash
    }
    return hash
  end
protected
  def values
    values = []
    values << self.class
    instance_variables.each { |name|
      values << instance_variable_get(name)
    }
    return values
  end
private
  def _eql?(other, &block)
    return false if other == nil
    return false unless other.is_a? Equals
    a = values
    b = other.values
    return false if a.length != b.length
    a.length.times{|i|
      return false unless block.call( a[i], b[i] )
    }
    return true
  end
end

使い方

includeして利用します。

class TestModel
  include Equals
end

valuesをオーバーライドすれば、比較する属性をカスタマイズすることもできます。

class TestModel
  include Equals
  def initialize( title, text )
    @title = title
    @text  = text
  end
  def values
    # タイトルのみ比較する。
    # title属性のみを保持していれば型も問わない。
    [@title]
  end
end

サンプル

サンプルです。テストケースにしてみた。以下のテストは一応通ります。

#!/usr/bin/ruby

require "runit/testcase"
require "runit/cui/testrunner"
require "equals"

class EqualsTest <  RUNIT::TestCase

  # テスト用モデル
  class TestModel
    include Equals

    def initialize( title, text )
      @title = title
      @text  = text
    end
  end

  # テスト用モデルの派生クラス
  class TestModel2 < TestModel; end

  # 比較値をカスタマイズしたモデルクラス
  class TestModel3
    include Equals

    def initialize( title, text )
      @title = title
      @text  = text
    end
    def values
      # タイトルのみ比較する。
      # title属性のみを保持していれば型も問わない。
      [@title]
    end
  end
  # テスト用モデル3の派生クラス
  class TestModel4 < TestModel3; end

  # テスト用モデルを持つモデル
  class TestModel5
    include Equals

    def initialize( title, text, title2, text2 )
      @m1 = TestModel.new title, text
      @m3 = TestModel3.new title2, text2
    end
  end
  
  def test_equals

    # 同じ型で同じ属性を持つ場合同一と見なされる
    a  = TestModel.new("a","a")
    b  = TestModel.new("a","a")
    assert_equals a, b


    # 属性が違う場合、eql?はtrueにならない。
    b  = TestModel.new("b","a")
    assert_not_equals a, b

    b  = TestModel.new("a","b")
    assert_not_equals a, b


    # 型が違う場合も違うと見なされる。
    a  = TestModel.new("a","a")
    b  = TestModel2.new("a","a")
    assert_not_equals a, b


    # valuesをカスタマイズしたもの。
    # 型を問わない。また、titleしか評価しない。
    a  = TestModel3.new("a","a")
    b  = TestModel3.new("a","b")
    assert_equals_ignore_type a, b

    b  = TestModel3.new("b","a")
    assert_not_equals a, b

    b  = TestModel4.new("a","b")
    assert_equals_ignore_type a, b


    # Equalsをincludeしていないオブジェクトも同一と見なされることはない
    assert_not_equals a, 1
    assert_not_equals a, "foo"
    assert_not_equals a, Object.new


    # Equalsを実装したモデルを持つモデル
    # 再帰的に評価される。
    a  = TestModel5.new("a","a","a","a")
    b  = TestModel5.new("a","a","a","a")
    assert_equals a, b

    b  = TestModel5.new("b","a","a","a")
    assert_not_equals a, b

    b  = TestModel5.new("a","b","a","a")
    assert_not_equals a, b

    b  = TestModel5.new("a","a","b","a")
    assert_not_equals a, b

    b  = TestModel5.new("a","a","a","b")
    assert_equals a, b
  end

  def assert_equals( a, b )
    assert a.eql?( b )
    assert a == b
    assert !(a === b) # Clas#===(other) がfalseを返すのでfalseになる。
    assert b.eql?( a )
    assert b == a
    assert !(b === a)
    assert_equal a.hash, b.hash
    assert !a.equal?( b )
    assert !b.equal?( a )
  end

  def assert_not_equals( a, b )
    assert !a.eql?( b )
    assert !(a == b)
    assert !(a === b)
    assert !b.eql?( a )
    assert !(b == a)
    assert !(b === a)
    assert_not_equal a.hash, b.hash
    assert !a.equal?( b )
    assert !b.equal?( a )
  end

  def assert_equals_ignore_type( a, b )
    assert a.eql?( b )
    assert a == b
    assert a === b
    assert b.eql?( a )
    assert b == a
    assert b === a
    assert_equal a.hash, b.hash
    assert !a.equal?( b )
    assert !b.equal?( a )
  end

end

修正 (2007-11-29)

==が実行された場合、属性も==を使って評価する方がよいな、と思って修正。Class#===がtrueを返さないので===での比較時にfalseになってしまうのがちょっとアレだけど、まぁいいか。ちなみにClass#===の実装はModlueの===を引き継いでおり、以下の動作となっているようです。(Rubyリファレンスマニュアル - Modlue - self === objより。)

obj が self と Object#kind_of? の関係がある時、真になります。

あと、Equalsをincludeしたオブジェクトを持つオブジェクトのテストがなかったので追加しておきました。