everythingrb 0.9.0 → 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: cd5be3e26181ed61cc29aee8e316f3fd10d3bc9f5ea1d424069c4aefdc1c8125
4
- data.tar.gz: 86517a10235873c3075237ff7f774315dc390d68fe3cf23ee751f15ca750508f
3
+ metadata.gz: c1aaf48c681b67675cfd4a3e053086163c755a47fc3941507ac0b08bb33ddf09
4
+ data.tar.gz: 2e9389e279e225463b03d0ccaac1db55444aa3d5c99f4ec06c5d86e89a235efa
5
5
  SHA512:
6
- metadata.gz: 643b0c34ffa81f136d1abb91def2e6bdbb65a00888a735cdef95950db9877d3ea0d06c22119c36f3909283985711792bc0530944107ed76d744742daff40dd3c
7
- data.tar.gz: 40db61bcf4b8c77a791b07fa30fcdd8e4d3b1f0054ff9124030d15b67ad8194db312cbb6f1ba2261499e5431132952746a5ef184ced15e212401207c15bf6dd6
6
+ metadata.gz: 16cdf21b81457e94d393b246dd762c33400de2e39b01435be9334946cdb2b0639583db9064fdac7c2053e6bc533f3b699fcdd5fe7c119e3052f1b51e8cb911ae
7
+ data.tar.gz: fc9e95bbd7b0dd0fdbc5a8474f00ffccc0968e703b45c63a7aef1ddc8b6f0418123dfb7474813ce03e889f35244c6e7bf42cab368d31e5db4174551a93ee9f20
data/CHANGELOG.md CHANGED
@@ -15,6 +15,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  ### Removed
16
16
  -->
17
17
 
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`.
24
+
25
+ ### Changed
26
+
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.
38
+
18
39
  ## [0.9.0] - 12025-08-01
19
40
 
20
41
  ### Added
@@ -206,10 +227,6 @@ This change aligns our method signatures with Ruby's conventions and matches our
206
227
  - `#rename_keys!` - Same as `#rename_keys` but modifies the hash in place
207
228
  - `#rename_key_unordered` - Renames a key without preserving element order (faster operation)
208
229
  - `#rename_key_unordered!` - Same as `#rename_key_unordered` but modifies the hash in place
209
- - Added `to_deep_h` to core Ruby classes for consistent deep hash conversion:
210
- - `Struct#to_deep_h` - Recursively converts Struct objects and all nested values to hashes
211
- - `OpenStruct#to_deep_h` - Recursively converts OpenStruct objects and all nested values to hashes
212
- - `Data#to_deep_h` - Recursively converts Data objects and all nested values to hashes
213
230
  - Added `depth` parameter to `Hash.new_nested_hash` to control nesting behaviors
214
231
 
215
232
  ### Changed
@@ -235,7 +252,6 @@ This change aligns our method signatures with Ruby's conventions and matches our
235
252
 
236
253
  - Added `Array#to_or_sentence`, creates a sentence with "or" connector between items
237
254
  - Added `#with_key` method to `Hash#transform_values` and `Hash#transform_values!`, grants access to both keys and values during transformations
238
- - Added `Array#to_deep_h` and `Hash#to_deep_h`, recursively converts underlying values to hashes
239
255
  - Added `Enumerable#group_by_key`, group an array of hashes by their keys
240
256
  - Added `Hash#new_nested_hash`, creates a new Hash that automatically initializes the value to a hash
241
257
  - Added `Hash#value_where` and `Hash#values_where`, easily find values in a hash based on key-value conditions
@@ -335,14 +351,14 @@ This change aligns our method signatures with Ruby's conventions and matches our
335
351
  - `join_map` method consistent with Array/Hash implementations
336
352
  - Enhanced `String` class with:
337
353
  - `to_h` and `to_a` methods for JSON parsing with `nil` fallback on error
338
- - `to_deep_h` for recursive JSON string parsing
339
354
  - `to_istruct`, `to_ostruct`, and `to_struct` conversion methods
340
355
 
341
356
  ### Changed
342
357
 
343
358
  - Added alias `each` to `each_pair` in OpenStruct for better enumerable compatibility
344
359
 
345
- [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.9.0...HEAD
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
346
362
  [0.9.0]: https://github.com/itsthedevman/everythingrb/compare/v0.8.3...v0.9.0
347
363
  [0.8.3]: https://github.com/itsthedevman/everythingrb/compare/v0.8.2...v0.8.3
348
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,28 +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:
309
283
 
310
- Work with hashes more intuitively:
284
+ ---
311
285
 
312
- ```ruby
313
- # BEFORE
314
- stats = {}
315
- stats[:server] ||= {}
316
- stats[:server][:region] ||= {}
317
- stats[:server][:region][:us_east] ||= {}
318
- stats[:server][:region][:us_east][:errors] ||= []
319
- stats[:server][:region][:us_east][:errors] << "Connection timeout"
320
- ```
321
-
322
- ```ruby
323
- # AFTER
324
- stats = Hash.new_nested_hash(depth: 3)
325
- (stats[:server][:region][:us_east][:errors] ||= []) << "Connection timeout"
326
- # No need to initialize each level first!
327
- ```
328
-
329
- _Methods used: [`new_nested_hash`](https://itsthedevman.com/docs/everythingrb/Hash.html#new_nested_hash-class_method)_
286
+ ### Hash Convenience
330
287
 
331
288
  Transform values with access to their keys:
332
289
 
@@ -363,13 +320,13 @@ admins = users.select { |_k, v| v[:role] == "admin" }.values
363
320
 
364
321
  ```ruby
365
322
  # AFTER
366
- users.values_where { |_k, v| v[:role] == "admin" }
323
+ users.select_values { |_k, v| v[:role] == "admin" }
367
324
  # => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
368
325
  ```
369
326
 
370
- _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)_
371
328
 
372
- Just want the first match? Even simpler:
329
+ Just want the first match?
373
330
 
374
331
  ```ruby
375
332
  # BEFORE
@@ -379,11 +336,11 @@ users.find { |_k, v| v[:role] == "admin" }&.last
379
336
 
380
337
  ```ruby
381
338
  # AFTER
382
- users.value_where { |_k, v| v[:role] == "admin" }
339
+ users.find_value { |_k, v| v[:role] == "admin" }
383
340
  # => {name: "Alice", role: "admin"}
384
341
  ```
385
342
 
386
- _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)_
387
344
 
388
345
  Rename keys while preserving order:
389
346
 
@@ -393,14 +350,10 @@ config = {api_key: "secret", timeout: 30}
393
350
  new_config = config.each_with_object({}) do |(key, value), hash|
394
351
  new_key =
395
352
  case key
396
- when :api_key
397
- :key
398
- when :timeout
399
- :request_timeout
400
- else
401
- key
353
+ when :api_key then :key
354
+ when :timeout then :request_timeout
355
+ else key
402
356
  end
403
-
404
357
  hash[new_key] = value
405
358
  end
406
359
  # => {key: "secret", request_timeout: 30}
@@ -415,65 +368,45 @@ config.rename_keys(api_key: :key, timeout: :request_timeout)
415
368
 
416
369
  _Methods used: [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method)_
417
370
 
418
- Conditionally merge hashes with clear intent:
419
-
420
- ```ruby
421
- # BEFORE
422
- user_params = {name: "Alice", role: "user"}
423
- filtered = {verified: true, admin: true}.select { |k, v| v == true && k == :verified }
424
- user_params.merge(filtered)
425
- # => {name: "Alice", role: "user", verified: true}
426
- ```
427
-
428
- ```ruby
429
- # AFTER
430
- user_params = {name: "Alice", role: "user"}
431
- user_params.merge_if(verified: true, admin: true) { |k, v| v == true && k == :verified }
432
- # => {name: "Alice", role: "user", verified: true}
433
- ```
434
-
435
- _Methods used: [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method)_
436
-
437
- The nil-filtering pattern we've all written dozens of times:
371
+ Merge while dropping nils:
438
372
 
439
373
  ```ruby
440
374
  # BEFORE
441
375
  params = {sort: "created_at"}
442
- filtered = {filter: "active", search: nil}.compact
443
- params.merge(filtered)
376
+ search_params = {filter: "active", search: nil}.compact
377
+ params.merge(search_params)
444
378
  # => {sort: "created_at", filter: "active"}
445
379
  ```
446
380
 
447
381
  ```ruby
448
382
  # AFTER
449
- params = {sort: "created_at"}
450
- params.compact_merge(filter: "active", search: nil)
383
+ search_params = {filter: "active", search: nil}
384
+ params.compact_merge(search_params)
451
385
  # => {sort: "created_at", filter: "active"}
452
386
  ```
453
387
 
454
388
  _Methods used: [`compact_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_merge-instance_method)_
455
389
 
456
- Filter out blank values too when ActiveSupport is loaded:
390
+ Merge while dropping nils and blank values (requires ActiveSupport):
457
391
 
458
392
  ```ruby
459
393
  # BEFORE
460
- config = {api_key: "secret", timeout: 30}
461
- user_settings = {timeout: "", retries: nil, debug: true, tags: []}
462
- clean_settings = user_settings.reject { |k, v| v.blank? }
463
- config.merge(clean_settings)
464
- # => {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"}
465
398
  ```
466
399
 
467
400
  ```ruby
468
401
  # AFTER
469
- config = {api_key: "secret", timeout: 30}
470
- config.compact_blank_merge(timeout: "", retries: nil, debug: true, tags: [])
471
- # => {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"}
472
405
  ```
473
406
 
474
407
  _Methods used: [`compact_blank_merge`](https://itsthedevman.com/docs/everythingrb/Hash.html#compact_blank_merge-instance_method)_
475
408
 
476
- **Extensions:** [`new_nested_hash`](https://itsthedevman.com/docs/everythingrb/Hash.html#new_nested_hash-class_method), [`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)
477
410
 
478
411
  ### Array Cleaning
479
412
 
@@ -493,7 +426,7 @@ data.drop_while(&:nil?).reverse.drop_while(&:nil?).reverse
493
426
 
494
427
  _Methods used: [`trim_nils`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_nils-instance_method)_
495
428
 
496
- With ActiveSupport, remove blank values too:
429
+ With ActiveSupport, remove blank values from the edges too:
497
430
 
498
431
  ```ruby
499
432
  # BEFORE
@@ -513,7 +446,7 @@ _Methods used: [`trim_blanks`](https://itsthedevman.com/docs/everythingrb/Array.
513
446
 
514
447
  ### String Formatting
515
448
 
516
- Format strings and other values consistently:
449
+ Format values consistently without a helper method:
517
450
 
518
451
  ```ruby
519
452
  # BEFORE
@@ -552,7 +485,7 @@ message = "You selected #{selection.in_quotes}"
552
485
 
553
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)_
554
487
 
555
- Convert strings to camelCase with ease:
488
+ Convert strings to camelCase:
556
489
 
557
490
  ```ruby
558
491
  # BEFORE
@@ -573,7 +506,7 @@ camel_case
573
506
  "user_profile_settings".to_camelcase # => "UserProfileSettings"
574
507
  "user_profile_settings".to_camelcase(:lower) # => "userProfileSettings"
575
508
 
576
- # Handles all kinds of input consistently
509
+ # Handles mixed input consistently
577
510
  "please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
578
511
  ```
579
512
 
@@ -583,7 +516,7 @@ _Methods used: [`to_camelcase`](https://itsthedevman.com/docs/everythingrb/Strin
583
516
 
584
517
  ### Boolean Methods
585
518
 
586
- Create predicate methods with minimal code:
519
+ Define predicate methods from any attribute:
587
520
 
588
521
  ```ruby
589
522
  # BEFORE
@@ -614,6 +547,39 @@ user.admin? # => true
614
547
 
615
548
  _Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)_
616
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
+
617
583
  Works with Data objects too:
618
584
 
619
585
  ```ruby
@@ -638,7 +604,7 @@ _Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Mod
638
604
 
639
605
  ### Value Transformation
640
606
 
641
- Chain transformations with a more descriptive syntax:
607
+ An alias for `then`/`yield_self` that reads more naturally in transformation chains:
642
608
 
643
609
  ```ruby
644
610
  # BEFORE
@@ -668,10 +634,4 @@ Bug reports and pull requests are welcome! This project is intended to be a safe
668
634
 
669
635
  ## License
670
636
 
671
- [MIT License](LICENSE.txt)
672
-
673
- ## Looking for a Software Engineer?
674
-
675
- 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!
676
-
677
- [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