forester 3.2.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/forester.gemspec +2 -2
- data/lib/forester/node_content/base.rb +1 -1
- data/lib/forester/node_content/dictionary.rb +1 -1
- data/lib/forester/node_content/factory.rb +1 -1
- data/lib/forester/node_content/list.rb +1 -1
- data/lib/forester/tree_factory.rb +45 -18
- data/lib/forester/tree_node.rb +26 -8
- data/lib/forester/tree_node_ext/aggregators.rb +46 -37
- data/lib/forester/tree_node_ext/mutators.rb +14 -7
- data/lib/forester/tree_node_ext/views.rb +17 -8
- data/lib/forester/version.rb +2 -2
- data/test/simple_tree_helper.rb +5 -0
- data/test/test_aggregators.rb +94 -0
- data/test/test_mutators.rb +17 -12
- data/test/test_tree_factory.rb +33 -0
- data/test/test_tree_node.rb +34 -0
- data/test/test_views.rb +9 -7
- metadata +10 -4
- data/test/test_treenode.rb +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4bc0ddc9aa53d1bdab802726bbdc14871584d67
|
4
|
+
data.tar.gz: d43910fab0a17df6bee538e7822a68faba3616b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58c9d3199133b793ffc19f7a40dd85b37a6e93e4595b06c974447f091f274c77433d9be1df9291116cf2ba240b49178ab6813766fac65ddabf40f5176341ebd3
|
7
|
+
data.tar.gz: 05ca996da70214bfb0e5ba33db69ae8d7782b7955fbff7d72893df50ab99a70e9a13a01f92adfe3a94839bc53ff47cb96691b010b12c5625f224083814bb4f0d
|
data/Rakefile
CHANGED
data/forester.gemspec
CHANGED
@@ -6,7 +6,7 @@ require 'forester/version'
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = 'forester'
|
8
8
|
s.version = Forester::Version
|
9
|
-
s.date = '2016-08-
|
9
|
+
s.date = '2016-08-27'
|
10
10
|
s.summary = "A gem to represent and interact with tree data structures"
|
11
11
|
s.description = "Based on rubytree, this gem lets you build trees and run queries against them."
|
12
12
|
s.authors = ["Eugenio Bruno"]
|
@@ -27,4 +27,4 @@ Gem::Specification.new do |s|
|
|
27
27
|
s.add_development_dependency 'minitest', ['~> 5.9']
|
28
28
|
s.add_development_dependency 'pry-byebug', ['~> 3.4']
|
29
29
|
|
30
|
-
end
|
30
|
+
end
|
@@ -3,45 +3,72 @@ module Forester
|
|
3
3
|
|
4
4
|
class << self
|
5
5
|
|
6
|
-
def from_yaml_file(file)
|
6
|
+
def from_yaml_file(file, options = {})
|
7
|
+
default_options = {
|
8
|
+
max_level: :last
|
9
|
+
}
|
10
|
+
options = default_options.merge(options)
|
11
|
+
|
7
12
|
from_hash_with_root_key(YAML.load_file(file))
|
8
13
|
end
|
9
14
|
|
10
|
-
def from_root_hash(hash,
|
11
|
-
|
15
|
+
def from_root_hash(hash, options = {})
|
16
|
+
default_options = {
|
17
|
+
max_level: :last,
|
18
|
+
children_key: :children
|
19
|
+
}
|
20
|
+
options = default_options.merge(options)
|
21
|
+
|
22
|
+
from_hash_with_root_key({ root: hash }, options)
|
12
23
|
end
|
13
24
|
|
14
|
-
def from_hash_with_root_key(hash,
|
25
|
+
def from_hash_with_root_key(hash, options = {})
|
26
|
+
default_options = {
|
27
|
+
max_level: :last,
|
28
|
+
children_key: :children,
|
29
|
+
root_key: :root
|
30
|
+
}
|
31
|
+
options = default_options.merge(options)
|
32
|
+
|
15
33
|
dummy_root = TreeNode.new('<TEMP>')
|
16
|
-
real_root = fetch_indifferently(hash, root_key)
|
34
|
+
real_root = fetch_indifferently(hash, options[:root_key])
|
17
35
|
|
18
|
-
|
19
|
-
|
20
|
-
end
|
36
|
+
max_level = options[:max_level]
|
37
|
+
max_level = -2 if max_level == :last
|
21
38
|
|
22
|
-
|
23
|
-
|
24
|
-
content = NodeContent::Factory.from_hash(hash, children_key)
|
25
|
-
TreeNode.new(name, content)
|
39
|
+
tree = with_children(dummy_root, [real_root], options[:children_key], max_level + 1).first_child
|
40
|
+
tree.detached_subtree_copy
|
26
41
|
end
|
27
42
|
|
28
|
-
|
43
|
+
protected
|
29
44
|
|
30
45
|
def fetch_indifferently(hash, key, default = nil)
|
31
46
|
[key, key.to_s, key.to_s.to_sym].uniq.map { |k| hash[k] }.compact.first || default || hash.fetch(root_key)
|
32
47
|
end
|
33
48
|
|
34
|
-
def with_children(tree_node, children, children_key)
|
49
|
+
def with_children(tree_node, children, children_key, levels_remaining)
|
50
|
+
return tree_node if levels_remaining == 0
|
35
51
|
children.each do |child|
|
36
|
-
child_node =
|
37
|
-
child_children = fetch_indifferently(child, children_key, [])
|
52
|
+
child_node = node_from_hash(child, children_key)
|
53
|
+
child_children = fetch_indifferently(child, children_key, [])
|
38
54
|
|
39
|
-
tree_node << with_children(child_node, child_children, children_key)
|
55
|
+
tree_node << with_children(child_node, child_children, children_key, levels_remaining - 1)
|
40
56
|
end
|
41
57
|
tree_node
|
42
58
|
end
|
43
59
|
|
60
|
+
def node_from_hash(hash, children_key, options = {})
|
61
|
+
default_options = {
|
62
|
+
uid: SecureRandom.uuid
|
63
|
+
}
|
64
|
+
options = default_options.merge(options)
|
65
|
+
|
66
|
+
name = options[:uid]
|
67
|
+
content = NodeContent::Factory.from_hash(hash, children_key)
|
68
|
+
TreeNode.new(name, content)
|
69
|
+
end
|
70
|
+
|
44
71
|
end
|
45
72
|
|
46
73
|
end
|
47
|
-
end
|
74
|
+
end
|
data/lib/forester/tree_node.rb
CHANGED
@@ -8,30 +8,37 @@ module Forester
|
|
8
8
|
include Mutators
|
9
9
|
include Views
|
10
10
|
|
11
|
+
alias_method :max_level, :node_height
|
12
|
+
alias_method :each_node, :breadth_each
|
13
|
+
|
11
14
|
def nodes_of_level(l)
|
12
|
-
|
15
|
+
l.between?(0, max_level) ? each_level.take(l + 1).last : []
|
13
16
|
end
|
14
17
|
|
15
|
-
alias_method :max_level, :node_height
|
16
|
-
|
17
18
|
def each_level
|
18
19
|
Enumerator.new do |yielder|
|
19
20
|
level = [self]
|
20
|
-
|
21
|
+
until level.empty?
|
21
22
|
yielder << level
|
22
23
|
level = level.flat_map(&:children)
|
23
|
-
end
|
24
|
+
end
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
|
-
|
28
|
+
def get(field, options = {}, &if_missing)
|
29
|
+
default_options = {
|
30
|
+
default: :raise,
|
31
|
+
subtree: false
|
32
|
+
}
|
33
|
+
options = default_options.merge(options)
|
34
|
+
|
35
|
+
return own_and_descendants(field, &if_missing) if options[:subtree]
|
28
36
|
|
29
|
-
def get(field, default = :raise_error, &block)
|
30
37
|
if has?(field)
|
31
38
|
content.get(field)
|
32
39
|
elsif block_given?
|
33
40
|
yield self
|
34
|
-
elsif default != :
|
41
|
+
elsif default != :raise
|
35
42
|
default
|
36
43
|
else
|
37
44
|
raise ArgumentError.new("the node \"#{name}\" does not have \"#{field}\"")
|
@@ -46,5 +53,16 @@ module Forester
|
|
46
53
|
each_node.map(&:content)
|
47
54
|
end
|
48
55
|
|
56
|
+
def same_as?(other)
|
57
|
+
return false unless content == other.content
|
58
|
+
return false unless size == other.size
|
59
|
+
nodes_of_other = other.each_node.to_a
|
60
|
+
each_node.with_index do |n, i|
|
61
|
+
next if i == 0
|
62
|
+
return false unless n.same_as?(nodes_of_other[i])
|
63
|
+
end
|
64
|
+
true
|
65
|
+
end
|
66
|
+
|
49
67
|
end
|
50
68
|
end
|
@@ -1,82 +1,91 @@
|
|
1
1
|
module Forester
|
2
2
|
module Aggregators
|
3
3
|
|
4
|
-
def
|
5
|
-
|
4
|
+
def own_and_descendants(field, &if_missing)
|
5
|
+
if_missing = -> (node) { [] } unless block_given?
|
6
|
+
|
7
|
+
flat_map { |node| Array(node.get(field, &if_missing)) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def nodes_with(field, values, options = {})
|
11
|
+
default_options = {
|
12
|
+
single: false
|
13
|
+
}
|
14
|
+
options = default_options.merge(options)
|
15
|
+
|
16
|
+
method = options[:single] ? :find : :select
|
17
|
+
found_nodes = each_node.public_send(method) do |node|
|
18
|
+
not ( Array(node.get(field) { :no_match }) & Array(values) ).empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
Array(found_nodes)
|
6
22
|
end
|
7
23
|
|
8
24
|
def with_ancestry(options = {})
|
9
25
|
default_options = {
|
10
26
|
include_root: true,
|
11
27
|
include_self: true,
|
12
|
-
descending:
|
28
|
+
descending: true
|
13
29
|
}
|
14
30
|
options = default_options.merge(options)
|
15
31
|
|
16
32
|
ancestors = self.parentage || []
|
17
33
|
ancestors = ancestors[0...-1] unless options[:include_root]
|
18
34
|
ancestors = ancestors.unshift(self) if options[:include_self]
|
19
|
-
if options[:descending] then ancestors.reverse else ancestors end
|
20
|
-
end
|
21
35
|
|
22
|
-
|
23
|
-
if_field_missing = -> (node) { [] } unless block_given?
|
24
|
-
flat_map do |node|
|
25
|
-
Array(node.get(field_name, &if_field_missing))
|
26
|
-
end
|
36
|
+
options[:descending] ? ancestors.reverse : ancestors
|
27
37
|
end
|
28
38
|
|
29
39
|
def search(options)
|
30
40
|
default_options = {
|
31
|
-
single_node:
|
41
|
+
single_node: false,
|
32
42
|
by_field: :name,
|
33
43
|
keywords: :missing_search_keywords,
|
34
|
-
then_get: :nodes,
|
35
|
-
|
44
|
+
then_get: :nodes, # if :nodes, subtree is ignored
|
45
|
+
subtree: true
|
36
46
|
}
|
37
47
|
options = default_options.merge(options)
|
38
48
|
|
39
|
-
found_nodes = nodes_with(options[:by_field], options[:keywords])
|
40
|
-
|
41
|
-
found_nodes = found_nodes.slice(0, 1) if options[:single_node]
|
49
|
+
found_nodes = nodes_with(options[:by_field], options[:keywords], { single: options[:single_node] } )
|
42
50
|
|
43
51
|
return found_nodes if options[:then_get] == :nodes
|
44
52
|
|
45
53
|
found_nodes.flat_map do |node|
|
46
|
-
|
47
|
-
node.own_and_descendants(options[:then_get])
|
48
|
-
else
|
49
|
-
node.get(options[:then_get])
|
50
|
-
end
|
54
|
+
node.get(options[:then_get], { subtree: options[:subtree] })
|
51
55
|
end
|
52
|
-
|
53
56
|
end
|
54
57
|
|
55
|
-
def
|
58
|
+
def group_by_sibling_subtrees(options = {})
|
56
59
|
default_options = {
|
57
|
-
level:
|
58
|
-
group_field:
|
59
|
-
aggregation_field:
|
60
|
-
if_field_missing:
|
61
|
-
|
62
|
-
with_root:
|
60
|
+
level: 1,
|
61
|
+
group_field: 'name',
|
62
|
+
aggregation_field: 'value',
|
63
|
+
if_field_missing: -> (node) { [] },
|
64
|
+
ancestry_in_keys: false, # if false, with_root is ignored
|
65
|
+
with_root: false
|
63
66
|
}
|
64
67
|
options = default_options.merge(options)
|
65
68
|
|
66
69
|
nodes_of_level(options[:level]).each_with_object({}) do |node, hash|
|
70
|
+
key =
|
71
|
+
if options[:ancestry_in_keys]
|
72
|
+
nodes_for_key = node.with_ancestry({ include_root: options[:with_root] })
|
73
|
+
nodes_for_key.map { |n| get_or_id(n, options[:group_field]) }
|
74
|
+
else
|
75
|
+
get_or_id(node, options[:group_field])
|
76
|
+
end
|
67
77
|
|
68
|
-
key_nodes = if options[:include_ancestry_in_keys]
|
69
|
-
node.with_ancestry({ include_root: options[:with_root] })
|
70
|
-
else
|
71
|
-
node
|
72
|
-
end
|
73
|
-
|
74
|
-
key = key_nodes.map { |kn| kn.get(options[:group_field]) { |n| n.object_id } }
|
75
78
|
value = node.own_and_descendants(options[:aggregation_field], &options[:if_field_missing])
|
76
79
|
|
77
80
|
hash[key] = value
|
78
81
|
end
|
79
82
|
end
|
80
83
|
|
84
|
+
private
|
85
|
+
|
86
|
+
def get_or_id(node, field)
|
87
|
+
node.get(field) { |n| n.object_id }
|
88
|
+
end
|
89
|
+
|
81
90
|
end
|
82
|
-
end
|
91
|
+
end
|
@@ -1,13 +1,20 @@
|
|
1
1
|
module Forester
|
2
2
|
module Mutators
|
3
3
|
|
4
|
-
def add_field!(name, definition)
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
def add_field!(name, definition, options = {})
|
5
|
+
default_options = {
|
6
|
+
subtree: true
|
7
|
+
}
|
8
|
+
options = default_options.merge(options)
|
8
9
|
|
9
|
-
|
10
|
-
|
10
|
+
if options[:subtree]
|
11
|
+
each_node do |node|
|
12
|
+
node.add_field!(name, definition, options.merge({ subtree: false }))
|
13
|
+
end
|
14
|
+
else
|
15
|
+
value = definition.respond_to?(:call) ? definition.call(self) : definition
|
16
|
+
put!(name, value)
|
17
|
+
end
|
11
18
|
end
|
12
19
|
|
13
20
|
def remove_levels_past!(last_level_to_keep)
|
@@ -16,4 +23,4 @@ module Forester
|
|
16
23
|
end
|
17
24
|
|
18
25
|
end
|
19
|
-
end
|
26
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
module Forester
|
2
2
|
module Views
|
3
3
|
|
4
|
-
def
|
4
|
+
def as_root_hash(options = {})
|
5
5
|
default_options = {
|
6
6
|
fields_to_include: fields, # all of them
|
7
|
+
max_level: :last,
|
8
|
+
children_key: :children,
|
7
9
|
stringify_keys: false,
|
8
10
|
symbolize_keys: false
|
9
11
|
}
|
@@ -11,15 +13,22 @@ module Forester
|
|
11
13
|
|
12
14
|
hash = content.to_hash(options)
|
13
15
|
|
14
|
-
children_key = :
|
16
|
+
children_key = options[:children_key]
|
15
17
|
children_key = children_key.to_s if options[:stringify_keys]
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
max_level = options[:max_level]
|
20
|
+
max_level = -1 if max_level == :last
|
21
|
+
|
22
|
+
next_children =
|
23
|
+
if max_level == 0
|
24
|
+
[]
|
25
|
+
else
|
26
|
+
next_options = options.merge({ max_level: max_level - 1 })
|
27
|
+
children.map { |node| node.as_root_hash(next_options) }
|
28
|
+
end
|
29
|
+
|
30
|
+
hash.merge({ children_key => next_children })
|
22
31
|
end
|
23
32
|
|
24
33
|
end
|
25
|
-
end
|
34
|
+
end
|
data/lib/forester/version.rb
CHANGED
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'forester'
|
3
|
+
|
4
|
+
require_relative './simple_tree_helper'
|
5
|
+
|
6
|
+
class TestAggregators < Minitest::Test
|
7
|
+
|
8
|
+
include SimpleTreeHelper
|
9
|
+
|
10
|
+
def test_group_by_sibling_subtrees
|
11
|
+
|
12
|
+
expected = {
|
13
|
+
"First node of level 2" => ["Already in level 2", "I want to be the very best", "like no one ever was"],
|
14
|
+
"Second node of level 2" => ["I have a sibling to my left", "She wants to catch them all"],
|
15
|
+
"Third node of level 2" => ["Reached level 3", "It's dark", "A hidden secret lies in the deepest leaves...", "Just kidding.", "Could forester handle trees with hundreds of levels?", "Maybe."]
|
16
|
+
}
|
17
|
+
|
18
|
+
actual = @@tree.group_by_sibling_subtrees(
|
19
|
+
level: 2,
|
20
|
+
aggregation_field: 'strings'
|
21
|
+
)
|
22
|
+
|
23
|
+
assert_equal expected, actual
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_group_by_sibling_subtrees_with_ancestry
|
27
|
+
|
28
|
+
expected = {
|
29
|
+
["First node of level 1", "First node of level 2"] => ["Already in level 2", "I want to be the very best", "like no one ever was"],
|
30
|
+
["First node of level 1", "Second node of level 2"] => ["I have a sibling to my left", "She wants to catch them all"],
|
31
|
+
["Second node of level 1", "Third node of level 2"] => ["Reached level 3", "It's dark", "A hidden secret lies in the deepest leaves...", "Just kidding.", "Could forester handle trees with hundreds of levels?", "Maybe."]
|
32
|
+
}
|
33
|
+
|
34
|
+
actual = @@tree.group_by_sibling_subtrees(
|
35
|
+
level: 2,
|
36
|
+
aggregation_field: 'strings',
|
37
|
+
ancestry_in_keys: true
|
38
|
+
)
|
39
|
+
|
40
|
+
assert_equal expected, actual
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_nodes_with
|
44
|
+
expected_names = ["A hidden secret lies in the deepest leaves...", "Just kidding.", "Could forester handle trees with hundreds of levels?", "Maybe."]
|
45
|
+
|
46
|
+
found_nodes = @@tree.nodes_with('name', 'Second node of level 3')
|
47
|
+
assert_equal 1, found_nodes.length
|
48
|
+
|
49
|
+
actual_names = found_nodes.flat_map do |node|
|
50
|
+
node.own_and_descendants('strings')
|
51
|
+
end
|
52
|
+
|
53
|
+
assert_equal expected_names, actual_names
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_search
|
57
|
+
|
58
|
+
expected = [7]
|
59
|
+
|
60
|
+
actual_1 = @@tree.search({
|
61
|
+
by_field: 'name',
|
62
|
+
keywords: 'Second node of level 3',
|
63
|
+
then_get: 'value',
|
64
|
+
subtree: false
|
65
|
+
})
|
66
|
+
|
67
|
+
actual_2 = @@tree.search({
|
68
|
+
by_field: 'name',
|
69
|
+
keywords: ['Second node of level 3', 'Not present name'],
|
70
|
+
then_get: 'value',
|
71
|
+
subtree: false
|
72
|
+
})
|
73
|
+
|
74
|
+
actual_3 = @@tree.search({
|
75
|
+
by_field: 'strings',
|
76
|
+
keywords: 'A hidden secret lies in the deepest leaves...',
|
77
|
+
then_get: 'value',
|
78
|
+
subtree: false
|
79
|
+
})
|
80
|
+
assert_equal expected, actual_1
|
81
|
+
assert_equal expected, actual_2
|
82
|
+
assert_equal expected, actual_3
|
83
|
+
|
84
|
+
expected_values = [7, 8, 9]
|
85
|
+
actual_values = @@tree.search({
|
86
|
+
by_field: 'name',
|
87
|
+
keywords: 'Second node of level 3',
|
88
|
+
then_get: 'value',
|
89
|
+
subtree: true
|
90
|
+
})
|
91
|
+
assert_equal expected_values, actual_values
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
data/test/test_mutators.rb
CHANGED
@@ -1,29 +1,34 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
2
|
require 'forester'
|
3
3
|
|
4
|
+
require_relative './simple_tree_helper'
|
5
|
+
|
4
6
|
class TestMutators < Minitest::Test
|
5
7
|
|
6
|
-
def
|
8
|
+
def setup
|
9
|
+
path_to_trees = "#{File.dirname(__FILE__)}/trees"
|
10
|
+
path_to_simple_tree = "#{path_to_trees}/simple_tree.yml"
|
11
|
+
@tree = Forester::TreeFactory.from_yaml_file(path_to_simple_tree)
|
12
|
+
end
|
7
13
|
|
8
|
-
|
9
|
-
tree = Forester::TreeFactory.from_yaml_file("#{path_to_trees}/simple_tree.yml")
|
14
|
+
def test_add_field
|
10
15
|
|
11
|
-
tree.
|
16
|
+
@tree.add_field!('number_four', 4)
|
12
17
|
|
13
|
-
assert_equal 4, tree.get(:number_four)
|
14
|
-
assert_equal 4, tree.get('number_four')
|
18
|
+
assert_equal 4, @tree.get(:number_four)
|
19
|
+
assert_equal 4, @tree.get('number_four')
|
15
20
|
|
16
|
-
tree.
|
21
|
+
@tree.add_field!(:number_five, 5)
|
17
22
|
|
18
|
-
assert_equal 5, tree.get(:number_five)
|
19
|
-
assert_equal 5, tree.get('number_five')
|
23
|
+
assert_equal 5, @tree.get(:number_five)
|
24
|
+
assert_equal 5, @tree.get('number_five')
|
20
25
|
|
21
26
|
number_one = 1
|
22
27
|
|
23
|
-
tree.
|
28
|
+
@tree.add_field!(:number_six, -> (node) { node.get(:number_five) + number_one })
|
24
29
|
|
25
|
-
assert_equal 6, tree.get(:number_six)
|
30
|
+
assert_equal 6, @tree.get(:number_six)
|
26
31
|
|
27
32
|
end
|
28
33
|
|
29
|
-
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'forester'
|
3
|
+
|
4
|
+
require_relative './simple_tree_helper'
|
5
|
+
|
6
|
+
class TestTreeFactory < Minitest::Test
|
7
|
+
|
8
|
+
include SimpleTreeHelper
|
9
|
+
|
10
|
+
def test_from_root_hash
|
11
|
+
hash = YAML.load_file(PATH_TO_SIMPLE_TREE)
|
12
|
+
|
13
|
+
whole_trees = [:last, 29, 4].map { |ml| new_with_max_level(hash, ml) }
|
14
|
+
|
15
|
+
assert(whole_trees.product(whole_trees).all? { |t1, t2| t1.same_as?(t2) })
|
16
|
+
|
17
|
+
pruned_trees = (0..2).map { |ml| new_with_max_level(hash, ml) }
|
18
|
+
|
19
|
+
pruned_trees.each_with_index do |t, i|
|
20
|
+
assert_equal(i, t.max_level)
|
21
|
+
|
22
|
+
pruned = whole_trees[i].remove_levels_past!(i)
|
23
|
+
assert(t.same_as?(pruned))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def new_with_max_level(hash, max_level)
|
30
|
+
Forester::TreeFactory.from_root_hash(hash['root'], { max_level: max_level })
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'forester'
|
3
|
+
|
4
|
+
require_relative './simple_tree_helper'
|
5
|
+
|
6
|
+
class TestTreeNode < Minitest::Test
|
7
|
+
|
8
|
+
include SimpleTreeHelper
|
9
|
+
|
10
|
+
def test_size
|
11
|
+
assert_equal 10, @@tree.size
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_values
|
15
|
+
expected = (0..9).reduce(:+)
|
16
|
+
actual = @@tree.reduce(0) { |acum, node| acum + node.get('value') }
|
17
|
+
|
18
|
+
assert_equal expected, actual
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_levels
|
22
|
+
expected = [
|
23
|
+
["root"],
|
24
|
+
["First node of level 1", "Second node of level 1"],
|
25
|
+
["First node of level 2", "Second node of level 2", "Third node of level 2"],
|
26
|
+
["First node of level 3", "Second node of level 3"],
|
27
|
+
["First node of level 4", "Second node of level 4"]
|
28
|
+
]
|
29
|
+
actual = @@tree.each_level.map { |l| l.map { |n| n.get('name') } }.to_a
|
30
|
+
|
31
|
+
assert_equal expected, actual
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/test/test_views.rb
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
2
|
require 'forester'
|
3
3
|
|
4
|
-
|
4
|
+
require_relative './simple_tree_helper'
|
5
5
|
|
6
|
-
|
6
|
+
class TestViews < Minitest::Test
|
7
7
|
|
8
|
-
|
9
|
-
tree = Forester::TreeFactory.from_yaml_file("#{path_to_trees}/simple_tree.yml")
|
8
|
+
include SimpleTreeHelper
|
10
9
|
|
11
|
-
|
10
|
+
def test_as_root_hash
|
11
|
+
hash = (YAML.load(File.read(PATH_TO_SIMPLE_TREE)))
|
12
12
|
add_empty_children_keys(hash['root'])
|
13
13
|
|
14
|
-
|
14
|
+
expected = hash['root']
|
15
|
+
actual = @@tree.as_root_hash({ stringify_keys: true })
|
15
16
|
|
17
|
+
assert_equal expected, actual
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
@@ -22,4 +24,4 @@ class TestViews < Minitest::Test
|
|
22
24
|
hash['children'].each { |child| add_empty_children_keys(child) }
|
23
25
|
end
|
24
26
|
|
25
|
-
end
|
27
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forester
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eugenio Bruno
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubytree
|
@@ -89,8 +89,11 @@ files:
|
|
89
89
|
- lib/forester/tree_node_ext/mutators.rb
|
90
90
|
- lib/forester/tree_node_ext/views.rb
|
91
91
|
- lib/forester/version.rb
|
92
|
+
- test/simple_tree_helper.rb
|
93
|
+
- test/test_aggregators.rb
|
92
94
|
- test/test_mutators.rb
|
93
|
-
- test/
|
95
|
+
- test/test_tree_factory.rb
|
96
|
+
- test/test_tree_node.rb
|
94
97
|
- test/test_views.rb
|
95
98
|
- test/trees/simple_tree.yml
|
96
99
|
homepage: http://rubygems.org/gems/forester
|
@@ -118,7 +121,10 @@ signing_key:
|
|
118
121
|
specification_version: 4
|
119
122
|
summary: A gem to represent and interact with tree data structures
|
120
123
|
test_files:
|
124
|
+
- test/simple_tree_helper.rb
|
125
|
+
- test/test_aggregators.rb
|
121
126
|
- test/test_mutators.rb
|
122
|
-
- test/
|
127
|
+
- test/test_tree_factory.rb
|
128
|
+
- test/test_tree_node.rb
|
123
129
|
- test/test_views.rb
|
124
130
|
- test/trees/simple_tree.yml
|
data/test/test_treenode.rb
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
require 'minitest/autorun'
|
2
|
-
require 'forester'
|
3
|
-
|
4
|
-
class TestTreeNode < Minitest::Test
|
5
|
-
|
6
|
-
def test_tree_node
|
7
|
-
|
8
|
-
path_to_trees = "#{File.dirname(__FILE__)}/trees"
|
9
|
-
tree = Forester::TreeFactory.from_yaml_file("#{path_to_trees}/simple_tree.yml")
|
10
|
-
|
11
|
-
assert_equal 10, tree.size
|
12
|
-
|
13
|
-
assert_equal (0..9).reduce(:+), tree.reduce(0) { |acum, node| acum + node.get('value') }
|
14
|
-
|
15
|
-
expected_levels = [
|
16
|
-
["root"],
|
17
|
-
["First node of level 1", "Second node of level 1"],
|
18
|
-
["First node of level 2", "Second node of level 2", "Third node of level 2"],
|
19
|
-
["First node of level 3", "Second node of level 3"],
|
20
|
-
["First node of level 4", "Second node of level 4"]
|
21
|
-
]
|
22
|
-
|
23
|
-
assert_equal expected_levels, tree.each_level.map { |level| level.map { |n| n.get('name') } }.to_a
|
24
|
-
|
25
|
-
expected = {
|
26
|
-
["First node of level 1", "First node of level 2"] => ["Already in level 2", "I want to be the very best", "like no one ever was"],
|
27
|
-
["First node of level 1", "Second node of level 2"] => ["I have a sibling to my left", "She wants to catch them all"],
|
28
|
-
["Second node of level 1", "Third node of level 2"] => ["Reached level 3", "It's dark", "A hidden secret lies in the deepest leaves...", "Just kidding.", "Could forester handle trees with hundreds of levels?", "Maybe."]}
|
29
|
-
|
30
|
-
aggregation_result = tree.values_by_subtree_of_level(level: 2, aggregation_field: 'strings', include_ancestry_in_keys: true)
|
31
|
-
|
32
|
-
assert_equal expected, aggregation_result
|
33
|
-
|
34
|
-
|
35
|
-
expected_value = [7]
|
36
|
-
actual_value = tree.search({
|
37
|
-
by_field: 'name',
|
38
|
-
keywords: 'Second node of level 3',
|
39
|
-
then_get: 'value',
|
40
|
-
of_subtree: false,
|
41
|
-
})
|
42
|
-
assert_equal expected_value, actual_value
|
43
|
-
|
44
|
-
expected_value = [7]
|
45
|
-
actual_value = tree.search({
|
46
|
-
by_field: 'name',
|
47
|
-
keywords: ['Second node of level 3', 'Not present name'],
|
48
|
-
then_get: 'value',
|
49
|
-
of_subtree: false,
|
50
|
-
})
|
51
|
-
assert_equal expected_value, actual_value
|
52
|
-
|
53
|
-
expected_values = [7, 8, 9]
|
54
|
-
actual_values = tree.search({
|
55
|
-
by_field: 'name',
|
56
|
-
keywords: 'Second node of level 3',
|
57
|
-
then_get: 'value',
|
58
|
-
of_subtree: true,
|
59
|
-
})
|
60
|
-
assert_equal expected_values, actual_values
|
61
|
-
|
62
|
-
expected_value = [7]
|
63
|
-
actual_value = tree.search({
|
64
|
-
by_field: 'strings',
|
65
|
-
keywords: 'A hidden secret lies in the deepest leaves...',
|
66
|
-
then_get: 'value',
|
67
|
-
of_subtree: false
|
68
|
-
})
|
69
|
-
assert_equal expected_value, actual_value
|
70
|
-
|
71
|
-
|
72
|
-
expected_names = ["A hidden secret lies in the deepest leaves...", "Just kidding.", "Could forester handle trees with hundreds of levels?", "Maybe."]
|
73
|
-
|
74
|
-
found_nodes = tree.nodes_with('name', 'Second node of level 3')
|
75
|
-
assert_equal 1, found_nodes.length
|
76
|
-
|
77
|
-
actual_names = found_nodes.flat_map do |node|
|
78
|
-
node.own_and_descendants('strings')
|
79
|
-
end
|
80
|
-
|
81
|
-
assert_equal expected_names, actual_names
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|