Hashdiff is a ruby library to to compute the smallest difference between two hashes
Hashdiff is a ruby library to to compute the smallest difference between two hashes
Hashdiff is a ruby library to compute the smallest difference between two hashes.
It also supports comparing two arrays.
Hashdiff does not monkey-patch any existing class. All features are contained inside the Hashdiff module.
Docs: Documentation
WARNING: Don't use the library for comparing large arrays, say ~10K (see #49).
Given two Hashes A and B, sometimes you face the question: what's the smallest modification that can be made to change A into B?
An algorithm that responds to this question has to do following:
A + ChangeSet = B and B - ChangeSet = A.Hashdiff answers the question above using an opinionated approach:
{a:[{c:2}]} can be represented as ["a[0].c", 2].[['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]].diff = Hashdiff.diff(a, b) Hashdiff.patch!(a, diff).should == b
unpatch example:
```ruby
a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 1]
b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}]
diff = Hashdiff.diff(a, b) # diff two array is OK
Hashdiff.unpatch!(b, diff).should == aThe following options are available: :delimiter, :similarity, :strict, :ignore_keys,
:indifferent, :numeric_tolerance, :strip, :case_insensitive, :array_path,
:use_lcs, and :preserve_key_order
:delimiterYou can specify :delimiter to be something other than the default dot. For 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, delimiter: '\t')
diff.should == [['-', 'a\tx', 2], ['-', 'a\tz', 4], ['-', 'b\tx', 3], ['~', 'b\tz', 45, 30], ['+', 'b\ty', 3]]
…
ruby
a = { a: 4, g: 0, b: { a: 5, c: 6, e: 1 } }
b = { b: { a: 7, c: 3, f: 1 }, d: 8 }
diff = Hashdiff.diff(a, b, ignore_keys: %i[a f])
diff.should == [['-', 'g', 0], ['-', 'b.e', 1], ['~', 'b.c', 6, 3], ['+', 'd', 8]]If you wish instead to ignore keys at a particlar level you should use a custom comparison method instead. For example to diff only at the 2nd level of both hashes:
a = { a: 4, g: 0, b: { a: 5, c: 6, e: 1 } }
b = { b: { a: 7, c: 3, f: 1 }, d: 8 }
diff = Hashdiff.diff(a, b) do |path, _e, _a|
arr = path.split('.')
true if %w[a f].include?(arr.last) && arr.size == 2 # note '.' is the default delimiter
end
diff.should == [['-', 'a', 4], ['-', 'g', 0], ['-', 'b.e', 1], ['~', 'b.c', 6, 3], ['+', 'd', 8]]:indifferentThe :indifferent option, which defaults to false, specifies whether to treat hash keys indifferently. Setting :indifferent to true has the effect of ignoring differences between symbol keys (ie. {a: 1} ~= {'a' => 1})
:numeric_toleranceThe :numeric_tolerance option allows for a small numeric tolerance.
a = {x:5, y:3.75, z:7}
b = {x:6, y:3.76, z:7}
diff = Hashdiff.diff(a, b, numeric_tolerance: 0.1)
diff.should == [["~", "x", 5, 6]]:stripThe :strip option strips all strings before comparing.
a = {x:5, s:'foo '}
b = {x:6, s:'foo'}
diff = Hashdiff.diff(a, b, numeric_tolerance: 0.1, strip: true)
diff.should == [["~", "x", 5, 6]]:case_insensitiveThe :case_insensitive option makes string comparisons ignore case.
a = {x:5, s:'FooBar'}
b = {x:6, s:'foobar'}
diff = Hashdiff.diff(a, b, numeric_tolerance: 0.1, case_insensitive: true)
diff.should == [["~", "x", 5, 6]]:array_pathThe :array_path option represents the path of the diff in an array rather than
a string. This can be used to show differences in between hash key types and
is useful for patch! when used on hashes without string keys.
a = {x:5}
b = {'x'=>6}
diff = Hashdiff.diff(a, b, array_path: true)
diff.should == [['-', [:x], 5], ['+', ['x'], 6]]For cases where there are arrays in paths their index will be added to the path.
a = {x:[0,1]}
b = {x:[0,2]}
diff = Hashdiff.diff(a, b, array_path: true)
diff.should == [["-", [:x, 1], 1], ["+", [:x, 1], 2]]This shouldn't cause problems if you are comparing an array with a hash:
a = {x:{0=>1}}
b = {x:[1]}
diff = Hashdiff.diff(a, b, array_path: true)
diff.should == [["~", [:x], {0=>1}, [1]]]
…
ruby
a = {x: [0, 1, 2]}
b = {x: [0, 2, 2, 3]}
diff = Hashdiff.diff(a, b, use_lcs: false)
diff.should == [["~", "x[1]", 1, 2], ["+", "x[3]", 3]]:preserve_key_orderBy default, the change set is ordered by operation type: deletions (-) first, then updates (~), and finally additions (+). Within each operation group, keys are sorted alphabetically:
a = {d: 1, c: 1, a: 1}
b = {d: 2, b: 2, a: 2}
diff = Hashdiff.diff(a, b)
diff.should == [["-", "c", 1], ["~", "a", 1, 2], ["~", "d", 1, 2], ["+", "b", 2]]Setting :preserve_key_order to true processes keys in the order they appear in the first hash. Keys that only exist in the second hash are appended in their original order:
a = {d: 1, c: 1, a: 1}
b = {d: 2, b: 2, a: 2}
diff = Hashdiff.diff(a, b, preserve_key_order: true)
diff.should == [["~", "d", 1, 2], ["-", "c", 1], ["~", "a", 1, 2], ["+", "b", 2]]It's possible to specify how the values of a key should be compared.
a = {a:'car', b:'boat', c:'plane'}
b = {a:'bus', b:'truck', c:' plan'}
diff = Hashdiff.diff(a, b) do |path, obj1, obj2|
case path
when /a|b|c/
obj1.length == obj2.length
end
end
diff.should == [['~', 'b', 'boat', 'truck']]The yielded params of the comparison block is |path, obj1, obj2|, in which path is the key (or delimited compound key) to the value being compared. When comparing elements in array, the path is with the format array[*]. For example:
a = {a:'car', b:['boat', 'plane'] }
b = {a:'bus', b:['truck', ' plan'] }
diff = Hashdiff.diff(a, b) do |path, obj1, obj2|
case path
when 'b[*]'
obj1.length == obj2.length
end
end
diff.should == [["~", "a", "car", "bus"], ["~", "b[1]", "plane", " plan"], ["-", "b[0]", "boat"], ["+", "b[0]", "truck"]]
…
ruby
a = {a:'car', b:['boat', 'plane'] }
b = {a:'car', b:['plane', 'boat'] }
Hashdiff.diff(a, b).should == [["+", "b[0]", "plane"], ["-", "b[2]", "plane"]]
b[:b].sort!
Hashdiff.diff(a, b).should == []Hashdiff is distributed under the MIT-LICENSE.
No open issues yet, or sync has not completed.