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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 367667ecc8ca68b1e4da9252767849e41d85f535
4
- data.tar.gz: 04523c2d3b2f7968403cd2bd51c2418136923fd0
3
+ metadata.gz: 96576ab7442d55fd614afdad27de3ec3aca8d6d5
4
+ data.tar.gz: 35541c8d3106ee6118521772c5b1f949dee22f5b
5
5
  SHA512:
6
- metadata.gz: 1c96901b05579d97588ea3652622d2c270f8cc747698eb07462a73b84309dbec14b39668766e0b7aff913b397355f33a88659f8abea34cb40d3ce154e0c35d78
7
- data.tar.gz: 92317760649683d727cb3904d6e70a680e9a87cac405dba65f827e93e7ee1f2ca7901895d4c701d73b68bf8c7b79eaf953aebe696542374c5e4c99d471187341
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/spread2rdf/frames)
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 [Rubys native Hash API](http://ruby-doc.org/core-2.2.3/Hash.html).
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. The decision of its use can be tracked in [issue #1]().
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 hashes, this can also be done with the `dig` method.
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 to a hash, which is in fact just a consequence of the already mentioned difference, that the access methods (except `fetch`) **always** return trees, when asked for children: They even return a child 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.
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 hash compatibility. All query methods are supported by the `Sycamore::Nothing` tree with empty tree semantics.
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
1
+ 0.2.0
@@ -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
- # raised when calling {Tree#node} on a Tree with multiple nodes
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
- child_at(*path).replace(nodes_or_tree)
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 @data[node] = Nothing
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.1.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-03-28 00:00:00.000000000 Z
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: '0'
166
+ version: '2.1'
166
167
  required_rubygems_version: !ruby/object:Gem::Requirement
167
168
  requirements:
168
169
  - - ">="