tree_delta 1.0.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.
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: []