## # Myers 比较排序算法 ## module Aio::Base::Toolkit module Diff Line = Struct.new(:number, :text, :diff?) do def inspect text.to_s end end def self.lines(document, lines) # document = document.lines if document.is_a?(String) document.map.with_index do |text, i| bool = lines.include?(i) Line.new(i + 1, text, bool) end end def self.empty_line Line.new(0, '', false) end # CompareDiff def self.diff(cd, differ: Myers) differ.diff( lines(cd.content, cd.lines), lines(cd.content_compare, cd.lines_compare) ) end end class Myers def self.diff(a, b) new(a, b).diff end def initialize(a, b) @a, @b = a, b end def shortest_edit n, m = @a.size, @b.size max = n + m v = ::Array.new(2 * max + 1) v[1] = 0 trace = [] (0..max).step do |d| trace << v.clone (-d..d).step(2) do |k| if k == -d or (k != d and v[k-1] < v[k+1]) x = v[k+1] else x = v[k-1] + 1 end y = x - k # while x < n and y < m and @a[x].text == @b[y].text while x < n and y < m and !@a[x].diff? and !@b[y].diff? x, y = x + 1, y + 1 end v[k] = x return trace if x >= n and y >= m end end end def backtrack x, y = @a.size, @b.size shortest_edit.each_with_index.reverse_each do |v, d| k = x - y if k == -d or (k != d and v[k-1] < v[k+1]) prev_k = k + 1 else prev_k = k - 1 end prev_x = v[prev_k] prev_y = prev_x - prev_k while x > prev_x and y > prev_y yield x - 1, y - 1, x, y x, y = x - 1, y - 1 end yield prev_x, prev_y, x, y if d > 0 x, y = prev_x, prev_y end end def diff diff = [] backtrack do |prev_x, prev_y, x, y| a_line, b_line = @a[prev_x], @b[prev_y] if x == prev_x diff.unshift(Diff::Edit.new(:ins, nil, b_line)) elsif y == prev_y diff.unshift(Diff::Edit.new(:del, a_line, nil)) else diff.unshift(Diff::Edit.new(:eql, a_line, b_line)) end end # 此时得到的是按顺序排列的数组 diff end end module Diff Edit = Struct.new(:type, :old_line, :new_line) do def old_number return '' if old_line.number.to_s == '0' old_line ? old_line.number.to_s : '' end def new_number return '' if new_line.number.to_s == '0' new_line ? new_line.number.to_s : '' end def text (old_line || new_line).text end def old_text old_line.text end def new_text new_line.text end def empty? (old_line.empty? and new_line.empty?) end def type_class type.to_s + '_style' end def type_class_con type.to_s + '_style_con' end def self.empty_line Edit.new(:eql, Diff.empty_line, Diff.empty_line) end end end end