mystix
11/22/2012 - 6:36 AM

Recursively diff two Ruby hashes.

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