Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Recursively diff two Ruby hashes.
# Recursively diff two hashes, showing only the differing values.
# By Henrik Nyh <http://henrik.nyh.se> 2009-07-14 under the MIT license.
#
# Example:
#
# a = {
# "same" => "same",
# "diff" => "a",
# "only a" => "a",
# "nest" => {
# "same" => "same",
# "diff" => "a"
# }
# }
#
# b = {
# "same" => "same",
# "diff" => "b",
# "only b" => "b",
# "nest" => {
# "same" => "same",
# "diff" => "b"
# }
# }
#
# a.deep_diff(b) # =>
#
# {
# "diff" => ["a", "b"],
# "only a" => ["a", nil],
# "only b" => [nil, "b"],
# "nest" => {
# "diff" => ["a", "b"]
# }
# }
#
#
# ActiveSupport's Hash#diff would give this for a.diff(b):
#
# {
# "diff" => "a",
# "only a" => "a",
# "only b" => "b",
# "nest" => {
# "same" => "same",
# "diff" => "a"
# }
# }
#
class Hash
def deep_diff(b)
a = self
(a.keys | b.keys).inject({}) do |diff, k|
if a[k] != b[k]
if a[k].respond_to?(:deep_diff) && b[k].respond_to?(:deep_diff)
diff[k] = a[k].deep_diff(b[k])
else
diff[k] = [a[k], b[k]]
end
end
diff
end
end
end
if __FILE__ == $0
require "test/unit"
class DeepDiffTest < Test::Unit::TestCase
def assert_deep_diff(diff, a, b)
assert_equal(diff, a.deep_diff(b))
end
def test_no_difference
assert_deep_diff(
{},
{"one" => 1, "two" => 2},
{"two" => 2, "one" => 1}
)
end
def test_fully_different
assert_deep_diff(
{"one" => [1, nil], "two" => [nil, 2]},
{"one" => 1},
{"two" => 2}
)
end
def test_simple_difference
assert_deep_diff(
{"one" => [1, "1"]},
{"one" => 1},
{"one" => "1"}
)
end
def test_complex_difference
assert_deep_diff(
{
"diff" => ["a", "b"],
"only a" => ["a", nil],
"only b" => [nil, "b"],
"nested" => {
"y" => {
"diff" => ["a", "b"]
}
}
},
{
"one" => "1",
"diff" => "a",
"only a" => "a",
"nested" => {
"x" => "x",
"y" => {
"a" => "a",
"diff" => "a"
}
}
},
{
"one" => "1",
"diff" => "b",
"only b" => "b",
"nested" => {
"x" => "x",
"y" => {
"a" => "a",
"diff" => "b"
}
}
}
)
end
def test_default_value
assert_deep_diff(
{"one" => [1, "default"]},
{"one" => 1},
Hash.new("default")
)
end
end
end
@stephanwehner
Copy link

stephanwehner commented Aug 5, 2014

Nice! I've been wanting the same. I was going to make it a new class, and collect the differences found, maybe even in a new hash? Also cover arrays.

@kalami
Copy link

kalami commented Aug 11, 2017

Thanks for this. I added a bit to deal with arrays of hashes if anyone needs it.

def deep_diff(b, exceptions = [])
    a = self
    (a.keys | b.keys).inject({}) do |diff, k|
      if a[k] != b[k] && !exceptions.any? { |key| k.include?(key) }
        if a[k].respond_to?(:deep_diff) && b[k].respond_to?(:deep_diff)
          diff[k] = a[k].deep_diff(b[k])
        else
          if a[k].present? && b[k].present?
            if a[k].instance_of?(Array) && a[k].first.instance_of?(Hash)
              a[k].each_with_index do |hash, index|
                if (b[k][index]).present?
                  diff[k] = hash.deep_diff(b[k][index])
                else
                  diff[k] = [a[k], b[k]]
                end
              end
            else
              diff[k] = [a[k], b[k]]
            end
          else
            diff[k] = [a[k], b[k]]
          end
        end
      end
      diff.delete_blank
    end
  end
  
  def delete_blank
    delete_if{|k, v| v.empty? or v.instance_of?(Hash) && v.delete_blank.empty?}
  end

@Dascr32
Copy link

Dascr32 commented Nov 23, 2017

Changed a little to work without modifying the Hash class

def deep_diff(a, b)
  (a.keys | b.keys).each_with_object({}) do |k, diff|
    if a[k] != b[k]
      if a[k].is_a?(Hash) && b[k].is_a?(Hash)
        diff[k] = deep_diff(a[k], b[k])
      else
        diff[k] = [a[k], b[k]]
      end
    end
    diff
  end
end

@passalini
Copy link

passalini commented Apr 27, 2018

@Dascr32 tks!! =D

@luxious
Copy link

luxious commented Mar 3, 2021

Nice

@gw7nvw
Copy link

gw7nvw commented Aug 24, 2021

@kalami - your array diff doesn't work if there are multiple differences in different indexes of the same array - as it (over)writes the differences to the same diff[k]. Modified to diff[k+index.to_s]=... as a hack to get that working ... probably neater ways of solving it though ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment