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.0.4'

Class Method Summary collapse

Class Method Details

.best_diff(obj1, obj2) ⇒ Array

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

HashDiff.best_diff is only meaningful in case of comparing two objects which includes similar objects 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)

Returns:

  • (Array)

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

Since:

  • 0.0.1



20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/hashdiff/diff.rb', line 20

def self.best_diff(obj1, obj2)
  diffs_1 = diff(obj1, obj2, "", 0.3)
  count_1 = count_diff diffs_1

  diffs_2 = diff(obj1, obj2, "", 0.5)
  count_2 = count_diff diffs_2

  diffs_3 = diff(obj1, obj2, "", 0.8)
  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, prefix = "", similarity = 0.8) ⇒ 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)
  • similarity (float) (defaults to: 0.8)

    A value > 0 and <= 1. This parameter should be ignored in common usage. Similarity is only meaningful if there’re similar objects in arrays. See best_diff.

Returns:

  • (Array)

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

Since:

  • 0.0.1



53
54
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
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
# File 'lib/hashdiff/diff.rb', line 53

def self.diff(obj1, obj2, prefix = "", similarity = 0.8)
  if obj1.nil? and obj2.nil?
    return []
  end

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

  if obj2.nil?
    return [['~', 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 [['~', prefix, obj1, obj2]]
  end

  result = []
  if obj1.is_a?(Array)
    changeset = diff_array(obj1, obj2, similarity) do |lcs|
      # use a's index for similarity
      lcs.each do |pair|
        result.concat(diff(obj1[pair[0]], obj2[pair[1]], "#{prefix}[#{pair[0]}]", similarity))
      end
    end

    changeset.each do |change|
      if change[0] == '-'
        result << ['-', "#{prefix}[#{change[1]}]", change[2]]
      elsif change[0] == '+'
        result << ['+', "#{prefix}[#{change[1]}]", change[2]]
      end
    end
  elsif obj1.is_a?(Hash)
    prefix = prefix.empty? ? "" : "#{prefix}."

    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], "#{prefix}#{k}", similarity)) }

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

  result
end

.patch!(obj, changes) ⇒ 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’]]

Returns:

  • the object after patch

Since:

  • 0.0.1



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

def self.patch!(obj, changes)
  changes.each do |change|
    parts = decode_property_path(change[1])
    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) ⇒ 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’]]

Returns:

  • the object after unpatch

Since:

  • 0.0.1



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/hashdiff/patch.rb', line 49

def self.unpatch!(obj, changes)
  changes.reverse_each do |change|
    parts = decode_property_path(change[1])
    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