From: "John W. Long" Date: 2003-07-19T12:20:39+09:00 Subject: Ruby Golf: Object Diff The problem: When debugging a program that uses large objects and a test fails because the object is different from the expected it is sometimes hard to discern the difference between the objects from the output of Test::Unit. The goal of this hole is to create a method that will output the differences of two objects in an intelligent manner. Something similar in concept to this: # # You are free to use whatever method you might choose. Program size does not matter. Creativity and one-up-man-ship is encouraged. We will probably post the best solution on the wiki. The following test case is a guideline and may be changed if you have better ideas for how such a method should work: class TestObject1 attr_accessor :a, :b, :c, :d, :e def initialize(a, b, c, d, e = nil) @a, @b, @c, @d, @e = a, b, c, d, e end end class TestObject2 attr_accessor :a, :b, :c, :d, :f, :g def initialize(a, b, c, d, f, g) @a, @b, @c, @d, @f, @g = a, b, c, d, f, g end end class TC_ObjectDiff < Test::Unit::TestCase def test_to_s object = TestObject1.new('a', 'b', 'c', 'd', [1, 2, 3, 4]) def object.__id__ 0 end assert_equal('#', ObjectDiff.object_to_s(object)) end def test_compare object1 = TestObject1.new('a', 'b', 'c', 'd') def object1.__id__ 1 end object2 = TestObject1.new('a', '', 'c', '') def object2.__id__ 2 end string1, string2 = ObjectDiff.compare(object1, object2) puts [string1, string2] assert_equal('#', string1) assert_equal('#', string2) complexObject1 = TestObject1.new('1', {:a => 1, :b => 2, :j => 3}, [1, 2, 3], object1) def complexObject1.__id__ 3 end complexObject2 = TestObject1.new('1', {:a => 2, :b => object1, :j => 3}, [0, 2], object2) def complexObject2.__id__ 4 end string1, string2 = ObjectDiff.compare(complexObject1, complexObject2) puts [string1, string2] assert_equal('#1, :b=>2, ...}, @c=[1, ... 3], @d=#, ...>', string1) assert_equal('#2, :b=>#, ...}, @c=[0, ...], @d=#, ...>', string2) end def test_compare_different_classes object1 = TestObject1.new('a', 'b', 'c', 'd') def object1.__id__ 5 end object2 = TestObject2.new('a', '', 'c', '', 'f', 'g') def object2.__id__ 6 end string1, string2 = ObjectDiff.compare(object1, object2) puts [string1, string2] assert_equal('#', string1) assert_equal('#', string2) end def test_compare_array a = [1, 2, 3, 4, 5] b = [1, 3, 4, 4, 5, 6] string_a, string_b = ObjectDiff.compare_array(a, b) puts [string_a, string_b] assert_equal('[... 2, 3, ... ...]', string_a) assert_equal('[... 3, 4, ... ... 6]', string_b) end def test_compare_hash a = { :a => 1, :b => 2, :c => 3, :d => 4 } b = { :j => 2, :b => 3, :c => 3, :d => 5 } string_a, string_b = ObjectDiff.compare_hash(a, b) puts [string_a, string_b] assert_equal('{:a=>1, :b=>2, ... :d=>4}', string_a) assert_equal('{ :b=>3, ... :d=>5, :j=>2}', string_b) end end Here is my solution: class ObjectDiff class Node attr_reader :object, :nodes def initialize(value) @object = value @nodes = {} variables = @object.instance_variables variables.each { |key| variable = @object.instance_eval(key) @nodes[key] = Node.new(variable) } end def to_s return @object.inspect if [String, Integer, NilClass, Array, Hash, Fixnum, Integer, Bignum].index(@object.class) string = s_begin(self) @nodes.each { |key, node| string << "#{key}=#{node.to_s}, " } s_end(string) end def compare_with(node) return ::ObjectDiff::compare_hash(@object, node.object) if @object.instance_of?(Hash) and node.object.instance_of?(Hash) return ::ObjectDiff::compare_array(@object, node.object) if @object.instance_of?(Array) and node.object.instance_of?(Array) return [@object.inspect, node.object.inspect] if [String, Integer, NilClass, Array, Hash, Fixnum, Integer, Bignum].index(@object.class) keys = [] @nodes.each_key { |key| keys << key } node.nodes.each_key { |key| keys << key } keys.uniq! keys.sort! string1 = s_begin(self) string2 = s_begin(node) keys.each { |key| node1 = @nodes[key] node2 = node.nodes[key] if @nodes.has_key?(key) and node.nodes.has_key?(key) if node1.to_s == node2.to_s string1 << '... ' string2 << '... ' else s1, s2 = node1.compare_with(node2) string1 << "#{key}=#{s1}, " string2 << "#{key}=#{s2}, " end else if @nodes.has_key?(key) string1 << "#{key}=#{node1.to_s}, " end if node.nodes.has_key?(key) string2 << "#{key}=#{node2.to_s}, " end end } string1 = s_end(string1) string2 = s_end(string2) [string1, string2] end def s_begin(node) "#<#{node.object.class.name}:0x#{format('%x', node.object.__id__)} " end def s_end(string) string.chomp!(' ') string.chomp!(',') string << '>' end end def self.object_to_s(object) Node.new(object).to_s end def self.compare(object1, object2) node1 = Node.new(object1) node2 = Node.new(object2) node1.compare_with(node2) end def self.compare_array(array1, array2) if array1.size < array2.size a = array2 b = array1 flipped = true else a = array1 b = array2 flipped = false end string1 = '[' string2 = '[' for i in 0...a.size if i < b.size node1 = Node.new(a[i]) node2 = Node.new(b[i]) if node1.to_s == node2.to_s string1 << '... ' string2 << '... ' else string_a, string_b = compare(a[i], b[i]) string1 << "#{string_a}, " string2 << "#{string_b}, " end else string1 << Node.new(a[i]).to_s end end string1.strip! string1.chomp!(',') string2.strip! string2.chomp!(',') string1 << ']' string2 << ']' if flipped [string2, string1] else [string1, string2] end end def self.compare_hash(hash1, hash2) keys = [] hash1.each_key { |key| keys << key } hash2.each_key { |key| keys << key } keys.uniq! keys.sort!{ |a, b| a = a.inspect if a.is_a?(Symbol) b = b.inspect if b.is_a?(Symbol) a <=> b } string1 = '{' string2 = '{' keys.each { |key| node1 = Node.new(hash1[key]) node2 = Node.new(hash2[key]) if hash1.has_key?(key) and hash2.has_key?(key) if node1.to_s == node2.to_s string1 << '... ' string2 << '... ' else s1, s2 = node1.compare_with(node2) string1 << "#{key.inspect}=>#{s1}, " string2 << "#{key.inspect}=>#{s2}, " end else if hash1.has_key?(key) and not hash2.has_key?(key) append = "#{key.inspect}=>#{node1.to_s}, " string1 << append string2 << (' ' * append.size) else append = "#{key.inspect}=>#{node2.to_s}, " string2 << append string1 << (' ' * append.size) end end } string1.strip! string1.chomp!(',') string2.strip! string2.chomp!(',') string1 << '}' string2 << '}' [string1, string2] end end Feel free to improve upon my code or create your own. Remember this is for posterity, so be honest... ___________________ John Long www.wiseheartdesign.com