• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

halostatue / diff-lcs / 15813454968

23 Jun 2025 01:57AM UTC coverage: 88.366%. Remained the same
15813454968

Pull #159

github

web-flow
Merge d45cbd309 into fbabd3a7f
Pull Request #159: Bump ruby/setup-ruby from 1.244.0 to 1.245.0

533 of 797 branches covered (66.88%)

676 of 765 relevant lines covered (88.37%)

288.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.41
/lib/diff/lcs/internals.rb
1
# frozen_string_literal: true
2

3
class << Diff::LCS
2✔
4
  def diff_traversal(method, seq1, seq2, callbacks, &block)
2✔
5
    callbacks = callbacks_for(callbacks)
332✔
6
    case method
332!
7
    when :diff
84✔
8
      traverse_sequences(seq1, seq2, callbacks)
168✔
9
    when :sdiff
82✔
10
      traverse_balanced(seq1, seq2, callbacks)
164✔
11
    end
12
    callbacks.finish if callbacks.respond_to? :finish
332✔
13

14
    if block
332!
15
      callbacks.diffs.map do |hunk|
×
16
        if hunk.is_a? Array
×
17
          hunk.map { |hunk_block| block[hunk_block] }
×
18
        else
×
19
          block[hunk]
×
20
        end
21
      end
22
    else
166✔
23
      callbacks.diffs
332✔
24
    end
25
  end
26
  private :diff_traversal
2✔
27
end
28

29
module Diff::LCS::Internals # :nodoc:
2✔
30
end
31

32
class << Diff::LCS::Internals
2✔
33
  # Compute the longest common subsequence between the sequenced
34
  # Enumerables +a+ and +b+. The result is an array whose contents is such
35
  # that
36
  #
37
  #     result = Diff::LCS::Internals.lcs(a, b)
38
  #     result.each_with_index do |e, i|
39
  #       assert_equal(a[i], b[e]) unless e.nil?
40
  #     end
41
  def lcs(a, b)
2✔
42
    a_start = b_start = 0
550✔
43
    a_finish = a.size - 1
550✔
44
    b_finish = b.size - 1
550✔
45
    vector = []
550✔
46

47
    # Collect any common elements at the beginning...
48
    while (a_start <= a_finish) && (b_start <= b_finish) && (a[a_start] == b[b_start])
1,912✔
49
      vector[a_start] = b_start
812✔
50
      a_start += 1
812✔
51
      b_start += 1
812✔
52
    end
53

54
    # Now the end...
55
    while (a_start <= a_finish) && (b_start <= b_finish) && (a[a_finish] == b[b_finish])
2,114✔
56
      vector[a_finish] = b_finish
1,014✔
57
      a_finish -= 1
1,014✔
58
      b_finish -= 1
1,014✔
59
    end
60

61
    # Now, compute the equivalence classes of positions of elements.
62
    # An explanation for how this works: https://codeforces.com/topic/92191
63
    b_matches = position_hash(b, b_start..b_finish)
550✔
64

65
    thresh = []
550✔
66
    links = []
550✔
67
    string = a.is_a?(String)
550✔
68

69
    (a_start..a_finish).each do |i|
550✔
70
      ai = string ? a[i, 1] : a[i]
2,286✔
71
      bm = b_matches[ai]
2,286✔
72
      k = nil
2,286✔
73
      bm.reverse_each do |j|
2,286✔
74
        # Although the threshold check is not mandatory for this to work,
75
        # it may have an optimization purpose
76
        # An attempt to remove it: https://github.com/halostatue/diff-lcs/pull/72
77
        # Why it is reintroduced: https://github.com/halostatue/diff-lcs/issues/78
78
        if k && (thresh[k] > j) && (thresh[k - 1] < j)
1,042✔
79
          thresh[k] = j
4✔
80
        else
519✔
81
          k = replace_next_larger(thresh, j, k)
1,038✔
82
        end
83
        links[k] = [k.positive? ? links[k - 1] : nil, i, j] unless k.nil?
1,042✔
84
      end
85
    end
86

87
    unless thresh.empty?
550✔
88
      link = links[thresh.size - 1]
202✔
89
      until link.nil?
202✔
90
        vector[link[1]] = link[2]
994✔
91
        link = link[0]
994✔
92
      end
93
    end
94

95
    vector
550✔
96
  end
97

98
  # This method will analyze the provided patchset to provide a single-pass
99
  # normalization (conversion of the array form of Diff::LCS::Change objects to
100
  # the object form of same) and detection of whether the patchset represents
101
  # changes to be made.
102
  def analyze_patchset(patchset, depth = 0)
2✔
103
    fail "Patchset too complex" if depth > 1
910!
104

105
    has_changes = false
910✔
106
    new_patchset = []
910✔
107

108
    # Format:
109
    # [ # patchset
110
    #   # hunk (change)
111
    #   [ # hunk
112
    #     # change
113
    #   ]
114
    # ]
115

116
    patchset.each do |hunk|
910✔
117
      case hunk
2,908!
118
      when Diff::LCS::Change
1,156✔
119
        has_changes ||= !hunk.unchanged?
2,312✔
120
        new_patchset << hunk
2,312✔
121
      when Array
122
        # Detect if the 'hunk' is actually an array-format change object.
298✔
123
        if Diff::LCS::Change.valid_action? hunk[0]
596!
124
          hunk = Diff::LCS::Change.from_a(hunk)
×
125
          has_changes ||= !hunk.unchanged?
×
126
          new_patchset << hunk
×
127
        else
298✔
128
          with_changes, hunk = analyze_patchset(hunk, depth + 1)
596✔
129
          has_changes ||= with_changes
596✔
130
          new_patchset.concat(hunk)
596✔
131
        end
132
      else
×
133
        fail ArgumentError, "Cannot normalise a hunk of class #{hunk.class}."
×
134
      end
135
    end
136

137
    [has_changes, new_patchset]
910✔
138
  end
139

140
  # Examine the patchset and the source to see in which direction the
141
  # patch should be applied.
142
  #
143
  # WARNING: By default, this examines the whole patch, so this could take
144
  # some time. This also works better with Diff::LCS::ContextChange or
145
  # Diff::LCS::Change as its source, as an array will cause the creation
146
  # of one of the above.
147
  def intuit_diff_direction(src, patchset, limit = nil)
2✔
148
    string = src.is_a?(String)
114✔
149
    count = left_match = left_miss = right_match = right_miss = 0
114✔
150

151
    patchset.each do |change|
114✔
152
      count += 1
788✔
153

154
      case change
788!
155
      when Diff::LCS::ContextChange
288✔
156
        le = string ? src[change.old_position, 1] : src[change.old_position]
576!
157
        re = string ? src[change.new_position, 1] : src[change.new_position]
576!
158

159
        case change.action
576!
160
        when "-" # Remove details from the old string
64✔
161
          if le == change.old_element
128✔
162
            left_match += 1
64✔
163
          else
32✔
164
            left_miss += 1
64✔
165
          end
166
        when "+"
64✔
167
          if re == change.new_element
128✔
168
            right_match += 1
64✔
169
          else
32✔
170
            right_miss += 1
64✔
171
          end
172
        when "="
136✔
173
          left_miss += 1 if le != change.old_element
272✔
174
          right_miss += 1 if re != change.new_element
272✔
175
        when "!"
24✔
176
          if le == change.old_element
48✔
177
            left_match += 1
24✔
178
          elsif re == change.new_element
24!
179
            right_match += 1
24✔
180
          else
×
181
            left_miss += 1
×
182
            right_miss += 1
×
183
          end
184
        end
185
      when Diff::LCS::Change
186
        # With a simplistic change, we can't tell the difference between
187
        # the left and right on '!' actions, so we ignore those. On '='
188
        # actions, if there's a miss, we miss both left and right.
106✔
189
        element = string ? src[change.position, 1] : src[change.position]
212✔
190

191
        case change.action
212!
192
        when "-"
52✔
193
          if element == change.element
104✔
194
            left_match += 1
56✔
195
          else
24✔
196
            left_miss += 1
48✔
197
          end
198
        when "+"
54✔
199
          if element == change.element
108✔
200
            right_match += 1
56✔
201
          else
26✔
202
            right_miss += 1
52✔
203
          end
204
        when "="
×
205
          if element != change.element
×
206
            left_miss += 1
×
207
            right_miss += 1
×
208
          end
209
        end
210
      end
211

212
      break if !limit.nil? && (count > limit)
788!
213
    end
214

215
    no_left = left_match.zero? && left_miss.positive?
114✔
216
    no_right = right_match.zero? && right_miss.positive?
114✔
217

218
    case [no_left, no_right]
114✔
219
    when [false, true]
21✔
220
      :patch
42✔
221
    when [true, false]
20✔
222
      :unpatch
40✔
223
    else
16✔
224
      case left_match <=> right_match
32!
225
      when 1
8✔
226
        if left_miss.zero?
16✔
227
          :patch
12✔
228
        else
2✔
229
          :unpatch
4✔
230
        end
231
      when -1
8✔
232
        if right_miss.zero?
16✔
233
          :unpatch
12✔
234
        else
2✔
235
          :patch
4✔
236
        end
237
      else
×
238
        fail "The provided patchset does not appear to apply to the provided \
×
239
enumerable as either source or destination value."
240
      end
241
    end
242
  end
243

244
  # Find the place at which +value+ would normally be inserted into the
245
  # Enumerable. If that place is already occupied by +value+, do nothing
246
  # and return +nil+. If the place does not exist (i.e., it is off the end
247
  # of the Enumerable), add it to the end. Otherwise, replace the element
248
  # at that point with +value+. It is assumed that the Enumerable's values
249
  # are numeric.
250
  #
251
  # This operation preserves the sort order.
252
  def replace_next_larger(enum, value, last_index = nil)
2✔
253
    # Off the end?
254
    if enum.empty? || (value > enum[-1])
1,038✔
255
      enum << value
994✔
256
      return enum.size - 1
994✔
257
    end
258

259
    # Binary search for the insertion point
260
    last_index ||= enum.size - 1
44✔
261
    first_index = 0
44✔
262
    while first_index <= last_index
44✔
263
      i = (first_index + last_index) >> 1
48✔
264

265
      found = enum[i]
48✔
266

267
      return nil if value == found
48✔
268

269
      if value > found
24✔
270
        first_index = i + 1
4✔
271
      else
10✔
272
        last_index = i - 1
20✔
273
      end
274
    end
275

276
    # The insertion point is in first_index; overwrite the next larger
277
    # value.
278
    enum[first_index] = value
20✔
279
    first_index
20✔
280
  end
281
  private :replace_next_larger
2✔
282

283
  # If +vector+ maps the matching elements of another collection onto this
284
  # Enumerable, compute the inverse of +vector+ that maps this Enumerable
285
  # onto the collection. (Currently unused.)
286
  def inverse_vector(a, vector)
2✔
287
    inverse = a.dup
×
288
    (0...vector.size).each do |i|
×
289
      inverse[vector[i]] = i unless vector[i].nil?
×
290
    end
291
    inverse
×
292
  end
293
  private :inverse_vector
2✔
294

295
  # Returns a hash mapping each element of an Enumerable to the set of
296
  # positions it occupies in the Enumerable, optionally restricted to the
297
  # elements specified in the range of indexes specified by +interval+.
298
  def position_hash(enum, interval)
2✔
299
    string = enum.is_a?(String)
550✔
300
    hash = Hash.new { |h, k| h[k] = [] }
4,098✔
301
    interval.each do |i|
550✔
302
      k = string ? enum[i, 1] : enum[i]
2,300✔
303
      hash[k] << i
2,300✔
304
    end
305
    hash
550✔
306
  end
307
  private :position_hash
2✔
308
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc