bblib 0.3.0 → 0.4.1

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.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +11 -10
  3. data/.rspec +2 -2
  4. data/.travis.yml +4 -4
  5. data/CODE_OF_CONDUCT.md +13 -13
  6. data/Gemfile +4 -4
  7. data/LICENSE.txt +21 -21
  8. data/README.md +247 -757
  9. data/Rakefile +6 -6
  10. data/bblib.gemspec +34 -34
  11. data/bin/console +14 -14
  12. data/bin/setup +7 -7
  13. data/lib/array/bbarray.rb +71 -29
  14. data/lib/bblib.rb +12 -12
  15. data/lib/bblib/version.rb +3 -3
  16. data/lib/class/effortless.rb +23 -0
  17. data/lib/error/abstract.rb +3 -0
  18. data/lib/file/bbfile.rb +93 -52
  19. data/lib/hash/bbhash.rb +130 -46
  20. data/lib/hash/hash_struct.rb +24 -0
  21. data/lib/hash/tree_hash.rb +364 -0
  22. data/lib/hash_path/hash_path.rb +210 -0
  23. data/lib/hash_path/part.rb +83 -0
  24. data/lib/hash_path/path_hash.rb +84 -0
  25. data/lib/hash_path/proc.rb +93 -0
  26. data/lib/hash_path/processors.rb +239 -0
  27. data/lib/html/bbhtml.rb +2 -0
  28. data/lib/html/builder.rb +34 -0
  29. data/lib/html/tag.rb +49 -0
  30. data/lib/logging/bblogging.rb +42 -0
  31. data/lib/mixins/attrs.rb +422 -0
  32. data/lib/mixins/bbmixins.rb +7 -0
  33. data/lib/mixins/bridge.rb +17 -0
  34. data/lib/mixins/family_tree.rb +41 -0
  35. data/lib/mixins/hooks.rb +139 -0
  36. data/lib/mixins/logger.rb +31 -0
  37. data/lib/mixins/serializer.rb +71 -0
  38. data/lib/mixins/simple_init.rb +160 -0
  39. data/lib/number/bbnumber.rb +15 -7
  40. data/lib/object/bbobject.rb +46 -19
  41. data/lib/opal/bbopal.rb +0 -4
  42. data/lib/os/bbos.rb +24 -16
  43. data/lib/os/bbsys.rb +60 -43
  44. data/lib/string/bbstring.rb +165 -66
  45. data/lib/string/cases.rb +37 -29
  46. data/lib/string/fuzzy_matcher.rb +48 -50
  47. data/lib/string/matching.rb +43 -30
  48. data/lib/string/pluralization.rb +156 -0
  49. data/lib/string/regexp.rb +45 -0
  50. data/lib/string/roman.rb +17 -30
  51. data/lib/system/bbsystem.rb +42 -0
  52. data/lib/time/bbtime.rb +79 -58
  53. data/lib/time/cron.rb +174 -132
  54. data/lib/time/task_timer.rb +86 -70
  55. metadata +27 -10
  56. data/lib/gem/bbgem.rb +0 -28
  57. data/lib/hash/hash_path.rb +0 -344
  58. data/lib/hash/hash_path_proc.rb +0 -256
  59. data/lib/hash/path_hash.rb +0 -81
  60. data/lib/object/attr.rb +0 -182
  61. data/lib/object/hooks.rb +0 -69
  62. data/lib/object/lazy_class.rb +0 -73
@@ -1,73 +1,157 @@
1
- require_relative 'hash_path'
2
- require_relative 'path_hash'
3
1
 
4
- class Hash
2
+ require_relative 'tree_hash'
3
+ require_relative 'hash_struct'
5
4
 
5
+ class Hash
6
6
  # Merges with another hash but also merges all nested hashes and arrays/values.
7
- # Based on method found @ http://stackoverflow.com/questions/9381553/ruby-merge-nested-hash
8
- def deep_merge with, merge_arrays: true, overwrite: true
9
- merger = proc{ |k, v1, v2|
10
- if v1.is_a?(Hash) && v2.is_a?(Hash)
11
- v1.merge(v2, &merger)
12
- else
13
- if merge_arrays && v1.is_a?(Array) && v2.is_a?(Array)
14
- v1 + v2
15
- else
16
- overwrite || v1 == v2 ? v2 : [v1, v2].flatten
17
- end
18
- end
19
- }
20
- self.merge(with, &merger)
21
- end
22
-
23
- def deep_merge! with, merge_arrays: true, overwrite: true
24
- replace self.deep_merge(with, merge_arrays: merge_arrays, overwrite: overwrite)
7
+ def deep_merge(with, merge_arrays: true, overwrite: true, uniq: false)
8
+ merger = proc do |_k, v1, v2|
9
+ if BBLib.are_all?(Hash, v1, v2)
10
+ v1.merge(v2, &merger)
11
+ elsif merge_arrays && BBLib.are_all?(Array, v1, v2)
12
+ uniq ? (v1 + v2).uniq : v1 + v2
13
+ else
14
+ overwrite || v1 == v2 ? v2 : (uniq ? [v1, v2].flatten.uniq : [v1, v2].flatten)
15
+ end
16
+ end
17
+ merge(with, &merger)
18
+ end
19
+
20
+ # In place version of deep_merge
21
+ def deep_merge!(*args)
22
+ replace deep_merge(*args)
25
23
  end
26
24
 
27
25
  # Converts the keys of the hash as well as any nested hashes to symbols.
28
- # Based on method found @ http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
29
- def keys_to_sym clean: false
30
- self.inject({}){|memo,(k,v)| memo[clean ? k.to_s.to_clean_sym : k.to_s.to_sym] = (v.is_a?(Hash) || v.is_a?(Array) ? v.keys_to_sym(clean:clean) : v); memo}
26
+ def keys_to_sym(clean: false, recursive: true)
27
+ each_with_object({}) do |(k, v), memo|
28
+ key = clean ? k.to_s.to_clean_sym : k.to_s.to_sym
29
+ memo[key] = recursive && v.respond_to?(:keys_to_sym) ? v.keys_to_sym(clean: clean) : v
30
+ end
31
31
  end
32
32
 
33
- def keys_to_sym! clean: false
34
- replace(self.keys_to_sym clean:clean)
33
+ # In place version of keys_to_sym
34
+ def keys_to_sym!(clean: false, recursive: true)
35
+ replace(keys_to_sym(clean: clean, recursive: recursive))
35
36
  end
36
37
 
37
38
  # Converts the keys of the hash as well as any nested hashes to strings.
38
- def keys_to_s
39
- self.inject({}){|memo,(k,v)| memo[k.to_s] = (v.is_a?(Hash) || v.is_a?(Array) ? v.keys_to_s : v); memo}
39
+ def keys_to_s(recursive: true)
40
+ each_with_object({}) do |(k, v), memo|
41
+ memo[k.to_s] = recursive && v.respond_to?(:keys_to_sym) ? v.keys_to_s : v
42
+ end
40
43
  end
41
44
 
42
- def keys_to_s!
43
- replace(self.keys_to_s)
45
+ # In place version of keys_to_s
46
+ def keys_to_s!(recursive: true)
47
+ replace(keys_to_s(recursive: recursive))
44
48
  end
45
49
 
46
50
  # Reverses the order of keys in the Hash
47
51
  def reverse
48
- self.to_a.reverse.to_h
52
+ to_a.reverse.to_h
49
53
  end
50
54
 
55
+ # In place version of reverse
51
56
  def reverse!
52
- replace self.reverse
57
+ replace(reverse)
58
+ end
59
+
60
+ # Like unshift for Arrays. Adds a key to the beginning of a Hash rather than the end.
61
+ def unshift(hash, value = nil)
62
+ hash = { hash => value } unless hash.is_a?(Hash)
63
+ replace(hash.merge(self).merge(hash))
64
+ end
65
+
66
+ # Displays all of the differences between this hash and another.
67
+ # Checks both key and value pairs.
68
+ def diff(hash)
69
+ to_a.diff(hash.to_a).to_h
70
+ end
71
+
72
+ # Returns all matching values with a specific key (or keys) recursively within a Hash (including nested Arrays)
73
+ def dive(*keys)
74
+ matches = []
75
+ each do |k, v|
76
+ matches << v if keys.any? { |key| (key.is_a?(Regexp) ? key =~ k : key == k) }
77
+ matches += v.dive(*keys) if v.respond_to?(:dive)
78
+ end
79
+ matches
80
+ end
81
+
82
+ # Navigate a hash using a dot delimited path.
83
+ def path_nav(obj, path = '', delimiter = '.', &block)
84
+ case obj
85
+ when Hash
86
+ obj.each do |k, v|
87
+ path_nav(
88
+ v,
89
+ (path ? [path, k.to_s.gsub(delimiter, "\\#{delimiter}")].join(delimiter) : k.to_s.gsub(delimiter, "\\#{delimiter}")).to_s,
90
+ delimiter,
91
+ &block
92
+ )
93
+ end
94
+ when Array
95
+ obj.each_with_index do |ob, index|
96
+ path_nav(
97
+ ob,
98
+ (path ? [path, "[#{index}]"].join(delimiter) : "[#{index}]").to_s,
99
+ delimiter,
100
+ &block
101
+ )
102
+ end
103
+ else
104
+ yield path, obj
105
+ end
53
106
  end
54
107
 
55
- def unshift hash, value = nil
56
- hash = {hash => value} if !hash.is_a? Hash
57
- replace hash.merge(self).merge(hash)
108
+ # Turns nested values' keys into delimiter separated paths
109
+ def squish(delimiter: '.')
110
+ sh = {}
111
+ path_nav(dup, nil, delimiter) { |k, v| sh[k] = v }
112
+ sh
58
113
  end
59
114
 
60
- def to_xml level: 0, key:nil
61
- map do |k,v|
62
- nested = v.respond_to?(:to_xml)
63
- array = v.is_a?(Array)
64
- value = nested ? v.to_xml(level:level + (array ? 0 : 1), key:k) : v
65
- (array ? '' : ("\t" * level + "<#{k}>\n")) +
66
- (nested ? '' : "\t" * (level + 1)) +
67
- "#{value}\n" +
68
- "\t" * level +
69
- (array ? '' : "</#{k}>\n")
70
- end.join.split("\n").reject{ |r| r.strip == '' }.join("\n")
115
+ # Expands keys in a hash using a delimiter. Opposite of squish.
116
+ def expand
117
+ {}.to_tree_hash.tap do |hash|
118
+ each do |k, v|
119
+ hash.bridge(k => v)
120
+ end
121
+ end.value
71
122
  end
72
123
 
124
+ # Returns a version of the hash not including the specified keys
125
+ def only(*args)
126
+ select { |k, _v| args.include?(k) }
127
+ end
128
+
129
+ # Returns a version of the hash with the specified keys removed.
130
+ def except(*args)
131
+ reject { |k, _v| args.include?(k) }
132
+ end
133
+
134
+ # Convert this hash into a TreeHash object.
135
+ def to_tree_hash
136
+ TreeHash.new(self)
137
+ end
138
+
139
+ # Run a map iterator over the values in the hash without changing the keys.
140
+ def vmap
141
+ return map unless block_given?
142
+ map { |k, v| [k, yield(v)] }.to_h
143
+ end
144
+
145
+ # Run a map iterator over the keys in the hash without changing the values.
146
+ def kmap
147
+ return map unless block_given?
148
+ map { |k, v| [yield(k), v] }.to_h
149
+ end
150
+
151
+ # Map for hash that automatically converts the yield block to a hash.
152
+ # Each yield must produce an array with exactly two elements.
153
+ def hmap
154
+ return map unless block_given?
155
+ map { |k, v| yield(k, v) }.to_h
156
+ end
73
157
  end
@@ -0,0 +1,24 @@
1
+ # Similar to Ruby's OpenStruct but uses hash as its parent class providing
2
+ # all of the typical Hash methods. Useful for storing settings on objects.
3
+ module BBLib
4
+ class HashStruct < Hash
5
+
6
+ protected
7
+
8
+ def method_missing(method, *args, &block)
9
+ if args.empty?
10
+ define_singleton_method(method) do
11
+ self[method]
12
+ end
13
+ self[method]
14
+ elsif method.to_s.end_with?('=')
15
+ define_singleton_method(method) do |arg|
16
+ self[method[0..-2].to_sym] = arg
17
+ end
18
+ self[method[0..-2].to_sym] = args.first
19
+ else
20
+ super
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,364 @@
1
+ class TreeHash
2
+ attr_reader :node_class, :parent, :children
3
+
4
+ def initialize(object = {}, parent = nil)
5
+ @children = {}
6
+ @parent = parent
7
+ object = object.value if object.is_a?(TreeHash)
8
+ replace_with(object)
9
+ end
10
+
11
+ def replace_with(object)
12
+ @node_class = object.class
13
+ build_from(object)
14
+ end
15
+
16
+ def root?
17
+ @parent.nil?
18
+ end
19
+
20
+ def child?
21
+ !root?
22
+ end
23
+
24
+ def child(key, symbol_sensitive = false)
25
+ case [node_class]
26
+ when [Hash]
27
+ children[key] || (symbol_sensitive ? nil : children[key.to_s.to_sym])
28
+ when [Array]
29
+ children[key.to_i]
30
+ else
31
+ nil
32
+ end
33
+ end
34
+
35
+ def child_exists?(key, symbol_sensitive = false)
36
+ !case [node_class]
37
+ when [Hash]
38
+ children[key] || (!symbol_sensitive && children[key.to_s.to_sym])
39
+ when [Array]
40
+ [0...children.size] === key.to_i
41
+ else
42
+ false
43
+ end.nil?
44
+ end
45
+
46
+ def children?
47
+ (node_class == Hash || node_class == Array) && !children.empty?
48
+ end
49
+
50
+ def descendants
51
+ return [] unless children?
52
+ desc = []
53
+ children.each do |key, child|
54
+ desc << child
55
+ desc += child.descendants if child.children?
56
+ end
57
+ desc
58
+ end
59
+
60
+ def [](key)
61
+ children[key]
62
+ end
63
+
64
+ def []=(key, value)
65
+ add_child(key, value)
66
+ end
67
+
68
+ def find(path)
69
+ path = HashPath.new(*path) unless path.is_a?(HashPath)
70
+ matches = [self]
71
+ path.parts.each do |part|
72
+ break if matches.empty?
73
+ matches = matches.flat_map do |match|
74
+ part.matches(match)
75
+ end
76
+ end
77
+ matches
78
+ end
79
+
80
+ def find_multi(*paths)
81
+ paths.map do |path|
82
+ find(path)
83
+ end
84
+ end
85
+
86
+ def find_join(*paths)
87
+ results = find_multi(*paths)
88
+ (0..(results.max_by(&:size).size - 1)).map do |index|
89
+ results.map do |result|
90
+ result[index]
91
+ end
92
+ end
93
+ end
94
+
95
+ def find_join_hash(key_path, val_path)
96
+ find_join(key_path, val_path).to_h
97
+ end
98
+
99
+ def set(paths)
100
+ paths.each do |path, value|
101
+ find(path).each { |child| child.replace_with(value) }
102
+ end
103
+ self
104
+ end
105
+
106
+ # Generate a path using a dot (.) delimited path
107
+ # e.g. bridge('cats.jackson' => :my_value)
108
+ # This modifies the underlying container in place
109
+ def bridge(paths)
110
+ paths = paths.map { |a| [a, nil] }.to_h if paths.is_a?(Array)
111
+ paths.each do |path, value|
112
+ parts = path.to_s.split(/(?<=[^\\])\./)
113
+ node = self
114
+ next_part = false
115
+ until next_part.nil?
116
+ part = next_part || process_bridge_part(parts.shift)
117
+ next_part = process_bridge_part(parts.shift)
118
+ if node.child_exists?(part)
119
+ if next_part.is_a?(Integer)
120
+ next_next = process_bridge_part(parts.first)
121
+ if next_next.is_a?(Integer)
122
+ node[part][next_part] = [] unless node[part][next_part] && node[part][next_part].node_class == Array
123
+ else
124
+ node[part][next_part] = {} unless node[part][next_part] && node[part][next_part].node_class == Hash
125
+ end
126
+ end
127
+ next_part.nil? ? node[part] = value : node = node.child(part)
128
+ else
129
+ if next_part.nil?
130
+ node[part] = value
131
+ else
132
+ node[part] = next_part.is_a?(Integer) ? Array.new(next_part) : {}
133
+ end
134
+ node[part][next_part] = process_bridge_part(parts.first).is_a?(Integer) ? [] : {} if next_part.is_a?(Integer)
135
+ node = node[part]
136
+ end
137
+ end
138
+ end
139
+ self
140
+ end
141
+
142
+ def copy(paths)
143
+ paths.each do |from, to|
144
+ value = find(from).first
145
+ bridge(to => value)
146
+ end
147
+ self
148
+ end
149
+
150
+ def copy_all(paths)
151
+ paths.each do |from, to|
152
+ value = find(from)
153
+ bridge(to => value)
154
+ end
155
+ self
156
+ end
157
+
158
+ def move(paths)
159
+ paths.each do |from, to|
160
+ values = find(from)
161
+ next if values.empty?
162
+ value = values.first
163
+ bridge(to => value)
164
+ value.kill if value
165
+ end
166
+ self
167
+ end
168
+
169
+ def move_all(paths)
170
+ paths.each do |from, to|
171
+ value = find(from)
172
+ bridge(to => value)
173
+ value.map(&:kill)
174
+ end
175
+ self
176
+ end
177
+
178
+ def copy_to(hash, *paths)
179
+ hash = TreeHash.new(hash) unless hash.is_a?(TreeHash)
180
+ paths.each do |path|
181
+ hash.bridge(path => find(path).first)
182
+ end
183
+ hash
184
+ end
185
+
186
+ def move_to(hash, *paths)
187
+ hash = TreeHash.new(hash) unless hash.is_a?(TreeHash)
188
+ paths.each do |path|
189
+ value = find(path).first
190
+ hash.bridge(path => value)
191
+ value.kill if value
192
+ end
193
+ hash
194
+ end
195
+
196
+ def to_tree_hash
197
+ self
198
+ end
199
+
200
+ def process(processor)
201
+
202
+ end
203
+
204
+ def size
205
+ @children.respond_to?(:size) ? @children.size : 1
206
+ end
207
+
208
+ def paths
209
+ case [node_class]
210
+ when [Array], [Hash]
211
+ value.squish.keys
212
+ else
213
+ []
214
+ end
215
+ end
216
+
217
+ def ancestors
218
+ return [] if root?
219
+ return [parent] if parent.root?
220
+ parent.ancestors + [parent]
221
+ end
222
+
223
+ def absolute_path
224
+ anc = ancestors[1..-1]
225
+ (anc.nil? ? [] : anc.map(&:path) + [path]).join('.')
226
+ end
227
+
228
+ def inspect
229
+ value
230
+ end
231
+
232
+ def to_s
233
+ value.to_s
234
+ end
235
+
236
+ def siblings
237
+ parent.children.values.map { |c| c == self ? nil : c }.compact
238
+ end
239
+
240
+ def index
241
+ parent.children.values.index(self)
242
+ end
243
+
244
+ def delete(*paths)
245
+ paths.flat_map do |path|
246
+ find(path).map do |child|
247
+ if child.root?
248
+ delete_child(path)
249
+ else
250
+ child.parent.delete_child(child.key)
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ def kill
257
+ root.delete(absolute_path)
258
+ end
259
+
260
+ def value
261
+ case [node_class]
262
+ when [Hash]
263
+ children.map { |k, v| [k, v.value] }.to_h
264
+ when [Array]
265
+ children.values.map(&:value)
266
+ else
267
+ children
268
+ end
269
+ end
270
+
271
+ def key
272
+ return nil if root?
273
+ case [parent.node_class]
274
+ when [Hash]
275
+ parent.keys[index]
276
+ when [Array]
277
+ index
278
+ else
279
+ nil
280
+ end
281
+ end
282
+
283
+ def path
284
+ parent.node_class == Array ? "[#{key}]" : key.to_s.gsub('.', '\\.')
285
+ end
286
+
287
+ def absolute_paths
288
+ root.paths
289
+ end
290
+
291
+ def leaf_children
292
+ return self unless children?
293
+ children.map do |k, v|
294
+ v.children? ? v.leaf_children : v
295
+ end.flatten
296
+ end
297
+
298
+ def keys
299
+ return [] unless @children.respond_to?(:keys)
300
+ @children.keys
301
+ end
302
+
303
+ def following_siblings(limit = 0)
304
+ siblings[(index + 1)..-(limit.to_i + 1)]
305
+ end
306
+
307
+ def preceeding_siblings(limit = 0)
308
+ limit = limit.to_i
309
+ siblings[(limit.zero? ? limit : (index - limit))..(index - 1)]
310
+ end
311
+
312
+ def sibling(offset)
313
+ siblings[index + offset.to_i]
314
+ end
315
+
316
+ def next_sibling
317
+ sibling(1)
318
+ end
319
+
320
+ def previous_sibling
321
+ sibling(-1)
322
+ end
323
+
324
+ def root
325
+ return self if root?
326
+ ancestors.find(&:root?)
327
+ end
328
+
329
+ def delete_child(key, symbol_sensitive = false)
330
+ case [node_class]
331
+ when [Hash]
332
+ child = symbol_sensitive ? nil : children.delete(key.to_s.to_sym)
333
+ child = children.delete(key) unless child
334
+ when [Array]
335
+ children.delete(key.to_i)
336
+ else
337
+ nil
338
+ end
339
+ end
340
+
341
+ protected
342
+
343
+ def build_from(object)
344
+ @children = {}
345
+ case object
346
+ when Hash
347
+ object.each { |k, v| @children[k] = TreeHash.new(v, self) }
348
+ when Array
349
+ object.each_with_index { |a, i| @children[i] = TreeHash.new(a, self) }
350
+ else
351
+ @children = object
352
+ end
353
+ end
354
+
355
+ def add_child(key, child)
356
+ @children[key] = TreeHash.new(child, self)
357
+ end
358
+
359
+ def process_bridge_part(part)
360
+ return unless part
361
+ part =~ /^\[\d+\]$/ ? part.uncapsulate('[').to_i : part.to_sym
362
+ end
363
+
364
+ end