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 +4 -4
- data/lib/deepmap/deepmap.rb +56 -23
- data/lib/deepmap/version.rb +1 -1
- data/readme.md +39 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 240ee3a7cd29cd15d11424600fc2940cfbcc7983fefe6ae01be3c22fc8683a96
|
|
4
|
+
data.tar.gz: 52145a92013910d58b1130b1f6a18a68562db6d4c833ec8d800d3ccaaca1a403
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d850a9b0a49e7a19734c8ab7f94b074a01eb3216089d72a9c2f2911a4abacf545ecc4f8bcdc155df6c0c2ec7a24c4684e757b64ca4063f6b0596efe534a890d2
|
|
7
|
+
data.tar.gz: 7702ed7f53b51a6030712286d870491feafdb9544aa40f35442aa07324c0b0eea2e06a4cba8dd7dd34259fb045ba450d81084f362fb00408d90ecb7f20fde928
|
data/lib/deepmap/deepmap.rb
CHANGED
|
@@ -2,22 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
# Creating deep_map, key_map, val_map methods in DeepMap.
|
|
4
4
|
module DeepMap
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
41
|
+
def recurse(h, k = nil, t:, on_collision: :warn, &block)
|
|
34
42
|
case h
|
|
35
43
|
when Hash
|
|
36
|
-
hash_collect(
|
|
44
|
+
hash_collect(h, t:, on_collision:, &block)
|
|
37
45
|
|
|
38
46
|
when Array
|
|
39
|
-
h.map { |v| recurse(
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
68
|
+
def report_collisions(pairs, on_collision)
|
|
69
|
+
return if on_collision == :ignore
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
data/lib/deepmap/version.rb
CHANGED
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
|
-
|