deeply_enumerable 1.0.0 → 2.0.1

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: 8f9655518ad0bb3d8c2d3eb6616c2d9862e439d4b0ab4b45be6752b18628ecb1
4
- data.tar.gz: 280db3869191e9c9fb318baed1b99b6ebc3f90ff24c16993523fe47eceff26fb
3
+ metadata.gz: c47df8d3946c3b010f0af6c185ebd93a0bcf7cbb4007ddbd89d3105496dcb78c
4
+ data.tar.gz: 9c8e54ca07f94145a0efd58b5f3dab64760d4d58244b12cb520946ee0d206ee1
5
5
  SHA512:
6
- metadata.gz: 6939a317d815e1c0d6cfdc65f75f506cb549978b8338ef4f604086cb5efe7b4959d3ab3112691d068fa8f28082cb6753d3ae9177c5909ec56e0dd537c9c557b5
7
- data.tar.gz: 3d07a34bb72ed5c3f37ce106daf0c30eef47d46af22e29e3278cfcefbbafdd607db395abbb38b4b34531aceaef9af8c46a29d37587b170703dc4aafb17edafd6
6
+ metadata.gz: e413f4cb0fac8f72c728126fb62e0f5ade142add7aede779c25de83051e41c9d38d6ff84f9b093ef472c5edb789db02d4f82dc77582d621265faaa63d5d01a40
7
+ data.tar.gz: 9008c5e856de8a746c6be66664c59e02af8c948377bc55fde22a99628c63753e8a8601f07de1ec127eaf327f3ee28c090ca0410d363c8af1dece5a3c82b3469c
data/CHANGELOG.md ADDED
@@ -0,0 +1,80 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.0.0] - 2026-06-17
9
+
10
+ This is a **major, breaking release**. The argument-based `#deep_compact` API has
11
+ been replaced with four clearly-named methods, and "blank" now matches Rails'
12
+ `#compact_blank` semantics.
13
+
14
+ ### Breaking
15
+
16
+ - **`#deep_compact` changed meaning.** In v1, bare `deep_compact` defaulted to the
17
+ *most aggressive* behavior. In v2 it removes **only `nil`** (matching Ruby's
18
+ `#compact`). If you relied on the old default, switch to `#deep_compact_blank`.
19
+ - **The boolean arguments are gone.** `deep_compact(remove_emptied_elements,
20
+ remove_empty_elements)` is replaced by four dedicated methods (see Added).
21
+ - **"Blank" now means `blank?`, not just empty.** The `_blank` family now also
22
+ removes `false` and blank strings (`""`, `" "`), in addition to `nil` and empty
23
+ collections — matching Rails' `#compact_blank`.
24
+ - **A Hash's `default`/`default_proc` is no longer preserved through compaction.**
25
+ v1 mutated a `dup` (which carried the proc); v2 builds a fresh `Hash`, which has
26
+ none. This matches `Hash#reject` and Rails' `#compact_blank`, but can silently
27
+ break code relying on auto-vivification after compaction (e.g. an accumulator
28
+ `Hash.new { |h, k| h[k] = [] }`). Apply the default proc to the compacted
29
+ *result* rather than before compacting.
30
+
31
+ ### Added
32
+
33
+ - Four named compaction methods, each with an in-place `!` variant, available as
34
+ instance methods, class methods, and (via `require: "base_extensions"`) directly
35
+ on core `Hash`/`Array`:
36
+ - `#deep_compact` — removes `nil` only.
37
+ - `#deep_compact_existing_blank` — removes `nil` and values that are already
38
+ blank; keeps collections that only become blank through compaction.
39
+ - `#deep_compact_blanked` — removes `nil` and collections emptied by compaction;
40
+ keeps values that were already blank.
41
+ - `#deep_compact_blank` — removes every blank value.
42
+ - GitHub Actions CI running RSpec on Ruby 3.3, 3.4, and 4.0.
43
+ - A real, comprehensive test suite (43 examples) replacing the previous
44
+ placeholder specs.
45
+
46
+ ### Changed
47
+
48
+ - Rewrote the internals around a single recursive engine, with doc comments
49
+ linking each method to the Ruby/Rails source it mirrors. Blankness is determined
50
+ by an internal equivalent of ActiveSupport's `Object#blank?`, so the gem still
51
+ has **no ActiveSupport dependency**.
52
+ - `base_extensions` now mixes into `Hash`/`Array` the same way ActiveSupport does
53
+ (reopening the class).
54
+ - `rake` now runs the RSpec suite (`rake` / `rake spec`); dropped the unused
55
+ Minitest and RDoc tasks.
56
+ - Expanded the README with a semantics table, worked examples, and an
57
+ "Upgrading from v1" guide.
58
+
59
+ ### Fixed
60
+
61
+ - Fixed a latent bug in `#reverse_deep_merge!` (`ebuild` → `rebuild`) that was
62
+ hidden by placeholder specs.
63
+
64
+ ### Upgrading from v1
65
+
66
+ | v1 call | v2 equivalent |
67
+ | --- | --- |
68
+ | `deep_compact` / `deep_compact(true, true)` | `deep_compact_blank` |
69
+ | `deep_compact(true, false)` | `deep_compact_blanked` |
70
+ | `deep_compact(false, true)` | `deep_compact_existing_blank` |
71
+ | `deep_compact(false, false)` | `deep_compact` |
72
+
73
+ ## [1.0.0] - 2023-01-18
74
+
75
+ - Initial release: recursive `#deep_compact` (with `remove_emptied_elements` /
76
+ `remove_empty_elements` flags) and `#reverse_deep_merge` for nested `Hash` and
77
+ `Array` objects.
78
+
79
+ [2.0.0]: https://github.com/chaunce/deeply_enumerable/compare/1.0.0...2.0.0
80
+ [1.0.0]: https://github.com/chaunce/deeply_enumerable/releases/tag/1.0.0
data/README.md CHANGED
@@ -1,37 +1,110 @@
1
1
  # DeeplyEnumerable
2
- Extend Enumerable objects with recursive methods
2
+ Extend `Enumerable` objects with recursive versions of common operations.
3
+
4
+ `DeeplyEnumerable` walks a nested structure of `Hash` and `Array` objects and
5
+ applies an operation at every level. Nested `Enumerable` objects are converted
6
+ into `DeeplyEnumerable` types during the operation so the recursion can continue
7
+ all the way down.
8
+
9
+ ## Compaction
10
+ Ruby's `#compact` removes `nil`. Rails' `#compact_blank` removes anything that is
11
+ `blank?` (`nil`, `false`, `""`, `" "`, `[]`, `{}`). `DeeplyEnumerable` provides
12
+ recursive versions of both, plus two methods in between for finer control over
13
+ *which* blanks are removed:
14
+
15
+ | Method | Removes | Keeps |
16
+ | --- | --- | --- |
17
+ | `#deep_compact` | `nil` only | every other value, including `""`, `false`, `[]`, `{}` |
18
+ | `#deep_compact_existing_blank` | `nil` and values that are **already** blank | collections that only become blank through compaction |
19
+ | `#deep_compact_blanked` | `nil` and collections **emptied** by compaction | values that were already blank |
20
+ | `#deep_compact_blank` | every blank value | nothing blank |
21
+
22
+ The distinction between "existing" and "emptied" blanks matters once nesting is
23
+ involved. Consider `{ a: { b: nil }, c: [] }`:
24
+
25
+ * `a` holds `{ b: nil }`, which is *not* blank until its `nil` is removed — an
26
+ **emptied** blank.
27
+ * `c` holds `[]`, which is blank in the input — an **existing** blank.
3
28
 
4
- ## Usage
5
- Provides the following methods
6
29
  ```ruby
7
- $ DeeplyEnumerable::Hash#reverse_deep_merge(DeeplyEnumerable::Hash)
8
- $ DeeplyEnumerable::Hash#reverse_deep_merge!(DeeplyEnumerable::Hash)
9
- $ DeeplyEnumerable::Hash#deep_compact
10
- $ DeeplyEnumerable::Hash#deep_compact!
30
+ source = { a: :b, c: nil, d: { e: nil, f: [] }, g: [1, nil], h: {}, i: "", j: [nil] }
11
31
 
12
- $ DeeplyEnumerable::Array#deep_compact
13
- $ DeeplyEnumerable::Array#deep_compact!
32
+ DeeplyEnumerable::Hash.deep_compact(source)
33
+ # => { a: :b, d: { f: [] }, g: [1], h: {}, i: "", j: [] } # only nils removed
34
+
35
+ DeeplyEnumerable::Hash.deep_compact_existing_blank(source)
36
+ # => { a: :b, d: {}, g: [1], j: [] } # pre-existing [], {} and "" removed; emptied kept
37
+
38
+ DeeplyEnumerable::Hash.deep_compact_blanked(source)
39
+ # => { a: :b, d: { f: [] }, g: [1], h: {}, i: "" } # emptied collections removed; pre-existing blanks kept
40
+
41
+ DeeplyEnumerable::Hash.deep_compact_blank(source)
42
+ # => { a: :b, g: [1] } # every blank removed
14
43
  ```
15
44
 
16
- Nested `Enumerable` objects will be converted into `DeeplyEnumerable` type objects during merge or compact operations to allow recursive method calls
45
+ Each method has an in-place `!` variant (`#deep_compact!`, `#deep_compact_blank!`,
46
+ etc.) that mutates the receiver and returns it, mirroring `#compact!`.
17
47
 
18
- Class methods may be used for base ruby enumerable objects, such as `Hash` or `Array` objects
48
+ ## Merging
19
49
  ```ruby
20
- $ DeeplyEnumerable::Hash.deep_compact(Hash)
21
- $ DeeplyEnumerable::Array.deep_compact(Array)
50
+ DeeplyEnumerable::Hash#reverse_deep_merge(other_hash) # recursively fill in only the missing keys
51
+ DeeplyEnumerable::Hash#reverse_deep_merge!(other_hash) # in place
22
52
  ```
53
+ `#deep_reverse_merge` / `#deep_reverse_merge!` are aliases.
54
+
55
+ ## Usage
56
+ Call a class method to operate on a plain `Hash` or `Array`. The result is the
57
+ matching `DeeplyEnumerable` type (`Hash` returns a `DeeplyEnumerable::Hash`,
58
+ `Array` returns a `DeeplyEnumerable::Array`):
59
+ ```ruby
60
+ DeeplyEnumerable::Hash.deep_compact_blank({ a: 1, b: nil, c: { d: nil } })
61
+ # => { a: 1 }
62
+
63
+ DeeplyEnumerable::Array.deep_compact([1, nil, [2, nil]])
64
+ # => [1, [2]]
65
+ ```
66
+
67
+ Or extend the base `Hash` and `Array` classes (see *Installation*) and call the
68
+ methods directly:
69
+ ```ruby
70
+ { a: 1, b: nil }.deep_compact_blank # => { a: 1 }
71
+ [1, nil, [nil]].deep_compact # => [1, []]
72
+ ```
73
+
74
+ ## Upgrading from v1
75
+ v2 replaces the boolean arguments of `#deep_compact` with four named methods.
76
+ Map your old calls as follows:
77
+
78
+ | v1 call | v2 equivalent |
79
+ | --- | --- |
80
+ | `deep_compact` / `deep_compact(true, true)` | `deep_compact_blank` |
81
+ | `deep_compact(true, false)` | `deep_compact_blanked` |
82
+ | `deep_compact(false, true)` | `deep_compact_existing_blank` |
83
+ | `deep_compact(false, false)` | `deep_compact` |
84
+
85
+ ⚠️ **`#deep_compact` changed meaning.** In v1, bare `deep_compact` defaulted to
86
+ the *most aggressive* behavior. In v2, `#deep_compact` removes **only nils**
87
+ (matching Ruby's `#compact`). If you relied on the old default, switch to
88
+ `#deep_compact_blank`.
89
+
90
+ ⚠️ **Blank now means `blank?`, not just empty.** v1 only ever removed `nil` and
91
+ empty collections (`[]`, `{}`). The v2 `_blank` family also removes `false` and
92
+ blank strings (`""`, `" "`), matching Rails' `#compact_blank`. Blankness is
93
+ determined by an internal equivalent of ActiveSupport's `Object#blank?`, so the
94
+ gem still has **no ActiveSupport dependency**.
23
95
 
24
- These methos will retrun a `DeeplyEnumerable` type object matching the object passed, e.g: `Array` will return a `DeeplyEnumerable::Array`
96
+ ⚠️ **`default`/`default_proc` is not preserved.** v2 returns a freshly-built
97
+ `Hash`, so a default value or proc does not survive compaction (matching
98
+ `Hash#reject` and Rails' `#compact_blank`). v1 happened to keep it because it
99
+ mutated a `dup`. If you rely on auto-vivification, apply the default proc to the
100
+ compacted *result* rather than before compacting:
25
101
 
26
- You may also extend base classes if you `require: "base_extensions"` as described in *Installation*
27
102
  ```ruby
28
- $ Hash#reverse_deep_merge(DeeplyEnumerable::Hash)
29
- $ Hash#reverse_deep_merge!(DeeplyEnumerable::Hash)
30
- $ Hash#deep_compact
31
- $ Hash#deep_compact!
103
+ # Fragile — the proc is dropped by compaction:
104
+ Hash.new { |h, k| h[k] = [] }.merge(value).deep_compact_blank
32
105
 
33
- $ Array#deep_compact
34
- $ Array#deep_compact!
106
+ # Robust — compact first, then land the data in the proc-bearing Hash:
107
+ Hash.new { |h, k| h[k] = [] }.merge(value.deep_compact_blank)
35
108
  ```
36
109
 
37
110
  ## Installation
@@ -40,14 +113,14 @@ Add this line to your application's Gemfile:
40
113
  gem "deeply_enumerable"
41
114
  ```
42
115
 
43
- or this line to extend the base `Enumerable` classes:
116
+ or this line to mix the methods into the base `Hash` and `Array` classes:
44
117
  ```ruby
45
118
  gem "deeply_enumerable", require: "base_extensions"
46
119
  ```
47
120
 
48
121
  And then execute:
49
122
  ```bash
50
- $ bundle
123
+ $ bundle install
51
124
  ```
52
125
 
53
126
  Or install it yourself as:
data/Rakefile CHANGED
@@ -1,33 +1,6 @@
1
- begin
2
- require "bundler/setup"
3
- rescue LoadError
4
- puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
- end
6
-
7
- require "rdoc/task"
8
-
9
- RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = "rdoc"
11
- rdoc.title = "DeeplyEnumerable"
12
- rdoc.options << "--line-numbers"
13
- rdoc.rdoc_files.include("README.md")
14
- rdoc.rdoc_files.include("lib/**/*.rb")
15
- end
16
-
17
-
18
-
19
-
20
-
21
-
22
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
23
3
 
24
- require "rake/testtask"
25
-
26
- Rake::TestTask.new(:test) do |t|
27
- t.libs << "test"
28
- t.pattern = "test/**/*_test.rb"
29
- t.verbose = false
30
- end
31
-
4
+ RSpec::Core::RakeTask.new(:spec)
32
5
 
33
- task default: :test
6
+ task default: :spec
@@ -1,4 +1,11 @@
1
1
  require "deeply_enumerable"
2
2
 
3
- Hash.send(:include, DeeplyEnumerable::HashExtension)
4
- Array.send(:include, DeeplyEnumerable::ArrayExtension)
3
+ # Mirror the way Rails (ActiveSupport) mixes its enumerable extensions into the
4
+ # core classes: reopen each class and include the corresponding module.
5
+ class Hash
6
+ include DeeplyEnumerable::HashExtension
7
+ end
8
+
9
+ class Array
10
+ include DeeplyEnumerable::ArrayExtension
11
+ end
@@ -8,31 +8,63 @@ module DeeplyEnumerable
8
8
  end
9
9
  end
10
10
 
11
- def deep_compact!(remove_emptied_elements = true, remove_empty_elements = remove_emptied_elements)
12
- each.with_index do |value, index|
13
- next if unenumerable_object?(value)
14
-
15
- value = rebuild(value) unless value.respond_to?(:reverse_deep_merge)
16
- compact_method = %i[deep_compact! deep_compact compact! compact].detect{ |m| value.respond_to?(m) }
17
- next unless compact_method
18
-
19
- original_empty = value.respond_to?(:empty?) ? value.empty? : value.respond_to?(:none?) ? value.none? : false
20
- compact_value = value.send(*[compact_method].concat(value.method(compact_method).parameters.collect { |_,param| binding.local_variable_get(param) } ).compact) || value
21
- compact_empty = compact_value.respond_to?(:empty?) ? compact_value.empty? : compact_value.respond_to?(:none?) ? compact_value.none? : false
22
-
23
- if (original_empty && remove_empty_elements) || (!original_empty && compact_empty && remove_emptied_elements)
24
- self.delete_at(index)
25
- else
26
- self[index] = compact_value
27
- end
28
- end
29
- compact!
11
+ # Recursively removes nil elements, leaving every other element (including
12
+ # already-blank ones such as "", [] and {}) untouched. This is the
13
+ # recursive counterpart of Ruby's Array#compact.
14
+ # https://docs.ruby-lang.org/en/master/Array.html#method-i-compact
15
+ def deep_compact
16
+ deep_compact_each(false, false)
17
+ end
18
+
19
+ # In-place variant of #deep_compact, mirroring Array#compact!.
20
+ def deep_compact!
21
+ replace(deep_compact)
22
+ end
23
+
24
+ # Recursively removes nils and elements that are already blank, but keeps
25
+ # collections that only become blank as a result of compaction.
26
+ def deep_compact_existing_blank
27
+ deep_compact_each(false, true)
28
+ end
29
+
30
+ # In-place variant of #deep_compact_existing_blank.
31
+ def deep_compact_existing_blank!
32
+ replace(deep_compact_existing_blank)
33
+ end
30
34
 
31
- self
35
+ # Recursively removes nils and collections that became blank through
36
+ # compaction, but keeps elements that were already blank.
37
+ def deep_compact_blanked
38
+ deep_compact_each(true, false)
32
39
  end
33
40
 
34
- def deep_compact(remove_emptied_elements = true, remove_empty_elements = remove_emptied_elements)
35
- dup.deep_compact!(remove_emptied_elements, remove_empty_elements)
41
+ # In-place variant of #deep_compact_blanked.
42
+ def deep_compact_blanked!
43
+ replace(deep_compact_blanked)
44
+ end
45
+
46
+ # Recursively removes every blank element (nil, false, "", " ", [], {}).
47
+ # This is the recursive counterpart of Rails' Array#compact_blank.
48
+ # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/enumerable.rb
49
+ def deep_compact_blank
50
+ deep_compact_each(true, true)
51
+ end
52
+
53
+ # In-place variant of #deep_compact_blank, mirroring Array#compact_blank!.
54
+ def deep_compact_blank!
55
+ replace(deep_compact_blank)
56
+ end
57
+
58
+ private
59
+
60
+ # Shared engine for the deep_compact family: rebuilds an array of the same
61
+ # class, recursively compacting each element and discarding the ones that
62
+ # should be removed for the given combination of flags.
63
+ def deep_compact_each(remove_emptied_elements, remove_empty_elements)
64
+ each_with_object(self.class.new) do |element, result|
65
+ keep, element = deep_compact_element(element, remove_emptied_elements, remove_empty_elements)
66
+ result.push(element) if keep
67
+ end
36
68
  end
37
69
  end
38
70
 
@@ -1,7 +1,13 @@
1
1
  module DeeplyEnumerable
2
2
  module Enumerable
3
+ # Collection-like objects that should be treated as opaque elements rather
4
+ # than recursed into during deep operations.
3
5
  UNENUMERABLE = ["ActiveRecord::Relation", "Range"]
4
6
 
7
+ # Whitespace-only strings are considered blank, mirroring ActiveSupport.
8
+ # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/object/blank.rb
9
+ BLANK_RE = /\A[[:space:]]*\z/
10
+
5
11
  def self.included(klass)
6
12
  klass.extend(ClassMethods)
7
13
  end
@@ -24,9 +30,28 @@ module DeeplyEnumerable
24
30
  new.tap { |deeply_enumerable_object| object.each { |value| deeply_enumerable_object.push(rebuild(value)) } }
25
31
  end
26
32
 
27
- def deep_compact(object, remove_emptied_elements = true, remove_empty_elements = remove_emptied_elements)
28
- check_object_class(object)
29
- deep_rebuild(object).deep_compact(remove_emptied_elements, remove_empty_elements)
33
+ # Recursively removes nil values.
34
+ # Mirrors Ruby's Array#compact / Hash#compact.
35
+ def deep_compact(object)
36
+ deep_rebuild(object).deep_compact
37
+ end
38
+
39
+ # Recursively removes nils and values that are already blank, while
40
+ # keeping collections that only become blank as a result of compaction.
41
+ def deep_compact_existing_blank(object)
42
+ deep_rebuild(object).deep_compact_existing_blank
43
+ end
44
+
45
+ # Recursively removes nils and collections that became blank through
46
+ # compaction, while keeping values that were already blank.
47
+ def deep_compact_blanked(object)
48
+ deep_rebuild(object).deep_compact_blanked
49
+ end
50
+
51
+ # Recursively removes every blank value.
52
+ # Mirrors Rails' Enumerable#compact_blank.
53
+ def deep_compact_blank(object)
54
+ deep_rebuild(object).deep_compact_blank
30
55
  end
31
56
 
32
57
  def unenumerable
@@ -47,5 +72,42 @@ module DeeplyEnumerable
47
72
  def unenumerable_object?(object)
48
73
  self.class.unenumerable.any? { |unenumerable_klass| object.is_a?(unenumerable_klass) }
49
74
  end
75
+
76
+ private
77
+
78
+ # Decides what to do with a single element during deep compaction and
79
+ # returns [keep?, element], where +element+ may be a rebuilt/compacted
80
+ # copy.
81
+ #
82
+ # nil is always dropped. Nested enumerables are compacted recursively, then
83
+ # dropped if they were already blank (+remove_empty_elements+) or only
84
+ # became blank through compaction (+remove_emptied_elements+).
85
+ # Non-enumerable elements are dropped only when already blank and
86
+ # +remove_empty_elements+ is set.
87
+ def deep_compact_element(element, remove_emptied_elements, remove_empty_elements)
88
+ return [false, element] if element.nil?
89
+
90
+ element = rebuild(element) unless unenumerable_object?(element) || element.respond_to?(:deep_compact_each, true)
91
+
92
+ if !unenumerable_object?(element) && element.respond_to?(:deep_compact_each, true)
93
+ blank_before = deeply_blank?(element)
94
+ element = element.send(:deep_compact_each, remove_emptied_elements, remove_empty_elements)
95
+ keep = !((blank_before && remove_empty_elements) ||
96
+ (!blank_before && remove_emptied_elements && deeply_blank?(element)))
97
+ [keep, element]
98
+ else
99
+ [!(remove_empty_elements && deeply_blank?(element)), element]
100
+ end
101
+ end
102
+
103
+ # Internal equivalent of ActiveSupport's Object#blank? (with
104
+ # String#blank?), implemented here so the gem needs no ActiveSupport
105
+ # dependency.
106
+ # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/object/blank.rb
107
+ def deeply_blank?(value)
108
+ return value.match?(BLANK_RE) if value.is_a?(::String)
109
+
110
+ value.respond_to?(:empty?) ? !!value.empty? : !value
111
+ end
50
112
  end
51
113
  end
@@ -13,7 +13,7 @@ module DeeplyEnumerable
13
13
  this_value = self[current_key]
14
14
 
15
15
  self[current_key] = if this_value.is_a?(::Hash) && other_value.is_a?(::Hash)
16
- this_value = ebuild(this_value) unless this_value.respond_to?(:reverse_deep_merge)
16
+ this_value = rebuild(this_value) unless this_value.respond_to?(:reverse_deep_merge)
17
17
  this_value.reverse_deep_merge(other_value)
18
18
  else
19
19
  key?(current_key) ? this_value : other_value
@@ -29,27 +29,63 @@ module DeeplyEnumerable
29
29
  end
30
30
  alias_method :deep_reverse_merge, :reverse_deep_merge
31
31
 
32
- def deep_compact!(remove_emptied_elements = true, remove_empty_elements = remove_emptied_elements)
33
- each do |key, value|
34
- next if unenumerable_object?(value)
32
+ # Recursively removes nil values, leaving every other value (including
33
+ # already-blank ones such as "", [] and {}) untouched. This is the
34
+ # recursive counterpart of Ruby's Hash#compact.
35
+ # https://docs.ruby-lang.org/en/master/Hash.html#method-i-compact
36
+ def deep_compact
37
+ deep_compact_each(false, false)
38
+ end
35
39
 
36
- value = rebuild(value) unless value.respond_to?(:reverse_deep_merge)
37
- compact_method = %i[deep_compact! deep_compact compact! compact].detect{ |m| value.respond_to?(m) }
38
- next unless compact_method
40
+ # In-place variant of #deep_compact, mirroring Hash#compact!.
41
+ def deep_compact!
42
+ replace(deep_compact)
43
+ end
39
44
 
40
- original_empty = value.respond_to?(:empty?) ? value.empty? : value.respond_to?(:none?) ? value.none? : false rescue false
41
- compact_value = value.send(*[compact_method].concat(value.method(compact_method).parameters.collect { |_,param| binding.local_variable_get(param) } ).compact) || value
42
- compact_empty = compact_value.respond_to?(:empty?) ? compact_value.empty? : compact_value.respond_to?(:none?) ? compact_value.none? : false
45
+ # Recursively removes nils and values that are already blank, but keeps
46
+ # collections that only become blank as a result of compaction.
47
+ def deep_compact_existing_blank
48
+ deep_compact_each(false, true)
49
+ end
43
50
 
44
- self[key] = (original_empty && remove_empty_elements) || (!original_empty && compact_empty && remove_emptied_elements) ? nil : compact_value
45
- end
46
- compact!
51
+ # In-place variant of #deep_compact_existing_blank.
52
+ def deep_compact_existing_blank!
53
+ replace(deep_compact_existing_blank)
54
+ end
47
55
 
48
- self
56
+ # Recursively removes nils and collections that became blank through
57
+ # compaction, but keeps values that were already blank.
58
+ def deep_compact_blanked
59
+ deep_compact_each(true, false)
60
+ end
61
+
62
+ # In-place variant of #deep_compact_blanked.
63
+ def deep_compact_blanked!
64
+ replace(deep_compact_blanked)
49
65
  end
50
66
 
51
- def deep_compact(remove_emptied_elements = true, remove_empty_elements = remove_emptied_elements)
52
- dup.deep_compact!(remove_emptied_elements, remove_empty_elements)
67
+ # Recursively removes every blank value (nil, false, "", " ", [], {}). This
68
+ # is the recursive counterpart of Rails' Hash#compact_blank.
69
+ # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/hash/keys.rb
70
+ def deep_compact_blank
71
+ deep_compact_each(true, true)
72
+ end
73
+
74
+ # In-place variant of #deep_compact_blank, mirroring Hash#compact_blank!.
75
+ def deep_compact_blank!
76
+ replace(deep_compact_blank)
77
+ end
78
+
79
+ private
80
+
81
+ # Shared engine for the deep_compact family: rebuilds a hash of the same
82
+ # class, recursively compacting each value and discarding the keys whose
83
+ # value should be removed for the given combination of flags.
84
+ def deep_compact_each(remove_emptied_elements, remove_empty_elements)
85
+ each_with_object(self.class.new) do |(key, value), result|
86
+ keep, value = deep_compact_element(value, remove_emptied_elements, remove_empty_elements)
87
+ result[key] = value if keep
88
+ end
53
89
  end
54
90
  end
55
91
 
@@ -70,5 +106,3 @@ module DeeplyEnumerable
70
106
  end
71
107
  end
72
108
  end
73
-
74
-
@@ -1,3 +1,3 @@
1
1
  module DeeplyEnumerable
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.1"
3
3
  end
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deeply_enumerable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - chaunce
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-01-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: rspec
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -24,13 +37,14 @@ dependencies:
24
37
  - - ">="
25
38
  - !ruby/object:Gem::Version
26
39
  version: '0'
27
- description: recusrive support for enumerable operations
40
+ description: recursive support for enumerable operations
28
41
  email:
29
42
  - chaunce.slc@gmail.com
30
43
  executables: []
31
44
  extensions: []
32
45
  extra_rdoc_files: []
33
46
  files:
47
+ - CHANGELOG.md
34
48
  - MIT-LICENSE
35
49
  - README.md
36
50
  - Rakefile
@@ -43,8 +57,9 @@ files:
43
57
  homepage: https://github.com/chaunce/deeply_enumerable
44
58
  licenses:
45
59
  - MIT
46
- metadata: {}
47
- post_install_message:
60
+ metadata:
61
+ source_code_uri: https://github.com/chaunce/deeply_enumerable
62
+ changelog_uri: https://github.com/chaunce/deeply_enumerable/blob/master/CHANGELOG.md
48
63
  rdoc_options: []
49
64
  require_paths:
50
65
  - lib
@@ -59,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
74
  - !ruby/object:Gem::Version
60
75
  version: '0'
61
76
  requirements: []
62
- rubygems_version: 3.4.3
63
- signing_key:
77
+ rubygems_version: 4.0.6
64
78
  specification_version: 4
65
- summary: recusrive support for enumerable operations
79
+ summary: recursive support for enumerable operations
66
80
  test_files: []