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 +7 -0
- data/README.md +153 -0
- data/lib/tree_delta/base.rb +55 -0
- data/lib/tree_delta/intermediate.rb +172 -0
- data/lib/tree_delta/normaliser.rb +29 -0
- data/lib/tree_delta/operation.rb +21 -0
- data/lib/tree_delta/sorter.rb +12 -0
- data/lib/tree_delta/traversal.rb +34 -0
- data/lib/tree_delta.rb +6 -0
- metadata +119 -0
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,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
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: []
|