I am trying to compare two Ruby Hashes using the following code:
#!/usr/bin/env ruby
require "yaml"
require "active_support"
file1 = YAML::load(File.open('./en_20110207.yml'))
file2 = YAML::load(File.open('./locales/en.yml'))
arr = []
file1.select { |k,v|
file2.select { |k2, v2|
arr << "#{v2}" if "#{v}" != "#{v2}"
}
}
puts arr
The output to the screen is the full file from file2. I know for a fact that the files are different, but the script doesn't seem to pick it up.
what about convert both hash to_json and compare as string? but keeping in mind that
require "json"
h1 = {a: 20}
h2 = {a: "20"}
h1.to_json==h1.to_json
=> true
h1.to_json==h2.to_json
=> false
I had the same problem and sent a pull request to rails
https://github.com/elfassy/rails/commit/5f3410e04fe8c4d4745397db866c9633b80ccec6
... and now in module form to be applied to a variety of collection classes (Hash among them). It's not a deep inspection, but it's simple.
# Enable "diffing" and two-way transformations between collection objects
module Diffable
# Calculates the changes required to transform self to the given collection.
# @param b [Enumerable] The other collection object
# @return [Array] The Diff: A two-element change set representing items to exclude and items to include
def diff( b )
a, b = to_a, b.to_a
[a - b, b - a]
end
# Consume return value of Diffable#diff to produce a collection equal to the one used to produce the given diff.
# @param to_drop [Enumerable] items to exclude from the target collection
# @param to_add [Enumerable] items to include in the target collection
# @return [Array] New transformed collection equal to the one used to create the given change set
def apply_diff( to_drop, to_add )
to_a - to_drop + to_add
end
end
if __FILE__ == $0
# Demo: Hashes with overlapping keys and somewhat random values.
Hash.send :include, Diffable
rng = Random.new
a = (:a..:q).to_a.reduce(Hash[]){|h,k| h.merge! Hash[k, rng.rand(2)] }
b = (:i..:z).to_a.reduce(Hash[]){|h,k| h.merge! Hash[k, rng.rand(2)] }
raise unless a == Hash[ b.apply_diff(*b.diff(a)) ] # change b to a
raise unless b == Hash[ a.apply_diff(*a.diff(b)) ] # change a to b
raise unless a == Hash[ a.apply_diff(*a.diff(a)) ] # change a to a
raise unless b == Hash[ b.apply_diff(*b.diff(b)) ] # change b to b
end
If you need a quick and dirty diff between hashes which correctly supports nil in values you can use something like
def diff(one, other)
(one.keys + other.keys).uniq.inject({}) do |memo, key|
unless one.key?(key) && other.key?(key) && one[key] == other[key]
memo[key] = [one.key?(key) ? one[key] : :_no_key, other.key?(key) ? other[key] : :_no_key]
end
memo
end
end
This was answered in "Comparing ruby hashes". Rails adds a diff
method to hashes. It works well.
You can try the hashdiff gem, which allows deep comparison of hashes and arrays in the hash.
The following is an example:
a = {a:{x:2, y:3, z:4}, b:{x:3, z:45}}
b = {a:{y:3}, b:{y:3, z:30}}
diff = HashDiff.diff(a, b)
diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]]
Here is algorithm to deeply compare two Hashes, which also will compare nested Arrays:
HashDiff.new(
{val: 1, nested: [{a:1}, {b: [1, 2]}] },
{val: 2, nested: [{a:1}, {b: [1]}] }
).report
# Output:
val:
- 1
+ 2
nested > 1 > b > 1:
- 2
Implementation:
class HashDiff
attr_reader :left, :right
def initialize(left, right, config = {}, path = nil)
@left = left
@right = right
@config = config
@path = path
@conformity = 0
end
def conformity
find_differences
@conformity
end
def report
@config[:report] = true
find_differences
end
def find_differences
if hash?(left) && hash?(right)
compare_hashes_keys
elsif left.is_a?(Array) && right.is_a?(Array)
compare_arrays
else
report_diff
end
end
def compare_hashes_keys
combined_keys.each do |key|
l = value_with_default(left, key)
r = value_with_default(right, key)
if l == r
@conformity += 100
else
compare_sub_items l, r, key
end
end
end
private
def compare_sub_items(l, r, key)
diff = self.class.new(l, r, @config, path(key))
@conformity += diff.conformity
end
def report_diff
return unless @config[:report]
puts "#{@path}:"
puts "- #{left}" unless left == NO_VALUE
puts "+ #{right}" unless right == NO_VALUE
end
def combined_keys
(left.keys + right.keys).uniq
end
def hash?(value)
value.is_a?(Hash)
end
def compare_arrays
l, r = left.clone, right.clone
l.each_with_index do |l_item, l_index|
max_item_index = nil
max_conformity = 0
r.each_with_index do |r_item, i|
if l_item == r_item
@conformity += 1
r[i] = TAKEN
break
end
diff = self.class.new(l_item, r_item, {})
c = diff.conformity
if c > max_conformity
max_conformity = c
max_item_index = i
end
end or next
if max_item_index
key = l_index == max_item_index ? l_index : "#{l_index}/#{max_item_index}"
compare_sub_items l_item, r[max_item_index], key
r[max_item_index] = TAKEN
else
compare_sub_items l_item, NO_VALUE, l_index
end
end
r.each_with_index do |item, index|
compare_sub_items NO_VALUE, item, index unless item == TAKEN
end
end
def path(key)
p = "#{@path} > " if @path
"#{p}#{key}"
end
def value_with_default(obj, key)
obj.fetch(key, NO_VALUE)
end
module NO_VALUE; end
module TAKEN; end
end
How about another, simpler approach:
require 'fileutils'
FileUtils.cmp(file1, file2)
You could use a simple array intersection, this way you can know what differs in each hash.
hash1 = { a: 1 , b: 2 }
hash2 = { a: 2 , b: 2 }
overlapping_elements = hash1.to_a & hash2.to_a
exclusive_elements_from_hash1 = hash1.to_a - overlapping_elements
exclusive_elements_from_hash2 = hash2.to_a - overlapping_elements
I developed this to compare if two hashes are equal
def hash_equal?(hash1, hash2)
array1 = hash1.to_a
array2 = hash2.to_a
(array1 - array2 | array2 - array1) == []
end
The usage:
> hash_equal?({a: 4}, {a: 4})
=> true
> hash_equal?({a: 4}, {b: 4})
=> false
> hash_equal?({a: {b: 3}}, {a: {b: 3}})
=> true
> hash_equal?({a: {b: 3}}, {a: {b: 4}})
=> false
> hash_equal?({a: {b: {c: {d: {e: {f: {g: {h: 1}}}}}}}}, {a: {b: {c: {d: {e: {f: {g: {h: 1}}}}}}}})
=> true
> hash_equal?({a: {b: {c: {d: {e: {f: {g: {marino: 1}}}}}}}}, {a: {b: {c: {d: {e: {f: {g: {h: 2}}}}}}}})
=> false
If you want a nicely formatted diff, you can do this:
# Gemfile
gem 'awesome_print' # or gem install awesome_print
And in your code:
require 'ap'
def my_diff(a, b)
as = a.ai(plain: true).split("\n").map(&:strip)
bs = b.ai(plain: true).split("\n").map(&:strip)
((as - bs) + (bs - as)).join("\n")
end
puts my_diff({foo: :bar, nested: {val1: 1, val2: 2}, end: :v},
{foo: :bar, n2: {nested: {val1: 1, val2: 3}}, end: :v})
The idea is to use awesome print to format, and diff the output. The diff won't be exact, but it is useful for debugging purposes.
If you want to get what is the difference between two hashes, you can do this:
h1 = {:a => 20, :b => 10, :c => 44}
h2 = {:a => 2, :b => 10, :c => "44"}
result = {}
h1.each {|k, v| result[k] = h2[k] if h2[k] != v }
p result #=> {:a => 2, :c => "44"}
Source: Stackoverflow.com