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.
@@ -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