fertile_forest 0.0.0 → 1.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/LICENSE +22 -0
- data/README.rdoc +3 -0
- data/config/routes.rb +2 -0
- data/lib/fertile_forest/engine.rb +13 -0
- data/lib/fertile_forest/modules/calculators.rb +231 -0
- data/lib/fertile_forest/modules/configs.rb +96 -0
- data/lib/fertile_forest/modules/entities.rb +233 -0
- data/lib/fertile_forest/modules/finders.rb +736 -0
- data/lib/fertile_forest/modules/reconstructers.rb +843 -0
- data/lib/fertile_forest/modules/states.rb +271 -0
- data/lib/fertile_forest/modules/utilities.rb +188 -0
- data/lib/fertile_forest/saplings.rb +330 -0
- data/lib/fertile_forest/version.rb +1 -1
- data/lib/fertile_forest.rb +7 -3
- data/lib/tasks/fertile_forest_tasks.rake +4 -0
- metadata +19 -6
@@ -0,0 +1,271 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# states methods for Table
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module StewEucen
|
7
|
+
# Name space of Stew Eucen's Acts
|
8
|
+
module Acts
|
9
|
+
# Name space of Fertile Forest
|
10
|
+
module FertileForest
|
11
|
+
# Name space of class methods for Fertile Forest
|
12
|
+
module Table
|
13
|
+
# This module is for extending into derived class by ActiveRecord.<br>
|
14
|
+
# The caption contains "Instance Methods",
|
15
|
+
# but it means "Class Methods" of each derived class.
|
16
|
+
module States
|
17
|
+
#
|
18
|
+
# Are all nodes siblings?
|
19
|
+
# @param args [Array] Nodes.
|
20
|
+
# @return [Boolean] Returns true is those are sibling nodes.
|
21
|
+
# @todo is full flag
|
22
|
+
#
|
23
|
+
def siblings?(*args)
|
24
|
+
sibling_nodes = ff_resolve_nodes(args.flatten)
|
25
|
+
|
26
|
+
# get id hash by nested information
|
27
|
+
eldest_node = sibling_nodes.values.first
|
28
|
+
full_sibling_nodes = siblings_of(eldest_node, [@_id]).all
|
29
|
+
|
30
|
+
child_hash = {}
|
31
|
+
bingo_count = sibling_nodes.length
|
32
|
+
full_sibling_nodes.each do |the_node|
|
33
|
+
the_id = the_node.id
|
34
|
+
child_hash[the_id] = the_node
|
35
|
+
# ruby has no --xxxx
|
36
|
+
bingo_count -= 1 if sibling_nodes.has_key?(the_id)
|
37
|
+
end
|
38
|
+
|
39
|
+
# return value
|
40
|
+
if bingo_count == 0
|
41
|
+
child_hash
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Is root node?
|
49
|
+
# @param node_obj [Entity|Integer] Node of Entity|int to check.
|
50
|
+
# @return [Boolean] Returns true is this is root node.
|
51
|
+
#
|
52
|
+
def root?(node_obj)
|
53
|
+
aim_node = ff_resolve_nodes(node_obj)
|
54
|
+
return nil if aim_node.blank? # nil as dubious
|
55
|
+
|
56
|
+
aim_node.ff_depth == ROOT_DEPTH # never ===
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Has descendant?
|
61
|
+
# @param node_obj [Entity|Integer] Node of Entity|int to check.
|
62
|
+
# @return [Boolean] Returns true is this has descendant node.
|
63
|
+
#
|
64
|
+
def has_descendant?(node_obj)
|
65
|
+
aim_node = ff_resolve_nodes(node_obj)
|
66
|
+
return nil if aim_node.blank? # nil as dubious
|
67
|
+
|
68
|
+
aim_query = ff_subtree_scope(
|
69
|
+
aim_node,
|
70
|
+
false, # without top
|
71
|
+
true # use COALESCE()
|
72
|
+
)
|
73
|
+
.select(@_id)
|
74
|
+
|
75
|
+
# FIXME: When use COALESCE(), can not act query.count
|
76
|
+
# 0 < aim_query.count
|
77
|
+
aim_query.first.present?
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Is leaf node?
|
82
|
+
# @param node_obj [Entity|Integer] Node of Entity|int to check.
|
83
|
+
# @return [Boolean] Returns true is this is leaf node.
|
84
|
+
#
|
85
|
+
def leaf?(node_obj)
|
86
|
+
result = has_descendant?(node_obj) # nil as dubious
|
87
|
+
return nil if result.nil?
|
88
|
+
|
89
|
+
!result
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Is internal node?
|
94
|
+
# "internal" means non-leaf and non-root.
|
95
|
+
# @param node_obj [Entity|Integer] Node of Entity|int to check.
|
96
|
+
# @return [Boolean] Returns true is this is leaf node.
|
97
|
+
#
|
98
|
+
def internal?(node_obj)
|
99
|
+
aim_node = ff_resolve_nodes(node_obj)
|
100
|
+
return nil if aim_node.blank? # nil as dubious
|
101
|
+
|
102
|
+
aim_node.ff_depth != ROOT_DEPTH && has_descendant?(node_obj)
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# Has sibling node?
|
107
|
+
# @param node_obj [Entity|Integer] Node of Entity|int to check.
|
108
|
+
# @return [Boolean] Returns true is this has sibling node.
|
109
|
+
#
|
110
|
+
def has_sibling?(node_obj)
|
111
|
+
aim_node = ff_resolve_nodes(node_obj)
|
112
|
+
return nil if aim_node.blank? # nil as dubious
|
113
|
+
|
114
|
+
aim_depth = aim_node.ff_depth
|
115
|
+
# root node has no sibling
|
116
|
+
return false if aim_depth == ROOT_DEPTH
|
117
|
+
|
118
|
+
parent_node = genitor(aim_node)
|
119
|
+
# null as dubious, because no parent is irregular
|
120
|
+
return nil if parent_node.blank?
|
121
|
+
|
122
|
+
ffdd = arel_table[@_ff_depth]
|
123
|
+
aim_query = ff_subtree_scope(
|
124
|
+
parent_node,
|
125
|
+
false, # without top
|
126
|
+
false # use COALESCE()
|
127
|
+
# true # FIXME: COALESCE() true makes error
|
128
|
+
)
|
129
|
+
.where(ffdd.eq(aim_depth))
|
130
|
+
|
131
|
+
1 < aim_query.count
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Is only child?
|
136
|
+
# @param node_obj [Entity|Integer] Node of Entity|int to check.
|
137
|
+
# @return [Boolean] Returns true is this is only child node.
|
138
|
+
#
|
139
|
+
def only_child?(node_obj)
|
140
|
+
has_sibling = has_sibling?(node_obj)
|
141
|
+
return nil if has_sibling.nil? # nil as dubious
|
142
|
+
|
143
|
+
!has_sibling
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Is reserching node descendant of base node?
|
148
|
+
# @param base_obj [Entity|Integer] Entity|int of base node to check.
|
149
|
+
# @param researches [Array] Research nodes.
|
150
|
+
# @return [Array] Item of array true is it is descendant node of base node.
|
151
|
+
#
|
152
|
+
def descendant?(base_obj, researches = [])
|
153
|
+
aim_node = ff_resolve_nodes(base_obj)
|
154
|
+
return nil if aim_node.blank? # nil as dubious
|
155
|
+
|
156
|
+
is_plural = researches.is_a?(Array)
|
157
|
+
return (is_plural ? [] : nil) if researches.blank?
|
158
|
+
|
159
|
+
# need to be "id => node" for checking grove
|
160
|
+
research_nodes = ff_resolve_nodes(
|
161
|
+
is_plural ? researches : [researches],
|
162
|
+
true # refresh
|
163
|
+
)
|
164
|
+
|
165
|
+
boundary_queue = ff_get_boundary_queue(aim_node)
|
166
|
+
aim_tail_queue = (boundary_queue.blank? \
|
167
|
+
? QUEUE_MAX_VALUE
|
168
|
+
: boundary_queue - 1
|
169
|
+
)
|
170
|
+
|
171
|
+
aim_queue = aim_node.ff_queue
|
172
|
+
aim_grove = aim_node.ff_grove
|
173
|
+
|
174
|
+
res = {}
|
175
|
+
|
176
|
+
research_nodes.each_pair do |the_id, the_node|
|
177
|
+
if the_node.present? && the_node.ff_grove == aim_grove
|
178
|
+
the_queue = the_node.ff_queue
|
179
|
+
res[the_id] = aim_queue < the_queue && the_queue <= aim_tail_queue
|
180
|
+
else
|
181
|
+
res[the_id] = nil
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
is_plural ? res : res.values.first
|
186
|
+
end
|
187
|
+
|
188
|
+
#
|
189
|
+
# Is reserching node ancestor of base node?
|
190
|
+
# @param base_obj [Entity|Integer] Entity|int of base node to check.
|
191
|
+
# @param researches [Array] Research nodes.
|
192
|
+
# @return [Array] Item of array true is it is ancestor node of base node.
|
193
|
+
#
|
194
|
+
def ancestor?(base_obj, researches = [])
|
195
|
+
aim_node = ff_resolve_nodes(base_obj)
|
196
|
+
return nil if aim_node.blank? # nil as dubious
|
197
|
+
|
198
|
+
is_plural = researches.is_a?(Array)
|
199
|
+
return (is_plural ? [] : nil) if researches.blank?
|
200
|
+
|
201
|
+
# need to be "id => node" for checking grove
|
202
|
+
research_nodes = ff_resolve_nodes(
|
203
|
+
is_plural ? researches : [researches],
|
204
|
+
true # refresh
|
205
|
+
)
|
206
|
+
|
207
|
+
exists_hash = {}
|
208
|
+
Array(ancestors(aim_node)).each { |node| exists_hash[node.id] = true }
|
209
|
+
|
210
|
+
res = {}
|
211
|
+
research_nodes.each_pair do |the_id, the_node|
|
212
|
+
res[the_id] = exists_hash[the_id]
|
213
|
+
end
|
214
|
+
|
215
|
+
is_plural ? res : res.values.first
|
216
|
+
end
|
217
|
+
|
218
|
+
#
|
219
|
+
# Calculate height of subtree.
|
220
|
+
# When want to get root height as:
|
221
|
+
# (1) get height of any node.
|
222
|
+
# (2) root height = height of the node + depth of the node.
|
223
|
+
# Height of empty tree is "-1"<br>
|
224
|
+
# http://en.wikipedia.org/wiki/Tree_(data_structure)
|
225
|
+
# @param base_obj [Entity|Integer] Base node|id to check.
|
226
|
+
# @return [Integer] Height of subtree of base node.
|
227
|
+
# @return [nil] Invalid input (base node is nil).
|
228
|
+
#
|
229
|
+
def height(base_obj)
|
230
|
+
aim_node = ff_resolve_nodes(base_obj)
|
231
|
+
return nil if aim_node.blank? # nil as dubious
|
232
|
+
|
233
|
+
ffdd = arel_table[@_ff_depth]
|
234
|
+
|
235
|
+
# with top, use COALESCE()
|
236
|
+
height_res = ff_subtree_scope(aim_node, SUBTREE_WITH_TOP_NODE, true)
|
237
|
+
.select(ffdd.maximum.as('ff_height'))
|
238
|
+
.first
|
239
|
+
|
240
|
+
return nil if height_res.blank? # nil as dubious
|
241
|
+
|
242
|
+
height_res.ff_height - aim_node.ff_depth
|
243
|
+
end
|
244
|
+
|
245
|
+
#
|
246
|
+
# Calculate size of subtree.
|
247
|
+
# @param base_obj [Entity|Integer] Base node|id to check.
|
248
|
+
# @return [Integer] Size of subtree of base node.
|
249
|
+
# @return [nil] Invalid input (base node is nil).
|
250
|
+
#
|
251
|
+
def size(base_obj)
|
252
|
+
aim_node = ff_resolve_nodes(base_obj)
|
253
|
+
return nil if aim_node.blank? # nil as dubious
|
254
|
+
|
255
|
+
ffdd = arel_table[@_ff_depth]
|
256
|
+
|
257
|
+
# with top, use COALESCE()
|
258
|
+
size_res = ff_subtree_scope(aim_node, SUBTREE_WITH_TOP_NODE, true)
|
259
|
+
.select(ffdd.count.as('ff_count'))
|
260
|
+
.first
|
261
|
+
|
262
|
+
return nil if size_res.blank? # nil as dubious
|
263
|
+
|
264
|
+
size_res.ff_count
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
#
|
2
|
+
# Utilities methods.
|
3
|
+
# Fertile Forest for Ruby: The new model for storing hierarchical data in a database.
|
4
|
+
#
|
5
|
+
# @author StewEucen
|
6
|
+
# @copyright Copyright (c) 2015 Stew Eucen (http://lab.kochlein.com)
|
7
|
+
# @license http://www.opensource.org/licenses/mit-license.php MIT License
|
8
|
+
#
|
9
|
+
# @link http://lab.kochlein.com/FertileForest
|
10
|
+
# @since File available since Release 1.0.0
|
11
|
+
# @version 1.0.0
|
12
|
+
#
|
13
|
+
module StewEucen
|
14
|
+
# Name space of Stew Eucen's Acts
|
15
|
+
module Acts
|
16
|
+
# Name space of Fertile Forest
|
17
|
+
module FertileForest
|
18
|
+
# Name space of class methods for Fertile Forest
|
19
|
+
module Table
|
20
|
+
# This module is for extending into derived class by ActiveRecord.<br>
|
21
|
+
# The caption contains "Instance Methods",
|
22
|
+
# but it means "Class Methods" of each derived class.
|
23
|
+
# @private
|
24
|
+
module Utilities
|
25
|
+
|
26
|
+
def ff_is_bool(aim_obj)
|
27
|
+
aim_obj.kind_of?(TrueClass) || aim_obj.kind_of?(FalseClass)
|
28
|
+
end
|
29
|
+
|
30
|
+
def ff_raw_query(query_string)
|
31
|
+
connection.execute(query_string, :skip_logging)
|
32
|
+
end
|
33
|
+
|
34
|
+
def ff_quoted_column(column, with_table = true)
|
35
|
+
keyString = column.to_s
|
36
|
+
resolved_column = attribute_aliases[keyString] || keyString
|
37
|
+
|
38
|
+
quoted_column = connection.quote_column_name(resolved_column)
|
39
|
+
|
40
|
+
if with_table
|
41
|
+
"#{quoted_table_name}.#{quoted_column}"
|
42
|
+
else
|
43
|
+
quoted_column
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Update all by raw query with ORDER BY clause and pre-query of user variable.
|
49
|
+
# Update() do not use order() in Ruby on Rails 4.x.
|
50
|
+
# This method is the solution to workaround it.
|
51
|
+
#
|
52
|
+
# 2015/06/01
|
53
|
+
# This method is for raw query to update,
|
54
|
+
# because Rails standard method "update_all()" can not use @ffqq.
|
55
|
+
# Reason why connection is broken from "SET @ffqq = started_value".
|
56
|
+
#
|
57
|
+
# @param predicates [Hash] A hash of predicates for SET clause.
|
58
|
+
# @param conditions [Hash] Conditions to be used, accepts anything Query::where() can take.
|
59
|
+
# @param order [Hash] Order clause.
|
60
|
+
# @param prequeries [Hash] Execute Prequeries before UPDATE for setting the specified local variables.
|
61
|
+
# @return [Integer|Boolean] Count Returns the affected rows | false.
|
62
|
+
#
|
63
|
+
def ff_update_all_in_order(predicates, conditions, order, prequeries = nil)
|
64
|
+
update_fields = []
|
65
|
+
predicates.each_pair do |key, value| # NOTICE string value
|
66
|
+
quoted = ff_quoted_column(key)
|
67
|
+
update_fields << "#{quoted} = #{value}"
|
68
|
+
end
|
69
|
+
|
70
|
+
update_conditions = []
|
71
|
+
conditions.each_pair do |key, value|
|
72
|
+
if key.is_a?(Integer)
|
73
|
+
update_conditions << value
|
74
|
+
else
|
75
|
+
quoted = ff_quoted_column(key)
|
76
|
+
if value.is_a?(Array)
|
77
|
+
joined = value.join(',')
|
78
|
+
update_conditions << "#{quoted} IN (#{joined})"
|
79
|
+
else
|
80
|
+
info = key.to_s.split(' ')
|
81
|
+
if 1 < info.length
|
82
|
+
picked_column = ff_quoted_column(info.shift.to_sym)
|
83
|
+
join_list = [picked_column] + info + [value]
|
84
|
+
update_conditions << join_list.join(' ')
|
85
|
+
else
|
86
|
+
update_conditions << "#{quoted} = #{value}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
update_order = []
|
93
|
+
order.each_pair do |key, value|
|
94
|
+
direction = key.is_a?(Integer) ? 'ASC' : value
|
95
|
+
quoted = ff_quoted_column(key)
|
96
|
+
update_order << "#{quoted} #{direction}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# pre-query to SET @xxx := value
|
100
|
+
# can execuete two query() at once, however can not get affectedRows.
|
101
|
+
prequeries = [prequeries] unless prequeries.instance_of?(Array)
|
102
|
+
prequeries.each { |query| ff_raw_query(query) }
|
103
|
+
|
104
|
+
# use raw query, because can not use ORDER BY in standard updateAll().
|
105
|
+
update_query_string = [
|
106
|
+
'UPDATE',
|
107
|
+
quoted_table_name(), # OK `categories`
|
108
|
+
'SET',
|
109
|
+
update_fields.join(', '),
|
110
|
+
'WHERE',
|
111
|
+
update_conditions.map { |cond| "(#{cond})" }.join(' AND '),
|
112
|
+
'ORDER BY',
|
113
|
+
update_order.join(', '),
|
114
|
+
].join(' ')
|
115
|
+
|
116
|
+
# return value
|
117
|
+
connection.update(update_query_string)
|
118
|
+
end
|
119
|
+
|
120
|
+
def ff_create_case_expression(key, whens_thens, else_str)
|
121
|
+
joined_list = ["CASE", key]
|
122
|
+
whens_thens.each do |item|
|
123
|
+
joined_list += ["WHEN", item[0], "THEN", item[1]]
|
124
|
+
end
|
125
|
+
joined_list << ["ELSE", else_str, "END"]
|
126
|
+
|
127
|
+
joined_list.join(' ')
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Resolve node from Entity|int params.
|
132
|
+
#
|
133
|
+
# @param nodes [ActiveRecord::Base|Integer|Array] To identify the nodes.
|
134
|
+
# @param refresh [Boolean] true:Refind each Entity by id.
|
135
|
+
# @return [ActiveRecord::Base|Hash] When nodes is array, return value is hash.
|
136
|
+
#
|
137
|
+
def ff_resolve_nodes(nodes, refresh = false)
|
138
|
+
return nodes if nodes.blank?
|
139
|
+
is_plural = nodes.is_a?(Array)
|
140
|
+
nodes = [nodes] unless is_plural
|
141
|
+
|
142
|
+
res_entities = {}
|
143
|
+
refind_ids = []
|
144
|
+
nodes.each do |item|
|
145
|
+
is_node = item.is_a?(ActiveRecord::Base)
|
146
|
+
if is_node
|
147
|
+
the_id = item.id
|
148
|
+
else
|
149
|
+
the_id = item.to_i
|
150
|
+
end
|
151
|
+
|
152
|
+
if !is_node || refresh
|
153
|
+
refind_ids << the_id
|
154
|
+
res_entities[the_id] = nil
|
155
|
+
else
|
156
|
+
res_entities[the_id] = item
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# get node orderd by id
|
162
|
+
#
|
163
|
+
if refind_ids.present?
|
164
|
+
aim_query = ff_usual_conditions_scope(nil)
|
165
|
+
.where(id: refind_ids)
|
166
|
+
.ff_required_columns_scope()
|
167
|
+
|
168
|
+
aim_query.all.each do |node|
|
169
|
+
res_entities[node.id] = node
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# return value
|
174
|
+
is_plural ? res_entities : res_entities.values.first
|
175
|
+
end
|
176
|
+
|
177
|
+
protected :ff_is_bool,
|
178
|
+
:ff_raw_query,
|
179
|
+
:ff_quoted_column,
|
180
|
+
:ff_update_all_in_order,
|
181
|
+
:ff_create_case_expression,
|
182
|
+
:ff_resolve_nodes
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|