forester 3.2.0 → 4.0.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 +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
|