tree_delta 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|