class SuperDiff::OperationTreeBuilders::Hash
Public Class Methods
applies_to?(expected, actual)
click to toggle source
# File lib/super_diff/operation_tree_builders/hash.rb, line 4 def self.applies_to?(expected, actual) expected.is_a?(::Hash) && actual.is_a?(::Hash) end
Protected Instance Methods
build_operation_tree()
click to toggle source
# File lib/super_diff/operation_tree_builders/hash.rb, line 14 def build_operation_tree OperationTrees::Hash.new([]) end
unary_operations()
click to toggle source
# File lib/super_diff/operation_tree_builders/hash.rb, line 10 def unary_operations unary_operations_using_variant_of_patience_algorithm end
Private Instance Methods
all_keys()
click to toggle source
# File lib/super_diff/operation_tree_builders/hash.rb, line 214 def all_keys actual.keys | expected.keys end
should_add_noop_operation?(key)
click to toggle source
# File lib/super_diff/operation_tree_builders/hash.rb, line 210 def should_add_noop_operation?(key) expected.include?(key) && expected[key] == actual[key] end
unary_operations_using_variant_of_patience_algorithm()
click to toggle source
# File lib/super_diff/operation_tree_builders/hash.rb, line 20 def unary_operations_using_variant_of_patience_algorithm operations = [] aks, eks = actual.keys, expected.keys previous_ei, ei = nil, 0 ai = 0 # When diffing a hash, we're more interested in the 'actual' version # than the 'expected' version, because that's the ultimate truth. # Therefore, the diff is presented from the perspective of the 'actual' # hash, and we start off by looping over it. while ai < aks.size ak = aks[ai] av, ev = actual[ak], expected[ak] # While we iterate over 'actual' in order, we jump all over # 'expected', trying to match up its keys with the keys in 'actual' as # much as possible. ei = eks.index(ak) if should_add_noop_operation?(ak) # (If we're here, it probably means that the key we're pointing to # in the 'actual' and 'expected' hashes have the same value.) if ei && previous_ei && (ei - previous_ei) > 1 # If we've jumped from one operation in the 'expected' hash to # another operation later in 'expected' (due to the fact that the # 'expected' hash is in a different order than 'actual'), collect # any delete operations in between and add them to our operations # array as deletes before adding the noop. If we don't do this # now, then those deletes will disappear. (Again, we are mainly # iterating over 'actual', so this is the only way to catch all of # the keys in 'expected'.) (previous_ei + 1).upto(ei - 1) do |ei2| ek = eks[ei2] ev2, av2 = expected[ek], actual[ek] if ( (!actual.include?(ek) || ev2 != av2) && operations.none? { |operation| [:delete, :noop].include?(operation.name) && operation.key == ek } ) operations << Operations::UnaryOperation.new( name: :delete, collection: expected, key: ek, value: ev2, index: ei2, ) end end end operations << Operations::UnaryOperation.new( name: :noop, collection: actual, key: ak, value: av, index: ai, ) else # (If we're here, it probably means that the key in 'actual' isn't # present in 'expected' or the values don't match.) if ( (operations.empty? || operations.last.name == :noop) && (ai == 0 || eks.include?(aks[ai - 1])) ) # If we go from a match in the last iteration to a missing or # extra key in this one, or we're at the first key in 'actual' and # it's missing or extra, look for deletes in the 'expected' hash # and add them to our list of operations before we add the # inserts. In most cases we will accomplish this by backtracking a # bit to the key in 'expected' that matched the key in 'actual' we # processed in the previous iteration (or just the first key in # 'expected' if this is the first key in 'actual'), and then # iterating from there through 'expected' until we reach the end # or we hit some other condition (see below). start_index = if ai > 0 eks.index(aks[ai - 1]) + 1 else 0 end start_index.upto(eks.size - 1) do |ei2| ek = eks[ei2] ev, av2 = expected[ek], actual[ek] if actual.include?(ek) && ev == av2 # If the key in 'expected' we've landed on happens to be a # match in 'actual', then stop, because it's going to be # handled in some future iteration of the 'actual' loop. break elsif ( aks[ai + 1 .. -1].any? { |k| expected.include?(k) && expected[k] != actual[k] } ) # While we backtracked a bit to iterate over 'expected', we # now have to look ahead. If we will end up encountering a # insert that matches this delete later, stop and go back to # iterating over 'actual'. This is because the delete we would # have added now will be added later when we encounter the # associated insert, so we don't want to add it twice. break else operations << Operations::UnaryOperation.new( name: :delete, collection: expected, key: ek, value: ev, index: ei2, ) end if ek == ak && ev != av # If we're pointing to the same key in 'expected' as in # 'actual', but with different values, go ahead and add an # insert now to accompany the delete added above. That way # they appear together, which will be easier to read. operations << Operations::UnaryOperation.new( name: :insert, collection: actual, key: ak, value: av, index: ai, ) end end end if ( expected.include?(ak) && ev != av && operations.none? { |op| op.name == :delete && op.key == ak } ) # If we're here, it means that we didn't encounter any delete # operations above for whatever reason and so we need to add a # delete to represent the fact that the value for this key has # changed. operations << Operations::UnaryOperation.new( name: :delete, collection: expected, key: ak, value: expected[ak], index: ei, ) end if operations.none? { |op| op.name == :insert && op.key == ak } # If we're here, it means that we didn't encounter any insert # operations above. Since we already handled delete, the only # alternative is that this key must not exist in 'expected', so # we need to add an insert. operations << Operations::UnaryOperation.new( name: :insert, collection: actual, key: ak, value: av, index: ai, ) end end ai += 1 previous_ei = ei end # The last thing to do is this: if there are keys in 'expected' that # aren't in 'actual', and they aren't associated with any inserts to # where they would have been added above, tack those deletes onto the # end of our operations array. (eks - aks - operations.map(&:key)).each do |ek| ei = eks.index(ek) ev = expected[ek] operations << Operations::UnaryOperation.new( name: :delete, collection: expected, key: ek, value: ev, index: ei, ) end operations end