everythingrb 0.9.1 → 1.0.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: 7361ef45ed502564c8e27fd3b2173702320f2076665ae9f495f5645fc7b57587
4
- data.tar.gz: 411dc777df9851c7204903dde8ce12ce6951cf2308d9c02e6184bf0fef67a056
3
+ metadata.gz: c1aaf48c681b67675cfd4a3e053086163c755a47fc3941507ac0b08bb33ddf09
4
+ data.tar.gz: 2e9389e279e225463b03d0ccaac1db55444aa3d5c99f4ec06c5d86e89a235efa
5
5
  SHA512:
6
- metadata.gz: 548f88f645e3d1731ccbe258e9e8d4157003f9b5f584942f8a2b14657bab1c04de965c16bd2a7bb0f2305edfc92c3e73c5bcd7a07f9627cccd3eef46e5488076
7
- data.tar.gz: fb00c066860dc38a4b8b1091978618af0d6ee06bc477dbdb9d2a8603e043e0de8b7b93aab09964035f13efae77e0ec0a05fa00ea6caa3a70ad9d1c32b577c8d4
6
+ metadata.gz: 16cdf21b81457e94d393b246dd762c33400de2e39b01435be9334946cdb2b0639583db9064fdac7c2053e6bc533f3b699fcdd5fe7c119e3052f1b51e8cb911ae
7
+ data.tar.gz: fc9e95bbd7b0dd0fdbc5a8474f00ffccc0968e703b45c63a7aef1ddc8b6f0418123dfb7474813ce03e889f35244c6e7bf42cab368d31e5db4174551a93ee9f20
data/CHANGELOG.md CHANGED
@@ -15,11 +15,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  ### Removed
16
16
  -->
17
17
 
18
- ## [0.9.1] - 12026-01-22
18
+ ## [1.0.0] - 12026-03-21
19
+
20
+ ### Added
21
+
22
+ - **Added `from:` option to `attr_predicate`** - Map a predicate to a differently-named source. Use `@` prefix for instance variables (e.g., `from: :@started_at`), omit for methods (e.g., `from: :error_messages`). This enables patterns like `attr_predicate :started, from: :@started_at` and `attr_predicate :errored, from: :error_messages`.
23
+ - **Added `private:` option to `attr_predicate`** - Define predicates as private methods with `attr_predicate :verified, private: true`.
19
24
 
20
25
  ### Changed
21
26
 
22
- - **Deprecated `Hash.new_nested_hash`** - This method is now deprecated and will be removed in v1.0.0. Consider using `Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }` instead.
27
+ - **BREAKING: Renamed `Hash#value_where` to `Hash#find_value` and `Hash#values_where` to `Hash#select_values`** - The new names better align with Ruby conventions and clearly express the methods' purpose of finding values that match a condition.
28
+ - **BREAKING: Renamed `String#to_h` to `String#parse_json`** - This change improves compatibility with Ruby's type coercion expectations and avoids conflicts with methods like `Array(obj)` that rely on standard `to_a`/`to_h` behavior. The method parses JSON strings with symbolized keys by default and returns `nil` for invalid JSON.
29
+ - **Fixed Ruby 3.4+ compatibility** - Added `require "date"` to ensure Date and DateTime classes are properly loaded before extending them.
30
+ - **BREAKING: `attr_predicate` no longer falls back to method calls** - For performance reasons (~5x faster), `attr_predicate` now assumes direct instance variable access by default. The previous behavior checked `instance_variable_defined?` and `respond_to?` on every call, which was slow. Struct, OpenStruct, and Data classes are automatically detected and use method access. For other cases where you need method delegation, use the new `from:` option explicitly (e.g., `attr_predicate :errored, from: :error_messages`).
31
+ - **Improved `attr_predicate` empty value handling** - Without ActiveSupport, values responding to `empty?` (arrays, hashes, strings) now return `false` when empty. Previously, an empty array would return `true` since `!![]` is truthy. With ActiveSupport, behavior is unchanged (uses `present?`).
32
+
33
+ ### Removed
34
+
35
+ - **BREAKING: Removed `Hash.new_nested_hash`** - Use Ruby's built-in `Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }` instead.
36
+ - **BREAKING: Removed `#to_deep_h` from all classes** - This method has been removed from String, Hash, Array, Struct, OpenStruct, and Data classes. If you need this functionality, implement it locally in your project.
37
+ - **Removed `String#to_a`** - This method was removed along with the JSON parsing refactor.
23
38
 
24
39
  ## [0.9.0] - 12025-08-01
25
40
 
@@ -212,10 +227,6 @@ This change aligns our method signatures with Ruby's conventions and matches our
212
227
  - `#rename_keys!` - Same as `#rename_keys` but modifies the hash in place
213
228
  - `#rename_key_unordered` - Renames a key without preserving element order (faster operation)
214
229
  - `#rename_key_unordered!` - Same as `#rename_key_unordered` but modifies the hash in place
215
- - Added `to_deep_h` to core Ruby classes for consistent deep hash conversion:
216
- - `Struct#to_deep_h` - Recursively converts Struct objects and all nested values to hashes
217
- - `OpenStruct#to_deep_h` - Recursively converts OpenStruct objects and all nested values to hashes
218
- - `Data#to_deep_h` - Recursively converts Data objects and all nested values to hashes
219
230
  - Added `depth` parameter to `Hash.new_nested_hash` to control nesting behaviors
220
231
 
221
232
  ### Changed
@@ -241,7 +252,6 @@ This change aligns our method signatures with Ruby's conventions and matches our
241
252
 
242
253
  - Added `Array#to_or_sentence`, creates a sentence with "or" connector between items
243
254
  - Added `#with_key` method to `Hash#transform_values` and `Hash#transform_values!`, grants access to both keys and values during transformations
244
- - Added `Array#to_deep_h` and `Hash#to_deep_h`, recursively converts underlying values to hashes
245
255
  - Added `Enumerable#group_by_key`, group an array of hashes by their keys
246
256
  - Added `Hash#new_nested_hash`, creates a new Hash that automatically initializes the value to a hash
247
257
  - Added `Hash#value_where` and `Hash#values_where`, easily find values in a hash based on key-value conditions
@@ -341,15 +351,14 @@ This change aligns our method signatures with Ruby's conventions and matches our
341
351
  - `join_map` method consistent with Array/Hash implementations
342
352
  - Enhanced `String` class with:
343
353
  - `to_h` and `to_a` methods for JSON parsing with `nil` fallback on error
344
- - `to_deep_h` for recursive JSON string parsing
345
354
  - `to_istruct`, `to_ostruct`, and `to_struct` conversion methods
346
355
 
347
356
  ### Changed
348
357
 
349
358
  - Added alias `each` to `each_pair` in OpenStruct for better enumerable compatibility
350
359
 
351
- [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.9.1...HEAD
352
- [0.9.1]: https://github.com/itsthedevman/everythingrb/compare/v0.9.0...v0.9.1
360
+ [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v1.0.0...HEAD
361
+ [1.0.0]: https://github.com/itsthedevman/everythingrb/compare/v0.9.0...v1.0.0
353
362
  [0.9.0]: https://github.com/itsthedevman/everythingrb/compare/v0.8.3...v0.9.0
354
363
  [0.8.3]: https://github.com/itsthedevman/everythingrb/compare/v0.8.2...v0.8.3
355
364
  [0.8.3]: https://github.com/itsthedevman/everythingrb/compare/v0.8.2...v0.8.3
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  ![Ruby Version](https://img.shields.io/badge/ruby-3.3.7-ruby)
5
5
  [![Tests](https://github.com/itsthedevman/everythingrb/actions/workflows/main.yml/badge.svg)](https://github.com/everythingrb/sortsmith/actions/workflows/main.yml)
6
6
 
7
- Super handy extensions to Ruby core classes that make your code more expressive, readable, and fun to write. Stop writing boilerplate and start writing code that _actually matters_!
7
+ Practical extensions to Ruby core classes that let your code say what it means.
8
8
 
9
9
  ## Express Your Intent, Not Your Logic
10
10
 
@@ -33,8 +33,6 @@ users.join_map(", ") { |u| u[:name] if u[:role] == "admin" }
33
33
 
34
34
  _Methods used: [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method)_
35
35
 
36
- Because life's too short to write all that boilerplate!
37
-
38
36
  ## Installation
39
37
 
40
38
  ```ruby
@@ -68,7 +66,7 @@ config.server.port # => 443
68
66
 
69
67
  #### Cherry-Pick Extensions
70
68
 
71
- If you only need specific extensions (or you're a minimalist at heart):
69
+ If you only need specific extensions:
72
70
 
73
71
  ```ruby
74
72
  require "everythingrb/prelude" # Required base module
@@ -87,7 +85,7 @@ Available modules:
87
85
 
88
86
  - `array`: Array extensions (join_map, key_map, etc.)
89
87
  - `boolean`: Boolean extensions (in_quotes, with_quotes)
90
- - `data`: Data extensions (to_deep_h, in_quotes)
88
+ - `data`: Data extensions (in_quotes)
91
89
  - `date`: Date and DateTime extensions (in_quotes)
92
90
  - `enumerable`: Enumerable extensions (join_map, group_by_key)
93
91
  - `hash`: Hash extensions (to_ostruct, transform_values(with_key: true), etc.)
@@ -98,14 +96,14 @@ Available modules:
98
96
  - `ostruct`: OpenStruct extensions (map, join_map, etc.)
99
97
  - `range`: Range extensions (in_quotes)
100
98
  - `regexp`: Regexp extensions (in_quotes)
101
- - `string`: String extensions (to_h, to_ostruct, to_camelcase, etc.)
102
- - `struct`: Struct extensions (to_deep_h, in_quotes)
99
+ - `string`: String extensions (parse_json, to_ostruct, to_camelcase, etc.)
100
+ - `struct`: Struct extensions (in_quotes)
103
101
  - `symbol`: Symbol extensions (with_quotes)
104
102
  - `time`: Time extensions (in_quotes)
105
103
 
106
104
  ### Rails Applications
107
105
 
108
- EverythingRB works out of the box with Rails! Just add it to your Gemfile and you're all set.
106
+ EverythingRB works out of the box with Rails. Just add it to your Gemfile and you're all set.
109
107
 
110
108
  If you only want specific extensions, configure them in an initializer:
111
109
 
@@ -122,8 +120,6 @@ By default (when `config.everythingrb.extensions` is not set), all extensions ar
122
120
 
123
121
  ### Data Structure Conversions
124
122
 
125
- Stop writing complicated parsers and nested transformations:
126
-
127
123
  ```ruby
128
124
  # BEFORE
129
125
  json_string = '{"user":{"name":"Alice","roles":["admin"]}}'
@@ -137,8 +133,6 @@ result = OpenStruct.new(
137
133
  result.user.name # => "Alice"
138
134
  ```
139
135
 
140
- With EverythingRB, it's one elegant step:
141
-
142
136
  ```ruby
143
137
  # AFTER
144
138
  '{"user":{"name":"Alice","roles":["admin"]}}'.to_ostruct.user.name # => "Alice"
@@ -146,7 +140,7 @@ With EverythingRB, it's one elegant step:
146
140
 
147
141
  _Methods used: [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/String.html#to_ostruct-instance_method)_
148
142
 
149
- Convert between data structures with ease:
143
+ Convert between data structures:
150
144
 
151
145
  ```ruby
152
146
  # BEFORE
@@ -164,31 +158,11 @@ config.server.host # => "example.com"
164
158
 
165
159
  _Methods used: [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method)_
166
160
 
167
- Deep conversion to plain hashes is just as easy:
168
-
169
- ```ruby
170
- # BEFORE
171
- data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
172
- result = {
173
- user: {
174
- name: data.user.name
175
- }
176
- }
177
- ```
178
-
179
- ```ruby
180
- # AFTER
181
- data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
182
- data.to_deep_h # => {user: {name: "Bob"}}
183
- ```
184
-
185
- _Methods used: [`to_deep_h`](https://itsthedevman.com/docs/everythingrb/OpenStruct.html#to_deep_h-instance_method)_
186
-
187
- **Extensions:** [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method), [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_ostruct-instance_method), [`to_istruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_istruct-instance_method), [`to_h`](https://itsthedevman.com/docs/everythingrb/String.html#to_h-instance_method), [`to_deep_h`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_deep_h-instance_method)
161
+ **Extensions:** [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method), [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_ostruct-instance_method), [`to_istruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_istruct-instance_method), [`parse_json`](https://itsthedevman.com/docs/everythingrb/String.html#parse_json-instance_method)
188
162
 
189
163
  ### Collection Processing
190
164
 
191
- Extract and transform data with elegant, expressive code:
165
+ Extract and transform data:
192
166
 
193
167
  ```ruby
194
168
  # BEFORE
@@ -223,7 +197,7 @@ users.dig_map(:user, :profile, :name) # => ["Alice", "Bob"]
223
197
 
224
198
  _Methods used: [`dig_map`](https://itsthedevman.com/docs/everythingrb/Array.html#dig_map-instance_method)_
225
199
 
226
- Combine filter, map, and join operations in one step:
200
+ Combine filter, map, and join in one step:
227
201
 
228
202
  ```ruby
229
203
  # BEFORE
@@ -240,7 +214,7 @@ result = data.compact.filter_map { |n| "Item #{n}" if n.odd? }.join(" | ")
240
214
 
241
215
  _Methods used: [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method)_
242
216
 
243
- Need position-aware processing? Both Array and Hash support `with_index`:
217
+ Both Array and Hash support `with_index` when you need position-aware processing:
244
218
 
245
219
  ```ruby
246
220
  # BEFORE
@@ -257,7 +231,7 @@ users.join_map(", ", with_index: true) { |(k, v), i| "#{i + 1}. #{v}" }
257
231
 
258
232
  _Methods used: [`join_map`](https://itsthedevman.com/docs/everythingrb/Hash.html#join_map-instance_method)_
259
233
 
260
- Group elements with natural syntax:
234
+ Group by a nested key path directly:
261
235
 
262
236
  ```ruby
263
237
  # BEFORE
@@ -278,7 +252,7 @@ users.group_by_key(:department, :name)
278
252
 
279
253
  _Methods used: [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)_
280
254
 
281
- Create natural language lists:
255
+ Build 'or'-joined lists:
282
256
 
283
257
  ```ruby
284
258
  # BEFORE
@@ -305,9 +279,11 @@ _Methods used: [`to_or_sentence`](https://itsthedevman.com/docs/everythingrb/Arr
305
279
 
306
280
  **Extensions:** [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method), [`key_map`](https://itsthedevman.com/docs/everythingrb/Array.html#key_map-instance_method), [`dig_map`](https://itsthedevman.com/docs/everythingrb/Array.html#dig_map-instance_method), [`to_or_sentence`](https://itsthedevman.com/docs/everythingrb/Array.html#to_or_sentence-instance_method), [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)
307
281
 
308
- ### Hash Convenience
282
+ Here's just that section with the fixes:
283
+
284
+ ---
309
285
 
310
- Work with hashes more intuitively.
286
+ ### Hash Convenience
311
287
 
312
288
  Transform values with access to their keys:
313
289
 
@@ -344,13 +320,13 @@ admins = users.select { |_k, v| v[:role] == "admin" }.values
344
320
 
345
321
  ```ruby
346
322
  # AFTER
347
- users.values_where { |_k, v| v[:role] == "admin" }
323
+ users.select_values { |_k, v| v[:role] == "admin" }
348
324
  # => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
349
325
  ```
350
326
 
351
- _Methods used: [`values_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#values_where-instance_method)_
327
+ _Methods used: [`select_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#select_values-instance_method)_
352
328
 
353
- Just want the first match? Even simpler:
329
+ Just want the first match?
354
330
 
355
331
  ```ruby
356
332
  # BEFORE
@@ -360,11 +336,11 @@ users.find { |_k, v| v[:role] == "admin" }&.last
360
336
 
361
337
  ```ruby
362
338
  # AFTER
363
- users.value_where { |_k, v| v[:role] == "admin" }
339
+ users.find_value { |_k, v| v[:role] == "admin" }
364
340
  # => {name: "Alice", role: "admin"}
365
341
  ```
366
342
 
367
- _Methods used: [`value_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#value_where-instance_method)_
343
+ _Methods used: [`find_value`](https://itsthedevman.com/docs/everythingrb/Hash.html#find_value-instance_method)_
368
344
 
369
345
  Rename keys while preserving order:
370
346
 
@@ -374,14 +350,10 @@ config = {api_key: "secret", timeout: 30}
374
350
  new_config = config.each_with_object({}) do |(key, value), hash|
375
351
  new_key =
376
352
  case key
377
- when :api_key
378
- :key
379
- when :timeout
380
- :request_timeout
381
- else
382
- key
353
+ when :api_key then :key
354
+ when :timeout then :request_timeout
355
+ else key
383
356
  end
384
-
385
357
  hash[new_key] = value
386
358
  end
387
359
  # => {key: "secret", request_timeout: 30}
@@ -396,65 +368,45 @@ config.rename_keys(api_key: :key, timeout: :request_timeout)
396
368
 
397
369
  _Methods used: [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method)_
398
370
 
399
- Conditionally merge hashes with clear intent:
400
-
401
- ```ruby
402
- # BEFORE
403
- user_params = {name: "Alice", role: "user"}
404
- filtered = {verified: true, admin: true}.select { |k, v| v == true && k == :verified }
405
- user_params.merge(filtered)
406
- # => {name: "Alice", role: "user", verified: true}
407
- ```
408
-
409
- ```ruby
410
- # AFTER
411
- user_params = {name: "Alice", role: "user"}
412
- user_params.merge_if(verified: true, admin: true) { |k, v| v == true && k == :verified }
413
- # => {name: "Alice", role: "user", verified: true}
414
- ```
415
-
416
- _Methods used: [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method)_
417
-
418
- The nil-filtering pattern we've all written dozens of times:
371
+ Merge while dropping nils:
419
372
 
420
373
  ```ruby
421
374
  # BEFORE
422
375
  params = {sort: "created_at"}
423
- filtered = {filter: "active", search: nil}.compact
424
- params.merge(filtered)
376
+ search_params = {filter: "active", search: nil}.compact
377
+ params.merge(search_params)
425
378
  # => {sort: "created_at", filter: "active"}
426
379
  ```
427
380
 
428
381
  ```ruby
429
382
  # AFTER
430
- params = {sort: "created_at"}
431
- params.compact_merge(filter: "active", search: nil)
383
+ search_params = {filter: "active", search: nil}
384
+ params.compact_merge(search_params)
432
385
  # => {sort: "created_at", filter: "active"}
433
386
  ```
434
387
 
435
388
  _Methods used: [`compact_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_merge-instance_method)_
436
389
 
437
- Filter out blank values too when ActiveSupport is loaded:
390
+ Merge while dropping nils and blank values (requires ActiveSupport):
438
391
 
439
392
  ```ruby
440
393
  # BEFORE
441
- config = {api_key: "secret", timeout: 30}
442
- user_settings = {timeout: "", retries: nil, debug: true, tags: []}
443
- clean_settings = user_settings.reject { |k, v| v.blank? }
444
- config.merge(clean_settings)
445
- # => {api_key: "secret", timeout: 30, debug: true}
394
+ params = {sort: "created_at"}
395
+ search_params = {filter: "active", search: nil, query: ""}.reject { |_k, v| v.blank? }
396
+ params.merge(search_params)
397
+ # => {sort: "created_at", filter: "active"}
446
398
  ```
447
399
 
448
400
  ```ruby
449
401
  # AFTER
450
- config = {api_key: "secret", timeout: 30}
451
- config.compact_blank_merge(timeout: "", retries: nil, debug: true, tags: [])
452
- # => {api_key: "secret", timeout: 30, debug: true}
402
+ search_params = {filter: "active", search: nil, query: ""}
403
+ params.compact_blank_merge(search_params)
404
+ # => {sort: "created_at", filter: "active"}
453
405
  ```
454
406
 
455
407
  _Methods used: [`compact_blank_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_blank_merge-instance_method)_
456
408
 
457
- **Extensions:** [`transform_values(with_key: true)`](https://itsthedevman.com/docs/everythingrb/Hash.html#transform_values-instance_method), [`value_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#value_where-instance_method), [`values_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#values_where-instance_method), [`rename_key`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_key-instance_method), [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method), [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method), [`merge_if!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if%21-instance_method), [`merge_if_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values-instance_method), [`merge_if_values!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values%21-instance_method), [`compact_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_merge-instance_method), [`compact_merge!`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_merge%21-instance_method), [`compact_blank_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_blank_merge-instance_method), [`compact_blank_merge!`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_blank_merge%21-instance_method)
409
+ **Extensions:** [`transform_values(with_key: true)`](https://itsthedevman.com/docs/everythingrb/Hash.html#transform_values-instance_method), [`find_value`](https://itsthedevman.com/docs/everythingrb/Hash.html#find_value-instance_method), [`select_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#select_values-instance_method), [`rename_key`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_key-instance_method), [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method), [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method), [`merge_if!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if%21-instance_method), [`merge_if_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values-instance_method), [`merge_if_values!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values%21-instance_method), [`compact_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_merge-instance_method), [`compact_merge!`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_merge%21-instance_method), [`compact_blank_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_blank_merge-instance_method), [`compact_blank_merge!`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_blank_merge%21-instance_method)
458
410
 
459
411
  ### Array Cleaning
460
412
 
@@ -474,7 +426,7 @@ data.drop_while(&:nil?).reverse.drop_while(&:nil?).reverse
474
426
 
475
427
  _Methods used: [`trim_nils`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_nils-instance_method)_
476
428
 
477
- With ActiveSupport, remove blank values too:
429
+ With ActiveSupport, remove blank values from the edges too:
478
430
 
479
431
  ```ruby
480
432
  # BEFORE
@@ -494,7 +446,7 @@ _Methods used: [`trim_blanks`](https://itsthedevman.com/docs/everythingrb/Array.
494
446
 
495
447
  ### String Formatting
496
448
 
497
- Format strings and other values consistently:
449
+ Format values consistently without a helper method:
498
450
 
499
451
  ```ruby
500
452
  # BEFORE
@@ -533,7 +485,7 @@ message = "You selected #{selection.in_quotes}"
533
485
 
534
486
  _Methods used: [`in_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#in_quotes-instance_method), [`with_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#with_quotes-instance_method)_
535
487
 
536
- Convert strings to camelCase with ease:
488
+ Convert strings to camelCase:
537
489
 
538
490
  ```ruby
539
491
  # BEFORE
@@ -554,7 +506,7 @@ camel_case
554
506
  "user_profile_settings".to_camelcase # => "UserProfileSettings"
555
507
  "user_profile_settings".to_camelcase(:lower) # => "userProfileSettings"
556
508
 
557
- # Handles all kinds of input consistently
509
+ # Handles mixed input consistently
558
510
  "please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
559
511
  ```
560
512
 
@@ -564,7 +516,7 @@ _Methods used: [`to_camelcase`](https://itsthedevman.com/docs/everythingrb/Strin
564
516
 
565
517
  ### Boolean Methods
566
518
 
567
- Create predicate methods with minimal code:
519
+ Define predicate methods from any attribute:
568
520
 
569
521
  ```ruby
570
522
  # BEFORE
@@ -595,6 +547,39 @@ user.admin? # => true
595
547
 
596
548
  _Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)_
597
549
 
550
+ Map predicates to differently-named sources with `from:`:
551
+
552
+ ```ruby
553
+ # BEFORE
554
+ class Task
555
+ attr_accessor :started_at, :stopped_at
556
+
557
+ def started?
558
+ !@started_at.nil?
559
+ end
560
+
561
+ def finished?
562
+ !@stopped_at.nil?
563
+ end
564
+ end
565
+ ```
566
+
567
+ ```ruby
568
+ # AFTER
569
+ class Task
570
+ attr_accessor :started_at, :stopped_at
571
+ attr_predicate :started, from: :@started_at
572
+ attr_predicate :finished, from: :@stopped_at
573
+ end
574
+
575
+ task = Task.new
576
+ task.started? # => false
577
+ task.started_at = Time.now
578
+ task.started? # => true
579
+ ```
580
+
581
+ _Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)_
582
+
598
583
  Works with Data objects too:
599
584
 
600
585
  ```ruby
@@ -619,7 +604,7 @@ _Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Mod
619
604
 
620
605
  ### Value Transformation
621
606
 
622
- Chain transformations with a more descriptive syntax:
607
+ An alias for `then`/`yield_self` that reads more naturally in transformation chains:
623
608
 
624
609
  ```ruby
625
610
  # BEFORE
@@ -649,10 +634,4 @@ Bug reports and pull requests are welcome! This project is intended to be a safe
649
634
 
650
635
  ## License
651
636
 
652
- [MIT License](LICENSE.txt)
653
-
654
- ## Looking for a Software Engineer?
655
-
656
- I'm currently looking for opportunities where I can tackle meaningful problems and help build reliable software while mentoring the next generation of developers. If you're looking for a senior engineer with full-stack Rails expertise and a passion for clean, maintainable code, let's talk!
657
-
658
- [bryan@itsthedevman.com](mailto:bryan@itsthedevman.com)
637
+ [MIT License](LICENSE.txt)
data/Rakefile CHANGED
@@ -17,3 +17,27 @@ require "standard/rake"
17
17
 
18
18
  task test: [:test_regular, :test_active_support]
19
19
  task default: %i[test standard]
20
+
21
+ # Benchmark tasks
22
+ namespace :benchmark do
23
+ desc "Run all benchmarks"
24
+ task :all do
25
+ ruby "benchmarks/run_all.rb"
26
+ end
27
+
28
+ desc "Run all benchmarks with ActiveSupport"
29
+ task :all_with_active_support do
30
+ ENV["LOAD_ACTIVE_SUPPORT"] = "true"
31
+ ruby "benchmarks/run_all.rb"
32
+ end
33
+
34
+ %w[array hash string enumerable ostruct module kernel quotable].each do |name|
35
+ desc "Run #{name} benchmarks"
36
+ task name.to_sym do
37
+ ruby "benchmarks/#{name}_benchmark.rb"
38
+ end
39
+ end
40
+ end
41
+
42
+ desc "Run all benchmarks"
43
+ task benchmark: "benchmark:all"
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "benchmark_helper"
4
+
5
+ BenchmarkHelper.header("Array Extension Benchmarks")
6
+
7
+ # Test data
8
+ small_array = (1..10).to_a
9
+ medium_array = (1..100).to_a
10
+ large_array = (1..1000).to_a
11
+
12
+ small_hashes = [{name: "Alice", age: 30}, {name: "Bob", age: 25}, {name: "Charlie", age: 35}]
13
+ medium_hashes = (1..100).map { |i| {name: "User#{i}", age: 20 + (i % 50)} }
14
+ large_hashes = (1..1000).map { |i| {name: "User#{i}", age: 20 + (i % 50)} }
15
+
16
+ nested_hashes = [
17
+ {user: {profile: {name: "Alice"}}},
18
+ {user: {profile: {name: "Bob"}}},
19
+ {user: {profile: {name: "Charlie"}}}
20
+ ]
21
+
22
+ array_with_nils_prefix = [nil, nil, nil, 1, 2, 3, nil, 4, 5]
23
+ array_with_nils_suffix = [1, 2, 3, nil, 4, 5, nil, nil, nil]
24
+ array_with_nils_both = [nil, nil, 1, 2, nil, 3, 4, nil, nil]
25
+
26
+ # ============================================================================
27
+ # join_map benchmarks
28
+ # ============================================================================
29
+
30
+ BenchmarkHelper.run("join_map - small array (10 elements)") do |x|
31
+ x.report("join_map") { small_array.join_map(", ") { |n| n.to_s if n.even? } }
32
+ x.report("join_map(with_index)") { small_array.join_map(", ", with_index: true) { |n, i| "#{i}:#{n}" } }
33
+ end
34
+
35
+ BenchmarkHelper.run("join_map - medium array (100 elements)") do |x|
36
+ x.report("join_map") { medium_array.join_map(", ") { |n| n.to_s if n.even? } }
37
+ x.report("join_map(with_index)") { medium_array.join_map(", ", with_index: true) { |n, i| "#{i}:#{n}" } }
38
+ end
39
+
40
+ BenchmarkHelper.run("join_map - large array (1000 elements)") do |x|
41
+ x.report("join_map") { large_array.join_map(", ") { |n| n.to_s if n.even? } }
42
+ x.report("join_map(with_index)") { large_array.join_map(", ", with_index: true) { |n, i| "#{i}:#{n}" } }
43
+ end
44
+
45
+ # ============================================================================
46
+ # key_map benchmarks
47
+ # ============================================================================
48
+
49
+ BenchmarkHelper.run("key_map - small array of hashes (3 elements)") do |x|
50
+ x.report("key_map") { small_hashes.key_map(:name) }
51
+ end
52
+
53
+ BenchmarkHelper.run("key_map - medium array of hashes (100 elements)") do |x|
54
+ x.report("key_map") { medium_hashes.key_map(:name) }
55
+ end
56
+
57
+ BenchmarkHelper.run("key_map - large array of hashes (1000 elements)") do |x|
58
+ x.report("key_map") { large_hashes.key_map(:name) }
59
+ end
60
+
61
+ # ============================================================================
62
+ # dig_map benchmarks
63
+ # ============================================================================
64
+
65
+ BenchmarkHelper.run("dig_map - nested hashes") do |x|
66
+ x.report("dig_map(:user, :profile, :name)") { nested_hashes.dig_map(:user, :profile, :name) }
67
+ end
68
+
69
+ # ============================================================================
70
+ # compact_prefix / compact_suffix / trim_nils benchmarks
71
+ # ============================================================================
72
+
73
+ BenchmarkHelper.run("nil trimming operations") do |x|
74
+ x.report("compact_prefix") { array_with_nils_prefix.compact_prefix }
75
+ x.report("compact_suffix") { array_with_nils_suffix.compact_suffix }
76
+ x.report("trim_nils") { array_with_nils_both.trim_nils }
77
+ end
78
+
79
+ # ============================================================================
80
+ # in_quotes benchmarks
81
+ # ============================================================================
82
+
83
+ BenchmarkHelper.run("in_quotes / with_quotes") do |x|
84
+ x.report("small array in_quotes") { small_array.in_quotes }
85
+ x.report("medium array in_quotes") { medium_array.in_quotes }
86
+ end
87
+
88
+ # ============================================================================
89
+ # ActiveSupport-only benchmarks
90
+ # ============================================================================
91
+
92
+ if BenchmarkHelper.active_support?
93
+ array_with_blanks = ["", nil, "foo", "", "bar", nil, ""]
94
+ words = %w[red blue green]
95
+
96
+ BenchmarkHelper.run("compact_blank_prefix / compact_blank_suffix / trim_blanks") do |x|
97
+ x.report("compact_blank_prefix") { array_with_blanks.compact_blank_prefix }
98
+ x.report("compact_blank_suffix") { array_with_blanks.compact_blank_suffix }
99
+ x.report("trim_blanks") { array_with_blanks.trim_blanks }
100
+ end
101
+
102
+ BenchmarkHelper.run("to_or_sentence") do |x|
103
+ x.report("to_or_sentence") { words.to_or_sentence }
104
+ x.report("to_or_sentence(custom)") { words.to_or_sentence(locale: :en) }
105
+ end
106
+ else
107
+ puts "\n[SKIPPED] ActiveSupport benchmarks - run with LOAD_ACTIVE_SUPPORT=true"
108
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "benchmark/ips"
5
+
6
+ # Load ActiveSupport first if requested
7
+ if ENV["LOAD_ACTIVE_SUPPORT"] == "true"
8
+ require "active_support"
9
+ require "active_support/core_ext"
10
+ end
11
+
12
+ require "everythingrb"
13
+
14
+ # Helper module for benchmark formatting and utilities
15
+ module BenchmarkHelper
16
+ class << self
17
+ def header(title)
18
+ puts "\n" + "=" * 70
19
+ puts title.center(70)
20
+ puts "=" * 70
21
+ end
22
+
23
+ def section(title)
24
+ puts "\n#{"-" * 30}"
25
+ puts title
26
+ puts "-" * 30
27
+ end
28
+
29
+ def run(title, warmup: 2, time: 5, &block)
30
+ section(title)
31
+
32
+ Benchmark.ips do |x|
33
+ x.config(warmup:, time:)
34
+ block.call(x)
35
+ end
36
+ end
37
+
38
+ def active_support?
39
+ defined?(ActiveSupport)
40
+ end
41
+ end
42
+ end