deep_enumerable 0.1.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/deep_enumerable.rb +530 -0
  3. metadata +45 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7b6da22194de4650dcbf0368c515d833de4de310
4
+ data.tar.gz: 68291b67e7d344d808c1ef3f02afe2f66d8867d2
5
+ SHA512:
6
+ metadata.gz: ee73d1a3c19f0cc237c42a03a3b75b42f89aa2c5f7b8f9831ef568fb04bbe997a66c1585cfc3d9660ef5ef3aa02f76275287e722e949d1aaa8ed12420ce28977
7
+ data.tar.gz: 1469498836041175ea1a32542e84d64cba7c1a879ef8ea7081321dee6c2d3e1c9ee2b5b64fd09d09ec7c46adf724bb492781963d17ff3d00445b6ba073570fb6
@@ -0,0 +1,530 @@
1
+ ##
2
+ # A set of general methods that can be applied to any conformant nested structure
3
+ module DeepEnumerable
4
+ ##
5
+ # Subtracts the leaves of one DeepEnumerable from another.
6
+ #
7
+ # @return a result of the same structure as the primary DeepEnumerable.
8
+ #
9
+ # @example
10
+ # >> alice = {name: "alice", age: 26}
11
+ # >> bob = {name: "bob", age: 26}
12
+ # >> alice.deep_diff(bob)
13
+ # => {:name=>"alice"}
14
+ #
15
+ # >> bob = {friends: ["alice","carol"]}
16
+ # >> carol = {friends: ["alice","bob"]}
17
+ # >> bob.deep_diff(carol)
18
+ # => {:friends=>"carol"}
19
+ #
20
+ def deep_diff(other, &block)
21
+ shallow_keys.each_with_object(empty) do |key, res|
22
+ s_val = (self[key] rescue nil) #TODO don't rely on rescue
23
+ o_val = (other[key] rescue nil)
24
+
25
+ comparator = block || :==.to_proc
26
+
27
+ if s_val.respond_to?(:deep_diff) && o_val.respond_to?(:deep_diff)
28
+ diff = s_val.deep_diff(o_val, &block)
29
+ res[key] = diff if diff.any?
30
+ elsif !comparator.call(s_val, o_val)
31
+ res[key] = s_val
32
+ end
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Computes the compliment of the intersection of two DeepEnumerables.
38
+ #
39
+ # @return The common structure of both arguments, with tuples containing differing values in the leaf nodes.
40
+ #
41
+ # @example
42
+ # >> alice = {:name=>"alice", :age=>26}
43
+ # >> bob = {:name=>"bob", :age=>26}
44
+ # >> alice.deep_diff(bob)
45
+ # => {:name=>["alice", "bob"]}
46
+ #
47
+ # >> bob = {:friends=>["alice","carol"]}
48
+ # >> carol = {:friends=>["alice","bob"]}
49
+ # >> bob.deep_diff_symmetric(carol)
50
+ # => {:friends=>{1=>["carol", "bob"]}}
51
+ #
52
+ def deep_diff_symmetric(other, &block)
53
+ (shallow_keys + other.shallow_keys).each_with_object({}) do |key, res|
54
+ s_val = (self[key] rescue nil) #TODO don't rely on rescue
55
+ o_val = (other[key] rescue nil)
56
+
57
+ comparator = block || :==.to_proc
58
+
59
+ if s_val.respond_to?(:deep_diff_symmetric) && o_val.respond_to?(:deep_diff_symmetric)
60
+ diff = s_val.deep_diff_symmetric(o_val, &block)
61
+ res[key] = diff if diff.any?
62
+ elsif !comparator.call(s_val, o_val)
63
+ res[key] = [s_val, o_val]
64
+ end
65
+ end
66
+ end
67
+ alias_method :deep_outersect, :deep_diff_symmetric
68
+
69
+ ##
70
+ # Deeply copy a DeepEnumerable
71
+ #
72
+ # @return the same data structure at a different memory address
73
+ def deep_dup
74
+ deep_select{true}
75
+ end
76
+
77
+ ##
78
+ # Iterate elements of a DeepEnumerable
79
+ #
80
+ # @example
81
+ # >> {event: {id: 1, title: 'bowling'}}.deep_each.to_a
82
+ # => [[{:event=>:id}, 1], [{:event=>:title}, "bowling"]]
83
+ #
84
+ # >> [:a, [:b, :c]].deep_each.to_a
85
+ # => [[0, :a], [{1=>0}, :b], [{1=>1}, :c]]
86
+ #
87
+ # >> {events: [{title: 'movie'}, {title: 'dinner'}]}.deep_each.to_a
88
+ # => [[{:events=>{0=>:title}}, "movie"], [{:events=>{1=>:title}}, "dinner"]]
89
+ #
90
+ # @return an iterator over each deep-key/value pair for every leaf
91
+ def deep_each(&block)
92
+ depth_first_map.each(&block)
93
+ end
94
+
95
+ ##
96
+ # Concatenate all the results from the supplied code block together.
97
+ #
98
+ # @return an array with the results of running +block+ once for every leaf element in the original structure, all flattened together.
99
+ #
100
+ # @example
101
+ # >> {a: {b: 1, c: {d: 2, e: 3}, f: 4}, g: 5}.deep_flat_map{|k,v| v*2}
102
+ # => [2, 4, 6, 8, 10]
103
+ #
104
+ # >> {a: {b: 1, c: {d: 2, e: 3}, f: 4}, g: 5}.deep_flat_map{|k,v| [v, v*2]}
105
+ # => [1, 2, 2, 4, 3, 6, 4, 8, 5, 10]
106
+ def deep_flat_map(&block)
107
+ deep_each.flat_map(&block)
108
+ end
109
+
110
+ ##
111
+ # Retrieve a nested element from a DeepEnumerable
112
+ #
113
+ # @example
114
+ #
115
+ # >> prefix_tree = {"a"=>{"a"=>"aardvark", "b"=>["abacus", "abadon"], "c"=>"actuary"}}
116
+ #
117
+ # >> prefix_tree.deep_get("a")
118
+ # => {"a"=>"aardvark", "b"=>["abacus", "abadon"], "c"=>"actuary"}
119
+ #
120
+ # >> prefix_tree.deep_get("a"=>"b")
121
+ # => ["abacus", "abadon"]
122
+ #
123
+ # @return a DeepEnumerable representing the subtree specified by the query key
124
+ #
125
+ def deep_get(key)
126
+ if nested_key?(key)
127
+ key_head, key_tail = split_key(key)
128
+ if self[key_head].respond_to?(:deep_get)
129
+ self[key_head].deep_get(key_tail)
130
+ else
131
+ nil #SHOULD? raise an error
132
+ end
133
+ else
134
+ self[key]
135
+ end
136
+ end
137
+
138
+ ##
139
+ # Fold over all leaf nodes
140
+ #
141
+ # @example
142
+ # >> friends = [{name: 'alice', age: 26}, {name: 'bob', age: 26}]
143
+ # >> friends.deep_inject(Hash.new{[]}) {|sum, (k, v)| sum[k.values.first] <<= v; sum}
144
+ # => {:name=>["alice", "bob"], :age=>[26, 26]}
145
+ #
146
+ # @return The accumulation of the results of executing the provided block over every element in the DeepEnumerable
147
+ def deep_inject(initial, &block)
148
+ deep_each.inject(initial, &block)
149
+ end
150
+
151
+ ##
152
+ # Describes the similarities between two DeepEnumerables.
153
+ #
154
+ # @example
155
+ # >> alice = {:name=>"alice", :age=>26}
156
+ # >> bob = {:name=>"bob", :age=>26}
157
+ # >> alice.deep_intersect(bob)
158
+ # => {:age=>26}
159
+ #
160
+ # >> bob = {:friends=>["alice","carol"]}
161
+ # >> carol = {:friends=>["alice","bob"]}
162
+ # >> bob.deep_intersect(carol)
163
+ # => {:friends=>["alice"]}
164
+ #
165
+ # @return a result of the same structure as the primary DeepEnumerable.
166
+ #
167
+ def deep_intersect(other, &block)
168
+ (shallow_keys + other.shallow_keys).each_with_object(empty) do |key, res|
169
+ s_val = (self[key] rescue nil) #TODO don't rely on rescue
170
+ o_val = (other[key] rescue nil)
171
+
172
+ comparator = block || :==.to_proc
173
+
174
+ if s_val.respond_to?(:deep_intersect) && o_val.respond_to?(:deep_intersect)
175
+ int = s_val.deep_intersect(o_val, &block)
176
+ res[key] = int if int.any?
177
+ elsif comparator.call(s_val, o_val)
178
+ res[key] = s_val
179
+ end
180
+ end
181
+ end
182
+
183
+ ##
184
+ # Returns the result of running block on each leaf of this DeepEnumerable
185
+ #
186
+ # @example
187
+ # >> h = {a: [1, 2]}
188
+ # >> h.deep_map!{|k, v| [k, v]}
189
+ # >> h
190
+ # => {:a=>[[{:a=>0}, 1], [{:a=>1}, 2]]}
191
+ #
192
+ # @return The original structure updated by the result of the block
193
+ def deep_map!(&block)
194
+ if block_given?
195
+ deep_each{|k,v| deep_set(k, block.call([k, v]))}
196
+ self
197
+ else
198
+ deep_each
199
+ end
200
+ end
201
+
202
+ ##
203
+ # Create a new nested structure populated by the result of executing +block+ on the deep-keys and values of the original DeepEnumerable
204
+ #
205
+ # @example
206
+ # >> {a: [1, 2]}.deep_map{|k, v| [k, v]}
207
+ # => {:a=>[[{:a=>0}, 1], [{:a=>1}, 2]]}
208
+ #
209
+ # @return A copy of the input, updated by the result of the block
210
+ def deep_map(&block)
211
+ deep_dup.deep_map!(&block)
212
+ end
213
+
214
+ ##
215
+ # Modifies this collection to use the result of +block+ as the values
216
+ #
217
+ # @example
218
+ # >> h = {a: [1, 2]}
219
+ # >> h.deep_map_values!{v| v*2}
220
+ # >> h
221
+ # => {:a=>[2, 4]}
222
+ #
223
+ # @return The original structure updated by the result of the block
224
+ def deep_map_values!(&block)
225
+ deep_map!{|_, v| block.call(v)}
226
+ end
227
+
228
+ ##
229
+ # Creates a new nested structure populated by the result of executing +block+ on the values of the original DeepEnumerable
230
+ #
231
+ # @example
232
+ # >> {a: [1, 2].deep_map_values{v| v*2}
233
+ # => {:a=>[2, 4]}
234
+ #
235
+ # @return A copy of the input, updated by the result of the block
236
+ def deep_map_values(&block)
237
+ deep_dup.deep_map_values!(&block)
238
+ end
239
+
240
+ ##
241
+ # Filter leaf nodes by the result of the given block
242
+ #
243
+ # @example
244
+ # >> inventory = {fruit: {apples: 4, oranges: 7}}
245
+ #
246
+ # >> inventory.deep_reject{|k, v| v > 5}
247
+ # => {:fruit=>{:apples=>4}}
248
+ #
249
+ # >> inventory.deep_reject(&:even?)
250
+ # => {:fruit=>{:oranges=>7}}
251
+ #
252
+ # @return a copy of the input, filtered by the given predicate
253
+ #
254
+ def deep_reject(&block)
255
+ new_block =
256
+ case block.arity
257
+ when 2 then ->(k,v){!block.call(k, v)}
258
+ else ->(v){ !block.call(v)}
259
+ end
260
+ deep_select(&new_block)
261
+ end
262
+
263
+ ##
264
+ # Filter leaf nodes by the result of the given block
265
+ #
266
+ # @example
267
+ # >> inventory = {fruit: {apples: 4, oranges: 7}}
268
+ #
269
+ # >> inventory.deep_select{|k, v| v > 5}
270
+ # => {:fruit=>{:oranges=>7}}
271
+ #
272
+ # >> inventory.deep_select(&:even?)
273
+ # => {:fruit=>{:apples=>4}}
274
+ #
275
+ # @return a copy of the input, filtered by the given predicate
276
+ def deep_select(&block)
277
+ copy = self.select{false} # get an empty version of this shallow collection
278
+
279
+ # insert/push a selected item into the copied enumerable
280
+ accept = lambda do |k, v|
281
+ # Don't insert elements at arbitrary positions in an array if appending is an option
282
+ if copy.respond_to?('push') # jruby has a Hash#<< method
283
+ copy.push(v)
284
+ else
285
+ copy[k] = v
286
+ end
287
+ end
288
+
289
+ shallow_each do |k, v|
290
+ if v.respond_to?(:deep_select)
291
+ selected = v.deep_select(&block)
292
+ accept.call(k, selected)
293
+ else
294
+ res =
295
+ case block.arity
296
+ when 2 then block.call(k, v)
297
+ else block.call(v)
298
+ end
299
+
300
+ if res
301
+ accept.call(k, (v.dup rescue v)) # FixNum's and Symbol's can't/shouldn't be dup'd
302
+ end
303
+ end
304
+ end
305
+
306
+ copy
307
+ end
308
+
309
+ ##
310
+ # Update a DeepEnumerable, indexed by a deep-key
311
+ # Intermediate values are created when necessary, with the same type as its parent.
312
+ #
313
+ # @example
314
+ # >> [].deep_set({1 => 2}, 3)
315
+ # => [nil, [nil, nil, 3]]
316
+ # >> {}.deep_set({1 => 2}, 3)
317
+ # => {1=>{2=>3}}
318
+ #
319
+ # @return (tentative) returns the object that's been modified. Warning: This behavior is subject to change.
320
+ #
321
+ def deep_set(key, val)
322
+ if nested_key?(key)
323
+ key_head, key_tail = split_key(key)
324
+ if self[key_head].respond_to?(:deep_set)
325
+ self[key_head].deep_set(key_tail, val)
326
+ self
327
+ else
328
+ self[key_head] = empty.deep_set(key_tail, val)
329
+ self
330
+ end
331
+ else
332
+ self[key] = val
333
+ self #SHOULD? return val instead of self
334
+ end
335
+ end
336
+
337
+ ##
338
+ # List the values stored at every leaf
339
+ #
340
+ # @example
341
+ # >> prefix_tree = {"a"=>{"a"=>"aardvark", "b"=>["abacus", "abadon"], "c"=>"actuary"}}
342
+ # >> prefix_tree.deep_values
343
+ # => ["aardvark", "abacus", "abadon", "actuary"]
344
+ #
345
+ # @return a list of every leaf value
346
+ def deep_values
347
+ deep_flat_map{|_, v| v}
348
+ end
349
+
350
+ ##
351
+ # Combine two DeepEnumerables into one, with the elements from each joined into tuples
352
+ #
353
+ # @example
354
+ # >> inventory = {fruit: {apples: 4, oranges: 7}}
355
+ # >> prices = {fruit: {apples: 0.79, oranges: 1.21}}
356
+ # >> inventory.deep_zip(prices)
357
+ # => {:fruit=>{:apples=>[4, 0.79], :oranges=>[7, 1.21]}}
358
+ #
359
+ # @return one data structure with elements from both arguments joined together
360
+ #
361
+ def deep_zip(other)
362
+ (shallow_keys).inject(empty) do |res, key|
363
+ s_val = self[key]
364
+ o_val = (other[key] rescue nil) #TODO don't rely on rescue
365
+
366
+ comparator = :==.to_proc
367
+
368
+ if s_val.respond_to?(:deep_zip) && o_val.respond_to?(:deep_zip)
369
+ diff = s_val.deep_zip(o_val)
370
+ diff.empty? ? res : res.deep_set(key, diff)
371
+ else
372
+ res.deep_set(key, [s_val, o_val])
373
+ end
374
+ end
375
+ end
376
+
377
+ ##
378
+ # A copy of the DeepEnumerable containing no elements
379
+ #
380
+ # @example
381
+ # >> inventory = {fruit: {apples: 4, oranges: 7}}
382
+ # >> inventory.empty
383
+ # => {}
384
+ #
385
+ # @return a new object of the same type as the original collection, only empty
386
+ #
387
+ def empty
388
+ select{false}
389
+ end
390
+
391
+ # Provide a homogenous |k,v| iterator for Arrays/Hashes/DeepEnumerables
392
+ #TODO test this
393
+ def shallow_key_value_pairs
394
+ shallow_keys.map{|k| [k, self[k]]}
395
+ end
396
+
397
+ ##
398
+ # Replaces every top-level element with the result of the given block
399
+ def shallow_map_keys!(&block)
400
+ new_kvs = shallow_key_value_pairs.map do |k, v|
401
+ new_key =
402
+ if block.arity == 2
403
+ block.call(k, v)
404
+ else
405
+ block.call(k)
406
+ end
407
+
408
+ self.delete(k) #TODO This is not defined on Enumerable!
409
+ [new_key, v]
410
+ end
411
+
412
+ new_kvs.each do |k, v|
413
+ self[k] = v
414
+ end
415
+
416
+ self
417
+ end
418
+
419
+ ##
420
+ # Returns a new collection where every top-level element is replaced with the result of the given block
421
+ def shallow_map_keys(&block)
422
+ deep_dup.shallow_map_keys!(&block)
423
+ end
424
+
425
+ ##
426
+ # Replaces every top-level element with the result of the given block
427
+ def shallow_map_values!(&block)
428
+ shallow_key_value_pairs.each do |k, v|
429
+ self[k] =
430
+ if block.arity == 2
431
+ block.call(k, v)
432
+ else
433
+ block.call(v)
434
+ end
435
+ end
436
+
437
+ self
438
+ end
439
+
440
+ ##
441
+ # Returns a new collection where every top-level element is replaced with the result of the given block
442
+ def shallow_map_values(&block)
443
+ deep_dup.shallow_map_values!(&block)
444
+ end
445
+
446
+ ##
447
+ # The primary iterator of a DeepEnumerable
448
+ # If this method is implemented DeepEnumerable can construct every other method in terms of shallow_each.
449
+ #TODO test this
450
+ def shallow_each(&block)
451
+ shallow_key_value_pairs.each(&block)
452
+ end
453
+
454
+ # This method is commented out because redefining '.to_a' on Array, for example,
455
+ # seems like a terrible idea
456
+ #def to_a
457
+ # deep_each.to_a
458
+ #end
459
+
460
+ protected
461
+
462
+ #def shallow_get(x) # this should technically be defined in Hash/Array individually
463
+ # self[x]
464
+ #end
465
+
466
+ def depth_first_map(ancestry=[])
467
+ shallow_each.flat_map do |key, val|
468
+ full_ancestry = ancestry + [key]
469
+ full_key = deep_key_from_array(full_ancestry) #TODO this is an n^2 operation
470
+
471
+ if val.respond_to?(:depth_first_map, true) # Search protected methods as well
472
+ val.depth_first_map(full_ancestry)
473
+ else
474
+ [[full_key, val]]
475
+ end
476
+ end
477
+ end
478
+
479
+ # Everything below should be a class method, but Ruby method visibility is a nightmare
480
+ def deep_key_from_array(array)
481
+ if array.size > 1
482
+ {array.first => deep_key_from_array(array.drop(1))}
483
+ else
484
+ array.first
485
+ end
486
+ end
487
+
488
+ def nested_key?(key)
489
+ key.is_a?(Hash)
490
+ end
491
+
492
+ # Disassembles a key into its head and tail elements
493
+ #
494
+ # for example: {a: {0 => :a}} goes to [:a, {0 => :a}]
495
+ def split_key(key)
496
+ case key
497
+ when Hash then
498
+ key_head = key.keys.first
499
+ key_tail = key[key_head]
500
+ [key_head, key_tail]
501
+ when nil then [nil, nil]
502
+ else [key, nil]
503
+ end
504
+ end
505
+
506
+ # Get the lowest-level key
507
+ #
508
+ # for example: {a: {b: :c}} goes to :c
509
+ def self.leaf_key(key)
510
+ key.is_a?(Hash) ? leaf_key(key) : key
511
+ end
512
+ end
513
+
514
+ ##
515
+ # This class implements the necessary methods to qualify Hash as a DeepEnumerable
516
+ class Hash
517
+ include DeepEnumerable
518
+
519
+ alias_method :shallow_keys, :keys
520
+ end
521
+
522
+ ##
523
+ # This class implements the necessary methods to qualify Array as a DeepEnumerable
524
+ class Array
525
+ include DeepEnumerable
526
+
527
+ def shallow_keys
528
+ (0...size).to_a
529
+ end
530
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: deep_enumerable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dan Gopstein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-08 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A port of Enumerable to deeply nested enumerables
14
+ email: dan@gopstein.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/deep_enumerable.rb
20
+ homepage: https://github.com/dgopstein/deep_enumerable
21
+ licenses:
22
+ - MIT
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubyforge_project:
40
+ rubygems_version: 2.4.5
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: A library for manipulating nested collections
44
+ test_files: []
45
+ has_rdoc: