File Manager
Viewing File: linear_compare_array.rb
# frozen_string_literal: true
module Hashdiff
# @private
#
# Used to compare arrays in a linear complexity, which produces longer diffs
# than using the lcs algorithm but is considerably faster
class LinearCompareArray
def self.call(old_array, new_array, options = {})
instance = new(old_array, new_array, options)
instance.call
end
def call
return [] if old_array.empty? && new_array.empty?
self.old_index = 0
self.new_index = 0
# by comparing the array lengths we can expect that a number of items
# are either added or removed
self.expected_additions = new_array.length - old_array.length
loop do
if extra_items_in_old_array?
append_deletion(old_array[old_index], old_index)
elsif extra_items_in_new_array?
append_addition(new_array[new_index], new_index)
else
compare_at_index
end
self.old_index = old_index + 1
self.new_index = new_index + 1
break if iterated_through_both_arrays?
end
changes
end
private
attr_reader :old_array, :new_array, :options, :additions, :deletions, :differences
attr_accessor :old_index, :new_index, :expected_additions
def initialize(old_array, new_array, options)
@old_array = old_array
@new_array = new_array
@options = { prefix: '' }.merge!(options)
@additions = []
@deletions = []
@differences = []
end
def extra_items_in_old_array?
old_index < old_array.length && new_index >= new_array.length
end
def extra_items_in_new_array?
new_index < new_array.length && old_index >= old_array.length
end
def iterated_through_both_arrays?
old_index >= old_array.length && new_index >= new_array.length
end
def compare_at_index
difference = item_difference(old_array[old_index], new_array[new_index], old_index)
return if difference.empty?
index_after_additions = index_of_match_after_additions
append_addititions_before_match(index_after_additions)
index_after_deletions = index_of_match_after_deletions
append_deletions_before_match(index_after_deletions)
match = index_after_additions || index_after_deletions
append_differences(difference) unless match
end
def item_difference(old_item, new_item, item_index)
prefix = Hashdiff.prefix_append_array_index(options[:prefix], item_index, options)
Hashdiff.diff(old_item, new_item, options.merge(prefix: prefix))
end
# look ahead in the new array to see if the current item appears later
# thereby having new items added
def index_of_match_after_additions
return unless expected_additions > 0
(1..expected_additions).each do |i|
next_difference = item_difference(
old_array[old_index],
new_array[new_index + i],
old_index
)
return new_index + i if next_difference.empty?
end
nil
end
# look ahead in the old array to see if the current item appears later
# thereby having items removed
def index_of_match_after_deletions
return unless expected_additions < 0
(1..(expected_additions.abs)).each do |i|
next_difference = item_difference(
old_array[old_index + i],
new_array[new_index],
old_index
)
return old_index + i if next_difference.empty?
end
nil
end
def append_addititions_before_match(match_index)
return unless match_index
(new_index...match_index).each { |i| append_addition(new_array[i], i) }
self.expected_additions = expected_additions - (match_index - new_index)
self.new_index = match_index
end
def append_deletions_before_match(match_index)
return unless match_index
(old_index...match_index).each { |i| append_deletion(old_array[i], i) }
self.expected_additions = expected_additions + (match_index - new_index)
self.old_index = match_index
end
def append_addition(item, index)
key = Hashdiff.prefix_append_array_index(options[:prefix], index, options)
additions << ['+', key, item]
end
def append_deletion(item, index)
key = Hashdiff.prefix_append_array_index(options[:prefix], index, options)
deletions << ['-', key, item]
end
def append_differences(difference)
differences.concat(difference)
end
def changes
# this algorithm only allows there to be additions or deletions
# deletions are reverse so they don't change the index of earlier items
differences + additions + deletions.reverse
end
end
end