Comparing ruby hashes [duplicate]

here is a slightly modified version from colin’s.

class Hash
  def diff(other)
    (self.keys + other.keys).uniq.inject({}) do |memo, key|
      unless self[key] == other[key]
        if self[key].kind_of?(Hash) &&  other[key].kind_of?(Hash)
          memo[key] = self[key].diff(other[key])
        else
          memo[key] = [self[key], other[key]] 
        end
      end
      memo
    end
  end
end

It recurses into the hashes for more efficient left and right

{a: {c: 1, b: 2}, b: 2}.diff({a: {c: 2, b: 2}})

returns

{:a=>{:c=>[1, 2]}, :b=>[2, nil]}

instead of

{:a=>[{:c=>1, :b=>2}, {:c=>2, :b=>2}], :b=>[2, nil]}

Great idea colin

here is how to apply the diff to the original hashes

  def apply_diff!(changes, direction = :right)
    path = [[self, changes]]
    pos, local_changes = path.pop
    while local_changes
      local_changes.each_pair {|key, change|
        if change.kind_of?(Array)
          pos[key] = (direction == :right) ? change[1] : change[0]
        else
          path.push([pos[key], change])
        end
      }
      pos, local_changes = path.pop
    end
    self
  end
  def apply_diff(changes, direction = :right)
    cloned = self.clone
    path = [[cloned, changes]]
    pos, local_changes = path.pop
    while local_changes
      local_changes.each_pair {|key, change|
        if change.kind_of?(Array)
          pos[key] = (direction == :right) ? change[1] : change[0]
        else
          pos[key] = pos[key].clone
          path.push([pos[key], change])
        end
      }
      pos, local_changes = path.pop
    end
    cloned
  end 

so to make the left look like the right you run

{a: {c: 1, b: 2}, b: 2}.apply_diff({:a=>{:c=>[1, 2]}, :b=>[2, nil]})

to get

{a: {c: 2, b: 2}, b: nil}

to get exact we would have to go a little farther and record a difference between between nil and no key
and it would also be nice to shorten long arrays by just providing adds and removes

Leave a Comment