sycamore 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +42 -6
- data/VERSION +1 -1
- data/lib/sycamore/exceptions.rb +7 -1
- data/lib/sycamore/tree.rb +81 -12
- data/sycamore.gemspec +3 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96576ab7442d55fd614afdad27de3ec3aca8d6d5
|
4
|
+
data.tar.gz: 35541c8d3106ee6118521772c5b1f949dee22f5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03b10ac2b86de83ef79fd2ec420ddcbfc0b7482d2aba51b052e338d295e8a975e5ee867e61eebf4dc80e6a339db5fd95cdfb470f747c4e528b07b297c7574d8b
|
7
|
+
data.tar.gz: e11355d50ae83691faabab59def13d2f5e840cb164f90d7db53b62886122c46b176c5c1bcb9bde1113bdbe24fde1c08beae872869659eddc346ef65e3c56c007
|
data/CHANGELOG.md
CHANGED
@@ -4,7 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/) and
|
5
5
|
[Keep a CHANGELOG](http://keepachangelog.com).
|
6
6
|
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [0.2.0] - 2016-04-05
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- assigning `Sycamore::Nothing` via `Tree#[]=` removes a child tree
|
13
|
+
- `Tree#search` for searching the tree for one or multiple nodes or a tree
|
14
|
+
- `Tree#node!` as a more strict variant of `Tree#node`, which raises an error
|
15
|
+
when no node present
|
7
16
|
|
8
17
|
## 0.1.0 - 2016-03-28
|
9
18
|
|
10
19
|
Initial release
|
20
|
+
|
21
|
+
|
22
|
+
[Unreleased]: https://github.com/marcelotto/sycamore/compare/v0.2.0...HEAD
|
23
|
+
[0.2.0]: https://github.com/marcelotto/sycamore/compare/v0.1.0...v0.2.0
|
data/README.md
CHANGED
@@ -4,10 +4,11 @@
|
|
4
4
|
> _"The Egyptians' Holy Sycamore also stood on the threshold of life and death, connecting the two worlds."_
|
5
5
|
> -- [Wikipedia: Tree of Life](http://en.wikipedia.org/wiki/Tree_of_life)
|
6
6
|
|
7
|
+
[![Gem Version](https://badge.fury.io/rb/sycamore.svg)](http://badge.fury.io/rb/sycamore)
|
7
8
|
[![Travis CI Build Status](https://secure.travis-ci.org/marcelotto/sycamore.png)](https://travis-ci.org/marcelotto/sycamore?branch=master)
|
8
9
|
[![Coverage Status](https://coveralls.io/repos/marcelotto/sycamore/badge.png)](https://coveralls.io/r/marcelotto/sycamore)
|
9
10
|
[![Inline docs](http://inch-ci.org/github/marcelotto/sycamore.png)](http://inch-ci.org/github/marcelotto/sycamore)
|
10
|
-
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://rubydoc.org/gems/
|
11
|
+
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://rubydoc.org/gems/sycamore/frames)
|
11
12
|
[![Gitter Chat](http://img.shields.io/badge/chat-gitter.im-orange.svg)](https://gitter.im/marcelotto/sycamore)
|
12
13
|
[![License](http://img.shields.io/license/MIT.png?color=green)](http://opensource.org/licenses/MIT)
|
13
14
|
|
@@ -62,7 +63,7 @@ The recommended installation method is via [RubyGems](http://rubygems.org/).
|
|
62
63
|
|
63
64
|
## Usage
|
64
65
|
|
65
|
-
I will introduce Sycamore's Tree API by comparing it with [
|
66
|
+
I will introduce Sycamore's Tree API by comparing it with [Ruby's Hash API](http://ruby-doc.org/core-2.2.3/Hash.html).
|
66
67
|
|
67
68
|
In the following I'll always write `Tree` for the Sycamore tree class, instead of the fully qualified `Sycamore::Tree`. By default, this global `Tree` constant is not available. If you want this, you'll have to
|
68
69
|
|
@@ -92,7 +93,7 @@ tree = Tree.new
|
|
92
93
|
tree.empty? # => true
|
93
94
|
```
|
94
95
|
|
95
|
-
No additional arguments are supported at the time. As you'll see, for a `Sycamore::Tree` the functionality of the Hash constructor to specify the default value behaviour is of too little value to justify its use in the default constructor
|
96
|
+
No additional arguments are supported at the time. As you'll see, for a `Sycamore::Tree` the functionality of the Hash constructor to specify the default value behaviour is of too little value to justify its use in the default constructor, so I'd like to reserve them for something more useful.
|
96
97
|
|
97
98
|
The `[]` operator creates a new `Tree` and adds the arguments as its initial input. It can handle a single node value, a collection of nodes or a complete tree.
|
98
99
|
|
@@ -166,13 +167,20 @@ tree[:x][1].node # => nil
|
|
166
167
|
tree.node # Sycamore::NonUniqueNodeSet: multiple nodes present: [:x, :y]
|
167
168
|
```
|
168
169
|
|
170
|
+
The bang variant `node!` raises an error when the node set is empty, instead of returning `nil`.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
tree[:y][2].node! # => "a"
|
174
|
+
tree[:x][1].node! # => # Sycamore::EmptyNodeSet: no node present
|
175
|
+
```
|
176
|
+
|
169
177
|
As opposed to Hash, the `[]` operator of `Sycamore::Tree` also supports multiple arguments which get interpreted as a path.
|
170
178
|
|
171
179
|
```ruby
|
172
180
|
tree[:y, 2].node # => "a"
|
173
181
|
```
|
174
182
|
|
175
|
-
For compatibility with Ruby 2.3
|
183
|
+
For compatibility with Ruby 2.3 Hashes, this can also be done with the `dig` method.
|
176
184
|
|
177
185
|
```ruby
|
178
186
|
tree.dig(:y, 2).node # => "a"
|
@@ -247,10 +255,15 @@ tree.include? [:x, :y] # => true
|
|
247
255
|
tree.include?(x: 1, y: 2) # => true
|
248
256
|
```
|
249
257
|
|
258
|
+
`to_h` returns the tree as a Hash.
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
tree.to_h # => {:x=>1, :y=>{2=>"a"}}
|
262
|
+
```
|
250
263
|
|
251
264
|
### Accessing absent trees
|
252
265
|
|
253
|
-
There is another major difference
|
266
|
+
There is another major difference in the access method behaviour of a Scyamore tree in comparison to hashes: The child access methods even return a tree when it does not exist. When you ask a hash for a non-existent element with the `[]` operator, you'll get a `nil`, which is an incarnation of the null-problem and the cause of many bug tracking sessions.
|
254
267
|
|
255
268
|
```ruby
|
256
269
|
hash = {x: 1, y: {2 => "a"}}
|
@@ -274,7 +287,7 @@ Sycamore::Nothing.size # => 0
|
|
274
287
|
Sycamore::Nothing[42] # => #<Sycamore::Nothing>
|
275
288
|
```
|
276
289
|
|
277
|
-
Sycamore adheres to a strict [command-query-separation (CQS)](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation). A method is either a command changing the state of the tree and returning `self` or a query method, which only computes and returns the results of the query, but leaves the state unchanged. The only exception to this strict separation is made, when it is necessary in order to preserve
|
290
|
+
Sycamore adheres to a strict [command-query-separation (CQS)](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation). A method is either a command changing the state of the tree and returning `self` or a query method, which only computes and returns the results of the query, but leaves the state unchanged. The only exception to this strict separation is made, when it is necessary in order to preserve Hash compatibility. All query methods are supported by the `Sycamore::Nothing` tree with empty tree semantics.
|
278
291
|
|
279
292
|
Among the command methods are two subclasses: additive command methods, which add elements and destructive command methods, which remove elements. These are further refined into pure additive and pure destructive command methods, which either support additions or deletions only, not both operations at once. The `Sycamore::Tree` extends Ruby's reflection API with class methods to retrieve the respective methods: `query_methods`, `command_methods`, `additive_command_methods`, `destructive_command_methods`, `pure_additive_command_methods`, `pure_destructive_command_methods`.
|
280
293
|
|
@@ -436,6 +449,12 @@ tree[:foo] = []
|
|
436
449
|
tree[:foo] = {}
|
437
450
|
```
|
438
451
|
|
452
|
+
To remove a child tree entirely, you can assign `Nothing` to the parent node.
|
453
|
+
|
454
|
+
```ruby
|
455
|
+
tree[:foo] = Nothing
|
456
|
+
```
|
457
|
+
|
439
458
|
Note that these values are interpreted similarly inside tree structures, i.e. empty Enumerables become empty child trees, while `Nothing` or `nil` are used as place holders for the explicit negation of a child.
|
440
459
|
|
441
460
|
```ruby
|
@@ -500,6 +519,23 @@ Tree['some possibly very big data chunk' => [1, 2]].each_path.to_a
|
|
500
519
|
```
|
501
520
|
|
502
521
|
|
522
|
+
### Searching in trees
|
523
|
+
|
524
|
+
`search` returns the set of all paths to child trees containing a node or tree.
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
tree = Tree[ 1 => {a: 'foo'}, 2 => :b, 3 => [:a, :b, :c] ]
|
528
|
+
tree.search :a # => [#<Sycamore::Path[1]>, #<Sycamore::Path[1]>]
|
529
|
+
tree.search a: 'foo' # => [#<Sycamore::Path[1]>]
|
530
|
+
```
|
531
|
+
|
532
|
+
If you search for multiple nodes, only the paths to child trees containing all of the given nodes are returned.
|
533
|
+
|
534
|
+
```ruby
|
535
|
+
tree.search [:b, :c] # => [#<Sycamore::Path[3]>]
|
536
|
+
```
|
537
|
+
|
538
|
+
|
503
539
|
## Getting help
|
504
540
|
|
505
541
|
- [RDoc](http://www.rubydoc.info/gems/sycamore/)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/sycamore/exceptions.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
module Sycamore
|
2
2
|
# raised when a value is not a valid node
|
3
3
|
class InvalidNode < ArgumentError ; end
|
4
|
+
|
4
5
|
# raised when trying to call a additive command method of the {Nothing} tree
|
5
6
|
class NothingMutation < StandardError ; end
|
6
|
-
|
7
|
+
|
8
|
+
# raised when calling {Tree#node} or {Tree#node!} on a Tree with multiple nodes
|
7
9
|
class NonUniqueNodeSet < StandardError ; end
|
10
|
+
|
11
|
+
# raised when calling {Tree#node!} on a Tree without nodes
|
12
|
+
class EmptyNodeSet < StandardError ; end
|
13
|
+
|
8
14
|
# raised when trying to fetch the child of a leaf
|
9
15
|
class ChildError < KeyError ; end
|
10
16
|
end
|
data/lib/sycamore/tree.rb
CHANGED
@@ -18,10 +18,12 @@ module Sycamore
|
|
18
18
|
########################################################################
|
19
19
|
|
20
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
|
21
|
+
ADDITIVE_COMMAND_METHODS = %i[add << replace add_node_with_empty_child
|
22
|
+
clear_child_of_node] << :[]=
|
22
23
|
|
23
24
|
# the names of all command methods, which delete elements from a Tree
|
24
|
-
DESTRUCTIVE_COMMAND_METHODS = %i[delete >> clear compact replace
|
25
|
+
DESTRUCTIVE_COMMAND_METHODS = %i[delete >> clear compact replace
|
26
|
+
clear_child_of_node] << :[]=
|
25
27
|
|
26
28
|
# the names of all additive command methods, which only add elements from a Tree
|
27
29
|
PURE_ADDITIVE_COMMAND_METHODS = ADDITIVE_COMMAND_METHODS - DESTRUCTIVE_COMMAND_METHODS
|
@@ -44,7 +46,7 @@ module Sycamore
|
|
44
46
|
# the names of all methods, which side-effect-freeze return only a value
|
45
47
|
QUERY_METHODS = PREDICATE_METHODS +
|
46
48
|
%i[new_child dup hash to_native_object to_h to_s inspect
|
47
|
-
node nodes keys child_of child_at dig fetch
|
49
|
+
node node! nodes keys child_of child_at dig fetch search
|
48
50
|
size total_size tsize height
|
49
51
|
each each_path paths each_node each_key each_pair] << :[]
|
50
52
|
|
@@ -223,12 +225,6 @@ module Sycamore
|
|
223
225
|
end
|
224
226
|
|
225
227
|
##
|
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
228
|
# @api private
|
233
229
|
#
|
234
230
|
def add_node_with_empty_child(node)
|
@@ -241,6 +237,17 @@ module Sycamore
|
|
241
237
|
self
|
242
238
|
end
|
243
239
|
|
240
|
+
##
|
241
|
+
# @api private
|
242
|
+
#
|
243
|
+
def clear_child_of_node(node)
|
244
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
245
|
+
|
246
|
+
@data[node] = Nothing
|
247
|
+
|
248
|
+
self
|
249
|
+
end
|
250
|
+
|
244
251
|
private def add_child(node, children)
|
245
252
|
return add_node(node) if Nothing.like?(children)
|
246
253
|
|
@@ -289,7 +296,6 @@ module Sycamore
|
|
289
296
|
# tree.delete "d" => {foo: :baz}
|
290
297
|
# tree.to_h # => {}
|
291
298
|
#
|
292
|
-
# @todo differentiate a greedy and a non-greedy variant
|
293
299
|
# @todo support Paths
|
294
300
|
#
|
295
301
|
def delete(nodes_or_tree)
|
@@ -374,6 +380,9 @@ module Sycamore
|
|
374
380
|
# Note that even if you assign a {Sycamore::Tree} directly the given tree
|
375
381
|
# will not become part of this tree by reference.
|
376
382
|
#
|
383
|
+
# An exception is the assignment of the {Nothing} tree: it will delete the
|
384
|
+
# child tree at the given path entirely.
|
385
|
+
#
|
377
386
|
# @overload []=(*path, node)
|
378
387
|
# Replaces the contents of the child at the given path with a single node.
|
379
388
|
# @param path [Array<Object>, Sycamore::Path] a path as a sequence of nodes or a {Path} object
|
@@ -406,12 +415,23 @@ module Sycamore
|
|
406
415
|
# tree.to_h # => {:foo => :baz, 1 => :baz}
|
407
416
|
# tree[:foo] << :bar
|
408
417
|
# tree.to_h # => {:foo => [:baz, :bar], 1 => :baz}
|
418
|
+
# tree[:foo] = Sycamore::Nothing
|
419
|
+
# tree.to_h # => {:foo => nil, 1 => :baz}
|
409
420
|
#
|
410
421
|
def []=(*args)
|
411
422
|
path, nodes_or_tree = args[0..-2], args[-1]
|
412
423
|
raise ArgumentError, 'wrong number of arguments (given 1, expected 2)' if path.empty?
|
413
424
|
|
414
|
-
|
425
|
+
if nodes_or_tree.equal? Sycamore::Nothing
|
426
|
+
if path.size == 1
|
427
|
+
clear_child_of_node(path.first)
|
428
|
+
else
|
429
|
+
path, node = path[0..-2], path[-1]
|
430
|
+
child_at(*path).clear_child_of_node(node)
|
431
|
+
end
|
432
|
+
else
|
433
|
+
child_at(*path).replace(nodes_or_tree)
|
434
|
+
end
|
415
435
|
end
|
416
436
|
|
417
437
|
##
|
@@ -446,7 +466,7 @@ module Sycamore
|
|
446
466
|
def compact
|
447
467
|
@data.each do |node, child| case
|
448
468
|
when child.nothing? then next
|
449
|
-
when child.empty? then
|
469
|
+
when child.empty? then clear_child_of_node(node)
|
450
470
|
else child.compact
|
451
471
|
end
|
452
472
|
end
|
@@ -489,6 +509,8 @@ module Sycamore
|
|
489
509
|
# tree[:baz].node # => nil
|
490
510
|
# tree[:bar].node # => raise Sycamore::NonUniqueNodeSet, "multiple nodes present: [2, 3]"
|
491
511
|
#
|
512
|
+
# @see Tree#node!
|
513
|
+
#
|
492
514
|
def node
|
493
515
|
nodes = self.nodes
|
494
516
|
raise NonUniqueNodeSet, "multiple nodes present: #{nodes}" if nodes.size > 1
|
@@ -496,6 +518,27 @@ module Sycamore
|
|
496
518
|
nodes.first
|
497
519
|
end
|
498
520
|
|
521
|
+
##
|
522
|
+
# The only node of this tree or an exception, if none or more {#nodes nodes} present.
|
523
|
+
#
|
524
|
+
# @return [Object] the single present node
|
525
|
+
#
|
526
|
+
# @raise [EmptyNodeSet] if no nodes present
|
527
|
+
# @raise [NonUniqueNodeSet] if more than one node present
|
528
|
+
#
|
529
|
+
# @example
|
530
|
+
# tree = Tree[foo: 1, bar: [2,3]]
|
531
|
+
# tree[:foo].node! # => 1
|
532
|
+
# tree[:baz].node! # => raise Sycamore::EmptyNodeSet, "no node present"
|
533
|
+
# tree[:bar].node! # => raise Sycamore::NonUniqueNodeSet, "multiple nodes present: [2, 3]"
|
534
|
+
#
|
535
|
+
# @see Tree#node
|
536
|
+
#
|
537
|
+
def node!
|
538
|
+
raise EmptyNodeSet, 'no node present' if empty?
|
539
|
+
node
|
540
|
+
end
|
541
|
+
|
499
542
|
##
|
500
543
|
# The child tree of a node.
|
501
544
|
#
|
@@ -785,6 +828,32 @@ module Sycamore
|
|
785
828
|
end
|
786
829
|
end
|
787
830
|
|
831
|
+
##
|
832
|
+
# Searches the tree for one or multiple nodes or a complete tree.
|
833
|
+
#
|
834
|
+
# @param nodes_or_tree [Object, Array, Tree, Hash]
|
835
|
+
# @return [Array<Path>]
|
836
|
+
#
|
837
|
+
# @example
|
838
|
+
# tree = Tree[ "a" => [:foo, 100], "b" => { foo: [:bar, :baz] } ]
|
839
|
+
# tree.search :bar # => [Sycamore::Path["b", :foo]]
|
840
|
+
# tree.search :foo # => [Sycamore::Path["a"], Sycamore::Path["b"]]
|
841
|
+
# tree.search [:bar, :baz] # => [Sycamore::Path["b", :foo]]
|
842
|
+
# tree.search foo: :bar # => [Sycamore::Path["b"]]
|
843
|
+
# tree.search 42 # => []
|
844
|
+
#
|
845
|
+
def search(nodes_or_tree)
|
846
|
+
_search(nodes_or_tree)
|
847
|
+
end
|
848
|
+
|
849
|
+
protected def _search(query, current_path: Path::ROOT, results: [])
|
850
|
+
results << current_path if include?(query)
|
851
|
+
each do |node, child|
|
852
|
+
child._search(query, current_path: current_path/node, results: results)
|
853
|
+
end
|
854
|
+
results
|
855
|
+
end
|
856
|
+
|
788
857
|
##
|
789
858
|
# The number of {#nodes nodes} in this tree.
|
790
859
|
#
|
data/sycamore.gemspec
CHANGED
@@ -12,6 +12,9 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.summary = %q{An unordered tree data structure for Ruby.}
|
13
13
|
spec.description = %q{Sycamore is an implementation of an unordered tree data structure of immutable values solely based on Ruby Hashes.}
|
14
14
|
spec.homepage = 'https://github.com/marcelotto/sycamore'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.required_ruby_version = '>= 2.1'
|
15
18
|
|
16
19
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
20
|
spec.bindir = 'exe'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sycamore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marcel Otto
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-04-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -152,7 +152,8 @@ files:
|
|
152
152
|
- support/doctest_helper.rb
|
153
153
|
- sycamore.gemspec
|
154
154
|
homepage: https://github.com/marcelotto/sycamore
|
155
|
-
licenses:
|
155
|
+
licenses:
|
156
|
+
- MIT
|
156
157
|
metadata: {}
|
157
158
|
post_install_message:
|
158
159
|
rdoc_options: []
|
@@ -162,7 +163,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
162
163
|
requirements:
|
163
164
|
- - ">="
|
164
165
|
- !ruby/object:Gem::Version
|
165
|
-
version: '
|
166
|
+
version: '2.1'
|
166
167
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
168
|
requirements:
|
168
169
|
- - ">="
|