deepmap 1.0.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e83602369521a1799962c9cba500b4f4a5c0d16a16555a7738c5de3687c5b7c5
4
- data.tar.gz: a58bdf656de275c1957c6e9a42fbc4399f1480e84129f112c1cb554f8a407823
3
+ metadata.gz: 240ee3a7cd29cd15d11424600fc2940cfbcc7983fefe6ae01be3c22fc8683a96
4
+ data.tar.gz: 52145a92013910d58b1130b1f6a18a68562db6d4c833ec8d800d3ccaaca1a403
5
5
  SHA512:
6
- metadata.gz: 4f6c34154cef76baca26ae3bd24bda7badc8063e865ac190bba8340c42757914dffc2aec4bd78b84132910a466131854641101576bfba8d08a648f7f474519ae
7
- data.tar.gz: 91b53fcda525ab6e2cc6915de7964471d6a16e8b1e0c6b3a9a46ce57dc7ec82daeb731a19ea42b4e8517dbf21dd7f7e41bfe66a4e0eb29a04470663572556318
6
+ metadata.gz: d850a9b0a49e7a19734c8ab7f94b074a01eb3216089d72a9c2f2911a4abacf545ecc4f8bcdc155df6c0c2ec7a24c4684e757b64ca4063f6b0596efe534a890d2
7
+ data.tar.gz: 7702ed7f53b51a6030712286d870491feafdb9544aa40f35442aa07324c0b0eea2e06a4cba8dd7dd34259fb045ba450d81084f362fb00408d90ecb7f20fde928
@@ -2,22 +2,29 @@
2
2
 
3
3
  # Creating deep_map, key_map, val_map methods in DeepMap.
4
4
  module DeepMap
5
- def deep_map(&)
6
- return self unless block_given?
5
+ # Raised when on_collision: :raise is set and a key transform produces
6
+ # duplicate keys within the same hash level.
7
+ class KeyCollisionError < StandardError; end
7
8
 
8
- recurse(&)
9
+ # Valid on_collision modes for deep_map and key_map.
10
+ COLLISION_MODES = %i[warn raise ignore].freeze
11
+
12
+ def deep_map(on_collision: :warn, &block)
13
+ return self unless block
14
+
15
+ recurse(self, t: 'both', on_collision: collision_mode(on_collision), &pairable(block))
9
16
  end
10
17
 
11
- def key_map(&)
12
- return self unless block_given?
18
+ def key_map(on_collision: :warn, &block)
19
+ return self unless block
13
20
 
14
- recurse('key', &)
21
+ recurse(self, t: 'key', on_collision: collision_mode(on_collision), &pairable(block))
15
22
  end
16
23
 
17
- def val_map(&)
18
- return self unless block_given?
24
+ def val_map(&block)
25
+ return self unless block
19
26
 
20
- recurse('val', &)
27
+ recurse(self, t: 'val', &pairable(block))
21
28
  end
22
29
 
23
30
  private
@@ -28,33 +35,59 @@ module DeepMap
28
35
  # the key is a hash, than collect the results of calling it recursively. If
29
36
  # the key is an array, then map the recursive method over each item in the
30
37
  # array. If it is niether a hash or an array, then apply the block to that
31
- # item, and return the result.
38
+ # item, and return the result. 'k' carries the nearest ancestor hash key down
39
+ # through arrays/leaves so value blocks can optionally see it too.
32
40
 
33
- def recurse(t = 'both', h = self, &block)
41
+ def recurse(h, k = nil, t:, on_collision: :warn, &block)
34
42
  case h
35
43
  when Hash
36
- hash_collect(t, h, &block)
44
+ hash_collect(h, t:, on_collision:, &block)
37
45
 
38
46
  when Array
39
- h.map { |v| recurse(t, v, &block) }
47
+ h.map { |v| recurse(v, k, t:, on_collision:, &block) }
40
48
 
41
49
  else # apply to value
42
- t == 'key' ? h : yield(h)
50
+ t == 'key' ? h : yield(h, k)
43
51
 
44
52
  end
45
53
  end
46
54
 
47
- def hash_collect(t, h, &block)
48
- h.to_h do |k, v|
49
- case t # type
50
- when 'both', 'key'
51
- [yield(k), recurse(t, v, &block)]
55
+ # Transformed keys that collide collapse in to_h (last pair wins), which is
56
+ # detectable as the result shrinking. Val mode never transforms keys, so it
57
+ # can never shrink and needs no special casing here.
58
+ def hash_collect(h, t:, on_collision:, &block)
59
+ pairs = h.map do |k, v|
60
+ [t == 'val' ? k : yield(k, v), recurse(v, k, t:, on_collision:, &block)]
61
+ end
62
+
63
+ result = pairs.to_h
64
+ report_collisions(pairs, on_collision) if result.size < pairs.size
65
+ result
66
+ end
52
67
 
53
- when 'val'
54
- [k, recurse(t, v, &block)]
68
+ def report_collisions(pairs, on_collision)
69
+ return if on_collision == :ignore
55
70
 
56
- end
57
- end
71
+ dupes = pairs.map(&:first).tally.select { |_, count| count > 1 }.keys
72
+ message = "deepmap: key collision(s) produced duplicate key(s) #{dupes.inspect}; " \
73
+ 'earlier entries will be overwritten'
74
+ raise KeyCollisionError, message if on_collision == :raise
75
+
76
+ warn message
77
+ end
78
+
79
+ def collision_mode(mode)
80
+ return mode if COLLISION_MODES.include?(mode)
81
+
82
+ raise ArgumentError, "on_collision must be one of #{COLLISION_MODES.inspect}, got #{mode.inspect}"
83
+ end
84
+
85
+ # Blocks are yielded two arguments (the mapped item plus its counterpart).
86
+ # Plain blocks that mention one argument just discard the second, but strict
87
+ # arity callables (&:method, method references, lambdas) would not -- wrap
88
+ # those so they keep receiving the single argument they expect.
89
+ def pairable(block)
90
+ block.lambda? && block.arity != 2 ? proc { |a| block.call(a) } : block
58
91
  end
59
92
  end
60
93
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepMap
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
data/readme.md CHANGED
@@ -57,6 +57,45 @@ irb(main):005:0> test.val_map(&:upcase)
57
57
  => {"a"=>"B", "c"=>["D", "E"], "f"=>{"g"=>["H", "I", {"j"=>"K"}]}}
58
58
  ```
59
59
 
60
+ Blocks may also take a second argument to make decisions based on both the key
61
+ and the value, even while nominally mapping over just one of them. For
62
+ `key_map`, the first argument is the key and the second is the value; for
63
+ `val_map` it's the other way around, so single-argument blocks (including the
64
+ `&:method` shortcut) keep working exactly as before:
65
+
66
+ ```ruby
67
+ irb(main):002:0> test = { 1 => 'a', -2 => 'b', 3 => 'c' }
68
+ => {1=>"a", -2=>"b", 3=>"c"}
69
+
70
+ irb(main):003:0> test.key_map {|k, v| k.positive? ? k * 10 : v }
71
+ => {10=>"a", "b"=>"b", 30=>"c"}
72
+
73
+ irb(main):004:0> test.val_map {|v, k| k.positive? ? v.upcase : v }
74
+ => {1=>"A", -2=>"b", 3=>"C"}
75
+ ```
76
+
77
+ ## key collisions
78
+
79
+ Since `key_map` and `deep_map` can map two different keys to the same new key,
80
+ it's possible for the resulting hash to silently drop an entry (the last pair
81
+ wins). By default this is checked for and reported with a warning; pass
82
+ `on_collision:` to `key_map`/`deep_map` to change that behavior:
83
+
84
+ ```ruby
85
+ irb(main):002:0> { 1 => 'a', 2 => 'b' }.key_map {|k| 0 }
86
+ deepmap: key collision(s) produced duplicate key(s) [0]; earlier entries will be overwritten
87
+ => {0=>"b"}
88
+
89
+ irb(main):003:0> { 1 => 'a', 2 => 'b' }.key_map(on_collision: :raise) {|k| 0 }
90
+ DeepMap::KeyCollisionError: deepmap: key collision(s) produced duplicate key(s) [0]; earlier entries will be overwritten
91
+
92
+ irb(main):004:0> { 1 => 'a', 2 => 'b' }.key_map(on_collision: :ignore) {|k| 0 }
93
+ => {0=>"b"}
94
+ ```
95
+
96
+ `val_map` never transforms keys, so it has no `on_collision` option. Passing
97
+ anything other than `:warn`, `:raise`, or `:ignore` raises an `ArgumentError`.
98
+
60
99
  ## development / testing
61
100
 
62
101
  First, clone this repo:
@@ -79,10 +118,3 @@ tests. To run the full suite:
79
118
 
80
119
  just ci
81
120
 
82
- ## todo
83
-
84
- * collison checking/warning/confirmation workflow
85
- * support key-val iteration in mapping over vals/keys. This would allow support
86
- for mapping conditionally based on a key/value, even if you are iterating
87
- over all values/keys (`key_map {|k, v| k > 0 ? k : v }`).
88
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deepmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Warner