module LCS
## How Diff Works (by Mark-Jason Dominus)
I once read an article written by the authors of ‘diff`; they said that they hard worked very hard on the algorithm until they found the right one.
I think what they ended up using (and I hope someone will correct me, because I am not very confident about this) was the ‘longest common subsequence’ method. In the LCS problem, you have two sequences of items:
“‘ a b c d f g h j q z a b c d e f g i j k r x y z “`
and you want to find the longest sequence of items that is present in both original sequences in the same order. That is, you want to find a new sequence S which can be obtained from the first sequence by deleting some items, and from the second sequence by deleting other items. You also want S to be as long as possible. In this case S is:
“‘ a b c d f g j z “`
From there it’s only a small step to get diff-like output:
“‘ e h i k q r x y + - + + - + + + “`
This module solves the LCS problem. It also includes a canned function to generate ‘diff`-like output.
It might seem from the example above that the LCS of two sequences is always pretty obvious, but that’s not always the case, especially when the two sequences have many repeated elements. For example, consider
“‘ a x b y c z p d q a b c a x b y c z “`
A naive approach might start by matching up the ‘a` and `b` that appear at the beginning of each sequence, like this:
“‘ a x b y c z p d q a b c a b y c z “`
This finds the common subsequence ‘a b c z`. But actually, the LCS is `a x b y c z`:
“‘
a x b y c z p d q
a b c a x b y c z “‘
Constants
- BalancedCallbacks
-
This callback object implements the default set of callback events, which only returns the event itself.
“‘ruby
Diff::LCS.lcs(seq1, seq2,Diff::LCS::DefaultCallbacks) “` - SequenceCallbacks
-
This callback object implements the default set of callback events, which only returns the event itself.
“‘ruby
Diff::LCS.lcs(seq1, seq2,Diff::LCS::DefaultCallbacks) “` - VERSION
Public Class Methods
‘diff` computes the smallest set of additions and deletions necessary to turn the first sequence into the second, and returns a description of these changes.
See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be implemented with Diff::LCS::ContextDiffCallbacks. If the ‘callbacks` object responds to finish, it will be called.
# File lib/diff/lcs.rb, line 160 def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset) = patch(src, patchset
Returns an Array containing the longest common subsequence(s) between ‘seq` and `seq2`.
> NOTE on comparing objects: Diff::LCS only works properly when each object can be > used as a key in a Hash. This means that those objects must implement the methods > ‘#hash` and `#eql?` such that two objects containing identical values compare > identically for key purposes. That is: > > “` > O.new(’a’).eql?(O.new(‘a’)) == true && O.new(‘a’).hash == O.new(‘a’).hash > “‘
# File lib/diff/lcs.rb, line 140 def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end
Applies a ‘patchset` to the sequence `src` according to the `direction` (`:patch` or `:unpatch`), producing a new sequence.
If the ‘direction` is not specified, Diff::LCS::patch will attempt to discover the direction of the `patchset`.
A ‘patchset` can be considered to apply forward (`:patch`) if the following expression is true:
“‘ruby patch(s1, diff(s1, s2)) # => s2 “`
A ‘patchset` can be considered to apply backward (`:unpatch`) if the following expression is true:
“‘ruby patch(s2, diff(s1, s2)) # => s1 “`
If the ‘patchset` contains no changes, the `src` value will be returned as either `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following predicate returns true:
“‘ruby patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } “`
### Patchsets
A ‘patchset` is always an enumerable sequence of changes, hunks of changes, or a mix of the two. A hunk of changes is an enumerable sequence of changes:
“‘ [ # patchset
# change [ # hunk # change ]
] “‘
The ‘patch` method accepts `patchset`s that are enumerable sequences containing either Diff::LCS::Change objects (or a subclass) or the array representations of those objects. Prior to application, array representations of Diff::LCS::Change objects will be reified.
# File lib/diff/lcs.rb, line 607 def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end
Given a patchset, convert the current version to the next version. Does no auto-discovery.
# File lib/diff/lcs.rb, line 714 def self.patch!(src, patchset) = patch(src, patchset, :patch) end
‘sdiff` computes all necessary components to show two sequences and their minimized differences side by side, just like the Unix utility sdiff does:
“‘ old < - same same before | after
-
> new
“‘
See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be implemented with Diff::LCS::ContextDiffCallbacks. If the ‘callbacks` object responds to finish, it will be called.
Each element of a returned array is a Diff::LCS::ContextChange object, which can be implicitly converted to an array.
“‘ruby Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)|
case action when '!' # replace when '-' # delete when '+' # insert end
end “‘
# File lib/diff/lcs.rb, line 193 def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset) = patch(src, patchset,
traverse_balanced is an alternative to traverse_sequences. It uses a different algorithm to iterate through the entries in the computed longest common subsequence. Instead of viewing the changes as insertions or deletions from one of the sequences, traverse_balanced will report changes between the sequences.
The arguments to traverse_balanced are the two sequences to traverse and a callback object, like this:
“‘ruby traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) “`
sdiff is implemented using traverse_balanced.
### Callback Methods
-
‘callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` and `B`.
-
‘callbacks#discard_a`: Called when `a` is pointing to an element not in `B`.
-
‘callbacks#discard_b`: Called when `b` is pointing to an element not in `A`.
-
‘callbacks#change`: Called when `a` and `b` are pointing to the same relative position, but `A` and `B` are not the same; a change has occurred. Optional.
traverse_balanced might be a bit slower than traverse_sequences, noticeable only while processing large amounts of data.
### Algorithm
“‘ a—+
v
A = a b c e h j l m n p B = b c d e f j k l m r s t
^
b—+ “‘
#### Matches
If there are two arrows (‘a` and `b`) pointing to elements of sequences `A` and `B`, the arrows will initially point to the first elements of their respective sequences. traverse_sequences will advance the arrows through the sequences one element at a time, calling a method on the user-specified callback object before each advance. It will advance the arrows in such a way that if there are elements `A` and `B` which are both equal and part of the longest common subsequence, there will be some moment during the execution of traverse_sequences when arrow `a` is pointing to `A` and arrow `b` is pointing to `B`. When this happens, traverse_sequences will call `callbacks#match` and then it will advance both arrows.
#### Discards
Otherwise, one of the arrows is pointing to an element of its sequence that is not part of the longest common subsequence. traverse_sequences will advance that arrow and will call ‘callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow it advanced.
#### Changes
If both ‘a` and `b` point to elements that are not part of the longest common subsequence, then traverse_sequences will try to call `callbacks#change` and advance both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and `callbacks#discard_b` will be called in turn.
The methods for ‘callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and `callbacks#change` are invoked with an event comprising the action (“=”, “+”, “-”, or “!”, respectively), the indexes `i` and `j`, and the elements `A` and `B`. Return values are discarded by traverse_balanced.
Context¶ ↑
Note that ‘i` and `j` may not be the same index position, even if `a` and `b` are considered to be pointing to matching or changed elements.
# File lib/diff/lcs.rb, line 450 def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end
traverse_sequences is the most general facility provided by this module; diff and lcs are implemented using traverse_sequences.
The arguments to traverse_sequence are the two sequences to traverse, and a callback object, like this:
“‘ruby traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) “`
### Callback Methods
-
‘callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` and `B`.
-
‘callbacks#discard_a`: Called when `a` is pointing to an element not in `B`.
-
‘callbacks#discard_b`: Called when `b` is pointing to an element not in `A`.
-
‘callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. Optional.
-
‘callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. Optional.
### Algorithm
“‘ a—+
v
A = a b c e h j l m n p B = b c d e f j k l m r s t
^
b—+ “‘
If there are two arrows (‘a` and `b`) pointing to elements of sequences `A` and `B`, the arrows will initially point to the first elements of their respective sequences. traverse_sequences will advance the arrows through the sequences one element at a time, calling a method on the user-specified callback object before each advance. It will advance the arrows in such a way that if there are elements `A` and `B` which are both equal and part of the longest common subsequence, there will be some moment during the execution of traverse_sequences when arrow `a` is pointing to `A` and arrow `b` is pointing to `B`. When this happens, traverse_sequences will call `callbacks#match` and then it will advance both arrows.
Otherwise, one of the arrows is pointing to an element of its sequence that is not part of the longest common subsequence. traverse_sequences will advance that arrow and will call ‘callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow it advanced. If both arrows point to elements that are not part of the longest common subsequence, then traverse_sequences will advance arrow `a` and call the appropriate callback, then it will advance arrow `b` and call the appropriate callback.
The methods for ‘callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` are invoked with an event comprising the action (“=”, “+”, or “-”, respectively), the indexes `i` and `j`, and the elements `A` and `B`. Return values are discarded by traverse_sequences.
#### End of Sequences
If arrow ‘a` reaches the end of its sequence before arrow `b` does, traverse_sequence will try to call `callbacks#finished_a` with the last index and element of `A` (`A`) and the current index and element of `B` (`B`). If `callbacks#finished_a` does not exist, then `callbacks#discard_b` will be called on each element of `B` until the end of the sequence is reached (the call will be done with `A` and `B` for each element).
If ‘b` reaches the end of `B` before `a` reaches the end of `A`, `callbacks#finished_b` will be called with the current index and element of `A` (`A`) and the last index and element of `B` (`A`). Again, if `callbacks#finished_b` does not exist on the callback object, then `callbacks#discard_a` will be called on each element of `A` until the end of the sequence is reached (`A` and `B`).
There is a chance that one additional ‘callbacks#discard_a` or `callbacks#discard_b` will be called after the end of the sequence is reached, if `a` has not yet reached the end of `A` or `b` has not yet reached the end of `B`.
# File lib/diff/lcs.rb, line 269 def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end
Given a patchset, convert the current version to the prior version. Does no auto-discovery.
# File lib/diff/lcs.rb, line 710 def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset) = patch(src, patchset, :patch)
Public Instance Methods
Returns the difference set between ‘self` and `other`. See Diff::LCS.diff.
# File lib/diff/lcs.rb, line 75 def diff(other, callbacks = nil, &block) = Diff::LCS.diff(self, other, callbacks, &block) # Returns the balanced ("side-by-side") difference set between `self` and `other`. See # Diff::LCS.sdiff. def sdiff(other, callbacks = nil, &block) = Diff::LCS.sdiff(self, other, callbacks, &block) # Traverses the discovered longest common subsequences between `self` and `other`. See # Diff::LCS.traverse_sequences. def traverse_sequences(other, callbacks = nil, &block) = Diff::LCS.traverse_sequences(self, other, callbacks || Diff::LCS::SequenceCallbacks, &block) # Traverses the discovered longest common subsequences between `self` and `other` using # the alternate, balanced algorithm. See Diff::LCS.traverse_balanced. def traverse_balanced(other, callbacks = nil, &block) = Diff::LCS.traverse_balanced(self, other, callbacks || Diff::LCS::BalancedCallbacks, &block) # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch. Attempts to autodiscover the # direction of the patch. def patch(patchset) = Diff::LCS.patch(self, patchset) alias_method :unpatch, :patch # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction # autodiscovery. def patch!(patchset) = Diff::LCS.patch!(self, patchset) # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch # direction autodiscovery. def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(
Returns an Array containing the longest common subsequence(s) between ‘self` and `other`. See Diff::LCS.lcs.
# File lib/diff/lcs.rb, line 71 def lcs(other, &block) = # :yields: self[i] if there are matched subsequences Diff::LCS.lcs(self, other, &block) # Returns the difference set between `self` and `other`. See Diff::LCS.diff. def diff(other, callbacks = nil, &block) = Diff::LCS.diff(self, other, callbacks, &block) # Returns the balanced ("side-by-side") difference set between `self` and `other`. See # Diff::LCS.sdiff. def sdiff(other, callbacks = nil, &block) = Diff::LCS.sdiff(self, other, callbacks, &block) # Traverses the discovered longest common subsequences between `self` and `other`. See # Diff::LCS.traverse_sequences. def traverse_sequences(other, callbacks = nil, &block) = Diff::LCS.traverse_sequences(self, other, callbacks || Diff::LCS::SequenceCallbacks, &block) # Traverses the discovered longest common subsequences between `self` and `other` using # the alternate, balanced algorithm. See Diff::LCS.traverse_balanced. def traverse_balanced(other, callbacks = nil, &block) = Diff::LCS.traverse_balanced(self, other, callbacks || Diff::LCS::BalancedCallbacks, &block) # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch. Attempts to autodiscover the # direction of the patch. def patch(patchset) = Diff::LCS.patch(self, patchset) alias_method :unpatch, :patch # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction # autodiscovery. def patch!(patchset) = Diff::LCS.patch!(self, patchset) # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch # direction autodiscovery. def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.
Attempts to patch ‘self` with the provided `patchset`. A new sequence based on `self` and the `patchset` will be created. See Diff::LCS.patch. Attempts to autodiscover the direction of the patch.
# File lib/diff/lcs.rb, line 94 def patch(patchset) = Diff::LCS.patch(self, patchset) alias_method :unpatch, :patch # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction # autodiscovery. def patch!(patchset) = Diff::LCS.patch!(self, patchset) # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch # direction autodiscovery. def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset) =
Attempts to patch ‘self` with the provided `patchset`. A new sequence based on `self` and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction autodiscovery.
# File lib/diff/lcs.rb, line 100 def patch!(patchset) = Diff::LCS.patch!(self, patchset) # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch # direction autodiscovery. def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset) = patch(
Attempts to patch ‘self` with the provided `patchset`, using patch!. If the sequence this is used on supports replace, the value of `self` will be replaced. See Diff::LCS.patch!. Does no patch direction autodiscovery.
# File lib/diff/lcs.rb, line 110 def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end
Returns the balanced (“side-by-side”) difference set between ‘self` and `other`. See Diff::LCS.sdiff.
# File lib/diff/lcs.rb, line 79 def sdiff(other, callbacks = nil, &block) = Diff::LCS.sdiff(self, other, callbacks, &block) # Traverses the discovered longest common subsequences between `self` and `other`. See # Diff::LCS.traverse_sequences. def traverse_sequences(other, callbacks = nil, &block) = Diff::LCS.traverse_sequences(self, other, callbacks || Diff::LCS::SequenceCallbacks, &block) # Traverses the discovered longest common subsequences between `self` and `other` using # the alternate, balanced algorithm. See Diff::LCS.traverse_balanced. def traverse_balanced(other, callbacks = nil, &block) = Diff::LCS.traverse_balanced(self, other, callbacks || Diff::LCS::BalancedCallbacks, &block) # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch. Attempts to autodiscover the # direction of the patch. def patch(patchset) = Diff::LCS.patch(self, patchset) alias_method :unpatch, :patch # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction # autodiscovery. def patch!(patchset) = Diff::LCS.patch!(self, patchset) # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch # direction autodiscovery. def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src,
Traverses the discovered longest common subsequences between ‘self` and `other` using the alternate, balanced algorithm. See Diff::LCS.traverse_balanced.
# File lib/diff/lcs.rb, line 88 def traverse_balanced(other, callbacks = nil, &block) = Diff::LCS.traverse_balanced(self, other, callbacks || Diff::LCS::BalancedCallbacks, &block) # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch. Attempts to autodiscover the # direction of the patch. def patch(patchset) = Diff::LCS.patch(self, patchset) alias_method :unpatch, :patch # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction # autodiscovery. def patch!(patchset) = Diff::LCS.patch!(self, patchset) # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch # direction autodiscovery. def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset)
Traverses the discovered longest common subsequences between ‘self` and `other`. See Diff::LCS.traverse_sequences.
# File lib/diff/lcs.rb, line 83 def traverse_sequences(other, callbacks = nil, &block) = Diff::LCS.traverse_sequences(self, other, callbacks || Diff::LCS::SequenceCallbacks, &block) # Traverses the discovered longest common subsequences between `self` and `other` using # the alternate, balanced algorithm. See Diff::LCS.traverse_balanced. def traverse_balanced(other, callbacks = nil, &block) = Diff::LCS.traverse_balanced(self, other, callbacks || Diff::LCS::BalancedCallbacks, &block) # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch. Attempts to autodiscover the # direction of the patch. def patch(patchset) = Diff::LCS.patch(self, patchset) alias_method :unpatch, :patch # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` # and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction # autodiscovery. def patch!(patchset) = Diff::LCS.patch!(self, patchset) # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch # direction autodiscovery. def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset
Attempts to unpatch ‘self` with the provided `patchset`. A new sequence based on `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch direction autodiscovery.
# File lib/diff/lcs.rb, line 105 def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence # this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) else patch!(patchset) end end # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the # sequence this is used on supports #replace, the value of `self` will be replaced. See # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end # Returns an Array containing the longest common subsequence(s) between `seq` and # `seq2`. # # > NOTE on comparing objects: Diff::LCS only works properly when each object can be # > used as a key in a Hash. This means that those objects must implement the methods # > `#hash` and `#eql?` such that two objects containing identical values compare # > identically for key purposes. That is: # > # > ``` # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash # > ``` def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) [].tap { |result| matches.each_index do next if matches[_1].nil? v = seq1[_1] v = block.call(v) if block result << v end } end # `diff` computes the smallest set of additions and deletions necessary to turn the # first sequence into the second, and returns a description of these changes. # # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) # `sdiff` computes all necessary components to show two sequences and their minimized # differences side by side, just like the Unix utility _sdiff_ does: # # # ``` # old < - # same same # before | after # - > new # ``` # # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds # to #finish, it will be called. # # Each element of a returned array is a Diff::LCS::ContextChange object, which can be # implicitly converted to an array. # # ```ruby # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| # case action # when '!' # # replace # when '-' # # delete # when '+' # # insert # end # end # ``` def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) # #traverse_sequences is the most general facility provided by this module; #diff and # #lcs are implemented using #traverse_sequences. # # The arguments to #traverse_sequence are the two sequences to traverse, and a callback # object, like this: # # ```ruby # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. # Optional. # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. # Optional. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` # which are both equal and part of the longest common subsequence, there will be some # moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. If both arrows point to elements that are not part of the longest common # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate # callback, then it will advance arrow `b` and call the appropriate callback. # # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` # are invoked with an event comprising the action ("=", "+", or "-", respectively), the # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded # by #traverse_sequences. # # #### End of Sequences # # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence # will try to call `callbacks#finished_a` with the last index and element of `A` # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` # does not exist, then `callbacks#discard_b` will be called on each element of `B` until # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for # each element). # # If `b` reaches the end of `B` before `a` reaches the end of `A`, # `callbacks#finished_b` will be called with the current index and element of `A` # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if # `callbacks#finished_b` does not exist on the callback object, then # `callbacks#discard_a` will be called on each element of `A` until the end of the # sequence is reached (`A[i]` and `B[-1]`). # # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` # will be called after the end of the sequence is reached, if `a` has not yet reached # the end of `A` or `b` has not yet reached the end of `B`. def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false a_size = seq1.size b_size = seq2.size a_i = b_j = 0 matches.each do |b_line| if b_line.nil? unless seq1[a_i].nil? a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else a_x = seq1[a_i] loop do break unless b_j < b_line b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) b_j += 1 end a_i += 1 end # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the # last matching lines in their sequences. while (a_i < a_size) || (b_j < b_size) # last A? if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a a_x = seq1[-1] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else a_x = seq1[a_i] loop do b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 break unless b_j < b_size end end end # last B? if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b a_x = seq1[a_i] b_x = seq2[-1] event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else b_x = seq2[b_j] loop do a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 break unless b_j < b_size end end end if a_i < a_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 end if b_j < b_size a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # #traverse_balanced is an alternative to #traverse_sequences. It uses a different # algorithm to iterate through the entries in the computed longest common subsequence. # Instead of viewing the changes as insertions or deletions from one of the sequences, # #traverse_balanced will report _changes_ between the sequences. # # The arguments to #traverse_balanced are the two sequences to traverse and a callback # object, like this: # # ```ruby # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) # ``` # # #sdiff is implemented using #traverse_balanced. # # ### Callback Methods # # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` # and `B`. # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. # # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only # while processing large amounts of data. # # ### Algorithm # # ``` # a---+ # v # A = a b c e h j l m n p # B = b c d e f j k l m r s t # ^ # b---+ # ``` # # #### Matches # # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, # the arrows will initially point to the first elements of their respective sequences. # #traverse_sequences will advance the arrows through the sequences one element at # a time, calling a method on the user-specified callback object before each advance. It # will advance the arrows in such a way that if there are elements `A[i]` and # `B[j]` which are both equal and part of the longest common subsequence, there will be # some moment during the execution of #traverse_sequences when arrow `a` is pointing to # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences # will call `callbacks#match` and then it will advance both arrows. # # #### Discards # # Otherwise, one of the arrows is pointing to an element of its sequence that is not # part of the longest common subsequence. #traverse_sequences will advance that arrow # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow # it advanced. # # #### Changes # # If both `a` and `b` point to elements that are not part of the longest common # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and # `callbacks#discard_b` will be called in turn. # # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # # Note that `i` and `j` may not be the same index position, even if `a` and `b` are # considered to be pointing to matching or changed elements. def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size a_i = b_j = m_b = 0 m_a = -1 # Process all the lines in the match vector. loop do # Find next match indexes `m_a` and `m_b` loop do m_a += 1 break unless m_a < matches.size && matches[m_a].nil? end break if m_a >= matches.size # end of matches? m_b = matches[m_a] # Change(seq2) while (a_i < m_a) || (b_j < m_b) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end # Match a_x = seq1[a_i] b_x = seq2[b_j] event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) a_i += 1 b_j += 1 end while (a_i < a_size) || (b_j < b_size) a_x = seq1[a_i] b_x = seq2[b_j] case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) a_i += 1 else event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 a_x = seq1[a_i] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end b_j += 1 when [true, false] event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) a_i += 1 when [false, true] event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) b_j += 1 end end end # standard:disable Style/HashSyntax PATCH_MAP = { # :nodoc: :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze private_constant :PATCH_MAP # standard:enable Style/HashSyntax # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or # `:unpatch`), producing a new sequence. # # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the # direction of the `patchset`. # # A `patchset` can be considered to apply forward (`:patch`) if the following expression # is true: # # ```ruby # patch(s1, diff(s1, s2)) # => s2 # ``` # # A `patchset` can be considered to apply backward (`:unpatch`) if the following # expression is true: # # ```ruby # patch(s2, diff(s1, s2)) # => s1 # ``` # # If the `patchset` contains no changes, the `src` value will be returned as either # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following # predicate returns true: # # ```ruby # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } # ``` # # ### Patchsets # # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix # of the two. A hunk of changes is an enumerable sequence of changes: # # ``` # [ # patchset # # change # [ # hunk # # change # ] # ] # ``` # # The `patch` method accepts `patchset`s that are enumerable sequences containing either # Diff::LCS::Change objects (or a subclass) or the array representations of those # objects. Prior to application, array representations of Diff::LCS::Change objects will # be reified. def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) a_i = b_j = 0 patch_map = PATCH_MAP[direction] patchset.each do |change| # Both Change and ContextChange support #action action = patch_map[change.action] case change when Diff::LCS::ContextChange case direction when :patch el = change.new_element op = change.old_position np = change.new_position when :unpatch el = change.old_element op = change.new_position np = change.old_position end case action when "-" # Remove details from the old string while a_i < op res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < np res << src[a_i] a_i += 1 b_j += 1 end res << el b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el a_i += 1 b_j += 1 when "!" while a_i < op res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 a_i += 1 res << el end when Diff::LCS::Change case action when "-" while a_i < change.position res << src[a_i] a_i += 1 b_j += 1 end a_i += 1 when "+" while b_j < change.position res << src[a_i] a_i += 1 b_j += 1 end b_j += 1 res << change.element end end end while a_i < src.size res << src[a_i] a_i += 1 b_j += 1 end res end # Given a patchset, convert the current version to the prior version. Does no # auto-discovery. def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) # Given a patchset, convert the current version to the next version. Does no # auto-discovery. def self.patch!(src, patchset) = patch(src,
Attempts to unpatch ‘self` with the provided `patchset`, using unpatch!. If the sequence this is used on supports replace, the value of `self` will be replaced. See Diff::LCS#unpatch. Does no patch direction autodiscovery.
# File lib/diff/lcs.rb, line 121 def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) else unpatch!(patchset) end end