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

Class Method Summary collapse

Class Method Details

.best_diff(obj1, obj2) ⇒ Array

Best diff two objects, which tries to generate the smallest change set.

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

def self.best_diff(obj1, obj2)
  diffs_1 = diff(obj1, obj2, "", 0.3)
  diffs_2 = diff(obj1, obj2, "", 0.5)
  diffs_3 = diff(obj1, obj2, "", 0.8)

  diffs = diffs_1.size < diffs_2.size ? diffs_1 : diffs_2
  diffs = diffs.size < diffs_3.size ? 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



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

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

  if obj1.nil?
    return [['-', prefix, nil]] + changed(obj2, '+', prefix)
  end

  if obj2.nil?
    return changed(obj1, '-', prefix) + [['+', prefix, 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 changed(obj1, '-', prefix) + changed(obj2, '+', prefix)
  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.concat(changed(change[2], '-', "#{prefix}[#{change[1]}]"))
      elsif change[0] == '+'
        result.concat(changed(change[2], '+', "#{prefix}[#{change[1]}]"))
      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.concat(changed(obj1[k], '-', "#{prefix}#{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.concat(changed(obj2[k], '+', "#{prefix}#{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
40
41
42
43
44
45
46
47
48
49
# 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

    dest_node = node(obj, parts[0, parts.size-1])

    if change[0] == '+'
      if dest_node == nil
        parent_key = parts[parts.size-2]
        parent_node = node(obj, parts[0, parts.size-2])
        if last_part.is_a?(Fixnum)
          dest_node = parent_node[parent_key] = []
        else
          dest_node = parent_node[parent_key] = {}
        end
      end

      if last_part.is_a?(Fixnum)
        dest_node.insert(last_part, change[2])
      else
        dest_node[last_part] = change[2]
      end
    elsif change[0] == '-'
      if last_part.is_a?(Fixnum)
        dest_node.delete_at(last_part)
      else
        dest_node.delete(last_part)
      end
    elsif change[0] == '~'
      dest_node[last_part] = change[3]
    end
  end

  obj
end

.unpatch!(hash, 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



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
# File 'lib/hashdiff/patch.rb', line 59

def self.unpatch!(hash, changes)
  changes.reverse_each do |change|
    parts = decode_property_path(change[1])
    last_part = parts.last

    dest_node = node(hash, parts[0, parts.size-1])

    if change[0] == '+'
      if last_part.is_a?(Fixnum)
        dest_node.delete_at(last_part)
      else
        dest_node.delete(last_part)
      end
    elsif change[0] == '-'
      if dest_node == nil
        parent_key = parts[parts.size-2]
        parent_node = node(hash, parts[0, parts.size-2])
        if last_part.is_a?(Fixnum)
          dest_node = parent_node[parent_key] = []
        else
          dest_node = parent_node[parent_key] = {}
        end
      end

      if last_part.is_a?(Fixnum)
        dest_node.insert(last_part, change[2])
      else
        dest_node[last_part] = change[2]
      end
    elsif change[0] == '~'
      dest_node[last_part] = change[2]
    end
  end

  hash
end