dyph 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ module Dyph
2
+ module Support
3
+ module SanityCheck
4
+ extend self
5
+
6
+ # rubocop:disable Metrics/AbcSize
7
+ def ensure_no_lost_data(left, base, right, final_result)
8
+ result_word_map = {}
9
+ final_result.each do |result_block|
10
+ blocks = case result_block
11
+ when Outcome::Resolved then result_block.result
12
+ when Outcome::Conflicted then [result_block.left, result_block.right].flatten
13
+ else raise "Unknown block type, #{result_block[:type]}"
14
+ end
15
+ count_blocks(blocks, result_word_map)
16
+ end
17
+
18
+ left_word_map, base_word_map, right_word_map = [left, base, right].map { |str| count_blocks(str) }
19
+
20
+ # new words are words that are in left or right, but not in base
21
+ new_left_words = subtract_words(left_word_map, base_word_map)
22
+ new_right_words = subtract_words(right_word_map, base_word_map)
23
+
24
+ # now make sure all new words are somewhere in the result
25
+ missing_new_left_words = subtract_words(new_left_words, result_word_map)
26
+ missing_new_right_words = subtract_words(new_right_words, result_word_map)
27
+
28
+ if missing_new_left_words.any? || missing_new_right_words.any?
29
+ raise BadMergeException.new(final_result)
30
+ end
31
+ end
32
+ # rubocop:enable Metrics/AbcSize
33
+
34
+ private
35
+ def count_blocks(blocks, hash={})
36
+ blocks.reduce(hash) do |map, block|
37
+ map[block] ||= 0
38
+ map[block] += 1
39
+ map
40
+ end
41
+ end
42
+
43
+ def subtract_words(left_map, right_map)
44
+ remaining_words = {}
45
+
46
+ left_map.each do |word, count|
47
+ count_in_right = right_map[word] || 0
48
+
49
+ new_count = count - count_in_right
50
+ remaining_words[word] = new_count if new_count > 0
51
+ end
52
+
53
+ remaining_words
54
+ end
55
+ end
56
+
57
+ class BadMergeException < StandardError
58
+ attr_accessor :merge_result
59
+
60
+ def initialize(merge_result)
61
+ @merge_result = merge_result
62
+ end
63
+
64
+ def inspect
65
+ "<#{self.class}: #{merge_result}>"
66
+ end
67
+
68
+ def to_s
69
+ inspect
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,190 @@
1
+ module Dyph
2
+ module TwoWayDiffers
3
+
4
+ class HeckelDiff
5
+ # Algorithm adapted from http://www.rad.upenn.edu/sbia/software/basis/apidoc/v1.2/diff3_8py_source.html
6
+
7
+ def self.execute_diff(old_text_array, new_text_array)
8
+ raise ArgumentError, "Argument is not an array." unless old_text_array.is_a?(Array) && new_text_array.is_a?(Array)
9
+ diff_result = diff(old_text_array, new_text_array)
10
+ # convert to typed differ's output (wrapped with change types eg. Add, Delete, Change)
11
+ HeckelDiffWrapper.new(old_text_array, new_text_array, diff_result).convert_to_typed_ouput
12
+ end
13
+
14
+ # Two-way diff based on the algorithm by P. Heckel.
15
+ # @param [in] left Array of anything implementing hash and equals
16
+ # @param [in] right Array of anything implementing hash and equals
17
+ def self.diff(left, right)
18
+ differ = HeckelDiff.new(left,right)
19
+ differ.perform_diff
20
+ end
21
+
22
+ def initialize(left, right)
23
+ @left = left
24
+ @right = right
25
+ end
26
+
27
+ def perform_diff
28
+ unique_positions = identify_unique_postions
29
+ unique_positions.sort!{ |a, b| a[0] <=> b[0] } # sort by the line in which the line was found in a
30
+ left_change_pos, right_change_pos = find_next_change
31
+ init_changes = ChangeData.new(left_change_pos, right_change_pos, [])
32
+ final_changes = unique_positions.reduce(init_changes, &method(:get_differences))
33
+ final_changes.change_ranges
34
+ end
35
+
36
+ ChangeData = Struct.new(:left_change_pos, :right_change_pos, :change_ranges)
37
+
38
+ private
39
+
40
+ def get_differences(change_data, unique_positions)
41
+ left_pos, right_pos = change_data.left_change_pos, change_data.right_change_pos
42
+ left_uniq_pos, right_uniq_pos = unique_positions
43
+ if left_uniq_pos < left_pos || right_uniq_pos < right_pos
44
+ change_data
45
+ else
46
+ left_lo, left_hi, right_lo, right_hi = find_prev_change(left_pos, right_pos, left_uniq_pos-1, right_uniq_pos-1)
47
+ next_left_pos, next_right_pos = find_next_change(left_uniq_pos+1, right_uniq_pos+1)
48
+
49
+ updated_ranges = append_change_range(change_data.change_ranges, left_lo, left_hi, right_lo, right_hi)
50
+ ChangeData.new(next_left_pos, next_right_pos, updated_ranges)
51
+ end
52
+ end
53
+
54
+ def find_next_change(left_start_pos=0, right_start_pos=0)
55
+ l_arr, r_arr = (@left[left_start_pos..-1] || []), (@right[right_start_pos..-1] || [])
56
+ offset = mismatch_offset l_arr, r_arr
57
+ [ left_start_pos + offset, right_start_pos + offset]
58
+ end
59
+
60
+
61
+ def find_prev_change(left_lo, right_lo, left_hi, right_hi)
62
+ if left_lo > left_hi || right_lo > right_hi
63
+ [left_lo, left_hi, right_lo, right_hi]
64
+ else
65
+ l_arr, r_arr = (@left[left_lo .. left_hi].reverse || []), (@right[right_lo .. right_hi].reverse || [])
66
+ offset = mismatch_offset l_arr, r_arr
67
+ [left_lo, left_hi - offset, right_lo, right_hi - offset]
68
+ end
69
+ end
70
+
71
+ def mismatch_offset(l_arr, r_arr)
72
+ _ , index = l_arr.zip(r_arr).each_with_index.detect { |pair, _| pair[0] != pair[1] }
73
+ index || [l_arr.length, r_arr.length].min
74
+ end
75
+
76
+ def identify_unique_postions
77
+ left_uniques = find_unique(@left)
78
+ right_uniques = find_unique(@right)
79
+ shared_keys = left_uniques.keys & right_uniques.keys
80
+ uniq_ranges = shared_keys.map { |k| [left_uniques[k], right_uniques[k]] }
81
+ uniq_ranges.unshift([ @left.length, @right.length])
82
+ end
83
+
84
+ def find_unique(array)
85
+ flagged_uniques = array.each_with_index.reduce({}) do |hash, item_index|
86
+ item, pos = item_index
87
+ hash[item] = {pos: pos, unique: hash[item].nil?}
88
+ hash
89
+ end
90
+ flagged_uniques.select { |_, v| v[:unique] }.map { |k, v| [k, v[:pos]] }.to_h
91
+ end
92
+
93
+ # given the calculated bounds of the 2 way diff, create the proper change type and add it to the queue.
94
+ def append_change_range(changes_ranges, left_lo, left_hi, right_lo, right_hi)
95
+ if left_lo <= left_hi && right_lo <= right_hi # for this change, the bounds are both 'normal'. the beginning of the change is before the end.
96
+ changes_ranges << [:change, left_lo + 1, left_hi + 1, right_lo + 1, right_hi + 1]
97
+ elsif left_lo <= left_hi
98
+ changes_ranges << [:delete, left_lo + 1, left_hi + 1, right_lo + 1, right_lo]
99
+ elsif right_lo <= right_hi
100
+ changes_ranges << [:add, left_lo + 1, left_lo, right_lo + 1, right_hi + 1]
101
+ end
102
+ changes_ranges
103
+ end
104
+ end
105
+
106
+ class TextNode
107
+ attr_accessor :text, :row
108
+
109
+ def initialize(text:, row:)
110
+ @text = text
111
+ @row = row
112
+ end
113
+ end
114
+
115
+ class TwoWayChunk
116
+ attr_reader :action, :left_lo, :left_hi, :right_lo, :right_hi
117
+ def initialize(raw_chunk)
118
+ @action = raw_chunk[0]
119
+ @left_lo = raw_chunk[1]
120
+ @left_hi = raw_chunk[2]
121
+ @right_lo = raw_chunk[3]
122
+ @right_hi = raw_chunk[4]
123
+ end
124
+ end
125
+ class HeckelDiffWrapper
126
+ def initialize(old_text_array, new_text_array, heckel_diff)
127
+ @chunks = heckel_diff.map { |block| TwoWayChunk.new(block) }
128
+ @old_text_array = old_text_array
129
+ @new_text_array = new_text_array
130
+ @old_text = []
131
+ @new_text = []
132
+ end
133
+ IndexTracker = Struct.new(:old_index, :new_index)
134
+
135
+ def convert_to_typed_ouput()
136
+ final_indexes = @chunks.reduce(IndexTracker.new(0,0)) do |index_tracker, chunk|
137
+ old_iteration, new_iteration = set_text_node_indexes(chunk, index_tracker.old_index, index_tracker.new_index)
138
+ old_index, new_index = append_changes(chunk, index_tracker.old_index + old_iteration, index_tracker.new_index + new_iteration)
139
+ IndexTracker.new(old_index, new_index)
140
+ end
141
+
142
+ set_the_remaining_text_node_indexes(final_indexes.old_index, final_indexes.new_index)
143
+ { old_text: @old_text, new_text: @new_text}
144
+ end
145
+
146
+ private
147
+ def set_text_node_indexes(chunk, old_index, new_index)
148
+ old_iteration = 0
149
+ while old_index + old_iteration < chunk.left_lo - 1 # chunk indexes are from 1
150
+ @old_text << TextNode.new(text: @old_text_array[old_index + old_iteration], row: new_index + old_iteration)
151
+ old_iteration += 1
152
+ end
153
+
154
+ new_iteration = 0
155
+ while new_index + new_iteration < chunk.right_lo - 1 # chunk indexes are from 1
156
+ @new_text << TextNode.new(text: @new_text_array[new_index + new_iteration], row: old_index + new_iteration)
157
+ new_iteration += 1
158
+ end
159
+ [old_iteration, new_iteration]
160
+ end
161
+
162
+ def append_changes(chunk, old_index, new_index)
163
+ while old_index <= chunk.left_hi - 1 # chunk indexes are from 1
164
+ @old_text << @old_text_array[old_index]
165
+ old_index += 1
166
+ end
167
+
168
+ while new_index <= chunk.right_hi - 1 # chunk indexes are from 1
169
+ @new_text << @new_text_array[new_index]
170
+ new_index += 1
171
+ end
172
+ [old_index, new_index]
173
+ end
174
+
175
+ def set_the_remaining_text_node_indexes(old_index, new_index)
176
+ iteration = 0
177
+ while old_index + iteration < @old_text_array.length
178
+ @old_text << TextNode.new(text: @old_text_array[old_index + iteration], row: new_index + iteration)
179
+ iteration += 1
180
+ end
181
+
182
+ iteration = 0
183
+ while new_index + iteration < @new_text_array.length
184
+ @new_text << TextNode.new(text: @new_text_array[new_index + iteration], row: old_index + iteration)
185
+ iteration += 1
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,179 @@
1
+ module Dyph
2
+ module TwoWayDiffers
3
+ # rubocop:disable Metrics/ModuleLength
4
+ module OutputConverter
5
+ extend self
6
+
7
+ def convert_to_dyph_output(old_text, new_text)
8
+ actions = merge_and_partition(old_text, new_text)
9
+ selected_actions = extract_add_deletes_changes(actions)
10
+ correct_offsets(selected_actions)
11
+ end
12
+
13
+ def objectify(merge_results)
14
+ merge_results.map do |result|
15
+ action = result[:action]
16
+ line = result[:line]
17
+ old_index = result[:old_index]
18
+ new_index = result[:new_index]
19
+
20
+ case action
21
+ when :add
22
+ Dyph::Action::Add.new(value: line, old_index: old_index, new_index: new_index)
23
+ when :delete
24
+ Dyph::Action::Delete.new(value: line, old_index: old_index, new_index: new_index)
25
+ when :no_change
26
+ Dyph::Action::NoChange.new(value: line.text, old_index: old_index, new_index: new_index)
27
+ else
28
+ raise "unhandled action"
29
+ end
30
+ end
31
+ end
32
+
33
+ def merge_results(old_text, new_text)
34
+ merged_text = []
35
+
36
+ if (new_text.empty?)
37
+ no_new_text(old_text, merged_text)
38
+ else
39
+ prepend_old_text(old_text, merged_text)
40
+ gather_up_actions(old_text, new_text, merged_text)
41
+ end
42
+ merged_text
43
+ end
44
+
45
+ private
46
+ def merge_and_partition(old_text, new_text)
47
+ merged_text = merge_results(old_text, new_text)
48
+ merge_output_lines = merged_text.map { |x| to_output_format x }
49
+ partition_into_actions(merge_output_lines)
50
+ end
51
+
52
+ def extract_add_deletes_changes(actions)
53
+ collapsed_actions = actions.map { |action| collapse_action(action) }
54
+ paired_results = pair_up_add_deletes collapsed_actions
55
+ paired_results.reject { |res| res[:action] == :no_change}
56
+ end
57
+
58
+ def correct_offsets(selected_actions)
59
+ fix_offsets = set_offset(selected_actions)
60
+ fix_offsets.map { |r| Dyph::Support::AssignAction.get_action(
61
+ lo_a: r[:old_lo]-1 ,
62
+ lo_b: r[:new_lo]-1,
63
+ hi_a: r[:old_hi]-1,
64
+ hi_b: r[:new_hi]-1
65
+ )}
66
+ end
67
+
68
+ def gather_up_actions(old_text, new_text, merged_text)
69
+ prev_no_change_old = - 1
70
+ new_text.map.with_index.each do |line, i|
71
+ if !line.is_a?(TextNode)
72
+ merged_text << {action: :add, line: line, old_index: prev_no_change_old + 1, new_index: i+1}
73
+ else
74
+ prev_no_change_old = line.row
75
+ change_or_delete(old_text, line, prev_no_change_old, merged_text, i)
76
+ end
77
+ end
78
+ end
79
+
80
+ def change_or_delete(old_text, line, prev_no_change_old, merged_text, index)
81
+ merged_text << {action: :no_change, line: line, old_index: line.row, new_index: index}
82
+
83
+ ((prev_no_change_old+1) ... old_text.length).each do |n|
84
+ break if old_text[n].is_a?(TextNode)
85
+ merged_text << {action: :delete, line: old_text[n], old_index: n+1, new_index: index+1}
86
+ end
87
+ end
88
+
89
+ def prepend_old_text(old_text, merged_text)
90
+ if !old_text.first.is_a?(TextNode)
91
+ old_text.map.with_index.each do |line, i|
92
+ break if line.is_a?(TextNode)
93
+ merged_text << {action: :delete, line: line, old_index:i+1, new_index: i}
94
+ end
95
+ end
96
+ end
97
+
98
+ def no_new_text(old_text, merged_text)
99
+ old_text.map.with_index do |line, i|
100
+ merged_text << { action: :delete, line: line, old_index: i+1, new_index: i}
101
+ end
102
+ end
103
+
104
+ def set_offset(results)
105
+ results.map do |result_row|
106
+ if result_row[:action] == :add
107
+ result_row[:old_lo] += 1
108
+ result_row
109
+ elsif result_row[:action] == :delete
110
+ result_row[:new_lo] += 1
111
+ result_row
112
+ else
113
+ result_row
114
+ end
115
+ end
116
+ end
117
+
118
+ def to_output_format(result_row)
119
+ {
120
+ action: result_row[:action],
121
+ old_lo: result_row[:old_index],
122
+ old_hi: result_row[:old_index],
123
+ new_lo: result_row[:new_index],
124
+ new_hi: result_row[:new_index]
125
+ }
126
+ end
127
+
128
+ def collapse_action(actions)
129
+ actions.inject({}) do | hash, action |
130
+ hash[:action] ||= action[:action]
131
+ hash[:old_lo] ||= action[:old_lo]
132
+ hash[:old_hi] = action[:old_hi]
133
+ hash[:new_lo] ||= action[:new_lo]
134
+ hash[:new_hi] = action[:new_hi]
135
+ hash
136
+ end
137
+ end
138
+
139
+ def is_a_pair?(actions, i)
140
+ action_one = actions[i-1][:action] if actions[i-1]
141
+ action_two = actions[i][:action] if actions[i]
142
+ Set.new([action_one, action_two]) == Set.new([:add, :delete])
143
+ end
144
+
145
+ def pair_up_add_deletes(actions)
146
+ results = []
147
+ found_change = false
148
+ (1 .. actions.length).each do |i|
149
+ if is_a_pair?(actions, i)
150
+ results << {
151
+ action: :change,
152
+ old_lo: actions[i-1][:old_lo],
153
+ old_hi: actions[i-1][:old_hi],
154
+ new_lo: actions[i][:new_lo],
155
+ new_hi: actions[i][:new_hi]
156
+ }
157
+ found_change = true
158
+ elsif found_change
159
+ found_change = false
160
+ else
161
+ results << actions[i-1]
162
+ end
163
+ end
164
+ results
165
+ end
166
+
167
+ def partition_into_actions(array)
168
+ array.inject([]) do |acc, x|
169
+ if acc.length == 0 || acc.last.last[:action] != x[:action]
170
+ acc << [x]
171
+ else
172
+ acc.last << x
173
+ end
174
+ acc
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,3 @@
1
+ module Dyph
2
+ VERSION = "0.6.0"
3
+ end
metadata ADDED
@@ -0,0 +1,220 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dyph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Mook
8
+ - Andrew Montalto
9
+ - Jacob Elder
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2020-09-28 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.1'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '2.1'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rake
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: pry
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: pry-rescue
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ - !ruby/object:Gem::Dependency
72
+ name: pry-stack_explorer
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ - !ruby/object:Gem::Dependency
86
+ name: rspec
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: 3.3.0
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: 3.3.0
99
+ - !ruby/object:Gem::Dependency
100
+ name: awesome_print
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ - !ruby/object:Gem::Dependency
114
+ name: factory_girl
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rspec_junit_formatter
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ - !ruby/object:Gem::Dependency
142
+ name: faker
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ - !ruby/object:Gem::Dependency
156
+ name: yard
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ description: A library of useful diffing algorithms for Ruby
170
+ email:
171
+ - kevin@kevinmook.com
172
+ executables: []
173
+ extensions: []
174
+ extra_rdoc_files: []
175
+ files:
176
+ - LICENSE
177
+ - README.md
178
+ - lib/dyph.rb
179
+ - lib/dyph/action.rb
180
+ - lib/dyph/action/add.rb
181
+ - lib/dyph/action/delete.rb
182
+ - lib/dyph/action/no_change.rb
183
+ - lib/dyph/differ.rb
184
+ - lib/dyph/equatable.rb
185
+ - lib/dyph/merge_result.rb
186
+ - lib/dyph/outcome.rb
187
+ - lib/dyph/outcome/conflicted.rb
188
+ - lib/dyph/outcome/resolved.rb
189
+ - lib/dyph/support/assign_action.rb
190
+ - lib/dyph/support/collater.rb
191
+ - lib/dyph/support/diff3.rb
192
+ - lib/dyph/support/merger.rb
193
+ - lib/dyph/support/sanity_check.rb
194
+ - lib/dyph/two_way_differs/heckel_diff.rb
195
+ - lib/dyph/two_way_differs/output_converter.rb
196
+ - lib/dyph/version.rb
197
+ homepage: https://github.com/kevinmookorg/dyph
198
+ licenses:
199
+ - MIT
200
+ metadata: {}
201
+ post_install_message:
202
+ rdoc_options: []
203
+ require_paths:
204
+ - lib
205
+ required_ruby_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: 2.6.0
210
+ required_rubygems_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ requirements: []
216
+ rubygems_version: 3.0.3
217
+ signing_key:
218
+ specification_version: 4
219
+ summary: A library of useful diffing algorithms for Ruby
220
+ test_files: []