dyph 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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