Comparing ruby hashes [duplicate]

2019-01-07 09:32发布

问题:

Possible Duplicate:
How do I compare two hashes?

I have two ruby hashes (which are essentially models) and am trying to find the differences between them, one is an old instance of an object where the other has new values assigned to some attributes. I'm trying to determine which keys have changed, but there doesn't seem to be anything built into the Hash for this. I can think of a few brute forceish solutions, but was wondering if there is perhaps an elegant solution out there.

Ideally I need to be able to take two hashs like so:

element1 = {:name => "Original", :description => "The original one!"}
element2 = {:name => "Original", :description => "The new one!"}

And be able to compare/diff them and get something back like this:

{:description => "The new one!"}

Right now all I can really think of is iterating through the keys in one hash and comparing the value at that key to the corresponding key in the second hash, but that seems too brute forced.

Any ideas? Thanks a lot!

回答1:

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



回答2:

Edit:

I keep coming back to this code to use it in projects I'm in. Here's the latest which is useful for deeply nested structures and based on Pete's code above. I usually drop it in config/initializers/core_ext.rb (in a Rails project):

class Hash
  def deep_diff(other)
    (self.keys + other.keys).uniq.inject({}) do |memo, key|
      left = self[key]
      right = other[key]

      next memo if left == right

      if left.respond_to?(:deep_diff) && right.respond_to?(:deep_diff)
        memo[key] = left.deep_diff(right)
      else
        memo[key] = [left, right]
      end

      memo
    end
  end
end

class Array
  def deep_diff(array)
    largest = [self.count, array.count].max
    memo = {}

    0.upto(largest - 1) do |index|
      left = self[index]
      right = array[index]

      next if left == right

      if left.respond_to?(:deep_diff) && right.respond_to?(:deep_diff)
        memo[index] = left.deep_diff(right)
      else
        memo[index] = [left, right]
      end
    end

    memo
  end
end

Here's a small demo:

> {a: [{b: "c", d: "e"}, {b: "c", f: "g"}]}.deep_diff({a: [{b: "c", d: "e"}, {b: "d", f: "g"}]})
=> {:a=>{1=>{:b=>["c", "d"]}}}

Older response:

I have found Rails' Hash diff method to not actually tell me what was on the left side and right side (which is far more useful). There was a plugin call "Riff", that has since disappeared, which would let you diff two ActiveRecord objects. Essentially:

class Hash
  def diff(other)
    self.keys.inject({}) do |memo, key|
      unless self[key] == other[key]
        memo[key] = [self[key], other[key]] 
      end
      memo
    end
  end
end


回答3:

If all you care about is what's unique in element2, you can just do:

element2.to_a - element1.to_a