deepmap 0.2.4 → 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: b4cf7c264a6edf69602a9ae213c0f039b4c819160c4661bdfe376512f58a971b
4
- data.tar.gz: d321c60e683abeb46d3d72aef3511df43fe243e4664ddbc5dc5ddfbbacfa8d7e
3
+ metadata.gz: 240ee3a7cd29cd15d11424600fc2940cfbcc7983fefe6ae01be3c22fc8683a96
4
+ data.tar.gz: 52145a92013910d58b1130b1f6a18a68562db6d4c833ec8d800d3ccaaca1a403
5
5
  SHA512:
6
- metadata.gz: 90caafddebe9ce0e01df2524c7485dbf9071de1c9224cf3b9422a6a510c3364c0a5a94315c235d5f2d699e429df934b83adbd8419ae24bbc99d3e640919a100f
7
- data.tar.gz: c09f6ed63e01037c3433dced30f3ad5d447cd6b72018526b9bfb149571cbec26459a43385a4931d68fca6488c071051c72b1d662038d71184dfdda5a970efc90
6
+ metadata.gz: d850a9b0a49e7a19734c8ab7f94b074a01eb3216089d72a9c2f2911a4abacf545ecc4f8bcdc155df6c0c2ec7a24c4684e757b64ca4063f6b0596efe534a890d2
7
+ data.tar.gz: 7702ed7f53b51a6030712286d870491feafdb9544aa40f35442aa07324c0b0eea2e06a4cba8dd7dd34259fb045ba450d81084f362fb00408d90ecb7f20fde928
@@ -1,19 +1,30 @@
1
- # Creating deep_map, key_map, val_map methods in DeepMap.
1
+ # frozen_string_literal: true
2
2
 
3
+ # Creating deep_map, key_map, val_map methods in DeepMap.
3
4
  module DeepMap
4
- def deep_map
5
- return self if !block_given?
6
- recurse { |x| yield x }
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
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))
7
16
  end
8
17
 
9
- def key_map
10
- return self if !block_given?
11
- recurse("key") { |x| yield x }
18
+ def key_map(on_collision: :warn, &block)
19
+ return self unless block
20
+
21
+ recurse(self, t: 'key', on_collision: collision_mode(on_collision), &pairable(block))
12
22
  end
13
23
 
14
- def val_map
15
- return self if !block_given?
16
- recurse("val") { |x| yield x }
24
+ def val_map(&block)
25
+ return self unless block
26
+
27
+ recurse(self, t: 'val', &pairable(block))
17
28
  end
18
29
 
19
30
  private
@@ -24,32 +35,59 @@ module DeepMap
24
35
  # the key is a hash, than collect the results of calling it recursively. If
25
36
  # the key is an array, then map the recursive method over each item in the
26
37
  # array. If it is niether a hash or an array, then apply the block to that
27
- # 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.
28
40
 
29
- def recurse(t = "both", h = self)
30
- if h.is_a?(Hash)
31
- hash_collect(t, h) { |x| yield x }
41
+ def recurse(h, k = nil, t:, on_collision: :warn, &block)
42
+ case h
43
+ when Hash
44
+ hash_collect(h, t:, on_collision:, &block)
32
45
 
33
- elsif h.is_a?(Array)
34
- h.map { |v| recurse(t, v) { |x| yield x } }
46
+ when Array
47
+ h.map { |v| recurse(v, k, t:, on_collision:, &block) }
35
48
 
36
49
  else # apply to value
37
- t == "key" ? h : yield(h)
50
+ t == 'key' ? h : yield(h, k)
51
+
52
+ end
53
+ end
38
54
 
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)]
39
61
  end
62
+
63
+ result = pairs.to_h
64
+ report_collisions(pairs, on_collision) if result.size < pairs.size
65
+ result
66
+ end
67
+
68
+ def report_collisions(pairs, on_collision)
69
+ return if on_collision == :ignore
70
+
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
40
77
  end
41
78
 
42
- def hash_collect(t, h)
43
- Hash[h.collect { |k, v|
44
- case t # type
45
- when "both", "key"
46
- [yield(k), recurse(t, v) { |x| yield x }]
79
+ def collision_mode(mode)
80
+ return mode if COLLISION_MODES.include?(mode)
47
81
 
48
- when "val"
49
- [k, recurse(t, v) { |x| yield x }]
82
+ raise ArgumentError, "on_collision must be one of #{COLLISION_MODES.inspect}, got #{mode.inspect}"
83
+ end
50
84
 
51
- end
52
- }]
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
53
91
  end
54
92
  end
55
93
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepMap
4
+ VERSION = '1.1.0'
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:
@@ -72,16 +111,10 @@ try running `gem install bundler`. Once that is done:
72
111
  Now, we should be able to build the gem locally. This will build the local
73
112
  deepmap gem and link it in your path, so you can playing around with `deepmap`.
74
113
 
75
- bundle exec rake build
76
-
77
- This uses `rspec` and `rake` to run a suite of unit tests. To run the suite:
78
-
79
- bundle exec rake
114
+ just build
80
115
 
81
- ## todo
116
+ This uses `rspec`, `rubocop`, and `simplecov` to run and check a suite of unit
117
+ tests. To run the full suite:
82
118
 
83
- * collison checking/warning/confirmation workflow
84
- * support key-val iteration in mapping over vals/keys. This would allow support
85
- for mapping conditionally based on a key/value, even if you are iterating
86
- over all values/keys (`key_map {|k, v| k > 0 ? k : v }`).
119
+ just ci
87
120
 
metadata CHANGED
@@ -1,75 +1,62 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deepmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Warner
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-03-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: rspec
13
+ name: colored
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '3.8'
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 3.8.0
18
+ version: '1.2'
23
19
  type: :development
24
20
  prerelease: false
25
21
  version_requirements: !ruby/object:Gem::Requirement
26
22
  requirements:
27
23
  - - "~>"
28
24
  - !ruby/object:Gem::Version
29
- version: '3.8'
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 3.8.0
25
+ version: '1.2'
33
26
  - !ruby/object:Gem::Dependency
34
- name: rake
27
+ name: rspec
35
28
  requirement: !ruby/object:Gem::Requirement
36
29
  requirements:
37
30
  - - "~>"
38
31
  - !ruby/object:Gem::Version
39
- version: '12.3'
32
+ version: '3.8'
40
33
  - - ">="
41
34
  - !ruby/object:Gem::Version
42
- version: 12.3.3
35
+ version: 3.8.0
43
36
  type: :development
44
37
  prerelease: false
45
38
  version_requirements: !ruby/object:Gem::Requirement
46
39
  requirements:
47
40
  - - "~>"
48
41
  - !ruby/object:Gem::Version
49
- version: '12.3'
42
+ version: '3.8'
50
43
  - - ">="
51
44
  - !ruby/object:Gem::Version
52
- version: 12.3.3
45
+ version: 3.8.0
53
46
  - !ruby/object:Gem::Dependency
54
- name: rufo
47
+ name: simplecov
55
48
  requirement: !ruby/object:Gem::Requirement
56
49
  requirements:
57
50
  - - "~>"
58
51
  - !ruby/object:Gem::Version
59
- version: '0.15'
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: 0.15.1
52
+ version: 0.22.0
63
53
  type: :development
64
54
  prerelease: false
65
55
  version_requirements: !ruby/object:Gem::Requirement
66
56
  requirements:
67
57
  - - "~>"
68
58
  - !ruby/object:Gem::Version
69
- version: '0.15'
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: 0.15.1
59
+ version: 0.22.0
73
60
  description: Map functions over nested hash/arrays objects (e.g., YAML, JSON).
74
61
  email: jeremywrnr@gmail.com
75
62
  executables: []
@@ -77,12 +64,12 @@ extensions: []
77
64
  extra_rdoc_files: []
78
65
  files:
79
66
  - lib/deepmap/deepmap.rb
67
+ - lib/deepmap/version.rb
80
68
  - readme.md
81
69
  homepage: http://github.com/jeremywrnr/deepmap
82
70
  licenses:
83
71
  - MIT
84
72
  metadata: {}
85
- post_install_message:
86
73
  rdoc_options: []
87
74
  require_paths:
88
75
  - lib/deepmap
@@ -90,15 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
77
  requirements:
91
78
  - - ">="
92
79
  - !ruby/object:Gem::Version
93
- version: '0'
80
+ version: '3.0'
94
81
  required_rubygems_version: !ruby/object:Gem::Requirement
95
82
  requirements:
96
83
  - - ">="
97
84
  - !ruby/object:Gem::Version
98
85
  version: '0'
99
86
  requirements: []
100
- rubygems_version: 3.3.7
101
- signing_key:
87
+ rubygems_version: 4.0.10
102
88
  specification_version: 4
103
89
  summary: Nested hash/array function mapping.
104
90
  test_files: []