tree_delta 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: df6e2308561f7c2800ec01e8e1205db41e3fbdd2
4
+ data.tar.gz: 678b3ee7309db439390356c716229f8fd9afd271
5
+ SHA512:
6
+ metadata.gz: dc0433f176917897f6175fa7cba1a70b69d51d76d67cbfee924f2604005559a981c40a887d29ed23babe0515d9deaa5e9d70b541cbc810bc9adf273f66475815
7
+ data.tar.gz: cabc88fd38856bbd3dc53e57b3ddf1fda5f33decafc7eb5e72e9bcbd2b678fd8002b8ed81727c59f171353d14fbe1b8b731f2c02b36e8fa7faccf9d51bf41c0b
data/README.md ADDED
@@ -0,0 +1,153 @@
1
+ ## Tree Delta
2
+
3
+ Calculates the minimum set of operations that transform one tree into another.
4
+
5
+ ## Overview
6
+
7
+ Let's say we have the following ordered trees:
8
+
9
+ ```
10
+ alpha beta
11
+ / \ / \
12
+ a b a e
13
+ / \ \ / \
14
+ c d e d c
15
+ ```
16
+
17
+ You can transform the 'alpha' tree into the 'beta' tree through a combination
18
+ of operations:
19
+
20
+ ```
21
+ create - adds a node to the tree
22
+ update - changes the attributes of a node
23
+ delete - removes a node from the tree
24
+ detach - orphan a node from its parent
25
+ attach - give a node a new parent
26
+ ```
27
+
28
+ Here are the operations for the above example:
29
+
30
+ ```
31
+ detach 'e'
32
+ detach 'd'
33
+ detach 'a'
34
+ delete 'alpha'
35
+ create 'beta' as root
36
+ attach 'a' to 'beta' at position 0
37
+ attach 'd' to 'a' at position 0
38
+ attach 'e' to 'beta' at position 1
39
+ ```
40
+
41
+ The order of these operations is somewhat important. For example, node 'a' can
42
+ only be attached to 'beta' after 'beta' has been created.
43
+
44
+ Updates are not shown here. An 'update' operation is used to make changes to the
45
+ value object for a node. This is where you'd store the node's attributes.
46
+
47
+ ## Usage
48
+
49
+ You must first define a node class with the following methods:
50
+
51
+ ```ruby
52
+ #id
53
+ Returns an identifier that uniquely identifies the node across trees.
54
+
55
+ #parent
56
+ Returns the parent of the node.
57
+
58
+ #children
59
+ Returns an array of child nodes.
60
+
61
+ #value
62
+ Returns the value object associated with the node.
63
+ ```
64
+
65
+ You can then use your node class to create separate trees:
66
+
67
+ ```ruby
68
+ alpha = Node.new("alpha", children: [
69
+ Node.new("a", children: [
70
+ Node.new("c"),
71
+ Node.new("d")
72
+ ]),
73
+ Node.new("b", children: [
74
+ Node.new("e")
75
+ ])
76
+ ])
77
+
78
+ beta = Node.new("beta", children: [
79
+ Node.new("a", children: [
80
+ Node.new("d"),
81
+ Node.new("c")
82
+ ]),
83
+ Node.new("e")
84
+ ])
85
+ ```
86
+
87
+ Finally, you can instantiate a delta from the roots of the trees:
88
+
89
+ ```ruby
90
+ delta = TreeDelta.new(from: alpha, to: beta)
91
+
92
+ delta.each do |operation|
93
+ # ...
94
+ end
95
+ ```
96
+
97
+ ## Operation
98
+
99
+ An operation is a simple object that describes a transformation.
100
+
101
+ It can contain up to five pieces of information, as shown here:
102
+
103
+ | | type | id | value | parent | position |
104
+ | --------:|:--------:|:--------:|:--------:|:--------:|:--------:|
105
+ | create | ✓ | ✓ | ✓ | ✓ | ✓ |
106
+ | update | ✓ | ✓ | ✓ | | |
107
+ | delete | ✓ | ✓ | | | |
108
+ | detach | ✓ | ✓ | | | |
109
+ | attach | ✓ | ✓ | | ✓ | ✓ |
110
+
111
+ Here is an example:
112
+
113
+ ```ruby
114
+ operation.type
115
+ #=> :create
116
+
117
+ operation.id
118
+ #=> "foo"
119
+
120
+ operation.value
121
+ #=> 123
122
+
123
+ operation.parent
124
+ #=> "bar"
125
+
126
+ operation.position
127
+ #=> 3
128
+ ```
129
+
130
+ The 'value' is the value object for the node. This can be a literal value
131
+ or an object, such as a hash of attributes.
132
+
133
+ The 'position' refers to the node's index position relative to its siblings,
134
+ starting at zero.
135
+
136
+ The 'parent' will be nil if the node is the root of the tree.
137
+
138
+ ## Assumptions
139
+
140
+ - The delta assumes that deletions cascade down to subtrees under a node. It
141
+ will not yield operations to delete descendants of a node.
142
+
143
+ - The delta assumes that positions for siblings are updated when a node is
144
+ created or attached. Any sibling to the right of the node should have its
145
+ position incremented by one.
146
+
147
+ - The delta assumes that positions for siblings are updated when a node is
148
+ deleted or detached. Any sibling to the right of the node should have its
149
+ position decremented by one.
150
+
151
+ ## Contribution
152
+
153
+ If you'd like to contribute, please open an issue or submit a pull request.
@@ -0,0 +1,55 @@
1
+ class TreeDelta < Enumerator
2
+
3
+ attr_reader :from, :to
4
+
5
+ def initialize(from:, to:)
6
+ @from, @to = from, to
7
+ end
8
+
9
+ def each(&block)
10
+ deletes_and_detaches.each { |o| yield o }
11
+ creates_and_attaches.each { |o| yield o }
12
+ updates.each { |o| yield o }
13
+ end
14
+
15
+ private
16
+
17
+ def deletes_and_detaches
18
+ traversal = Traversal.new(direction: :right_to_left, order: :post)
19
+ enumerator = traversal.traverse(from)
20
+
21
+ Sorter.sort(deletes.to_a + detaches.to_a, enumerator)
22
+ end
23
+
24
+ def creates_and_attaches
25
+ traversal = Traversal.new(direction: :left_to_right, order: :pre)
26
+ enumerator = traversal.traverse(to)
27
+
28
+ Sorter.sort(creates.to_a + attaches.to_a, enumerator)
29
+ end
30
+
31
+ def creates
32
+ intermediate.creates
33
+ end
34
+
35
+ def updates
36
+ intermediate.updates
37
+ end
38
+
39
+ def deletes
40
+ intermediate.deletes
41
+ end
42
+
43
+ def detaches
44
+ intermediate.detaches
45
+ end
46
+
47
+ def attaches
48
+ intermediate.attaches
49
+ end
50
+
51
+ def intermediate
52
+ @intermediate ||= Intermediate.new(from: from, to: to)
53
+ end
54
+
55
+ end
@@ -0,0 +1,172 @@
1
+ class TreeDelta::Intermediate
2
+
3
+ attr_reader :from, :to
4
+
5
+ def initialize(from:, to:)
6
+ @from, @to = from, to
7
+ end
8
+
9
+ def creates
10
+ Enumerator.new do |y|
11
+ additions.each do |node|
12
+ y.yield TreeDelta::Operation.new(
13
+ type: :create,
14
+ id: node.id,
15
+ value: node.value,
16
+ parent: parent_id(node),
17
+ position: position(node)
18
+ )
19
+ end
20
+ end
21
+ end
22
+
23
+ def updates
24
+ Enumerator.new do |y|
25
+ updated_nodes.each do |node|
26
+ y.yield TreeDelta::Operation.new(
27
+ type: :update,
28
+ id: node.id,
29
+ value: node.value,
30
+ )
31
+ end
32
+ end
33
+ end
34
+
35
+ def deletes
36
+ Enumerator.new do |y|
37
+ normalised_deletions.each do |node|
38
+ y.yield TreeDelta::Operation.new(
39
+ type: :delete,
40
+ id: node.id
41
+ )
42
+ end
43
+ end
44
+ end
45
+
46
+ def detaches
47
+ Enumerator.new do |y|
48
+ moves.each do |node|
49
+ unless previous_root?(node)
50
+ y.yield TreeDelta::Operation.new(
51
+ type: :detach,
52
+ id: node.id
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def attaches
60
+ Enumerator.new do |y|
61
+ moves.each do |node|
62
+ unless root?(node)
63
+ y.yield TreeDelta::Operation.new(
64
+ type: :attach,
65
+ id: node.id,
66
+ parent: parent_id(node),
67
+ position: position(node)
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def from_nodes
77
+ @from_nodes ||= nodes(from)
78
+ end
79
+
80
+ def to_nodes
81
+ @to_nodes ||= nodes(to)
82
+ end
83
+
84
+ def additions
85
+ subtract(to_nodes, from_nodes)
86
+ end
87
+
88
+ def updated_nodes
89
+ to_nodes.select do |to_node|
90
+ from_node = from_node_for(to_node)
91
+ from_node && from_node.value != to_node.value
92
+ end
93
+ end
94
+
95
+ def normalised_deletions
96
+ TreeDelta::Normaliser.normalise_deletions(deletions)
97
+ end
98
+
99
+ def deletions
100
+ subtract(from_nodes, to_nodes)
101
+ end
102
+
103
+ def nodes(tree)
104
+ traversal = TreeDelta::Traversal.new(direction: :left_to_right, order: :pre)
105
+ traversal.traverse(tree).to_a
106
+ end
107
+
108
+ def subtract(a, b)
109
+ a.reject { |e| b.any? { |f| e.id == f.id } }
110
+ end
111
+
112
+ def parent_id(node)
113
+ node && node.parent ? node.parent.id : nil
114
+ end
115
+
116
+ def position(node)
117
+ node.parent ? node.parent.children.index(node) : 0
118
+ end
119
+
120
+ def moves
121
+ @moves ||= parent_changes + normalised_position_changes
122
+ end
123
+
124
+ def parent_changes
125
+ nodes = from_nodes.select { |n| changed_parent?(n) }
126
+ nodes.map { |from_node| to_node_for(from_node) }
127
+ end
128
+
129
+ def changed_parent?(from_node)
130
+ to_node = to_node_for(from_node)
131
+ to_node && parent_id(to_node) != parent_id(from_node)
132
+ end
133
+
134
+ def normalised_position_changes
135
+ groups = position_changes.group_by { |n| parent_id(n) }
136
+
137
+ groups.map do |_, nodes|
138
+ TreeDelta::Normaliser.normalise_position_changes(nodes)
139
+ end.flatten
140
+ end
141
+
142
+ def position_changes
143
+ nodes = from_nodes.select { |n| changed_position?(n) }
144
+ nodes.map { |from_node| to_node_for(from_node) }
145
+ end
146
+
147
+ def changed_position?(from_node)
148
+ to_node = to_node_for(from_node)
149
+
150
+ to_node &&
151
+ parent_id(from_node) == parent_id(to_node) &&
152
+ position(from_node) != position(to_node)
153
+ end
154
+
155
+ def from_node_for(to_node)
156
+ from_nodes.detect { |n| n.id == to_node.id }
157
+ end
158
+
159
+ def to_node_for(from_node)
160
+ to_nodes.detect { |n| n.id == from_node.id }
161
+ end
162
+
163
+ def root?(to_node)
164
+ !to_node.parent
165
+ end
166
+
167
+ def previous_root?(to_node)
168
+ from_node = from_node_for(to_node)
169
+ !from_node.parent
170
+ end
171
+
172
+ end
@@ -0,0 +1,29 @@
1
+ module TreeDelta::Normaliser
2
+ class << self
3
+
4
+ def normalise_position_changes(nodes)
5
+ moving_nodes = []
6
+
7
+ previous_node = nil
8
+ nodes.each do |current_node|
9
+ if previous_node && position(current_node) < position(previous_node)
10
+ moving_nodes << current_node
11
+ end
12
+ previous_node = current_node
13
+ end
14
+
15
+ moving_nodes
16
+ end
17
+
18
+ def normalise_deletions(nodes)
19
+ nodes.reject { |n| nodes.any? { |m| n.parent == m } }
20
+ end
21
+
22
+ private
23
+
24
+ def position(node)
25
+ node.parent ? node.parent.children.index(node) : 0
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ class TreeDelta::Operation
2
+
3
+ attr_reader :type, :id, :value, :parent, :position
4
+
5
+ def initialize(type:, id:, value: nil, parent: nil, position: nil)
6
+ @type = type
7
+ @id = id
8
+ @value = value if value
9
+ @parent = parent if parent
10
+ @position = position if position
11
+ end
12
+
13
+ def ==(other)
14
+ @type == other.type &&
15
+ @id == other.id &&
16
+ @value == other.value &&
17
+ @parent == other.parent &&
18
+ @position == other.position
19
+ end
20
+
21
+ end
@@ -0,0 +1,12 @@
1
+ module TreeDelta::Sorter
2
+ def self.sort(array, enumerator)
3
+ sorted_array = []
4
+
5
+ enumerator.each do |object|
6
+ element = array.detect { |e| e.id == object.id }
7
+ sorted_array << element if element
8
+ end
9
+
10
+ sorted_array
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ class TreeDelta::Traversal < Enumerator
2
+
3
+ attr_reader :direction, :order
4
+
5
+ def initialize(direction:, order:)
6
+ @direction = direction
7
+ @order = order
8
+ end
9
+
10
+ def traverse(node)
11
+ Enumerator.new do |y|
12
+ if node
13
+ y.yield(node) if order == :pre
14
+
15
+ ordered_children(node).each do |child|
16
+ traverse(child).each { |n| y.yield(n) }
17
+ end
18
+
19
+ y.yield(node) if order == :post
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def ordered_children(node)
27
+ if direction == :left_to_right
28
+ node.children
29
+ elsif direction == :right_to_left
30
+ node.children.reverse
31
+ end
32
+ end
33
+
34
+ end
data/lib/tree_delta.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "tree_delta/base"
2
+ require "tree_delta/operation"
3
+ require "tree_delta/traversal"
4
+ require "tree_delta/sorter"
5
+ require "tree_delta/intermediate"
6
+ require "tree_delta/normaliser"
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tree_delta
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Patuzzo
8
+ - Nick Haworth
9
+ - Jason Pernthaller
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2015-03-03 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '10.4'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '10.4'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rspec
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '3.2'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '3.2'
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.10'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '0.10'
57
+ - !ruby/object:Gem::Dependency
58
+ name: ascii_tree
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '1.0'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 1.0.3
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.0'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 1.0.3
77
+ description: Calculates the minimum set of operations that transform one tree into
78
+ another.
79
+ email:
80
+ - chris@patuzzo.co.uk
81
+ - nick.haworth@which.co.uk
82
+ - jason.pernthaller@which.co.uk
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - README.md
88
+ - lib/tree_delta.rb
89
+ - lib/tree_delta/base.rb
90
+ - lib/tree_delta/intermediate.rb
91
+ - lib/tree_delta/normaliser.rb
92
+ - lib/tree_delta/operation.rb
93
+ - lib/tree_delta/sorter.rb
94
+ - lib/tree_delta/traversal.rb
95
+ homepage: https://github.com/whichdigital/tree_delta
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.2.2
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Tree Delta
119
+ test_files: []