From 69efcf697696a8800c75104be8ea6be15653a95b Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 10 Jan 2017 12:49:11 -0800 Subject: [PATCH 1/2] use array path when delimiter is false --- README.md | 22 +++++ lib/hashdiff/diff.rb | 160 +++++++++++++++++-------------- lib/hashdiff/lcs.rb | 8 +- lib/hashdiff/patch.rb | 24 +++-- lib/hashdiff/util.rb | 28 ++++-- spec/hashdiff/diff_array_spec.rb | 32 ++++--- spec/hashdiff/diff_spec.rb | 45 +++++++++ spec/hashdiff/lcs_spec.rb | 16 ++-- spec/hashdiff/util_spec.rb | 42 +++++--- 9 files changed, 250 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 5f12da1..096f558 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,28 @@ 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]] ``` +You can also disable delimiters by passing `false`, and paths will be returned as an array: + +```ruby +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 => false) +diff.should == [['-', ['a', 'x'], 2], ['-', ['a', 'z'], 4], ['-', ['b', 'x'], 3], ['~', ['b', 'z'], 45, 30], ['+', ['b', 'y'], 3]] +``` + +#### `:stringify_keys` + +By default, object keys are converted to strings in paths, you can override this behavior by specifying `:stringify_keys` as `false`. This only works when `:delimiter` is false. + +```ruby +a = {'a' => 1} +b = {'a' => 1, :b => 2} + +diff = HashDiff.diff(a, b, :delimiter => false, :stringify_keys => false) +diff.should == [['+', [:b], 2]] +``` + #### `:similarity` In cases where you have similar hash objects in arrays, you can pass a custom value for `:similarity` instead of the default `0.8`. This is interpreted as a ratio of similarity (default is 80% similar, whereas `:similarity => 0.5` would look for at least a 50% similarity). diff --git a/lib/hashdiff/diff.rb b/lib/hashdiff/diff.rb index 780e0fc..e3e132a 100644 --- a/lib/hashdiff/diff.rb +++ b/lib/hashdiff/diff.rb @@ -8,9 +8,10 @@ module HashDiff # @param [Array, Hash] obj2 # @param [Hash] options the options to use when comparing # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other - # * :delimiter (String) ['.'] the delimiter used when returning nested key references + # * :delimiter (String) ['.'] the delimiter used when returning nested key references, or false to use array paths # * :numeric_tolerance (Numeric) [0] should be a positive numeric value. Value by which numeric differences must be greater than. By default, numeric values are compared exactly; with the :tolerance option, the difference between numeric values must be greater than the given value. # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing + # * :stringify_keys [true] whether or not to convert object keys to strings # # @yield [path, value1, value2] Optional block is used to compare each value, instead of default #==. If the block returns value other than true of false, then other specified comparison options will be used to do the comparison. # @@ -25,18 +26,19 @@ module HashDiff # # @since 0.0.1 def self.best_diff(obj1, obj2, options = {}, &block) + options = { }.merge!(options) options[:comparison] = block if block_given? - opts = { :similarity => 0.3 }.merge!(options) - diffs_1 = diff(obj1, obj2, opts) + options[:similarity] = 0.3 + diffs_1 = diff(obj1, obj2, options) count_1 = count_diff diffs_1 - opts = { :similarity => 0.5 }.merge!(options) - diffs_2 = diff(obj1, obj2, opts) + options[:similarity] = 0.5 + diffs_2 = diff(obj1, obj2, options) count_2 = count_diff diffs_2 - opts = { :similarity => 0.8 }.merge!(options) - diffs_3 = diff(obj1, obj2, opts) + options[:similarity] = 0.8 + diffs_3 = diff(obj1, obj2, options) count_3 = count_diff diffs_3 count, diffs = count_1 < count_2 ? [count_1, diffs_1] : [count_2, diffs_2] @@ -50,9 +52,10 @@ def self.best_diff(obj1, obj2, options = {}, &block) # @param [Hash] options the options to use when comparing # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other # * :similarity (Numeric) [0.8] should be between (0, 1]. Meaningful if there are similar hashes in arrays. See {best_diff}. - # * :delimiter (String) ['.'] the delimiter used when returning nested key references + # * :delimiter (String) ['.'] the delimiter used when returning nested key references, or nil to use array paths # * :numeric_tolerance (Numeric) [0] should be a positive numeric value. Value by which numeric differences must be greater than. By default, numeric values are compared exactly; with the :tolerance option, the difference between numeric values must be greater than the given value. # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing + # * :stringify_keys [true] whether or not to convert object keys to strings # # @yield [path, value1, value2] Optional block is used to compare each value, instead of default #==. If the block returns value other than true of false, then other specified comparison options will be used to do the comparison. # @@ -68,19 +71,39 @@ def self.best_diff(obj1, obj2, options = {}, &block) # # @since 0.0.1 def self.diff(obj1, obj2, options = {}, &block) - opts = { - :prefix => '', + options = { + :prefix => [], :similarity => 0.8, :delimiter => '.', :strict => true, :strip => false, + :stringify_keys => true, :numeric_tolerance => 0 }.merge!(options) - opts[:comparison] = block if block_given? + options[:stringify_keys] = true if options[:delimiter] + options[:comparison] = block if block_given? + + change_set = diff_internal(obj1, obj2, options) + + if options[:delimiter] + change_set.each do |change| + change[1] = encode_property_path(change[1], options[:delimiter]) + end + else + change_set + end + end + + # @private + # + # diff two variables + + def self.diff_internal(obj1, obj2, options) + prefix = options[:prefix] # prefer to compare with provided block - result = custom_compare(opts[:comparison], opts[:prefix], obj1, obj2) + result = custom_compare(options, prefix, obj1, obj2) return result if result if obj1.nil? and obj2.nil? @@ -88,108 +111,103 @@ def self.diff(obj1, obj2, options = {}, &block) end if obj1.nil? - return [['~', opts[:prefix], nil, obj2]] + return [['~', prefix, nil, obj2]] end if obj2.nil? - return [['~', opts[:prefix], obj1, nil]] + return [['~', prefix, obj1, nil]] end - unless comparable?(obj1, obj2, opts[:strict]) - return [['~', opts[:prefix], obj1, obj2]] + unless comparable?(obj1, obj2, options[:strict]) + return [['~', prefix, obj1, obj2]] end - result = [] if obj1.is_a?(Array) - changeset = diff_array(obj1, obj2, opts) do |lcs| + return diff_array(obj1, obj2, options) do |lcs| # use a's index for similarity - lcs.each do |pair| - result.concat(diff(obj1[pair[0]], obj2[pair[1]], opts.merge(:prefix => "#{opts[:prefix]}[#{pair[0]}]"))) - end - end - - changeset.each do |change| - if change[0] == '-' - result << ['-', "#{opts[:prefix]}[#{change[1]}]", change[2]] - elsif change[0] == '+' - result << ['+', "#{opts[:prefix]}[#{change[1]}]", change[2]] + lcs.flat_map do |pair| + diff_internal(obj1[pair[0]], obj2[pair[1]], options.merge(:prefix => prefix + [pair[0]])) end end elsif obj1.is_a?(Hash) - if opts[:prefix].empty? - prefix = "" - else - prefix = "#{opts[:prefix]}#{opts[:delimiter]}" - end + return diff_object(obj1, obj2, options) + else + return [] if compare_values(obj1, obj2, options) + return [['~', prefix, obj1, obj2]] + end + end - deleted_keys = obj1.keys - obj2.keys - common_keys = obj1.keys & obj2.keys - added_keys = obj2.keys - obj1.keys + # @private + # + # diff object + def self.diff_object(a, b, options) + prefix = options[:prefix] + change_set = [] + deleted_keys = a.keys - b.keys + common_keys = a.keys & b.keys + added_keys = b.keys - a.keys - # add deleted properties - deleted_keys.sort_by{|k,v| k.to_s }.each do |k| - custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", obj1[k], nil) + # add deleted properties + deleted_keys.sort_by{|k,v| k.to_s }.each do |k| + subpath = prefix + [options[:stringify_keys] ? "#{k}" : k] + custom_result = custom_compare(options, subpath, a[k], nil) - if custom_result - result.concat(custom_result) - else - result << ['-', "#{prefix}#{k}", obj1[k]] - end + if custom_result + change_set.concat(custom_result) + else + change_set << ['-', subpath, a[k]] end + end - # recursive comparison for common keys - common_keys.sort_by{|k,v| k.to_s }.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(:prefix => "#{prefix}#{k}"))) } + # recursive comparison for common keys + common_keys.sort_by{|k,v| k.to_s }.each do |k| + subpath = prefix + [options[:stringify_keys] ? "#{k}" : k] + change_set.concat(diff_internal(a[k], b[k], options.merge(:prefix => subpath))) + end - # added properties - added_keys.sort_by{|k,v| k.to_s }.each do |k| - unless obj1.key?(k) - custom_result = custom_compare(opts[:comparison], "#{prefix}#{k}", nil, obj2[k]) + # added properties + added_keys.sort_by{|k,v| k.to_s }.each do |k| + unless a.key?(k) + subpath = prefix + [options[:stringify_keys] ? "#{k}" : k] + custom_result = custom_compare(options, subpath, nil, b[k]) - if custom_result - result.concat(custom_result) - else - result << ['+', "#{prefix}#{k}", obj2[k]] - end + if custom_result + change_set.concat(custom_result) + else + change_set << ['+', subpath, b[k]] end end - else - return [] if compare_values(obj1, obj2, opts) - return [['~', opts[:prefix], obj1, obj2]] end - result + change_set end # @private # # diff array using LCS algorithm def self.diff_array(a, b, options = {}) - opts = { - :prefix => '', - :similarity => 0.8, - :delimiter => '.' - }.merge!(options) - + prefix = options[:prefix] change_set = [] + if a.size == 0 and b.size == 0 return [] elsif a.size == 0 b.each_index do |index| - change_set << ['+', index, b[index]] + change_set << ['+', prefix + [index], b[index]] end return change_set elsif b.size == 0 a.each_index do |index| i = a.size - index - 1 - change_set << ['-', i, a[i]] + change_set << ['-', prefix + [i], a[i]] end return change_set end - links = lcs(a, b, opts) + links = lcs(a, b, options) # yield common - yield links if block_given? + change_set.concat(yield links) if block_given? # padding the end links << [a.size, b.size] @@ -201,12 +219,12 @@ def self.diff_array(a, b, options = {}) # remove from a, beginning from the end (x > last_x + 1) and (x - last_x - 2).downto(0).each do |i| - change_set << ['-', last_y + i + 1, a[i + last_x + 1]] + change_set << ['-', prefix + [last_y + i + 1], a[i + last_x + 1]] end # add from b, beginning from the head (y > last_y + 1) and 0.upto(y - last_y - 2).each do |i| - change_set << ['+', last_y + i + 1, b[i + last_y + 1]] + change_set << ['+', prefix + [last_y + i + 1], b[i + last_y + 1]] end # update flags diff --git a/lib/hashdiff/lcs.rb b/lib/hashdiff/lcs.rb index 1cc27d5..5cc2c6b 100644 --- a/lib/hashdiff/lcs.rb +++ b/lib/hashdiff/lcs.rb @@ -3,10 +3,8 @@ module HashDiff # # caculate array difference using LCS algorithm # http://en.wikipedia.org/wiki/Longest_common_subsequence_problem - def self.lcs(a, b, options = {}) - opts = { :similarity => 0.8 }.merge!(options) - - opts[:prefix] = "#{opts[:prefix]}[*]" + def self.lcs(a, b, options) + options = options.merge({:prefix => options[:prefix] + [-1]}) return [] if a.size == 0 or b.size == 0 @@ -19,7 +17,7 @@ def self.lcs(a, b, options = {}) (b_start..b_finish).each do |bi| lcs[bi] = [] (a_start..a_finish).each do |ai| - if similar?(a[ai], b[bi], opts) + if similar?(a[ai], b[bi], options) topleft = (ai > 0 and bi > 0)? lcs[bi-1][ai-1][1] : 0 lcs[bi][ai] = [:topleft, topleft + 1] elsif diff --git a/lib/hashdiff/patch.rb b/lib/hashdiff/patch.rb index 553a199..334d129 100644 --- a/lib/hashdiff/patch.rb +++ b/lib/hashdiff/patch.rb @@ -6,9 +6,11 @@ module HashDiff # Apply patch to object # # @param [Hash, Array] obj the object to be patched, can be an Array or a Hash - # @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']] + # @param [Array] changes with either string paths or array paths: + # [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']] + # [[ '+', ['a', 'b'], '45' ], [ '-', ['a', 'c'], '5' ], [ '~', ['a', 'x'], '45', '63']] # @param [Hash] options supports following keys: - # * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array + # * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array, ignored for array paths # # @return the object after patch # @@ -17,19 +19,19 @@ def self.patch!(obj, changes, options = {}) delimiter = options[:delimiter] || '.' changes.each do |change| - parts = decode_property_path(change[1], delimiter) + parts = change[1].is_a?(Array) ? change[1] : decode_property_path(change[1], delimiter) last_part = parts.last parent_node = node(obj, parts[0, parts.size-1]) if change[0] == '+' - if last_part.is_a?(Integer) + if parent_node.is_a?(Array) parent_node.insert(last_part, change[2]) else parent_node[last_part] = change[2] end elsif change[0] == '-' - if last_part.is_a?(Integer) + if parent_node.is_a?(Array) parent_node.delete_at(last_part) else parent_node.delete(last_part) @@ -45,9 +47,11 @@ def self.patch!(obj, changes, options = {}) # Unpatch an object # # @param [Hash, Array] obj the object to be unpatched, can be an Array or a Hash - # @param [Array] changes e.g. [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']] + # @param [Array] changes with either string paths or array paths: + # [[ '+', 'a.b', '45' ], [ '-', 'a.c', '5' ], [ '~', 'a.x', '45', '63']] + # [[ '+', ['a', 'b'], '45' ], [ '-', ['a', 'c'], '5' ], [ '~', ['a', 'x'], '45', '63']] # @param [Hash] options supports following keys: - # * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array + # * :delimiter (String) ['.'] delimiter string for representing nested keys in changes array, ignored for array paths # # @return the object after unpatch # @@ -56,19 +60,19 @@ def self.unpatch!(obj, changes, options = {}) delimiter = options[:delimiter] || '.' changes.reverse_each do |change| - parts = decode_property_path(change[1], delimiter) + parts = change[1].is_a?(Array) ? change[1] : decode_property_path(change[1], delimiter) last_part = parts.last parent_node = node(obj, parts[0, parts.size-1]) if change[0] == '+' - if last_part.is_a?(Integer) + if parent_node.is_a?(Array) parent_node.delete_at(last_part) else parent_node.delete(last_part) end elsif change[0] == '-' - if last_part.is_a?(Integer) + if parent_node.is_a?(Array) parent_node.insert(last_part, change[2]) else parent_node[last_part] = change[2] diff --git a/lib/hashdiff/util.rb b/lib/hashdiff/util.rb index 12cf534..90fbb8c 100644 --- a/lib/hashdiff/util.rb +++ b/lib/hashdiff/util.rb @@ -8,7 +8,7 @@ def self.similar?(a, b, options = {}) count_a = count_nodes(a) count_b = count_nodes(b) - diffs = count_diff diff(a, b, opts) + diffs = count_diff diff_internal(a, b, opts) if count_a + count_b == 0 return true @@ -48,12 +48,25 @@ def self.count_nodes(obj) # @private # - # decode property path into an array + # encode an array into a property path + # @param [String] path Property-string + # @param [String] delimiter Property-string delimiter + # + # e.g. ['a', 'b', 3, 'c'] => "a.b[3].c" + def self.encode_property_path(path, delimiter) + path.inject('') do |acc, x| + acc << (x.is_a?(Integer) ? "[#{x == -1 ? '*' : x}]" : "#{acc.length == 0 ? '' : delimiter}#{x}") + end + end + + # @private + # + # decode a property path into an array # @param [String] path Property-string # @param [String] delimiter Property-string delimiter # # e.g. "a.b[3].c" => ['a', 'b', 3, 'c'] - def self.decode_property_path(path, delimiter='.') + def self.decode_property_path(path, delimiter) parts = path.split(delimiter).collect do |part| if part =~ /^(.*)\[(\d+)\]$/ if $1.size > 0 @@ -116,13 +129,14 @@ def self.comparable?(obj1, obj2, strict = true) # @private # # try custom comparison - def self.custom_compare(method, key, obj1, obj2) - if method - res = method.call(key, obj1, obj2) + def self.custom_compare(options, path, obj1, obj2) + if options[:comparison] + user_path = options[:delimiter] ? encode_property_path(path, options[:delimiter]) : path + res = options[:comparison].call(user_path, obj1, obj2) # nil != false here if res == false - return [['~', key, obj1, obj2]] + return [['~', path, obj1, obj2]] elsif res == true return [] end diff --git a/spec/hashdiff/diff_array_spec.rb b/spec/hashdiff/diff_array_spec.rb index c22af75..ac9b550 100644 --- a/spec/hashdiff/diff_array_spec.rb +++ b/spec/hashdiff/diff_array_spec.rb @@ -5,7 +5,7 @@ a = [1, 2, 3] b = [1, 2, 3] - diff = HashDiff.diff_array(a, b) + diff = HashDiff.diff_array(a, b, :prefix => []) diff.should == [] end @@ -13,48 +13,54 @@ a = [1, 2, 3] b = [1, 8, 7] - diff = HashDiff.diff_array(a, b) - diff.should == [['-', 2, 3], ['-', 1, 2], ['+', 1, 8], ['+', 2, 7]] + diff = HashDiff.diff_array(a, b, :prefix => []) + diff.should == [['-', [2], 3], ['-', [1], 2], ['+', [1], 8], ['+', [2], 7]] end it "should be able to diff two arrays with nothing in common" do a = [1, 2] b = [] - diff = HashDiff.diff_array(a, b) - diff.should == [['-', 1, 2], ['-', 0, 1]] + diff = HashDiff.diff_array(a, b, :prefix => []) + diff.should == [['-', [1], 2], ['-', [0], 1]] end it "should be able to diff an empty array with an non-empty array" do a = [] b = [1, 2] - diff = HashDiff.diff_array(a, b) - diff.should == [['+', 0, 1], ['+', 1, 2]] + diff = HashDiff.diff_array(a, b, :prefix => []) + diff.should == [['+', [0], 1], ['+', [1], 2]] end it "should be able to diff two arrays with two elements in common" do a = [1, 3, 5, 7] b = [2, 3, 7, 5] - diff = HashDiff.diff_array(a, b) - diff.should == [['-', 0, 1], ['+', 0, 2], ['+', 2, 7], ['-', 4, 7]] + diff = HashDiff.diff_array(a, b, :prefix => []) + diff.should == [['-', [0], 1], ['+', [0], 2], ['+', [2], 7], ['-', [4], 7]] end it "should be able to test two arrays with two common elements in different order" do a = [1, 3, 4, 7] b = [2, 3, 7, 5] - diff = HashDiff.diff_array(a, b) - diff.should == [['-', 0, 1], ['+', 0, 2], ['-', 2, 4], ['+', 3, 5]] + diff = HashDiff.diff_array(a, b, :prefix => []) + diff.should == [['-', [0], 1], ['+', [0], 2], ['-', [2], 4], ['+', [3], 5]] end it "should be able to diff two arrays with similar elements" do a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, 3] b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}] - diff = HashDiff.diff_array(a, b) - diff.should == [['+', 0, 1], ['-', 2, 3]] + diff = HashDiff.diff_array(a, b, :prefix => []) + diff.should == [['+', [0], 1], ['-', [2], 3]] end + it "should return an array path if requested" do + a = [1, 2, 3] + b = [2, 3, 4] + diff = HashDiff.diff_array(a, b, :prefix => [], :delimiter => false) + diff.should == [['-', [0], 1], ['+', [2], 4]] + end end diff --git a/spec/hashdiff/diff_spec.rb b/spec/hashdiff/diff_spec.rb index 77ee0f1..0a9f444 100644 --- a/spec/hashdiff/diff_spec.rb +++ b/spec/hashdiff/diff_spec.rb @@ -163,6 +163,51 @@ diff.should == [["-", "[0]\td", 4], ["-", "[1]", {"x"=>5, "y"=>6, "z"=>3}]] end + context 'when :delimiter is false' do + it "should return an array path for a simple object" do + a = {'a' => 1} + b = {'a' => 1, 'b' => 2} + diff = HashDiff.diff(a, b, :delimiter => false) + diff.should == [['+', ['b'], 2]] + end + + it "should return an array path for an object nested in an object" do + a = {'subobj' => {'a' => 1}} + b = {'subobj' => {'a' => 1, 'b' => 2}} + diff = HashDiff.diff(a, b, :delimiter => false) + diff.should == [['+', ['subobj', 'b'], 2]] + end + + it "should return an array path for an object nested in an array" do + a = [{'a' => 1}] + b = [{'a' => 1, 'b' => 2}] + diff = HashDiff.diff(a, b, :delimiter => false) + diff.should == [['-', [0], {'a' => 1}], ['+', [0], {'a' => 1, 'b' => 2}]] + end + + it "should return an array path for an array nested in an array" do + a = [[1]] + b = [[1, 2]] + diff = HashDiff.diff(a, b, :delimiter => false) + diff.should == [['-', [0], [1]], ['+', [0], [1, 2]]] + end + + context 'when :stringify_keys is false' do + it "preserves symbol keys" do + a = {'a' => 1} + b = {'a' => 1, :b => 2} + diff = HashDiff.diff(a, b, :delimiter => false, :stringify_keys => false) + diff.should == [['+', [:b], 2]] + end + it "preserves object keys" do + a = {'a' => 1} + b = {'a' => 1, ['foo', 'bar'] => 2} + diff = HashDiff.diff(a, b, :delimiter => false, :stringify_keys => false) + diff.should == [['+', [['foo', 'bar']], 2]] + end + end + end + context 'when :numeric_tolerance requested' do it "should be able to diff changes in hash value" do a = {'a' => 0.558, 'b' => 0.0, 'c' => 0.65, 'd' => 'fin'} diff --git a/spec/hashdiff/lcs_spec.rb b/spec/hashdiff/lcs_spec.rb index 7b8cece..99ae66a 100644 --- a/spec/hashdiff/lcs_spec.rb +++ b/spec/hashdiff/lcs_spec.rb @@ -5,7 +5,7 @@ a = [1, 2, 3] b = [1, 2, 3] - lcs = HashDiff.lcs(a, b) + lcs = HashDiff.lcs(a, b, :prefix => [], :similarity => 0.8) lcs.should == [[0, 0], [1, 1], [2, 2]] end @@ -13,7 +13,7 @@ a = [1.05, 2, 3.25] b = [1.06, 2, 3.24] - lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1) + lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1, :prefix => [], :similarity => 0.8) lcs.should == [[0, 0], [1, 1], [2, 2]] end @@ -21,7 +21,7 @@ a = ['foo', 'bar', 'baz'] b = [' foo', 'bar', 'zab'] - lcs = HashDiff.lcs(a, b, :strip => true) + lcs = HashDiff.lcs(a, b, :strip => true, :prefix => [], :similarity => 0.8) lcs.should == [[0, 0], [1, 1]] end @@ -29,7 +29,7 @@ a = [1, 2, 3] b = [1, 8, 7] - lcs = HashDiff.lcs(a, b) + lcs = HashDiff.lcs(a, b, :prefix => [], :similarity => 0.8) lcs.should == [[0, 0]] end @@ -37,7 +37,7 @@ a = [1, 3, 5, 7] b = [2, 3, 7, 5] - lcs = HashDiff.lcs(a, b) + lcs = HashDiff.lcs(a, b, :prefix => [], :similarity => 0.8) lcs.should == [[1, 1], [2, 3]] end @@ -45,7 +45,7 @@ a = [1, 3.05, 5, 7] b = [2, 3.06, 7, 5] - lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1) + lcs = HashDiff.lcs(a, b, :numeric_tolerance => 0.1, :prefix => [], :similarity => 0.8) lcs.should == [[1, 1], [2, 3]] end @@ -53,7 +53,7 @@ a = [1, 3, 4, 7] b = [2, 3, 7, 5] - lcs = HashDiff.lcs(a, b) + lcs = HashDiff.lcs(a, b, :prefix => [], :similarity => 0.8) lcs.should == [[1, 1], [3, 2]] end @@ -68,7 +68,7 @@ {"value" => "Close", "onclick" => "CloseDoc()"} ] - lcs = HashDiff.lcs(a, b, :similarity => 0.5) + lcs = HashDiff.lcs(a, b, :similarity => 0.5, :prefix => []) lcs.should == [[0, 0], [1, 2]] end end diff --git a/spec/hashdiff/util_spec.rb b/spec/hashdiff/util_spec.rb index 18e745a..2cc38fe 100644 --- a/spec/hashdiff/util_spec.rb +++ b/spec/hashdiff/util_spec.rb @@ -1,12 +1,28 @@ require 'spec_helper' describe HashDiff do - it "should be able to decode property path" do - decoded = HashDiff.send(:decode_property_path, "a.b[0].c.city[5]") + it "should be able to encode property path with dot delimiter" do + encoded = HashDiff.send(:encode_property_path, ['a', 'b', 0, 'c', 'city', 5], '.') + encoded.should == "a.b[0].c.city[5]" + end + + it "should be able to encode property path with tab delimiter" do + encoded = HashDiff.send(:encode_property_path, ['a', 'b', 0, 'c', 'city', 5], "\t") + encoded.should == "a\tb[0]\tc\tcity[5]" + end + + # -1 is used internally for custom comparison callback paths in an array + it "should encode property path with -1 into a *" do + encoded = HashDiff.send(:encode_property_path, ['a', -1, 'b', 0], '.') + encoded.should == "a[*].b[0]" + end + + it "should be able to decode property path with dot delimiter" do + decoded = HashDiff.send(:decode_property_path, "a.b[0].c.city[5]", '.') decoded.should == ['a', 'b', 0, 'c', 'city', 5] end - it "should be able to decode property path with custom delimiter" do + it "should be able to decode property path with tab delimiter" do decoded = HashDiff.send(:decode_property_path, "a\tb[0]\tc\tcity[5]", "\t") decoded.should == ['a', 'b', 0, 'c', 'city', 5] end @@ -14,36 +30,36 @@ it "should be able to tell similiar hash" do a = {'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5} b = {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5} - HashDiff.similar?(a, b).should be true - HashDiff.similar?(a, b, :similarity => 1).should be false + HashDiff.similar?(a, b, :prefix => []).should be true + HashDiff.similar?(a, b, :similarity => 1, :prefix => []).should be false end it "should be able to tell similiar hash with values within tolerance" do a = {'a' => 1.5, 'b' => 2.25, 'c' => 3, 'd' => 4, 'e' => 5} b = {'a' => 1.503, 'b' => 2.22, 'c' => 3, 'e' => 5} - HashDiff.similar?(a, b, :numeric_tolerance => 0.05).should be true - HashDiff.similar?(a, b).should be false + HashDiff.similar?(a, b, :numeric_tolerance => 0.05, :prefix => []).should be true + HashDiff.similar?(a, b, :prefix => []).should be false end it "should be able to tell numbers and strings" do - HashDiff.similar?(1, 2).should be false - HashDiff.similar?("a", "b").should be false - HashDiff.similar?("a", [1, 2, 3]).should be false - HashDiff.similar?(1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}).should be false + HashDiff.similar?(1, 2, :prefix => []).should be false + HashDiff.similar?("a", "b", :prefix => []).should be false + HashDiff.similar?("a", [1, 2, 3], :prefix => []).should be false + HashDiff.similar?(1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}, :prefix => []).should be false end it "should be able to tell true when similarity == 0.5" do a = {"value" => "New1", "onclick" => "CreateNewDoc()"} b = {"value" => "New", "onclick" => "CreateNewDoc()"} - HashDiff.similar?(a, b, :similarity => 0.5).should be true + HashDiff.similar?(a, b, :similarity => 0.5, :prefix => []).should be true end it "should be able to tell false when similarity == 0.5" do a = {"value" => "New1", "onclick" => "open()"} b = {"value" => "New", "onclick" => "CreateNewDoc()"} - HashDiff.similar?(a, b, :similarity => 0.5).should be false + HashDiff.similar?(a, b, :similarity => 0.5, :prefix => []).should be false end describe '.compare_values' do From 2bbcc6859697da6b1914dc7f4d1cb61e43cd625a Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 22 Feb 2017 15:56:12 -0800 Subject: [PATCH 2/2] remove backwards compatibility of array path changes --- README.md | 31 +------ lib/hashdiff/diff.rb | 14 +-- lib/hashdiff/util.rb | 16 +--- spec/hashdiff/best_diff_spec.rb | 21 ++--- spec/hashdiff/diff_array_spec.rb | 7 -- spec/hashdiff/diff_spec.rb | 154 +++++++++++++++---------------- spec/hashdiff/patch_spec.rb | 13 --- spec/hashdiff/util_spec.rb | 16 ---- 8 files changed, 85 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index 096f558..2d4b63e 100644 --- a/README.md +++ b/README.md @@ -87,40 +87,19 @@ HashDiff.unpatch!(b, diff).should == a ### Options -There are six options available: `:delimiter`, `:similarity`, -`:strict`, `:numeric_tolerance`, `:strip` and `:case_insensitive`. +There are six options available: `:stringify_keys`, `:similarity`, `:strict`, +`:numeric_tolerance`, `:strip` and `:case_insensitive`. -#### `:delimiter` - -You can specify `:delimiter` to be something other than the default dot. For example: - -```ruby -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]] -``` - -You can also disable delimiters by passing `false`, and paths will be returned as an array: - -```ruby -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 => false) -diff.should == [['-', ['a', 'x'], 2], ['-', ['a', 'z'], 4], ['-', ['b', 'x'], 3], ['~', ['b', 'z'], 45, 30], ['+', ['b', 'y'], 3]] -``` #### `:stringify_keys` -By default, object keys are converted to strings in paths, you can override this behavior by specifying `:stringify_keys` as `false`. This only works when `:delimiter` is false. +By default, object keys are converted to strings in paths, you can override this behavior by specifying `:stringify_keys` as `false`. ```ruby a = {'a' => 1} b = {'a' => 1, :b => 2} -diff = HashDiff.diff(a, b, :delimiter => false, :stringify_keys => false) +diff = HashDiff.diff(a, b, :stringify_keys => false) diff.should == [['+', [:b], 2]] ``` @@ -186,7 +165,7 @@ 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: +The yielded params of the comparison block is `|path, obj1, obj2|`, in which path is the array of keys to the value being compared. When comparing elements in array, the key is with the format `array[*]`. For example: ```ruby a = {a:'car', b:['boat', 'plane'] } diff --git a/lib/hashdiff/diff.rb b/lib/hashdiff/diff.rb index e3e132a..19399ba 100644 --- a/lib/hashdiff/diff.rb +++ b/lib/hashdiff/diff.rb @@ -8,7 +8,6 @@ module HashDiff # @param [Array, Hash] obj2 # @param [Hash] options the options to use when comparing # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other - # * :delimiter (String) ['.'] the delimiter used when returning nested key references, or false to use array paths # * :numeric_tolerance (Numeric) [0] should be a positive numeric value. Value by which numeric differences must be greater than. By default, numeric values are compared exactly; with the :tolerance option, the difference between numeric values must be greater than the given value. # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing # * :stringify_keys [true] whether or not to convert object keys to strings @@ -52,7 +51,6 @@ def self.best_diff(obj1, obj2, options = {}, &block) # @param [Hash] options the options to use when comparing # * :strict (Boolean) [true] whether numeric values will be compared on type as well as value. Set to false to allow comparing Integer, Float, BigDecimal to each other # * :similarity (Numeric) [0.8] should be between (0, 1]. Meaningful if there are similar hashes in arrays. See {best_diff}. - # * :delimiter (String) ['.'] the delimiter used when returning nested key references, or nil to use array paths # * :numeric_tolerance (Numeric) [0] should be a positive numeric value. Value by which numeric differences must be greater than. By default, numeric values are compared exactly; with the :tolerance option, the difference between numeric values must be greater than the given value. # * :strip (Boolean) [false] whether or not to call #strip on strings before comparing # * :stringify_keys [true] whether or not to convert object keys to strings @@ -74,25 +72,15 @@ def self.diff(obj1, obj2, options = {}, &block) options = { :prefix => [], :similarity => 0.8, - :delimiter => '.', :strict => true, :strip => false, :stringify_keys => true, :numeric_tolerance => 0 }.merge!(options) - options[:stringify_keys] = true if options[:delimiter] options[:comparison] = block if block_given? - change_set = diff_internal(obj1, obj2, options) - - if options[:delimiter] - change_set.each do |change| - change[1] = encode_property_path(change[1], options[:delimiter]) - end - else - change_set - end + diff_internal(obj1, obj2, options) end # @private diff --git a/lib/hashdiff/util.rb b/lib/hashdiff/util.rb index 90fbb8c..022a59a 100644 --- a/lib/hashdiff/util.rb +++ b/lib/hashdiff/util.rb @@ -46,19 +46,6 @@ def self.count_nodes(obj) count end - # @private - # - # encode an array into a property path - # @param [String] path Property-string - # @param [String] delimiter Property-string delimiter - # - # e.g. ['a', 'b', 3, 'c'] => "a.b[3].c" - def self.encode_property_path(path, delimiter) - path.inject('') do |acc, x| - acc << (x.is_a?(Integer) ? "[#{x == -1 ? '*' : x}]" : "#{acc.length == 0 ? '' : delimiter}#{x}") - end - end - # @private # # decode a property path into an array @@ -131,8 +118,7 @@ def self.comparable?(obj1, obj2, strict = true) # try custom comparison def self.custom_compare(options, path, obj1, obj2) if options[:comparison] - user_path = options[:delimiter] ? encode_property_path(path, options[:delimiter]) : path - res = options[:comparison].call(user_path, obj1, obj2) + res = options[:comparison].call(path, obj1, obj2) # nil != false here if res == false diff --git a/spec/hashdiff/best_diff_spec.rb b/spec/hashdiff/best_diff_spec.rb index c444e88..9e3a42e 100644 --- a/spec/hashdiff/best_diff_spec.rb +++ b/spec/hashdiff/best_diff_spec.rb @@ -6,15 +6,7 @@ b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] } diff = HashDiff.best_diff(a, b) - diff.should == [["-", "x[0].c", 3], ["+", "x[0].b", 2], ["-", "x[1]", {"y"=>3}]] - end - - it "should use custom delimiter when provided" do - a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]} - b = {'x' => [{'a' => 1, 'b' => 2, 'e' => 5}] } - - diff = HashDiff.best_diff(a, b, :delimiter => "\t") - diff.should == [["-", "x[0]\tc", 3], ["+", "x[0]\tb", 2], ["-", "x[1]", {"y"=>3}]] + diff.should == [["-", ["x", 0, "c"], 3], ["+", ["x", 0, "b"], 2], ["-", ["x", 1], {"y"=>3}]] end it "should use custom comparison when provided" do @@ -22,13 +14,12 @@ b = {'x' => [{'a' => 'bar', 'b' => 'cow', 'e' => 'puppy'}] } diff = HashDiff.best_diff(a, b) do |path, obj1, obj2| - case path - when /^x\[.\]\..$/ + if path.length == 3 obj1.length == obj2.length if obj1 and obj2 end end - diff.should == [["-", "x[0].c", 'goat'], ["+", "x[0].b", 'cow'], ["-", "x[1]", {"y"=>'baz'}]] + diff.should == [["-", ["x", 0, "c"], 'goat'], ["+", ["x", 0, "b"], 'cow'], ["-", ["x", 1], {"y"=>'baz'}]] end it "should be able to best diff array in hash" do @@ -57,9 +48,9 @@ diff = HashDiff.best_diff(a, b) diff.should == [ - ['~', 'menu.id', 'file', 'file 2'], - ['~', 'menu.popup.menuitem[0].value', 'New', 'New1'], - ['+', 'menu.popup.menuitem[1]', {"value" => "Open", "onclick" => "OpenDoc()"}] + ['~', ['menu', 'id'], 'file', 'file 2'], + ['~', ['menu', 'popup', 'menuitem', 0, 'value'], 'New', 'New1'], + ['+', ['menu', 'popup', 'menuitem', 1], {"value" => "Open", "onclick" => "OpenDoc()"}] ] end end diff --git a/spec/hashdiff/diff_array_spec.rb b/spec/hashdiff/diff_array_spec.rb index ac9b550..61e0a45 100644 --- a/spec/hashdiff/diff_array_spec.rb +++ b/spec/hashdiff/diff_array_spec.rb @@ -55,12 +55,5 @@ diff = HashDiff.diff_array(a, b, :prefix => []) diff.should == [['+', [0], 1], ['-', [2], 3]] end - - it "should return an array path if requested" do - a = [1, 2, 3] - b = [2, 3, 4] - diff = HashDiff.diff_array(a, b, :prefix => [], :delimiter => false) - diff.should == [['-', [0], 1], ['+', [2], 4]] - end end diff --git a/spec/hashdiff/diff_spec.rb b/spec/hashdiff/diff_spec.rb index 0a9f444..2bb6857 100644 --- a/spec/hashdiff/diff_spec.rb +++ b/spec/hashdiff/diff_spec.rb @@ -11,10 +11,10 @@ b = {} diff = HashDiff.diff(a, b) - diff.should == [['-', 'a', 3], ['-', 'b', 2]] + diff.should == [['-', ['a'], 3], ['-', ['b'], 2]] diff = HashDiff.diff(b, a) - diff.should == [['+', 'a', 3], ['+', 'b', 2]] + diff.should == [['+', ['a'], 3], ['+', ['b'], 2]] end it "should be able to diff two equal hashes" do @@ -32,14 +32,14 @@ a = { 'a' => 1, :b => 1 } b = {} diff = HashDiff.diff(a, b) - diff.should == [["-", "a", 1], ["-", "b", 1]] + diff.should == [["-", ["a"], 1], ["-", ["b"], 1]] end it "should be able to diff if mixed key types are added" do a = { 'a' => 1, :b => 1 } b = {} diff = HashDiff.diff(b, a) - diff.should == [["+", "a", 1], ["+", "b", 1]] + diff.should == [["+", ["a"], 1], ["+", ["b"], 1]] end it "should be able to diff two hashes with equivalent numerics, when strict is false" do @@ -49,24 +49,24 @@ it "should be able to diff changes in hash value" do diff = HashDiff.diff({ 'a' => 2, 'b' => 3, 'c' => " hello" }, { 'a' => 2, 'b' => 4, 'c' => "hello" }) - diff.should == [['~', 'b', 3, 4], ['~', 'c', " hello", "hello"]] + diff.should == [['~', ['b'], 3, 4], ['~', ['c'], " hello", "hello"]] end it "should be able to diff changes in hash value which is array" do diff = HashDiff.diff({ 'a' => 2, 'b' => [1, 2, 3] }, { 'a' => 2, 'b' => [1, 3, 4]}) - diff.should == [['-', 'b[1]', 2], ['+', 'b[2]', 4]] + diff.should == [['-', ['b', 1], 2], ['+', ['b', 2], 4]] end it "should be able to diff changes in hash value which is hash" do diff = HashDiff.diff({ 'a' => { 'x' => 2, 'y' => 3, 'z' => 4 }, 'b' => { 'x' => 3, 'z' => 45 } }, { 'a' => { 'y' => 3 }, 'b' => { 'y' => 3, 'z' => 30 } }) - diff.should == [['-', 'a.x', 2], ['-', 'a.z', 4], ['-', 'b.x', 3], ['~', 'b.z', 45, 30], ['+', 'b.y', 3]] + diff.should == [['-', ['a', 'x'], 2], ['-', ['a', 'z'], 4], ['-', ['b', 'x'], 3], ['~', ['b', 'z'], 45, 30], ['+', ['b', 'y'], 3]] end it "should be able to diff similar objects in array" do diff = HashDiff.best_diff({ 'a' => [{ 'x' => 2, 'y' => 3, 'z' => 4 }, { 'x' => 11, 'y' => 22, 'z' => 33 }], 'b' => { 'x' => 3, 'z' => 45 } }, { 'a' => [{ 'y' => 3 }, { 'x' => 11, 'z' => 33 }], 'b' => { 'y' => 22 } }) - diff.should == [['-', 'a[0].x', 2], ['-', 'a[0].z', 4], ['-', 'a[1].y', 22], ['-', 'b.x', 3], ['-', 'b.z', 45], ['+', 'b.y', 22]] + diff.should == [['-', ['a', 0, 'x'], 2], ['-', ['a', 0, 'z'], 4], ['-', ['a', 1, 'y'], 22], ['-', ['b', 'x'], 3], ['-', ['b', 'z'], 45], ['+', ['b', 'y'], 22]] end it 'should be able to diff addition of key value pair' do @@ -74,10 +74,10 @@ b = {"a"=>3, "c"=>11, "d"=>45, "e"=>100, "f"=>200, "g"=>300} diff = HashDiff.diff(a, b) - diff.should == [['+', 'g', 300]] + diff.should == [['+', ['g'], 300]] diff = HashDiff.diff(b, a) - diff.should == [['-', 'g', 300]] + diff.should == [['-', ['g'], 300]] end it 'should be able to diff value type changes' do @@ -85,10 +85,10 @@ b = {"a" => {"a1" => 1, "a2" => 2}} diff = HashDiff.diff(a, b) - diff.should == [['~', 'a', 3, {"a1" => 1, "a2" => 2}]] + diff.should == [['~', ['a'], 3, {"a1" => 1, "a2" => 2}]] diff = HashDiff.diff(b, a) - diff.should == [['~', 'a', {"a1" => 1, "a2" => 2}, 3]] + diff.should == [['~', ['a'], {"a1" => 1, "a2" => 2}, 3]] end it "should be able to diff value changes: array <=> []" do @@ -96,7 +96,7 @@ b = {"a" => 1, "b" => []} diff = HashDiff.diff(a, b) - diff.should == [['-', 'b[1]', 2], ['-', 'b[0]', 1]] + diff.should == [['-', ['b', 1], 2], ['-', ['b', 0], 1]] end it "should be able to diff value changes: array <=> nil" do @@ -104,7 +104,7 @@ b = {"a" => 1, "b" => nil} diff = HashDiff.diff(a, b) - diff.should == [["~", "b", [1, 2], nil]] + diff.should == [["~", ["b"], [1, 2], nil]] end it "should be able to diff value chagnes: remove array completely" do @@ -112,7 +112,7 @@ b = {"a" => 1} diff = HashDiff.diff(a, b) - diff.should == [["-", "b", [1, 2]]] + diff.should == [["-", ["b"], [1, 2]]] end it "should be able to diff value changes: remove whole hash" do @@ -120,7 +120,7 @@ b = {"a" => 1} diff = HashDiff.diff(a, b) - diff.should == [["-", "b", {"b1"=>1, "b2"=>2}]] + diff.should == [["-", ["b"], {"b1"=>1, "b2"=>2}]] end it "should be able to diff value changes: hash <=> {}" do @@ -128,7 +128,7 @@ b = {"a" => 1, "b" => {}} diff = HashDiff.diff(a, b) - diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]] + diff.should == [['-', ['b', 'b1'], 1], ['-', ['b', 'b2'], 2]] end it "should be able to diff value changes: hash <=> nil" do @@ -136,7 +136,7 @@ b = {"a" => 1, "b" => nil} diff = HashDiff.diff(a, b) - diff.should == [["~", "b", {"b1"=>1, "b2"=>2}, nil]] + diff.should == [["~", ["b"], {"b1"=>1, "b2"=>2}, nil]] end it "should be able to diff similar objects in array" do @@ -144,7 +144,7 @@ b = [1, {'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}] diff = HashDiff.diff(a, b) - diff.should == [['-', '[0].d', 4], ['+', '[0]', 1], ['-', '[2]', 3]] + diff.should == [['-', [0, 'd'], 4], ['+', [0], 1], ['-', [2], 3]] end it "should be able to diff similar & equal objects in array" do @@ -152,59 +152,49 @@ b = [{'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}, 3] diff = HashDiff.diff(a, b) - diff.should == [["-", "[0].d", 4], ["-", "[1]", {"x"=>5, "y"=>6, "z"=>3}]] + diff.should == [["-", [0, 'd'], 4], ["-", [1], {"x"=>5, "y"=>6, "z"=>3}]] end - it "should use custom delimiter when provided" do - a = [{'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5}, {'x' => 5, 'y' => 6, 'z' => 3}, 3] - b = [{'a' => 1, 'b' => 2, 'c' => 3, 'e' => 5}, 3] - - diff = HashDiff.diff(a, b, :similarity => 0.8, :delimiter => "\t") - diff.should == [["-", "[0]\td", 4], ["-", "[1]", {"x"=>5, "y"=>6, "z"=>3}]] + it "should return an array path for a simple object" do + a = {'a' => 1} + b = {'a' => 1, 'b' => 2} + diff = HashDiff.diff(a, b) + diff.should == [['+', ['b'], 2]] end - context 'when :delimiter is false' do - it "should return an array path for a simple object" do - a = {'a' => 1} - b = {'a' => 1, 'b' => 2} - diff = HashDiff.diff(a, b, :delimiter => false) - diff.should == [['+', ['b'], 2]] - end + it "should return an array path for an object nested in an object" do + a = {'subobj' => {'a' => 1}} + b = {'subobj' => {'a' => 1, 'b' => 2}} + diff = HashDiff.diff(a, b) + diff.should == [['+', ['subobj', 'b'], 2]] + end - it "should return an array path for an object nested in an object" do - a = {'subobj' => {'a' => 1}} - b = {'subobj' => {'a' => 1, 'b' => 2}} - diff = HashDiff.diff(a, b, :delimiter => false) - diff.should == [['+', ['subobj', 'b'], 2]] - end + it "should return an array path for an object nested in an array" do + a = [{'a' => 1}] + b = [{'a' => 1, 'b' => 2}] + diff = HashDiff.diff(a, b) + diff.should == [['-', [0], {'a' => 1}], ['+', [0], {'a' => 1, 'b' => 2}]] + end - it "should return an array path for an object nested in an array" do - a = [{'a' => 1}] - b = [{'a' => 1, 'b' => 2}] - diff = HashDiff.diff(a, b, :delimiter => false) - diff.should == [['-', [0], {'a' => 1}], ['+', [0], {'a' => 1, 'b' => 2}]] - end + it "should return an array path for an array nested in an array" do + a = [[1]] + b = [[1, 2]] + diff = HashDiff.diff(a, b) + diff.should == [['-', [0], [1]], ['+', [0], [1, 2]]] + end - it "should return an array path for an array nested in an array" do - a = [[1]] - b = [[1, 2]] - diff = HashDiff.diff(a, b, :delimiter => false) - diff.should == [['-', [0], [1]], ['+', [0], [1, 2]]] + context 'when :stringify_keys is false' do + it "preserves symbol keys" do + a = {'a' => 1} + b = {'a' => 1, :b => 2} + diff = HashDiff.diff(a, b, :stringify_keys => false) + diff.should == [['+', [:b], 2]] end - - context 'when :stringify_keys is false' do - it "preserves symbol keys" do - a = {'a' => 1} - b = {'a' => 1, :b => 2} - diff = HashDiff.diff(a, b, :delimiter => false, :stringify_keys => false) - diff.should == [['+', [:b], 2]] - end - it "preserves object keys" do - a = {'a' => 1} - b = {'a' => 1, ['foo', 'bar'] => 2} - diff = HashDiff.diff(a, b, :delimiter => false, :stringify_keys => false) - diff.should == [['+', [['foo', 'bar']], 2]] - end + it "preserves object keys" do + a = {'a' => 1} + b = {'a' => 1, ['foo', 'bar'] => 2} + diff = HashDiff.diff(a, b, :stringify_keys => false) + diff.should == [['+', [['foo', 'bar']], 2]] end end @@ -214,10 +204,10 @@ b = {'a' => 0.557, 'b' => 'hats', 'c' => 0.67, 'd' => 'fin'} diff = HashDiff.diff(a, b, :numeric_tolerance => 0.01) - diff.should == [["~", "b", 0.0, 'hats'], ["~", "c", 0.65, 0.67]] + diff.should == [["~", ["b"], 0.0, 'hats'], ["~", ["c"], 0.65, 0.67]] diff = HashDiff.diff(b, a, :numeric_tolerance => 0.01) - diff.should == [["~", "b", 'hats', 0.0], ["~", "c", 0.67, 0.65]] + diff.should == [["~", ["b"], 'hats', 0.0], ["~", ["c"], 0.67, 0.65]] end it "should be able to diff changes in nested values" do @@ -225,10 +215,10 @@ b = {'a' => {'x' => 0.6, 'y' => 0.341}, 'b' => [14, 68.025]} diff = HashDiff.diff(a, b, :numeric_tolerance => 0.01) - diff.should == [["~", "a.x", 0.4, 0.6], ["-", "b[0]", 13], ["+", "b[0]", 14]] + diff.should == [["~", ["a", "x"], 0.4, 0.6], ["-", ["b", 0], 13], ["+", ["b", 0], 14]] diff = HashDiff.diff(b, a, :numeric_tolerance => 0.01) - diff.should == [["~", "a.x", 0.6, 0.4], ["-", "b[0]", 14], ["+", "b[0]", 13]] + diff.should == [["~", ["a", "x"], 0.6, 0.4], ["-", ["b", 0], 14], ["+", ["b", 0], 13]] end end @@ -237,14 +227,14 @@ a = { 'a' => " foo", 'b' => "fizz buzz"} b = { 'a' => "foo", 'b' => "fizzbuzz"} diff = HashDiff.diff(a, b, :strip => true) - diff.should == [['~', 'b', "fizz buzz", "fizzbuzz"]] + diff.should == [['~', ['b'], "fizz buzz", "fizzbuzz"]] end it "should strip nested strings before comparing" do a = { 'a' => { 'x' => " foo" }, 'b' => ["fizz buzz", "nerf"] } b = { 'a' => { 'x' => "foo" }, 'b' => ["fizzbuzz", "nerf"] } diff = HashDiff.diff(a, b, :strip => true) - diff.should == [['-', 'b[0]', "fizz buzz"], ['+', 'b[0]', "fizzbuzz"]] + diff.should == [['-', ['b', 0], "fizz buzz"], ['+', ['b', 0], "fizzbuzz"]] end end @@ -253,14 +243,14 @@ a = { 'a' => "Foo", 'b' => "fizz buzz"} b = { 'a' => "foo", 'b' => "fizzBuzz"} diff = HashDiff.diff(a, b, :case_insensitive => true) - diff.should == [['~', 'b', "fizz buzz", "fizzBuzz"]] + diff.should == [['~', ['b'], "fizz buzz", "fizzBuzz"]] end it "should ignore case on nested strings before comparing" do a = { 'a' => { 'x' => "Foo" }, 'b' => ["fizz buzz", "nerf"] } b = { 'a' => { 'x' => "foo" }, 'b' => ["fizzbuzz", "nerf"] } diff = HashDiff.diff(a, b, :case_insensitive => true) - diff.should == [['-', 'b[0]', "fizz buzz"], ['+', 'b[0]', "fizzbuzz"]] + diff.should == [['-', ['b', 0], "fizz buzz"], ['+', ['b', 0], "fizzbuzz"]] end end @@ -269,7 +259,7 @@ a = { 'a' => " foo", 'b' => 35, 'c' => 'bar', 'd' => 'baz' } b = { 'a' => "foo", 'b' => 35.005, 'c' => 'bar', 'd' => 18.5} diff = HashDiff.diff(a, b, :strict => false, :numeric_tolerance => 0.01, :strip => true) - diff.should == [['~', 'd', "baz", 18.5]] + diff.should == [['~', ['d'], "baz", 18.5]] end end @@ -278,7 +268,7 @@ a = { 'a' => " Foo", 'b' => "fizz buzz"} b = { 'a' => "foo", 'b' => "fizzBuzz"} diff = HashDiff.diff(a, b, :case_insensitive => true, :strip => true) - diff.should == [['~', 'b', "fizz buzz", "fizzBuzz"]] + diff.should == [['~', ['b'], "fizz buzz", "fizzBuzz"]] end end @@ -287,21 +277,21 @@ let(:b) { { 'a' => 'bus', 'b' => 'truck', 'c' => ' plan'} } it 'should compare using proc specified in block' do - diff = HashDiff.diff(a, b) do |prefix, obj1, obj2| - case prefix + diff = HashDiff.diff(a, b) do |path, obj1, obj2| + case path[0] when /a|b|c/ obj1.length == obj2.length end end - diff.should == [['~', 'b', 'boat', 'truck']] + diff.should == [['~', ['b'], 'boat', 'truck']] end it 'should yield added keys' do x = { 'a' => 'car', 'b' => 'boat'} y = { 'a' => 'car' } - diff = HashDiff.diff(x, y) do |prefix, obj1, obj2| - case prefix + diff = HashDiff.diff(x, y) do |path, obj1, obj2| + case path[0] when /b/ true end @@ -310,13 +300,13 @@ end it 'should compare with both proc and :strip when both provided' do - diff = HashDiff.diff(a, b, :strip => true) do |prefix, obj1, obj2| - case prefix + diff = HashDiff.diff(a, b, :strip => true) do |path, obj1, obj2| + case path[0] when 'a' obj1.length == obj2.length end end - diff.should == [['~', 'b', 'boat', 'truck'], ['~', 'c', 'plane', ' plan']] + diff.should == [['~', ['b'], 'boat', 'truck'], ['~', ['c'], 'plane', ' plan']] end end end diff --git a/spec/hashdiff/patch_spec.rb b/spec/hashdiff/patch_spec.rb index 9d67bf2..6c0f5b0 100644 --- a/spec/hashdiff/patch_spec.rb +++ b/spec/hashdiff/patch_spec.rb @@ -145,17 +145,4 @@ HashDiff.unpatch!(b, diff).should == a end - it "should be able to patch hash value removal with custom delimiter" do - a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}} - b = {"a" => 1, "b" => {"b1" => 3} } - diff = HashDiff.diff(a, b, :delimiter => "\n") - - HashDiff.patch!(a, diff, :delimiter => "\n").should == b - - a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}} - b = {"a" => 1, "b" => {"b1" => 3} } - HashDiff.unpatch!(b, diff, :delimiter => "\n").should == a - end - - end diff --git a/spec/hashdiff/util_spec.rb b/spec/hashdiff/util_spec.rb index 2cc38fe..adc53ba 100644 --- a/spec/hashdiff/util_spec.rb +++ b/spec/hashdiff/util_spec.rb @@ -1,22 +1,6 @@ require 'spec_helper' describe HashDiff do - it "should be able to encode property path with dot delimiter" do - encoded = HashDiff.send(:encode_property_path, ['a', 'b', 0, 'c', 'city', 5], '.') - encoded.should == "a.b[0].c.city[5]" - end - - it "should be able to encode property path with tab delimiter" do - encoded = HashDiff.send(:encode_property_path, ['a', 'b', 0, 'c', 'city', 5], "\t") - encoded.should == "a\tb[0]\tc\tcity[5]" - end - - # -1 is used internally for custom comparison callback paths in an array - it "should encode property path with -1 into a *" do - encoded = HashDiff.send(:encode_property_path, ['a', -1, 'b', 0], '.') - encoded.should == "a[*].b[0]" - end - it "should be able to decode property path with dot delimiter" do decoded = HashDiff.send(:decode_property_path, "a.b[0].c.city[5]", '.') decoded.should == ['a', 'b', 0, 'c', 'city', 5]