sycamore 0.1.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/.editorconfig +24 -0
- data/.gitignore +11 -0
- data/.rspec +6 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/.yardopts +11 -0
- data/AUTHORS +1 -0
- data/CHANGELOG.md +10 -0
- data/CONTRIBUTING.md +5 -0
- data/CREDITS +0 -0
- data/Gemfile +7 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +516 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/bin/console +9 -0
- data/bin/setup +8 -0
- data/lib/sycamore.rb +13 -0
- data/lib/sycamore/absence.rb +179 -0
- data/lib/sycamore/exceptions.rb +10 -0
- data/lib/sycamore/extension.rb +2 -0
- data/lib/sycamore/extension/nothing.rb +4 -0
- data/lib/sycamore/extension/path.rb +7 -0
- data/lib/sycamore/extension/tree.rb +4 -0
- data/lib/sycamore/nothing.rb +157 -0
- data/lib/sycamore/path.rb +261 -0
- data/lib/sycamore/path_root.rb +43 -0
- data/lib/sycamore/stree.rb +4 -0
- data/lib/sycamore/tree.rb +1272 -0
- data/lib/sycamore/version.rb +28 -0
- data/script/console +14 -0
- data/script/console.cmd +1 -0
- data/support/doctest_helper.rb +2 -0
- data/sycamore.gemspec +28 -0
- metadata +178 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Sycamore
|
4
|
+
class Path
|
5
|
+
##
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
class Root < Path
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@parent, @node = nil, nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def up(distance = 1)
|
16
|
+
super unless distance.is_a? Integer
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def root?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def length
|
25
|
+
0
|
26
|
+
end
|
27
|
+
|
28
|
+
def join(delimiter = '/')
|
29
|
+
''
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
'#<Path:Root>'
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
'#<Sycamore::Path::Root>'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
ROOT = Root.instance # @api private
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,1272 @@
|
|
1
|
+
module Sycamore
|
2
|
+
|
3
|
+
##
|
4
|
+
# A tree data structure as a recursively nested set of {#nodes nodes} of immutable values.
|
5
|
+
#
|
6
|
+
# See {file:README.md} for a general introduction.
|
7
|
+
#
|
8
|
+
class Tree
|
9
|
+
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
# the internal hash representation of this tree
|
13
|
+
attr_reader :data
|
14
|
+
protected :data
|
15
|
+
|
16
|
+
########################################################################
|
17
|
+
# @group CQS reflection
|
18
|
+
########################################################################
|
19
|
+
|
20
|
+
# the names of all command methods, which add elements to a Tree
|
21
|
+
ADDITIVE_COMMAND_METHODS = %i[add << replace add_node_with_empty_child] << :[]=
|
22
|
+
|
23
|
+
# the names of all command methods, which delete elements from a Tree
|
24
|
+
DESTRUCTIVE_COMMAND_METHODS = %i[delete >> clear compact replace] << :[]=
|
25
|
+
|
26
|
+
# the names of all additive command methods, which only add elements from a Tree
|
27
|
+
PURE_ADDITIVE_COMMAND_METHODS = ADDITIVE_COMMAND_METHODS - DESTRUCTIVE_COMMAND_METHODS
|
28
|
+
|
29
|
+
# the names of all destructive command methods, which only delete elements from a Tree
|
30
|
+
PURE_DESTRUCTIVE_COMMAND_METHODS = DESTRUCTIVE_COMMAND_METHODS - ADDITIVE_COMMAND_METHODS
|
31
|
+
|
32
|
+
# the names of all methods, which change the state of a Tree
|
33
|
+
COMMAND_METHODS = ADDITIVE_COMMAND_METHODS + DESTRUCTIVE_COMMAND_METHODS +
|
34
|
+
%i[freeze]
|
35
|
+
|
36
|
+
# the names of all query methods, which return a boolean
|
37
|
+
PREDICATE_METHODS =
|
38
|
+
%i[nothing? absent? existent? present? blank? empty?
|
39
|
+
include? include_node? member? key? has_key? include_path? path? >= > < <=
|
40
|
+
leaf? leaves? internal? external? flat? nested?
|
41
|
+
sleaf? sleaves? strict_leaf? strict_leaves?
|
42
|
+
eql? matches? === ==]
|
43
|
+
|
44
|
+
# the names of all methods, which side-effect-freeze return only a value
|
45
|
+
QUERY_METHODS = PREDICATE_METHODS +
|
46
|
+
%i[new_child dup hash to_native_object to_h to_s inspect
|
47
|
+
node nodes keys child_of child_at dig fetch
|
48
|
+
size total_size tsize height
|
49
|
+
each each_path paths each_node each_key each_pair] << :[]
|
50
|
+
|
51
|
+
%i[COMMAND_METHODS QUERY_METHODS PREDICATE_METHODS
|
52
|
+
ADDITIVE_COMMAND_METHODS DESTRUCTIVE_COMMAND_METHODS
|
53
|
+
PURE_ADDITIVE_COMMAND_METHODS PURE_DESTRUCTIVE_COMMAND_METHODS]
|
54
|
+
.each do |method_set|
|
55
|
+
define_singleton_method(method_set.downcase) { const_get method_set }
|
56
|
+
end
|
57
|
+
|
58
|
+
########################################################################
|
59
|
+
# @group Construction
|
60
|
+
########################################################################
|
61
|
+
|
62
|
+
##
|
63
|
+
# Creates a new empty Tree.
|
64
|
+
#
|
65
|
+
def initialize
|
66
|
+
@data = Hash.new
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Creates a new Tree and initializes it with the given data.
|
71
|
+
#
|
72
|
+
# @param (see #add)
|
73
|
+
# @return [Tree]
|
74
|
+
#
|
75
|
+
# @example
|
76
|
+
# Tree[1]
|
77
|
+
# Tree[1, 2, 3]
|
78
|
+
# Tree[1, 2, 2, 3] # duplicates are ignored, so this results in the same tree as the previous
|
79
|
+
# Tree[x: 1, y: 2]
|
80
|
+
#
|
81
|
+
def self.with(*args)
|
82
|
+
tree = new
|
83
|
+
tree.add( args.size == 1 ? args.first : args ) unless args.empty?
|
84
|
+
tree
|
85
|
+
end
|
86
|
+
|
87
|
+
class << self
|
88
|
+
alias from with
|
89
|
+
alias [] with
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Creates a new tree meant to be used as a child.
|
94
|
+
#
|
95
|
+
# This method is used for instantiation of child trees. When you want to a
|
96
|
+
# tree with different types child trees, maybe depending on the parent node,
|
97
|
+
# you can subclass {Sycamore::Tree} and override this method to your needs.
|
98
|
+
# By default it creates trees of the same type as this tree.
|
99
|
+
#
|
100
|
+
# @param parent_node [Object] of the child tree to be created
|
101
|
+
# @return [Tree]
|
102
|
+
#
|
103
|
+
# @api private
|
104
|
+
#
|
105
|
+
def new_child(parent_node, *args)
|
106
|
+
self.class.new(*args)
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
########################################################################
|
111
|
+
# @group Absence and Nothing predicates
|
112
|
+
########################################################################
|
113
|
+
|
114
|
+
##
|
115
|
+
# Checks if this is the {Nothing} tree.
|
116
|
+
#
|
117
|
+
# @return [Boolean]
|
118
|
+
#
|
119
|
+
def nothing?
|
120
|
+
false
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Checks if this is an unresolved {Absence} or {Nothing}.
|
125
|
+
#
|
126
|
+
# @return [Boolean]
|
127
|
+
#
|
128
|
+
def absent?
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Checks if this is not an {Absence} or {Nothing}.
|
134
|
+
#
|
135
|
+
# @return [Boolean]
|
136
|
+
#
|
137
|
+
def existent?
|
138
|
+
not absent?
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Checks if this is not {#blank? blank}, i.e. {#empty? empty}.
|
143
|
+
#
|
144
|
+
# @note This is not the negation of {#absent?}, since this would result in a
|
145
|
+
# different behaviour than {http://api.rubyonrails.org/classes/Object.html#method-i-present-3F ActiveSupports present?}
|
146
|
+
# method. For the negation of {#absent?}, see {#existent?}.
|
147
|
+
#
|
148
|
+
# @return [Boolean]
|
149
|
+
#
|
150
|
+
def present?
|
151
|
+
not blank?
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
########################################################################
|
156
|
+
# @group Element access
|
157
|
+
########################################################################
|
158
|
+
|
159
|
+
#####################
|
160
|
+
# command methods #
|
161
|
+
#####################
|
162
|
+
|
163
|
+
##
|
164
|
+
# Adds nodes or a tree structure to this tree.
|
165
|
+
#
|
166
|
+
# @overload add(node)
|
167
|
+
# adds a single node
|
168
|
+
# @param node [Object]
|
169
|
+
#
|
170
|
+
# @overload add(node_collection)
|
171
|
+
# adds multiple nodes
|
172
|
+
# @param node_collection [Enumerable]
|
173
|
+
#
|
174
|
+
# @overload add(tree_structure)
|
175
|
+
# adds a tree structure of nodes
|
176
|
+
# @param tree_structure [Hash, Tree]
|
177
|
+
#
|
178
|
+
# @return +self+ as a proper command method
|
179
|
+
#
|
180
|
+
# @raise [InvalidNode] when given a nested node set
|
181
|
+
#
|
182
|
+
# @example
|
183
|
+
# tree = Tree.new
|
184
|
+
# tree.add :foo
|
185
|
+
# tree.add [:bar, :baz]
|
186
|
+
# tree.add [:node, [:nested, :values]] # => raise Sycamore::InvalidNode, "[:nested, :values] is not a valid tree node"
|
187
|
+
# tree.add foo: 1, bar: {baz: 2}
|
188
|
+
# tree.add foo: [:node, [:nested, :values]] # => raise Sycamore::InvalidNode, "[:nested, :values] is not a valid tree node"
|
189
|
+
#
|
190
|
+
# tree = Tree.new
|
191
|
+
# tree[:foo][:bar] << :baz
|
192
|
+
# tree[:foo] << { bar: 1, qux: 2 }
|
193
|
+
# tree.to_h # => {:foo=>{:bar=>[:baz, 1], :qux=>2}}
|
194
|
+
#
|
195
|
+
# @todo support Paths
|
196
|
+
#
|
197
|
+
def add(nodes_or_tree)
|
198
|
+
case
|
199
|
+
when Tree.like?(nodes_or_tree) then add_tree(nodes_or_tree)
|
200
|
+
when nodes_or_tree.is_a?(Enumerable) then add_nodes(nodes_or_tree)
|
201
|
+
else add_node(nodes_or_tree)
|
202
|
+
end
|
203
|
+
|
204
|
+
self
|
205
|
+
end
|
206
|
+
|
207
|
+
alias << add
|
208
|
+
|
209
|
+
private def add_node(node)
|
210
|
+
return self if node.equal? Nothing
|
211
|
+
return add_tree(node) if Tree.like? node
|
212
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
213
|
+
|
214
|
+
@data[node] ||= Nothing
|
215
|
+
|
216
|
+
self
|
217
|
+
end
|
218
|
+
|
219
|
+
private def add_nodes(nodes)
|
220
|
+
nodes.each { |node| add_node(node) }
|
221
|
+
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# Adds a node with an empty child to this tree.
|
227
|
+
#
|
228
|
+
# @return +self+ as a proper command method
|
229
|
+
#
|
230
|
+
# @raise [InvalidNode]
|
231
|
+
#
|
232
|
+
# @api private
|
233
|
+
#
|
234
|
+
def add_node_with_empty_child(node)
|
235
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
236
|
+
|
237
|
+
if @data.fetch(node, Nothing).nothing?
|
238
|
+
@data[node] = new_child(node)
|
239
|
+
end
|
240
|
+
|
241
|
+
self
|
242
|
+
end
|
243
|
+
|
244
|
+
private def add_child(node, children)
|
245
|
+
return add_node(node) if Nothing.like?(children)
|
246
|
+
|
247
|
+
add_node_with_empty_child(node)
|
248
|
+
@data[node] << children
|
249
|
+
|
250
|
+
self
|
251
|
+
end
|
252
|
+
|
253
|
+
private def add_tree(tree)
|
254
|
+
tree.each { |node, child| add_child(node, child) }
|
255
|
+
|
256
|
+
self
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Remove nodes or a tree structure from this tree.
|
261
|
+
#
|
262
|
+
# If a given node is in the {#nodes} set, it gets deleted, otherwise it is
|
263
|
+
# silently ignored.
|
264
|
+
#
|
265
|
+
# @overload delete(node)
|
266
|
+
# deletes a single node
|
267
|
+
# @param node [Object]
|
268
|
+
#
|
269
|
+
# @overload delete(node_collection)
|
270
|
+
# deletes multiple nodes
|
271
|
+
# @param node_collection [Enumerable]
|
272
|
+
#
|
273
|
+
# @overload delete(tree_structure)
|
274
|
+
# deletes a tree structure of nodes
|
275
|
+
# @param tree_structure [Hash, Tree]
|
276
|
+
#
|
277
|
+
# @return +self+ as a proper command method
|
278
|
+
#
|
279
|
+
# @raise [InvalidNode] when given a nested node set
|
280
|
+
#
|
281
|
+
# @example
|
282
|
+
# tree = Tree[ "a" => 100, "b" => 200, "c" => 300, "d" => {foo: [:bar, :baz]} ]
|
283
|
+
# tree.delete "a"
|
284
|
+
# tree.to_h # => {"b" => 200, "c" => 300, "d" => {foo: [:bar, :baz]}}
|
285
|
+
# tree.delete ["a", "b", "c"]
|
286
|
+
# tree.to_h # => {"d" => {foo: [:bar, :baz]}}
|
287
|
+
# tree.delete "d" => {foo: :bar}
|
288
|
+
# tree.to_h # => {"d" => {foo: :baz}}
|
289
|
+
# tree.delete "d" => {foo: :baz}
|
290
|
+
# tree.to_h # => {}
|
291
|
+
#
|
292
|
+
# @todo differentiate a greedy and a non-greedy variant
|
293
|
+
# @todo support Paths
|
294
|
+
#
|
295
|
+
def delete(nodes_or_tree)
|
296
|
+
case
|
297
|
+
when Tree.like?(nodes_or_tree) then delete_tree(nodes_or_tree)
|
298
|
+
when nodes_or_tree.is_a?(Enumerable) then delete_nodes(nodes_or_tree)
|
299
|
+
else delete_node(nodes_or_tree)
|
300
|
+
end
|
301
|
+
|
302
|
+
self
|
303
|
+
end
|
304
|
+
|
305
|
+
alias >> delete
|
306
|
+
|
307
|
+
private def delete_node(node)
|
308
|
+
return delete_tree(node) if Tree.like? node
|
309
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
310
|
+
|
311
|
+
@data.delete(node)
|
312
|
+
|
313
|
+
self
|
314
|
+
end
|
315
|
+
|
316
|
+
private def delete_nodes(nodes)
|
317
|
+
nodes.each { |node| delete_node(node) }
|
318
|
+
|
319
|
+
self
|
320
|
+
end
|
321
|
+
|
322
|
+
private def delete_tree(tree)
|
323
|
+
tree.each do |node, child|
|
324
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
325
|
+
next unless include? node
|
326
|
+
if Nothing.like?(child) or (child.respond_to?(:empty?) and child.empty?)
|
327
|
+
delete_node node
|
328
|
+
else
|
329
|
+
child_of(node).tap do |this_child|
|
330
|
+
this_child.delete child
|
331
|
+
delete_node(node) if this_child.empty?
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
self
|
337
|
+
end
|
338
|
+
|
339
|
+
##
|
340
|
+
# Replaces the contents of this tree.
|
341
|
+
#
|
342
|
+
# @overload replace(node)
|
343
|
+
# Replaces the contents of this tree with a single node.
|
344
|
+
# @param node [Object]
|
345
|
+
#
|
346
|
+
# @overload replace(node_collection)
|
347
|
+
# Replaces the contents of this tree with multiple nodes.
|
348
|
+
# @param node_collection [Enumerable]
|
349
|
+
#
|
350
|
+
# @overload replace(tree_structure)
|
351
|
+
# Replaces the contents of this tree with a tree structure of nodes.
|
352
|
+
# @param tree_structure [Hash, Tree]
|
353
|
+
#
|
354
|
+
# @return +self+ as a proper command method
|
355
|
+
#
|
356
|
+
# @raise [InvalidNode] when given a nested node set
|
357
|
+
#
|
358
|
+
# @example
|
359
|
+
# tree = Tree[ "a" => 100, "b" => 200, "d" => {foo: [:bar, :baz]} ]
|
360
|
+
# tree.replace(new: :content)
|
361
|
+
# tree.to_h # => {new: :content}
|
362
|
+
#
|
363
|
+
def replace(nodes_or_tree)
|
364
|
+
clear.add(nodes_or_tree)
|
365
|
+
end
|
366
|
+
|
367
|
+
##
|
368
|
+
# Replaces the contents of a child tree.
|
369
|
+
#
|
370
|
+
# As this is just a call of {#replace} on the child tree, you can assign
|
371
|
+
# content to not existing child trees. Just like {#child_at} you can
|
372
|
+
# reference a deeper node with a path of nodes.
|
373
|
+
#
|
374
|
+
# Note that even if you assign a {Sycamore::Tree} directly the given tree
|
375
|
+
# will not become part of this tree by reference.
|
376
|
+
#
|
377
|
+
# @overload []=(*path, node)
|
378
|
+
# Replaces the contents of the child at the given path with a single node.
|
379
|
+
# @param path [Array<Object>, Sycamore::Path] a path as a sequence of nodes or a {Path} object
|
380
|
+
# @param node [Object]
|
381
|
+
#
|
382
|
+
# @overload []=(*path, node_collection)
|
383
|
+
# Replaces the contents of the child at the given path with multiple nodes.
|
384
|
+
# @param path [Array<Object>, Sycamore::Path] a path as a sequence of nodes or a {Path} object
|
385
|
+
# @param node_collection [Enumerable]
|
386
|
+
#
|
387
|
+
# @overload []=(*path, tree_structure)
|
388
|
+
# Replaces the contents of the child at the given path with a tree structure of nodes.
|
389
|
+
# @param path [Array<Object>, Sycamore::Path] a path as a sequence of nodes or a {Path} object
|
390
|
+
# @param tree_structure [Hash, Tree]
|
391
|
+
#
|
392
|
+
# @return the rvalue as for any Ruby assignment
|
393
|
+
#
|
394
|
+
# @raise [InvalidNode] when given a nested node set
|
395
|
+
#
|
396
|
+
# @example
|
397
|
+
# tree = Tree[:foo]
|
398
|
+
# tree[:foo] = :bar
|
399
|
+
# tree.to_h # => {:foo => :bar}
|
400
|
+
# tree[:foo] = :baz
|
401
|
+
# tree.to_h # => {:foo => :baz}
|
402
|
+
# tree[1][2][3] = 4
|
403
|
+
# tree[1, 2, 3] = 4
|
404
|
+
# tree.to_h # => {:foo => :baz, 1 => {2 => {3 => 4}}}
|
405
|
+
# tree[1] = tree[:foo]
|
406
|
+
# tree.to_h # => {:foo => :baz, 1 => :baz}
|
407
|
+
# tree[:foo] << :bar
|
408
|
+
# tree.to_h # => {:foo => [:baz, :bar], 1 => :baz}
|
409
|
+
#
|
410
|
+
def []=(*args)
|
411
|
+
path, nodes_or_tree = args[0..-2], args[-1]
|
412
|
+
raise ArgumentError, 'wrong number of arguments (given 1, expected 2)' if path.empty?
|
413
|
+
|
414
|
+
child_at(*path).replace(nodes_or_tree)
|
415
|
+
end
|
416
|
+
|
417
|
+
##
|
418
|
+
# Deletes all nodes and their children.
|
419
|
+
#
|
420
|
+
# @return +self+ as a proper command method
|
421
|
+
#
|
422
|
+
# @example
|
423
|
+
# tree = Tree[1, 2, 3]
|
424
|
+
# tree.size # => 3
|
425
|
+
# tree.clear
|
426
|
+
# tree.size # => 0
|
427
|
+
#
|
428
|
+
def clear
|
429
|
+
@data.clear
|
430
|
+
|
431
|
+
self
|
432
|
+
end
|
433
|
+
|
434
|
+
##
|
435
|
+
# Deletes all empty child trees recursively.
|
436
|
+
#
|
437
|
+
# @return +self+ as a proper command method
|
438
|
+
#
|
439
|
+
# @example
|
440
|
+
# tree = Tree[foo: {bar: :baz}]
|
441
|
+
# tree[:foo, :bar].clear
|
442
|
+
# tree.to_h # => {foo: {bar: []}}
|
443
|
+
# tree.compact
|
444
|
+
# tree.to_h # => {foo: :bar}
|
445
|
+
#
|
446
|
+
def compact
|
447
|
+
@data.each do |node, child| case
|
448
|
+
when child.nothing? then next
|
449
|
+
when child.empty? then @data[node] = Nothing
|
450
|
+
else child.compact
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
self
|
455
|
+
end
|
456
|
+
|
457
|
+
|
458
|
+
#####################
|
459
|
+
# query methods #
|
460
|
+
#####################
|
461
|
+
|
462
|
+
##
|
463
|
+
# The nodes of this tree (without their children).
|
464
|
+
#
|
465
|
+
# @return [Array<Object>]
|
466
|
+
#
|
467
|
+
# @example
|
468
|
+
# tree = Tree[foo: [:bar, :baz]]
|
469
|
+
# tree.nodes # => [:foo]
|
470
|
+
# tree[:foo].nodes # => [:bar, :baz]
|
471
|
+
#
|
472
|
+
def nodes
|
473
|
+
@data.keys
|
474
|
+
end
|
475
|
+
|
476
|
+
alias keys nodes # Hash compatibility
|
477
|
+
|
478
|
+
|
479
|
+
##
|
480
|
+
# The only node of this tree or an exception, if more {#nodes nodes} present.
|
481
|
+
#
|
482
|
+
# @return [Object, nil] the single present node or +nil+, if no nodes present
|
483
|
+
#
|
484
|
+
# @raise [NonUniqueNodeSet] if more than one node present
|
485
|
+
#
|
486
|
+
# @example
|
487
|
+
# tree = Tree[foo: 1, bar: [2,3]]
|
488
|
+
# tree[:foo].node # => 1
|
489
|
+
# tree[:baz].node # => nil
|
490
|
+
# tree[:bar].node # => raise Sycamore::NonUniqueNodeSet, "multiple nodes present: [2, 3]"
|
491
|
+
#
|
492
|
+
def node
|
493
|
+
nodes = self.nodes
|
494
|
+
raise NonUniqueNodeSet, "multiple nodes present: #{nodes}" if nodes.size > 1
|
495
|
+
|
496
|
+
nodes.first
|
497
|
+
end
|
498
|
+
|
499
|
+
##
|
500
|
+
# The child tree of a node.
|
501
|
+
#
|
502
|
+
# When a child to the given node is not a present, an {Absence} object
|
503
|
+
# representing the missing tree is returned.
|
504
|
+
#
|
505
|
+
# @param node [Object]
|
506
|
+
# @return [Tree, Absence] the child tree of a node if present, otherwise an {Absence}
|
507
|
+
#
|
508
|
+
# @raise [InvalidNode] when given an +Enumerable+
|
509
|
+
#
|
510
|
+
# @example
|
511
|
+
# tree = Tree[foo: 1]
|
512
|
+
# tree.child_of(:foo).inspect # "#<Sycamore::Tree:0x3fea48dd0e74 {1=>nil}>"
|
513
|
+
# tree.child_of(:bar).inspect # "absent child of node :bar in #<Sycamore::Tree:0x3fea48dd0f3c {:foo=>1}>"
|
514
|
+
#
|
515
|
+
# @todo Should we differentiate the case of a leaf and a not present node? How?
|
516
|
+
#
|
517
|
+
def child_of(node)
|
518
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
519
|
+
|
520
|
+
Nothing.like?(child = @data[node]) ? Absence.at(self, node) : child
|
521
|
+
end
|
522
|
+
|
523
|
+
##
|
524
|
+
# The child tree of a node at a path.
|
525
|
+
#
|
526
|
+
# When a child at the given node path is not a present, an {Absence} object
|
527
|
+
# representing the missing tree is returned.
|
528
|
+
#
|
529
|
+
# @overload child_at(*nodes)
|
530
|
+
# @param nodes [Array<Object>] a path as a sequence of nodes
|
531
|
+
#
|
532
|
+
# @overload child_at(path)
|
533
|
+
# @param path [Path] a path as a {Sycamore::Path} object
|
534
|
+
#
|
535
|
+
# @return [Tree, Absence] the child tree at the given path if present, otherwise an {Absence}
|
536
|
+
#
|
537
|
+
# @example
|
538
|
+
# tree = Tree[foo: {bar: 1}]
|
539
|
+
# tree[:foo].inspect # "#<Sycamore::Tree:0x3fea48e24f10 {:bar=>1}>"
|
540
|
+
# tree[:foo, :bar].inspect # "#<Sycamore::Tree:0x3fea48e24ed4 {1=>nil}>"
|
541
|
+
# tree[:foo, :baz].inspect # "absent child of node :baz in #<Sycamore::Tree:0x3fea48e24f10 {:bar=>1}>"
|
542
|
+
#
|
543
|
+
# @todo Should we differentiate the case of a leaf and a not present node? How?
|
544
|
+
#
|
545
|
+
def child_at(*path)
|
546
|
+
first = path.first
|
547
|
+
case path.length
|
548
|
+
when 0
|
549
|
+
raise ArgumentError, 'wrong number of arguments (given 0, expected 1+)'
|
550
|
+
when 1
|
551
|
+
if first.is_a? Enumerable
|
552
|
+
child_at(*first)
|
553
|
+
else
|
554
|
+
child_of(*path)
|
555
|
+
end
|
556
|
+
else
|
557
|
+
child_of(first).child_at(*path[1..-1])
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
alias [] child_at
|
562
|
+
alias dig child_at # Hash compatibility
|
563
|
+
|
564
|
+
##
|
565
|
+
# The child tree of a node.
|
566
|
+
#
|
567
|
+
# If the node can’t be found or has no child tree, there are several options:
|
568
|
+
# - With no other arguments, it will raise a +KeyError+ exception when the
|
569
|
+
# node can’t be found or a {ChildError} exception (which is a subclass of
|
570
|
+
# +KeyError+) when the node has no child tree
|
571
|
+
# - if +default+ is given, then that will be returned;
|
572
|
+
# - if the optional code block is specified, then that will be run and its result returned.
|
573
|
+
#
|
574
|
+
# @param node [Object]
|
575
|
+
# @param default [Object] optional
|
576
|
+
# @return [Tree, default]
|
577
|
+
#
|
578
|
+
# @raise [InvalidNode] when given an +Enumerable+ as node
|
579
|
+
# @raise [KeyError] when the given +node+ can't be found
|
580
|
+
# @raise [ChildError] when no child for the given +node+ present
|
581
|
+
#
|
582
|
+
# @example
|
583
|
+
# tree = Tree[x: 1, y: nil]
|
584
|
+
# tree.fetch(:x) # #<Sycamore::Tree:0x3fc798a63854(1)>
|
585
|
+
# tree.fetch(:y) # => raise Sycamore::ChildError, "node y has no child tree"
|
586
|
+
# tree.fetch(:z) # => raise KeyError, "key not found: :z"
|
587
|
+
# tree.fetch(:z, :default) # => :default
|
588
|
+
# tree.fetch(:y, :default) # => :default
|
589
|
+
# tree.fetch(:z) { :default } # => :default
|
590
|
+
#
|
591
|
+
# @todo Should we differentiate the case of a leaf and a not present node? How?
|
592
|
+
#
|
593
|
+
def fetch(node, *default, &block)
|
594
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
595
|
+
|
596
|
+
child = @data.fetch(node, *default, &block)
|
597
|
+
if child.equal? Nothing
|
598
|
+
child = case
|
599
|
+
when block_given? then yield
|
600
|
+
when !default.empty? then default.first
|
601
|
+
else raise ChildError, "node #{node} has no child tree"
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
child
|
606
|
+
end
|
607
|
+
|
608
|
+
##
|
609
|
+
# Iterates over all {#nodes nodes} of this tree.
|
610
|
+
#
|
611
|
+
# Note that does not include the nodes of the child trees.
|
612
|
+
#
|
613
|
+
# @overload each_node
|
614
|
+
# Iterates over all {#nodes nodes} of this tree.
|
615
|
+
# @yield [Object] each node
|
616
|
+
# @return [Tree]
|
617
|
+
#
|
618
|
+
# @overload each_node
|
619
|
+
# Returns an enumerator over all {#nodes nodes} of this tree.
|
620
|
+
# @return [Enumerator<Object>]
|
621
|
+
#
|
622
|
+
# @example
|
623
|
+
# tree = Tree[ "a" => 100, "b" => 200 ]
|
624
|
+
# tree.each_node {|node| puts node }
|
625
|
+
#
|
626
|
+
# > a
|
627
|
+
# > b
|
628
|
+
#
|
629
|
+
def each_node(&block)
|
630
|
+
return enum_for(__callee__) unless block_given?
|
631
|
+
|
632
|
+
@data.each_key(&block)
|
633
|
+
|
634
|
+
self
|
635
|
+
end
|
636
|
+
|
637
|
+
alias each_key each_node # Hash compatibility
|
638
|
+
|
639
|
+
##
|
640
|
+
# Iterates over all {#nodes nodes} and their child trees.
|
641
|
+
#
|
642
|
+
# @overload each_pair
|
643
|
+
# Iterates over all {#nodes nodes} and their child trees.
|
644
|
+
# @yield [Object, Tree] each node-child pair
|
645
|
+
# @return [Tree] +self+
|
646
|
+
#
|
647
|
+
# @overload each_pair
|
648
|
+
# Returns an enumerator over all {#nodes nodes} and their child trees.
|
649
|
+
# @return [Enumerator<Array(Object, Tree)>]
|
650
|
+
#
|
651
|
+
# @example
|
652
|
+
# tree = Tree[ "a" => 100, "b" => nil ]
|
653
|
+
# tree.each_pair {|node, child| puts "#{node} => #{child}" }
|
654
|
+
#
|
655
|
+
# > a => #<Tree[ 100 ]>
|
656
|
+
# > b => #<Tree: Nothing>
|
657
|
+
#
|
658
|
+
def each_pair(&block)
|
659
|
+
return enum_for(__callee__) unless block_given?
|
660
|
+
|
661
|
+
@data.each_pair(&block)
|
662
|
+
|
663
|
+
self
|
664
|
+
end
|
665
|
+
|
666
|
+
alias each each_pair
|
667
|
+
|
668
|
+
##
|
669
|
+
# Iterates over the {Path paths} to all leaves of this tree.
|
670
|
+
#
|
671
|
+
# @overload each_path
|
672
|
+
# Iterates over the {Path paths} to all leaves of this tree.
|
673
|
+
# @yieldparam [Path] path
|
674
|
+
# @return [Tree] +self+
|
675
|
+
#
|
676
|
+
# @overload each_path
|
677
|
+
# Returns an enumerator over the {Path paths} to all leaves of this tree.
|
678
|
+
# @return [Enumerator<Path>]
|
679
|
+
#
|
680
|
+
# @example
|
681
|
+
# tree = Tree[ "a" => 100, "b" => { foo: [:bar, :baz] } ]
|
682
|
+
# tree.each_path { |path| puts path }
|
683
|
+
#
|
684
|
+
# > #<Path: /a/100>
|
685
|
+
# > #<Path: /b/foo/bar>
|
686
|
+
# > #<Path: /b/foo/baz>
|
687
|
+
#
|
688
|
+
def each_path(with_ancestor: Path::ROOT, &block)
|
689
|
+
return enum_for(__callee__) unless block_given?
|
690
|
+
|
691
|
+
each do |node, child|
|
692
|
+
if child.empty?
|
693
|
+
yield Path[with_ancestor, node]
|
694
|
+
else
|
695
|
+
child.each_path(with_ancestor: with_ancestor.branch(node), &block)
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
self
|
700
|
+
end
|
701
|
+
|
702
|
+
alias paths each_path
|
703
|
+
|
704
|
+
##
|
705
|
+
# Checks if a path of nodes exists in this tree.
|
706
|
+
#
|
707
|
+
# @param args [Array<Object>, Path] a splat of nodes, an array of nodes or a {Path} object
|
708
|
+
# @return [Boolean]
|
709
|
+
#
|
710
|
+
# @example
|
711
|
+
# tree = Tree[ "a" => 100, "b" => 200, "c" => { foo: [:bar, :baz] } ]
|
712
|
+
# tree.include_path? "a", 200 # => false
|
713
|
+
# tree.include_path? "c", :foo, :bar # => true
|
714
|
+
# tree.include_path? ["c", :foo, :bar] # => true
|
715
|
+
# tree.include_path? Sycamore::Path["c", :foo, :bar] # => true
|
716
|
+
#
|
717
|
+
def include_path?(*args)
|
718
|
+
raise ArgumentError, 'wrong number of arguments (given 0, expected 1+)' if args.count == 0
|
719
|
+
first = args.first
|
720
|
+
if first.is_a? Enumerable
|
721
|
+
return include_path?(*first) if args.count == 1
|
722
|
+
raise InvalidNode, "#{first} is not a valid tree node"
|
723
|
+
end
|
724
|
+
|
725
|
+
if args.count == 1
|
726
|
+
include? first
|
727
|
+
else
|
728
|
+
include?(first) and child_of(first).include_path?(args[1..-1])
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
alias path? include_path?
|
733
|
+
|
734
|
+
##
|
735
|
+
# Checks if a node exists in the {#nodes nodes} set of this tree.
|
736
|
+
#
|
737
|
+
# @param node [Object]
|
738
|
+
# @return [Boolean]
|
739
|
+
#
|
740
|
+
# @example
|
741
|
+
# Tree[1,2,3].include_node? 3 # => true
|
742
|
+
# Tree[1 => 2].include_node? 2 # => false
|
743
|
+
#
|
744
|
+
def include_node?(node)
|
745
|
+
@data.include?(node)
|
746
|
+
end
|
747
|
+
|
748
|
+
alias member? include_node? # Hash compatibility
|
749
|
+
alias has_key? include_node? # Hash compatibility
|
750
|
+
alias key? include_node? # Hash compatibility
|
751
|
+
|
752
|
+
##
|
753
|
+
# Checks if some nodes or a full tree-like structure is included in this tree.
|
754
|
+
#
|
755
|
+
# @param elements [Object, Array, Tree, Hash]
|
756
|
+
# @return [Boolean]
|
757
|
+
#
|
758
|
+
# @example
|
759
|
+
# tree = Tree[ "a" => 100, "b" => 200, "c" => { foo: [:bar, :baz] } ]
|
760
|
+
# tree.include?("a") # => true
|
761
|
+
# tree.include?(:foo) # => false
|
762
|
+
# tree.include?(["a", "b"]) # => true
|
763
|
+
# tree.include?("c" => {foo: :bar}) # => true
|
764
|
+
# tree.include?("a", "b" => 200) # => true
|
765
|
+
#
|
766
|
+
def include?(*elements)
|
767
|
+
raise ArgumentError, 'wrong number of arguments (given 0, expected 1+)' if
|
768
|
+
elements.size == 0
|
769
|
+
return elements.all? { |element| include? element } if
|
770
|
+
elements.size > 1
|
771
|
+
|
772
|
+
elements = elements.first
|
773
|
+
case
|
774
|
+
when Tree.like?(elements)
|
775
|
+
elements.all? do |node, child|
|
776
|
+
include_node?(node) and ( child.nil? or child.equal?(Nothing) or
|
777
|
+
self.child_of(node).include?(child) )
|
778
|
+
end
|
779
|
+
when elements.is_a?(Path)
|
780
|
+
include_path? elements
|
781
|
+
when elements.is_a?(Enumerable)
|
782
|
+
elements.all? { |element| include_node? element }
|
783
|
+
else
|
784
|
+
include_node? elements
|
785
|
+
end
|
786
|
+
end
|
787
|
+
|
788
|
+
##
|
789
|
+
# The number of {#nodes nodes} in this tree.
|
790
|
+
#
|
791
|
+
# Note, this does not count the nodes in the child trees.
|
792
|
+
#
|
793
|
+
# @return [Fixnum]
|
794
|
+
#
|
795
|
+
# @example
|
796
|
+
# tree = Tree[ "d" => 100, "a" => 200, "v" => 300, "e" => [400, 500] ]
|
797
|
+
# tree.size # => 4
|
798
|
+
# tree.delete("a")
|
799
|
+
# tree.size # => 3
|
800
|
+
# tree["e"].size # => 2
|
801
|
+
#
|
802
|
+
def size
|
803
|
+
@data.size
|
804
|
+
end
|
805
|
+
|
806
|
+
##
|
807
|
+
# The number of {#nodes nodes} in this tree and all of their children.
|
808
|
+
#
|
809
|
+
# @return [Fixnum]
|
810
|
+
#
|
811
|
+
# @example
|
812
|
+
# tree = Tree[ "d" => 100, "a" => 200, "v" => 300, "e" => [400, 500] ]
|
813
|
+
# tree.total_size # => 9
|
814
|
+
# tree.delete("a")
|
815
|
+
# tree.total_size # => 7
|
816
|
+
# tree["e"].total_size # => 2
|
817
|
+
#
|
818
|
+
def total_size
|
819
|
+
total = size
|
820
|
+
@data.each { |_, child| total += child.total_size }
|
821
|
+
total
|
822
|
+
end
|
823
|
+
|
824
|
+
alias tsize total_size
|
825
|
+
|
826
|
+
##
|
827
|
+
# The length of the longest path of this tree.
|
828
|
+
#
|
829
|
+
# @return [Fixnum]
|
830
|
+
#
|
831
|
+
# @example
|
832
|
+
# tree = Tree[a: 1, b: {2 => 3}]
|
833
|
+
# tree.height # => 3
|
834
|
+
#
|
835
|
+
def height
|
836
|
+
return 0 if empty?
|
837
|
+
paths.map(&:length).max
|
838
|
+
end
|
839
|
+
|
840
|
+
##
|
841
|
+
# Checks if this tree is empty.
|
842
|
+
#
|
843
|
+
# @return [Boolean]
|
844
|
+
#
|
845
|
+
# @example
|
846
|
+
# Tree.new.empty? # => true
|
847
|
+
# Tree[a: 1].empty? # => false
|
848
|
+
#
|
849
|
+
def empty?
|
850
|
+
@data.empty?
|
851
|
+
end
|
852
|
+
|
853
|
+
alias blank? empty?
|
854
|
+
|
855
|
+
##
|
856
|
+
# Checks if the given node has no children.
|
857
|
+
#
|
858
|
+
# @param node [Object]
|
859
|
+
# @return [Boolean]
|
860
|
+
#
|
861
|
+
# @example
|
862
|
+
# tree = Tree[x: 1, y: [], z: nil]
|
863
|
+
# tree.leaf?(:x) # => false
|
864
|
+
# tree.leaf?(:y) # => true
|
865
|
+
# tree.leaf?(:z) # => true
|
866
|
+
#
|
867
|
+
def leaf?(node)
|
868
|
+
include_node?(node) && child_of(node).empty?
|
869
|
+
end
|
870
|
+
|
871
|
+
##
|
872
|
+
# Checks if the given node has no children, even not an empty child tree.
|
873
|
+
#
|
874
|
+
# @param node [Object]
|
875
|
+
# @return [Boolean]
|
876
|
+
#
|
877
|
+
# @example
|
878
|
+
# tree = Tree[x: 1, y: [], z: nil]
|
879
|
+
# tree.strict_leaf?(:x) # => false
|
880
|
+
# tree.strict_leaf?(:y) # => false
|
881
|
+
# tree.strict_leaf?(:z) # => true
|
882
|
+
#
|
883
|
+
def strict_leaf?(node)
|
884
|
+
include_node?(node) && child_of(node).absent?
|
885
|
+
end
|
886
|
+
|
887
|
+
alias sleaf? strict_leaf?
|
888
|
+
|
889
|
+
##
|
890
|
+
# Checks if all given nodes or that of the tree have no children, even not an empty child tree.
|
891
|
+
#
|
892
|
+
# @overload strict_leaves?()
|
893
|
+
# Returns if all {#nodes} of this tree have no children, even not an empty child tree.
|
894
|
+
# @return [Boolean]
|
895
|
+
#
|
896
|
+
# @overload strict_leaves?(*nodes)
|
897
|
+
# Checks if all of the given nodes have no children, even not an empty child tree.
|
898
|
+
# @param nodes [Array<Object>] splat of nodes
|
899
|
+
# @return [Boolean]
|
900
|
+
#
|
901
|
+
# @example
|
902
|
+
# Tree[1,2,3].strict_leaves? # => true
|
903
|
+
# tree = Tree[a: :foo, b: :bar, c: []]
|
904
|
+
# tree.strict_leaves? # => false
|
905
|
+
# tree.strict_leaves?(:x, :y) # => false
|
906
|
+
# tree.strict_leaves?(:y, :z) # => false
|
907
|
+
#
|
908
|
+
def strict_leaves?(*nodes)
|
909
|
+
nodes = self.nodes if nodes.empty?
|
910
|
+
|
911
|
+
nodes.all? { |node| strict_leaf?(node) }
|
912
|
+
end
|
913
|
+
|
914
|
+
alias sleaves? strict_leaves?
|
915
|
+
|
916
|
+
##
|
917
|
+
# Checks if all given nodes or that of the tree have no children.
|
918
|
+
#
|
919
|
+
# @overload external?
|
920
|
+
# Checks if all {#nodes} of this tree have no children.
|
921
|
+
# @return [Boolean]
|
922
|
+
#
|
923
|
+
# @overload external?(*nodes)
|
924
|
+
# Checks if all of the given nodes have no children.
|
925
|
+
# @param nodes [Array<Object>] splat of nodes
|
926
|
+
# @return [Boolean]
|
927
|
+
#
|
928
|
+
# @example
|
929
|
+
# Tree[1,2,3].leaves? # => true
|
930
|
+
# tree = Tree[x: 1, y: [], z: nil]
|
931
|
+
# tree.leaves? # => false
|
932
|
+
# tree.leaves?(:x, :y) # => false
|
933
|
+
# tree.leaves?(:y, :z) # => true
|
934
|
+
#
|
935
|
+
def external?(*nodes)
|
936
|
+
nodes = self.nodes if nodes.empty?
|
937
|
+
|
938
|
+
nodes.all? { |node| leaf?(node) }
|
939
|
+
end
|
940
|
+
|
941
|
+
alias leaves? external?
|
942
|
+
alias flat? external?
|
943
|
+
|
944
|
+
##
|
945
|
+
# Checks if all given nodes or that of the tree have children.
|
946
|
+
#
|
947
|
+
# @overload internal?
|
948
|
+
# Checks if all {#nodes} of this tree have children.
|
949
|
+
# @return [Boolean]
|
950
|
+
#
|
951
|
+
# @overload internal?(*nodes)
|
952
|
+
# Checks if all of the given nodes have children.
|
953
|
+
# @param nodes [Array<Object>] splat of nodes
|
954
|
+
# @return [Boolean]
|
955
|
+
#
|
956
|
+
# @example
|
957
|
+
# Tree[x: 1, y: 2].internal? # => true
|
958
|
+
# tree = Tree[x: 1, y: [], z: nil]
|
959
|
+
# tree.internal? # => false
|
960
|
+
# tree.internal?(:x, :y) # => false
|
961
|
+
# tree.internal?(:y, :z) # => false
|
962
|
+
# tree.internal?(:x) # => true
|
963
|
+
#
|
964
|
+
# @todo Does it make sense to support the no arguments variant here and with this semantics?
|
965
|
+
# One would expect it to be the negation of #external? without arguments.
|
966
|
+
#
|
967
|
+
def internal?(*nodes)
|
968
|
+
return false if self.empty?
|
969
|
+
nodes = self.nodes if nodes.empty?
|
970
|
+
|
971
|
+
nodes.all? { |node| not leaf?(node) and include_node?(node) }
|
972
|
+
end
|
973
|
+
|
974
|
+
alias nested? internal?
|
975
|
+
|
976
|
+
|
977
|
+
########################################################################
|
978
|
+
# @group Comparison
|
979
|
+
########################################################################
|
980
|
+
|
981
|
+
##
|
982
|
+
# A hash code of this tree.
|
983
|
+
#
|
984
|
+
# @return [Fixnum]
|
985
|
+
#
|
986
|
+
def hash
|
987
|
+
@data.hash ^ self.class.hash
|
988
|
+
end
|
989
|
+
|
990
|
+
##
|
991
|
+
# Checks if this tree has the same content as another tree.
|
992
|
+
#
|
993
|
+
# @param other [Object]
|
994
|
+
# @return [Boolean]
|
995
|
+
#
|
996
|
+
# @example
|
997
|
+
# tree1 = Tree[a: 1, b: 2]
|
998
|
+
# tree2 = Tree[a: 1, b: 2, c: 3]
|
999
|
+
# tree3 = Tree[b: 2, a: 1]
|
1000
|
+
# tree4 = Tree[a: 1, b: {2 => []}]
|
1001
|
+
# tree1.eql? tree2 # => false
|
1002
|
+
# tree1.eql? tree3 # => true
|
1003
|
+
# tree1.eql? tree4 # => false
|
1004
|
+
#
|
1005
|
+
def eql?(other)
|
1006
|
+
(other.instance_of?(self.class) and @data.eql?(other.data)) or
|
1007
|
+
(other.instance_of?(Absence) and other.eql?(self))
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
##
|
1011
|
+
# Checks if this tree has the same content as another tree, but ignores empty child trees.
|
1012
|
+
#
|
1013
|
+
# @param other [Object]
|
1014
|
+
# @return [Boolean]
|
1015
|
+
#
|
1016
|
+
# @example
|
1017
|
+
# tree1 = Tree[a: 1, b: 2]
|
1018
|
+
# tree2 = Tree[a: 1, b: 2, c: 3]
|
1019
|
+
# tree3 = Tree[b: 2, a: 1]
|
1020
|
+
# tree4 = Tree[a: 1, b: {2 => []}]
|
1021
|
+
# tree1 == tree2 # => false
|
1022
|
+
# tree1 == tree3 # => true
|
1023
|
+
# tree1 == tree4 # => true
|
1024
|
+
#
|
1025
|
+
def ==(other)
|
1026
|
+
(other.instance_of?(self.class) and size == other.size and
|
1027
|
+
all? { |node, child| other.include?(node) and other[node] == child }) or
|
1028
|
+
((other.equal?(Nothing) or other.instance_of?(Absence)) and
|
1029
|
+
other == self)
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
##
|
1033
|
+
# Checks if this tree is a subtree of another tree.
|
1034
|
+
#
|
1035
|
+
# @param other [Object]
|
1036
|
+
# @return [Boolean]
|
1037
|
+
#
|
1038
|
+
# @example
|
1039
|
+
# tree1 = Tree[a: 1, b: 2]
|
1040
|
+
# tree2 = Tree[a: 1, b: 2, c: 3]
|
1041
|
+
# tree1 < tree2 # => true
|
1042
|
+
# tree2 < tree1 # => false
|
1043
|
+
# tree1 < tree1 # => false
|
1044
|
+
#
|
1045
|
+
def <(other)
|
1046
|
+
(other.is_a?(Tree) or other.is_a?(Absence)) and other.include?(self) and self != other
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
##
|
1050
|
+
# Checks if this tree is a subtree or equal to another tree.
|
1051
|
+
#
|
1052
|
+
# @param other [Object]
|
1053
|
+
# @return [Boolean]
|
1054
|
+
#
|
1055
|
+
# @example
|
1056
|
+
# tree1 = Tree[a: 1, b: 2]
|
1057
|
+
# tree2 = Tree[a: 1, b: 2, c: 3]
|
1058
|
+
# tree1 <= tree2 # => true
|
1059
|
+
# tree2 <= tree1 # => false
|
1060
|
+
# tree1 <= tree1 # => true
|
1061
|
+
#
|
1062
|
+
def <=(other)
|
1063
|
+
(other.is_a?(Tree) or other.is_a?(Absence)) and other.include?(self)
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
##
|
1067
|
+
# Checks if another tree is a subtree or equal to this tree.
|
1068
|
+
#
|
1069
|
+
# @param other [Object]
|
1070
|
+
# @return [Boolean]
|
1071
|
+
#
|
1072
|
+
# @example
|
1073
|
+
# tree1 = Tree[a: 1, b: 2]
|
1074
|
+
# tree2 = Tree[a: 1, b: 2, c: 3]
|
1075
|
+
# tree1 >= tree2 # => false
|
1076
|
+
# tree2 >= tree1 # => true
|
1077
|
+
# tree1 >= tree1 # => true
|
1078
|
+
#
|
1079
|
+
def >=(other)
|
1080
|
+
(other.is_a?(Tree) or other.is_a?(Absence)) and self.include?(other)
|
1081
|
+
end
|
1082
|
+
|
1083
|
+
##
|
1084
|
+
# Checks if another tree is a subtree or equal to this tree.
|
1085
|
+
#
|
1086
|
+
# @param other [Object]
|
1087
|
+
# @return [Boolean]
|
1088
|
+
#
|
1089
|
+
# @example
|
1090
|
+
# tree1 = Tree[a: 1, b: 2]
|
1091
|
+
# tree2 = Tree[a: 1, b: 2, c: 3]
|
1092
|
+
# tree1 > tree2 # => false
|
1093
|
+
# tree2 > tree1 # => true
|
1094
|
+
# tree1 > tree1 # => false
|
1095
|
+
#
|
1096
|
+
def >(other)
|
1097
|
+
(other.is_a?(Tree) or other.is_a?(Absence)) and self.include?(other) and self != other
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
##
|
1101
|
+
# Checks if another object matches this tree structurally and by content.
|
1102
|
+
#
|
1103
|
+
# @param other [Object]
|
1104
|
+
# @return [Boolean]
|
1105
|
+
#
|
1106
|
+
# @example
|
1107
|
+
# Tree[foo: :bar] === Hash[foo: :bar] # => true
|
1108
|
+
# Tree[1, 2, 3] === Array[1, 2, 3] # => true
|
1109
|
+
# Tree[42] === 42 # => true
|
1110
|
+
#
|
1111
|
+
# @todo This should probably apply a less strict equivalence comparison on the nodes.
|
1112
|
+
# Problem: Requires a solution which doesn't use +Hash#include?+.
|
1113
|
+
#
|
1114
|
+
def matches?(other)
|
1115
|
+
case
|
1116
|
+
when Tree.like?(other) then matches_tree?(other)
|
1117
|
+
when other.is_a?(Enumerable) then matches_enumerable?(other)
|
1118
|
+
else matches_atom?(other)
|
1119
|
+
end
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
alias === matches?
|
1123
|
+
|
1124
|
+
private def matches_atom?(other)
|
1125
|
+
not other.nil? and (size == 1 and nodes.first == other and leaf? other)
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
private def matches_enumerable?(other)
|
1129
|
+
size == other.size and
|
1130
|
+
all? { |node, child| child.empty? and other.include?(node) }
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
private def matches_tree?(other)
|
1134
|
+
size == other.size and
|
1135
|
+
all? { |node, child|
|
1136
|
+
if child.nothing?
|
1137
|
+
other.include?(node) and begin other_child = other.fetch(node, nil)
|
1138
|
+
not other_child or
|
1139
|
+
(other_child.respond_to?(:empty?) and other_child.empty?)
|
1140
|
+
end
|
1141
|
+
else
|
1142
|
+
child.matches? other[node]
|
1143
|
+
end }
|
1144
|
+
end
|
1145
|
+
|
1146
|
+
|
1147
|
+
########################################################################
|
1148
|
+
# @group Conversion
|
1149
|
+
########################################################################
|
1150
|
+
|
1151
|
+
##
|
1152
|
+
# A native Ruby object representing the content of the tree.
|
1153
|
+
#
|
1154
|
+
# It is used by {#to_h} to produce flattened representations of child trees.
|
1155
|
+
#
|
1156
|
+
# @api private
|
1157
|
+
#
|
1158
|
+
def to_native_object(sleaf_child_as: nil, special_nil: false)
|
1159
|
+
case
|
1160
|
+
when empty?
|
1161
|
+
[]
|
1162
|
+
when strict_leaves?
|
1163
|
+
size == 1 && (!special_nil || !nodes.first.nil?) ? nodes.first : nodes
|
1164
|
+
else
|
1165
|
+
to_h(sleaf_child_as: sleaf_child_as, special_nil: special_nil)
|
1166
|
+
end
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
##
|
1170
|
+
# A hash representation of this tree.
|
1171
|
+
#
|
1172
|
+
# @return [Hash]
|
1173
|
+
#
|
1174
|
+
def to_h(*args)
|
1175
|
+
return {} if empty?
|
1176
|
+
|
1177
|
+
# not the nicest, but fastest way to inject on hashes, as noted here:
|
1178
|
+
# http://stackoverflow.com/questions/3230863/ruby-rails-inject-on-hashes-good-style
|
1179
|
+
hash = {}
|
1180
|
+
@data.each do |node, child|
|
1181
|
+
hash[node] = child.to_native_object(*args)
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
hash
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
##
|
1188
|
+
# A string representation of this tree.
|
1189
|
+
#
|
1190
|
+
# @return [String]
|
1191
|
+
#
|
1192
|
+
def to_s
|
1193
|
+
if (content = to_native_object(special_nil: true)).is_a? Enumerable
|
1194
|
+
"Tree[#{content.inspect[1..-2]}]"
|
1195
|
+
else
|
1196
|
+
"Tree[#{content.inspect}]"
|
1197
|
+
end
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
##
|
1201
|
+
# A developer-friendly string representation of this tree in the usual Ruby +Object#inspect+ style.
|
1202
|
+
#
|
1203
|
+
# @return [String]
|
1204
|
+
#
|
1205
|
+
def inspect
|
1206
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} #{
|
1207
|
+
to_h(sleaf_child_as: Sycamore::NothingTree::NestedString).inspect}>"
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
##
|
1211
|
+
# Checks if the given object can be converted into a Tree.
|
1212
|
+
#
|
1213
|
+
# Ideally these would be implemented with Refinements, but since they
|
1214
|
+
# aren't available anywhere (I'm looking at you, JRuby), we have to be
|
1215
|
+
# content with this.
|
1216
|
+
#
|
1217
|
+
# @param object [Object]
|
1218
|
+
# @return [Boolean]
|
1219
|
+
#
|
1220
|
+
# @api private
|
1221
|
+
#
|
1222
|
+
def self.tree_like?(object)
|
1223
|
+
case object
|
1224
|
+
when Hash, Tree, Absence # ... ?!
|
1225
|
+
true
|
1226
|
+
else
|
1227
|
+
(object.respond_to? :tree_like? and object.tree_like?) # or ...
|
1228
|
+
end
|
1229
|
+
end
|
1230
|
+
|
1231
|
+
class << self
|
1232
|
+
alias like? tree_like?
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
|
1236
|
+
########################################################################
|
1237
|
+
# @group Other standard Ruby methods
|
1238
|
+
########################################################################
|
1239
|
+
|
1240
|
+
##
|
1241
|
+
# Duplicates the whole tree.
|
1242
|
+
#
|
1243
|
+
# @return [Tree]
|
1244
|
+
#
|
1245
|
+
def dup
|
1246
|
+
duplicate = self.class.new.add(self)
|
1247
|
+
duplicate.taint if tainted?
|
1248
|
+
duplicate
|
1249
|
+
end
|
1250
|
+
|
1251
|
+
##
|
1252
|
+
# Clones the whole tree.
|
1253
|
+
#
|
1254
|
+
def initialize_clone(other)
|
1255
|
+
super
|
1256
|
+
@data = Hash.new
|
1257
|
+
add other
|
1258
|
+
end
|
1259
|
+
|
1260
|
+
##
|
1261
|
+
# Deep freezes the whole tree.
|
1262
|
+
#
|
1263
|
+
# @see http://ruby-doc.org/core/Object.html#method-i-freeze
|
1264
|
+
#
|
1265
|
+
def freeze
|
1266
|
+
@data.freeze
|
1267
|
+
each { |_, child| child.freeze }
|
1268
|
+
super
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
end # Tree
|
1272
|
+
end # Sycamore
|