Module: HashDiff

Defined in:
lib/hashdiff/patch.rb,
lib/hashdiff/lcs.rb,
lib/hashdiff/diff.rb,
lib/hashdiff/util.rb,
lib/hashdiff/version.rb

Overview

This module provides methods to diff two hash, patch and unpatch hash

Constant Summary collapse

VERSION =
'0.1.0'

Class Method Summary collapse

Class Method Details

.best_diff(obj1, obj2, options = {}) ⇒ Array

Best diff two objects, which tries to generate the smallest change set using different similarity values.

HashDiff.best_diff is useful in case of comparing two objects which includes similar hashes in array.

Examples:

a = {'x' => [{'a' => 1, 'c' => 3, 'e' => 5}, {'y' => 3}]}
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], ['-', 'x[1]', {}]]

Parameters:

  • obj1 (Arrary, Hash)
  • obj2 (Arrary, Hash)
  • options (Hash) (defaults to: {})

    ‘options` supports `:delimiter`. Default value for `:delimiter` is `.`(dot).

Returns:

  • (Array)

    an array of changes. e.g. [[ ‘+’, ‘a.b’, ‘45’ ], [ ‘-’, ‘a.c’, ‘5’ ], [ ‘~’, ‘a.x’, ‘45’, ‘63’]]

Since:

  • 0.0.1



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/hashdiff/diff.rb', line 22

def self.best_diff(obj1, obj2, options = {})
  opts = {similarity: 0.3}.merge!(options)
  diffs_1 = diff(obj1, obj2, opts)
  count_1 = count_diff diffs_1

  opts = {similarity: 0.5}.merge!(options)
  diffs_2 = diff(obj1, obj2, opts)
  count_2 = count_diff diffs_2

  opts = {similarity: 0.8}.merge!(options)
  diffs_3 = diff(obj1, obj2, opts)
  count_3 = count_diff diffs_3

  count, diffs = count_1 < count_2 ? [count_1, diffs_1] : [count_2, diffs_2]
  diffs = count < count_3 ? diffs : diffs_3
end

.diff(obj1, obj2, options = {}) ⇒ Array

Compute the diff of two hashes

Examples:

a = {"a" => 1, "b" => {"b1" => 1, "b2" =>2}}
b = {"a" => 1, "b" => {}}

diff = HashDiff.diff(a, b)
diff.should == [['-', 'b.b1', 1], ['-', 'b.b2', 2]]

Parameters:

  • obj1 (Arrary, Hash)
  • obj2 (Arrary, Hash)
  • options (Hash) (defaults to: {})

    ‘options` can contain `:similarity` or `:delimiter`.

    ‘:similarity` should be between (0, 1]. The default value is `0.8`. `:similarity` is meaningful if there’re similar hashes in arrays. See best_diff.

    ‘:delimiter` defaults to `.`(dot).

Returns:

  • (Array)

    an array of changes. e.g. [[ ‘+’, ‘a.b’, ‘45’ ], [ ‘-’, ‘a.c’, ‘5’ ], [ ‘~’, ‘a.x’, ‘45’, ‘63’]]

Since:

  • 0.0.1



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/hashdiff/diff.rb', line 61

def self.diff(obj1, obj2, options = {})
  opts = {
    :prefix      =>   '',
    :similarity  =>   0.8,
    :delimiter   =>   '.'
  }

  opts = opts.merge!(options)

  if obj1.nil? and obj2.nil?
    return []
  end

  if obj1.nil?
    return [['~', opts[:prefix], nil, obj2]]
  end

  if obj2.nil?
    return [['~', opts[:prefix], obj1, nil]]
  end

  if !(obj1.is_a?(Array) and obj2.is_a?(Array)) and !(obj1.is_a?(Hash) and obj2.is_a?(Hash)) and !(obj1.is_a?(obj2.class) or obj2.is_a?(obj1.class))
    return [['~', opts[:prefix], obj1, obj2]]
  end

  result = []
  if obj1.is_a?(Array)
    changeset = diff_array(obj1, obj2, opts[:similarity]) 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]]
      end
    end
  elsif obj1.is_a?(Hash)
    if opts[:prefix].empty?
      prefix = ""
    else
      prefix = "#{opts[:prefix]}#{opts[:delimiter]}"
    end

    deleted_keys = []
    common_keys = []

    obj1.each do |k, v|
      if obj2.key?(k)
        common_keys << k
      else
        deleted_keys << k
      end
    end

    # add deleted properties
    deleted_keys.each {|k| result << ['-', "#{prefix}#{k}", obj1[k]] }

    # recursive comparison for common keys
    common_keys.each {|k| result.concat(diff(obj1[k], obj2[k], opts.merge(prefix: "#{prefix}#{k}"))) }

    # added properties
    obj2.each do |k, v|
      unless obj1.key?(k)
        result << ['+', "#{prefix}#{k}", obj2[k]]
      end
    end
  else
    return [] if obj1 == obj2
    return [['~', opts[:prefix], obj1, obj2]]
  end

  result
end

.patch!(obj, changes, options = {}) ⇒ Object

Apply patch to object

Parameters:

  • obj (Hash, Array)

    the object to be patchted, can be an Array of a Hash

  • changes (Array)

    e.g. [[ ‘+’, ‘a.b’, ‘45’ ], [ ‘-’, ‘a.c’, ‘5’ ], [ ‘~’, ‘a.x’, ‘45’, ‘63’]]

  • options (Hash) (defaults to: {})

    ‘options` supports `:delimiter`. Default value for `:delimiter` is `.`(dot).

Returns:

  • the object after patch

Since:

  • 0.0.1



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/hashdiff/patch.rb', line 16

def self.patch!(obj, changes, options = {})
  delimiter = options[:delimiter] || '.'

  changes.each do |change|
    parts = 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?(Fixnum)
        parent_node.insert(last_part, change[2])
      else
        parent_node[last_part] = change[2]
      end
    elsif change[0] == '-'
      if last_part.is_a?(Fixnum)
        parent_node.delete_at(last_part)
      else
        parent_node.delete(last_part)
      end
    elsif change[0] == '~'
      parent_node[last_part] = change[3]
    end
  end

  obj
end

.unpatch!(obj, changes, options = {}) ⇒ Object

Unpatch an object

Parameters:

  • obj (Hash, Array)

    the object to be unpatchted, can be an Array of a Hash

  • changes (Array)

    e.g. [[ ‘+’, ‘a.b’, ‘45’ ], [ ‘-’, ‘a.c’, ‘5’ ], [ ‘~’, ‘a.x’, ‘45’, ‘63’]]

  • options (Hash) (defaults to: {})

    ‘options` supports `:delimiter`. Default value for `:delimiter` is `.`(dot).

Returns:

  • the object after unpatch

Since:

  • 0.0.1



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/hashdiff/patch.rb', line 55

def self.unpatch!(obj, changes, options = {})
  delimiter = options[:delimiter] || '.'

  changes.reverse_each do |change|
    parts = 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?(Fixnum)
        parent_node.delete_at(last_part)
      else
        parent_node.delete(last_part)
      end
    elsif change[0] == '-'
      if last_part.is_a?(Fixnum)
        parent_node.insert(last_part, change[2])
      else
        parent_node[last_part] = change[2]
      end
    elsif change[0] == '~'
      parent_node[last_part] = change[2]
    end
  end

  obj
end