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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 31873ea8f91e94aab1cc18aa9a2d5bbfe2bb871d5585df0c0b01d2189e8ee8fd
4
+ data.tar.gz: 1243dc1bfe1301be1b52ec8a4a865262445ec88c4cf741955b1300422c22fcdb
5
+ SHA512:
6
+ metadata.gz: 905655168bb4664168aa7f903d7ffc746c6f1886b474c06219235640014872490fb26373a53713f0a47328f519e63459c1f90559b20b0196bb4010bacda1855f
7
+ data.tar.gz: 04bec6b8ebeadcad31c2ef34eb36f44c752aa9f78ea278c824223f9ac63762f95692d20a95f9289baa78930ffdc8e0f5ba00291b8f46b964c06fc7d86a167a18
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Boundless
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,199 @@
1
+ Dyph
2
+ =====
3
+ [![Circle CI](https://img.shields.io/circleci/project/kevinmookorg/dyph/master.svg)](https://circleci.com/gh/kevinmookorg/dyph)
4
+ [![Documentation](https://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/kevinmook/dyph/master)
5
+
6
+ A library of useful diffing algorithms for Ruby.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'dyph'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install dyph
21
+
22
+ # Quick start
23
+ ## Two way diffing
24
+ To diff two arrays:
25
+
26
+ left = [:a, :b, :c, :d]
27
+ right = [:b, :c, :d, :e]
28
+ Dyph::Differ.two_way_diff(left, right)
29
+
30
+ which will return an array of `Dyph::Action` with offsets
31
+
32
+ [
33
+ <Action::Delete @new_index=0, @old_index=1, @value=:a>,
34
+ <Action::NoChange @new_index=0, @old_index=1, @value=:b>,
35
+ <Action::NoChange @new_index=1, @old_index=2, @value=:c>,
36
+ <Action::NoChange @new_index=2, @old_index=3, @value=:d>,
37
+ <Action::Add @new_index=4, @old_index=4, @value=:e>
38
+ ]
39
+
40
+ ## Three way diffing
41
+ Three way diffing is able to detect changes between two documents relative to a common base.
42
+
43
+ ### No conflicts
44
+ To execute a three way diff and merge:
45
+
46
+ left = [:a, :b, :c, :d]
47
+ base = [:a, :b, :c]
48
+ right = [:b, :c, :d, :e]
49
+ Dyph::Differ.merge(left, base, right)
50
+
51
+ Which returns a `Dyph::MergeResult` with a list of result outcomes:
52
+
53
+ [ <OutCome::Resolved(@result=[:b, :c, :d, :e]> ]
54
+
55
+ and has `MergeResult#conflict` set to `false`
56
+ ### Conflicts
57
+
58
+ Conflicts are when left and right make a change relative to base in the same relative place, so an end user must determine how to merge
59
+
60
+ For example:
61
+
62
+ left = [:a, :l, :c]
63
+ base = [:a, :b, :c]
64
+ right = [:a, :r, :c]
65
+ Dyph::Differ.merge(left, base, right)
66
+
67
+ returns the following `MergeResult#result`
68
+
69
+ [
70
+ <Outcome::Resolved @result=[:a]>
71
+ <Outcome::Conflicted @base=[:b], @left=[:l], @right=[:r]>,
72
+ <Outcome::Resolved @result=[:c]>
73
+ ]
74
+
75
+ and has `MergeResult#conflict` set to `true`
76
+
77
+ ## Split, Join, and Conflict functions
78
+ Dyph works on arrays of objects that implement equatable and hash (see `Dyph::Equatable`). For various reasons one might want to delegate the splitting and joining of the input/out to Dyph. (i.e. so one would not have to `map` over the input and output to do the transformation)
79
+
80
+ ### With merge parameter `lambdas`
81
+ One can define `split_funciton`, `join_function`, and `conflict_function` to `Dyph::Diff.merge` such as splitting on word boundries, (but keeping delimiters):
82
+
83
+ split_function = ->(string) { string.split(/\b/) }
84
+
85
+ and then a join function to handle the resulting arrays
86
+
87
+ join_function = ->(array) { array.join }
88
+
89
+ which may be invoked with
90
+
91
+ left = "The quick brown fox left the lazy dog"
92
+ base = "The quick brown fox jumped over the lazy dog."
93
+ right = "The right brown fox jumped over the lazy dog"
94
+ merge_results = Dyph::Differ.merge(left, base, right, split_function: split_function, join_function: join_function)
95
+ merge_results.joined_results
96
+ will then return
97
+
98
+ "The right brown fox left the lazy dog"
99
+
100
+ ### Conflict Handlers
101
+ Similarly one can instruct the differ on how to deal with conflicts. The `conflict_function` is passed a list of Outcomes from the diff:
102
+
103
+ conflict_funciton = ->(outcome_list) { ... }
104
+
105
+ which one can then pass to the `Differ#merge` method as
106
+
107
+ Dyph::Differ.merge(left, base, right, conflict_function: conflict_funciton)
108
+
109
+ ### Class Level Processor with Example
110
+ In addition to argument level `split`, `join`, `merge` functions, Dyph also supports object level processors:
111
+
112
+ DIFF_PREPROCESSOR = -> (object) { ... }
113
+ DIFF_POSTPROCESSOR = -> (array) { ... }
114
+ DIFF_CONFLICT_PROCESSOR = ->(outcome_list) { ... }
115
+
116
+ that will look something like:
117
+
118
+ class GreetingCard
119
+ attr_reader :message
120
+
121
+ #Dyph Processors
122
+ DIFF_PREPROCESSOR = -> (sentence) { sentence.message.split(/\b/) }
123
+ DIFF_POSTPROCESSOR = -> (array) { array.join }
124
+ DIFF_CONFLICT_PROCESSOR = ->(outcome_list) do
125
+ outcome_list.map do |outcome|
126
+ if outcome.conflicted?
127
+ [
128
+ "<span class='conflict_left'>#{outcome.left.join}</span>",
129
+ "<span class='conflict_base'>#{outcome.base.join}</span>",
130
+ "<span class='conflict_right'>#{outcome.right.join}</span>"
131
+ ].join
132
+ else
133
+ outcome.result.join
134
+ end
135
+ end.join
136
+ end
137
+
138
+ def initialize(message)
139
+ @message = message
140
+ end
141
+
142
+ end
143
+
144
+ When there are no conflictes:
145
+
146
+ left = GreetingCard.new("Ho! Ho! Ho! Merry Christmas!")
147
+ base = GreetingCard.new("Merry Christmas!")
148
+ right = GreetingCard.new("Merry Christmas! And a Happy New Year")
149
+ Dyph::Differ.merge(left, base, right).joined_results
150
+
151
+ => "Ho! Ho! Ho! Merry Christmas! And a Happy New Year"
152
+
153
+ and when there are:
154
+
155
+ left = GreetingCard.new("Happy Christmas!")
156
+ base = GreetingCard.new("Merry Christmas!")
157
+ right = GreetingCard.new("Just Christmas!")
158
+ Dyph::Differ.merge(left, base, right).joined_results
159
+
160
+ => "<span class='conflict_left'>Happy</span><span class='conflict_base'>Merry</span><span class='conflict_right'>Just</span> Christmas!"
161
+
162
+
163
+ ## References:
164
+ [Three-way file comparison algorithm (python)](https://www.cbica.upenn.edu/sbia/software/basis/apidoc/v1.2/diff3_8py_source.html)
165
+
166
+ [Moin Three way differ (python)](http://hg.moinmo.in/moin/2.0/file/4a997d9f5e26/MoinMoin/util/diff3.py)
167
+
168
+ [Text Diff3 (perl)](http://search.cpan.org/~tociyuki/Text-Diff3-0.10/lib/Text/Diff3.pm)
169
+
170
+
171
+ ## Forked from GoBoundless/dyph
172
+ This project was forked from [GoBoundless/dyph](https://github.com/GoBoundless/dyph).
173
+
174
+
175
+ ## The MIT License (MIT)
176
+
177
+ Copyright © `2016` `Boundless`
178
+
179
+ Permission is hereby granted, free of charge, to any person
180
+ obtaining a copy of this software and associated documentation
181
+ files (the “Software”), to deal in the Software without
182
+ restriction, including without limitation the rights to use,
183
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
184
+ copies of the Software, and to permit persons to whom the
185
+ Software is furnished to do so, subject to the following
186
+ conditions:
187
+
188
+ The above copyright notice and this permission notice shall be
189
+ included in all copies or substantial portions of the Software.
190
+
191
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
192
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
193
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
194
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
195
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
196
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
197
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
198
+ OTHER DEALINGS IN THE SOFTWARE.
199
+
@@ -0,0 +1,25 @@
1
+ require "dyph/version"
2
+ require "dyph/differ"
3
+
4
+ require "dyph/merge_result"
5
+
6
+ require "dyph/outcome"
7
+ require "dyph/outcome/resolved"
8
+ require "dyph/outcome/conflicted"
9
+
10
+ require "dyph/support/diff3"
11
+
12
+ require "dyph/support/collater"
13
+ require "dyph/support/merger"
14
+ require "dyph/support/sanity_check"
15
+ require "dyph/support/assign_action"
16
+
17
+ require "dyph/two_way_differs/heckel_diff"
18
+
19
+ require "dyph/two_way_differs/output_converter"
20
+
21
+ require "dyph/action"
22
+ require "dyph/action/add"
23
+ require "dyph/action/no_change"
24
+ require "dyph/action/delete"
25
+ require "dyph/equatable"
@@ -0,0 +1,12 @@
1
+ module Dyph
2
+ class Action
3
+ attr_accessor :value, :old_index, :new_index
4
+
5
+ def initialize(value:, old_index:, new_index:)
6
+ @value = value
7
+ @old_index = old_index
8
+ @new_index = new_index
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module Dyph
2
+ class Action::Add < Action
3
+ def symbol
4
+ :add
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Dyph
2
+ class Action::Delete < Action
3
+ def symbol
4
+ :delete
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Dyph
2
+ class Action::NoChange < Action ;
3
+ def symbol
4
+ :no_change
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,102 @@
1
+ module Dyph
2
+ class Differ
3
+ # Perform a three way diff, which attempts to merge left and right relative to a common base
4
+ # @param left [Object]
5
+ # @param base [Object]
6
+ # @param right [Object]
7
+ # @param options [Hash] custom split, join, conflict functions, can also override the diff2 and diff3 algorithm. (see default_merge_options)
8
+ # @return [MergeResult]
9
+ def self.merge(left, base, right, options = {})
10
+ options = default_merge_options.merge(options)
11
+
12
+ split_function, join_function, conflict_function = set_processors(base, options)
13
+ split_left, split_base, split_right = [left, base, right].map { |t| split_function.call(t) }
14
+ merge_result = Dyph::Support::Merger.merge(split_left, split_base, split_right, diff2: options[:diff2], diff3: options[:diff3] )
15
+ collated_merge_results = Dyph::Support::Collater.collate_merge(merge_result, join_function, conflict_function)
16
+
17
+ if collated_merge_results.success?
18
+ Dyph::Support::SanityCheck.ensure_no_lost_data(split_left, split_base, split_right, collated_merge_results.results)
19
+ end
20
+
21
+ collated_merge_results
22
+ end
23
+
24
+ # Perform a two way diff
25
+ # @param left [Array]
26
+ # @param right [Array]
27
+ # @param options [Hash] Pass in an optional diff2 class
28
+ # @return [Array] array of Dyph::Action
29
+ def self.two_way_diff(left, right, options = {})
30
+ diff2 = options[:diff2] || default_diff2
31
+ diff_results = diff2.execute_diff(left, right)
32
+ raw_merge = Dyph::TwoWayDiffers::OutputConverter.merge_results(diff_results[:old_text], diff_results[:new_text],)
33
+ Dyph::TwoWayDiffers::OutputConverter.objectify(raw_merge)
34
+ end
35
+
36
+ # @return [Proc] helper proc for keeping newlines on string
37
+ def self.split_on_new_line
38
+ -> (some_string) { some_string.split(/(\n)/).each_slice(2).map { |x| x.join } }
39
+ end
40
+
41
+ # @return [Proc] helper proc for joining an array
42
+ def self.standard_join
43
+ -> (array) { array.join }
44
+ end
45
+
46
+ # @return [Proc] helper proc for identity
47
+ def self.identity
48
+ -> (x) { x }
49
+ end
50
+
51
+ # @return [Hash] the default options for a merge
52
+ def self.default_merge_options
53
+ {
54
+ split_function: identity,
55
+ join_function: identity,
56
+ conflict_function: identity,
57
+ diff2: default_diff2,
58
+ diff3: default_diff3,
59
+ use_class_processors: true
60
+ }
61
+ end
62
+
63
+ # @return [TwoWayDiffer]
64
+ def self.default_diff2
65
+ Dyph::TwoWayDiffers::HeckelDiff
66
+ end
67
+
68
+ # @return [ThreeWayDiffer]
69
+ def self.default_diff3
70
+ Dyph::Support::Diff3
71
+ end
72
+
73
+ def self.set_processors(base, options)
74
+ split_function = options[:split_function]
75
+ join_function = options[:join_function]
76
+ conflict_function = options[:conflict_function]
77
+ if options[:use_class_processors]
78
+ check_for_class_overrides(base.class, split_function, join_function, conflict_function)
79
+ else
80
+ [split_function, join_function, conflict_function]
81
+ end
82
+ end
83
+
84
+ def self.check_for_class_overrides(klass, split_function, join_function, conflict_function)
85
+ if klass.constants.include?(:DIFF_PREPROCESSOR)
86
+ split_function = klass::DIFF_PREPROCESSOR
87
+ end
88
+
89
+ if klass.constants.include?(:DIFF_POSTPROCESSOR)
90
+ join_function = klass::DIFF_POSTPROCESSOR
91
+ end
92
+
93
+ if klass.constants.include?(:DIFF_CONFLICT_PROCESSOR)
94
+ conflict_function = klass::DIFF_CONFLICT_PROCESSOR
95
+ end
96
+
97
+ [split_function, join_function, conflict_function]
98
+ end
99
+
100
+ private_class_method :check_for_class_overrides, :set_processors
101
+ end
102
+ end
@@ -0,0 +1,24 @@
1
+ module Dyph
2
+ module Equatable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def equate_with(*fields)
9
+ self.class_eval <<-CODE, __FILE__, __LINE__ + 1
10
+ def hash
11
+ self.class.hash ^ #{fields.map { |field| "#{field}.hash"}.join(" ^ ")}
12
+ end
13
+ CODE
14
+
15
+ self.class_eval <<-CODE, __FILE__, __LINE__ + 1
16
+ def ==(other)
17
+ self.class == other.class && #{fields.map { |field| "#{field} == other.#{field}"}.join(" && ")}
18
+ end
19
+ alias_method :eql?, :==
20
+ CODE
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ module Dyph
2
+ class MergeResult
3
+
4
+ # @param results [Array] diff3 output
5
+ # @param join_function [Proc] how to join the results together
6
+ # @param conflict [Boolean] sets the conflict's state
7
+ # @param conflict_handler [Proc] what to do with the conflicted results
8
+ def initialize(results, join_function, conflict: false, conflict_handler: nil)
9
+ @results = results
10
+ @join_function = join_function
11
+ @conflict_handler = conflict_handler
12
+ @conflict = conflict
13
+ end
14
+
15
+ # @return [Array] of outcomes (Outcome::Conflicted or Outcome::Resolved)
16
+ def results
17
+ @results
18
+ end
19
+
20
+ #@return [Boolean] success state
21
+ def success?
22
+ !@conflict
23
+ end
24
+
25
+ #@return [Boolean] conflict state
26
+ def conflict?
27
+ @conflict
28
+ end
29
+
30
+ # Applies the join function or conflict handler to diff3 results array
31
+ # @return the results with the methods provided by user or defaults applied
32
+ def joined_results
33
+ if conflict?
34
+ if @conflict_handler
35
+ @conflict_handler[results]
36
+ else
37
+ results
38
+ end
39
+ else
40
+ first, rest = results.first, results[1..-1]
41
+ rest.reduce(first) { |rs, r| rs.combine(r) }.apply(@join_function).result
42
+ end
43
+ end
44
+ end
45
+ end