Question

I'm trying to implement a custom class including the comparable mixin. Later the class is used to do a diff. The class looks like

class Element
  include Comparable
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def <=>(other)
    @name <=> other.name
  end
end

Now some test values with two added entries compared to a.

a = Array.new
b = Array.new

a.push Element.new('y1')
a.push Element.new('y2')
a.push Element.new('y4')
a.push Element.new('x1')
a.push Element.new('x2')
a.push Element.new('x4')

b.push Element.new('y1')
b.push Element.new('y2')
b.push Element.new('y3')
b.push Element.new('y4')
b.push Element.new('x1')
b.push Element.new('x2')
b.push Element.new('x3')
b.push Element.new('x4')

And now run diff between both arrays

puts Diff::LCS.diff(a, b).inspect

I would expect two added objects now but it finds 8 changes... Any ideas why?

[[["-", 2, #<Element:0x007fa9fb567898 @name="y4">],
  ["-", 3, #<Element:0x007fa9fbc69830 @name="x1">],
  ["-", 4, #<Element:0x007fa9fbca3378 @name="x2">],
  ["+", 2, #<Element:0x007fa9fb5e5e78 @name="y3">],
  ["+", 3, #<Element:0x007fa9fb606920 @name="y4">],
  ["+", 4, #<Element:0x007fa9fb625848 @name="x1">],
  ["+", 5, #<Element:0x007fa9fb647da8 @name="x2">],
  ["+", 6, #<Element:0x007fa9fbde8670 @name="x3">]]]

If we run the test with

a = Array.new
b = Array.new

a.push 'y1'
a.push 'y2'
a.push 'y4'
a.push 'x1'
a.push 'x2'
a.push 'x4'

b.push 'y1'
b.push 'y2'
b.push 'y3'
b.push 'y4'
b.push 'x1'
b.push 'x2'
b.push 'x3'
b.push 'x4'

Everything works as expected:

[[["+", 2, "y3"]],
 [["+", 6, "x3"]]]
Was it helpful?

Solution

Inspecting from source of Diff::LCS, the elements in sequences are used as hash key.

The Element class you wrote mixed the Comparable module, it got the == method there. However, it doesn't have eql? and hash method override, which is used by the Hash to determine the key comparison.

With you Element class, we have

irb(main):013:0> a = Element.new("a")
=> #<Element:0x002ae4ce402da0 @name="a">
irb(main):014:0> b = Element.new("a")
=> #<Element:0x002ae4ce409ec0 @name="a">
irb(main):015:0> a == b
=> true
irb(main):016:0> a.eql? b
=> false
irb(main):017:0> a.hash == b.hash
=> false
irb(main):018:0> h = {}
=> {}
irb(main):019:0> h[a] = 1
=> 1
irb(main):020:0> h[b]
=> nil

This affects the LCS calculation.

The fix is to add the eql? and hash method for the Element class, I think.

class Element
  include Comparable
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def <=>(other)
    @name <=> other.name
  end

  def eql? other
    other.class == Element and self.name == other.name
  end

  def hash
    @name.hash ^ Element.hash
  end
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top