deep_enumerable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: