sortsmith 1.0.0 → 1.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 +4 -4
- data/CHANGELOG.md +30 -1
- data/README.md +72 -13
- data/Rakefile +15 -0
- data/benchmark/quick_benchmark.rb +48 -0
- data/benchmark/sorting_benchmark.rb +206 -0
- data/flake.lock +76 -4
- data/flake.nix +8 -6
- data/lib/sortsmith/sorter.rb +168 -87
- data/lib/sortsmith/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 594500201df14a274a22465d71f25e9471d67a9f6154047d73826974bdec2d62
|
|
4
|
+
data.tar.gz: cbf4a37cfeb106380825ea34aa9963c9342b2c2c8776149df7e1a1f2c9b60582
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1dba596a763ca288fa3bd45bc5d5f8f4dd7cbd5683e886bd4e765957d1ae4225605454af524825073a006cd63fe5c183063b613aa4317abb6ad3b2ffcf00739b
|
|
7
|
+
data.tar.gz: aa1d3317d8486227a8e33da7633101afa356fdf2a640ff678d1f8d1b140bd734abce58fc3767899f352609574c00400e02106e47fd1e404540ef9087490da9c6
|
data/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
### Removed
|
|
16
16
|
-->
|
|
17
17
|
|
|
18
|
+
## [1.0.1] - 12026-02-15
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
#### Nil Handling Control
|
|
23
|
+
|
|
24
|
+
- **`nil_first`** - Position nil values at the beginning of sort results
|
|
25
|
+
- **`nil_last`** - Explicitly position nil values at the end (default behavior)
|
|
26
|
+
- Nil positioning is independent of `asc`/`desc` modifiers for predictable behavior
|
|
27
|
+
- Graceful handling of nil values during comparison (should be no more comparison errors)
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
#### Internal Implementation
|
|
32
|
+
|
|
33
|
+
- **Major performance improvement**: Refactored sorting to use Schwartzian Transform (extract once, sort, map back) instead of comparison sort
|
|
34
|
+
- Reduced overhead from ~65x slower to ~5.9x slower compared to native Ruby sort (measured on Ruby 3.2.9, AMD Ryzen 7 3700X, 32GB RAM)
|
|
35
|
+
- ~11x speedup for typical use cases
|
|
36
|
+
- Tests now run 4.4x faster
|
|
37
|
+
- Refactored `asc`/`desc` from array ordering to comparison-time flag for better performance and consistency
|
|
38
|
+
- Improved nil comparison logic to handle edge cases consistently
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Fixed comparison errors when sorting collections with nil values
|
|
43
|
+
- Fixed `NoMethodError` when trying to negate nil result in descending sorts
|
|
44
|
+
|
|
45
|
+
|
|
18
46
|
## [1.0.0] - 12025-08-03
|
|
19
47
|
|
|
20
48
|
### 🎉 API Stability Milestone
|
|
@@ -185,7 +213,8 @@ objects.sort_by.dig(:calculate_score).sort
|
|
|
185
213
|
- Type checking with Steep/RBS
|
|
186
214
|
- GitHub Actions workflow for automated testing and type checking
|
|
187
215
|
|
|
188
|
-
[unreleased]: https://github.com/itsthedevman/sortsmith/compare/v1.0.
|
|
216
|
+
[unreleased]: https://github.com/itsthedevman/sortsmith/compare/v1.0.1...HEAD
|
|
217
|
+
[1.0.1]: https://github.com/itsthedevman/sortsmith/compare/v1.0.0...v1.0.1
|
|
189
218
|
[1.0.0]: https://github.com/itsthedevman/sortsmith/compare/v0.9.0...v1.0.0
|
|
190
219
|
[0.9.0]: https://github.com/itsthedevman/sortsmith/compare/v0.2.0...v0.9.0
|
|
191
220
|
[0.2.0]: https://github.com/itsthedevman/sortsmith/compare/v0.1.1...v0.2.0
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
users.sort_by { |user| user[:name].downcase }.reverse
|
|
12
12
|
|
|
13
13
|
# Write this!
|
|
14
|
-
users.sort_by
|
|
14
|
+
users.sort_by(:name).downcase.reverse
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Sortsmith extends Ruby's built-in `sort_by` method with a fluent, chainable API that handles the messy details so you can focus on expressing _what_ you want sorted, not _how_ to sort it.
|
|
@@ -46,12 +46,19 @@ users.sort_by { |u| (u[:name] || "").downcase }.reverse
|
|
|
46
46
|
|
|
47
47
|
# What about mixed string/symbol keys?
|
|
48
48
|
users.sort_by { |u| (u[:name] || u["name"] || "").downcase }.reverse
|
|
49
|
+
|
|
50
|
+
# Need nils first instead of last?
|
|
51
|
+
nils, non_nils = users.partition { |u| u[:name].nil? }
|
|
52
|
+
nils + non_nils.sort_by { |u| u[:name].downcase }
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
Sortsmith handles all the edge cases and gives you a clean, readable API:
|
|
52
56
|
|
|
53
57
|
```ruby
|
|
54
|
-
users.sort_by.dig(:name
|
|
58
|
+
users.sort_by.dig(:name).insensitive.desc.sort
|
|
59
|
+
|
|
60
|
+
# With nils first
|
|
61
|
+
users.sort_by.dig(:name).insensitive.nil_first.sort
|
|
55
62
|
```
|
|
56
63
|
|
|
57
64
|
**Features:**
|
|
@@ -59,7 +66,7 @@ users.sort_by.dig(:name, indifferent: true).insensitive.desc.sort
|
|
|
59
66
|
- **Fluent chaining** - Reads like English
|
|
60
67
|
- **Universal extraction** - Works with hashes, objects, and nested data
|
|
61
68
|
- **Indifferent key access** - Handles mixed symbol/string keys automatically
|
|
62
|
-
- **Nil-safe** - Graceful handling of missing data
|
|
69
|
+
- **Nil-safe** - Graceful handling of missing data with control over nil positioning
|
|
63
70
|
- **Minimal overhead** - Extends existing Ruby methods without breaking compatibility
|
|
64
71
|
|
|
65
72
|
## Installation
|
|
@@ -89,7 +96,7 @@ Sortsmith extends Ruby's `sort_by` method with a fluent, chainable API. Use it w
|
|
|
89
96
|
```ruby
|
|
90
97
|
require "sortsmith"
|
|
91
98
|
|
|
92
|
-
# Direct syntax for simple cases
|
|
99
|
+
# Direct syntax for simple cases
|
|
93
100
|
names = ["charlie", "Alice", "bob"]
|
|
94
101
|
names.sort_by(:name).insensitive.sort
|
|
95
102
|
# => ["Alice", "bob", "charlie"]
|
|
@@ -104,7 +111,7 @@ users = [
|
|
|
104
111
|
users.sort_by.dig(:name).sort
|
|
105
112
|
# => [{ name: "Alice", age: 30 }, { name: "Bob", age: 20 }, { name: "Charlie", age: 25 }]
|
|
106
113
|
|
|
107
|
-
# Seamless integration with enumerable methods
|
|
114
|
+
# Seamless integration with enumerable methods
|
|
108
115
|
users.sort_by(:age).desc.first(2)
|
|
109
116
|
# => [{ name: "Alice", age: 30 }, { name: "Charlie", age: 25 }]
|
|
110
117
|
|
|
@@ -267,24 +274,51 @@ api_response.sort_by.dig(:name, indifferent: true).sort
|
|
|
267
274
|
|
|
268
275
|
### Universal Extraction
|
|
269
276
|
|
|
270
|
-
- **`sort_by(field, **opts)
|
|
277
|
+
- **`sort_by(field, **opts)`** - Direct field extraction
|
|
271
278
|
- Works with hashes, objects, and any method name
|
|
272
279
|
- Supports all the same options as `dig` and `method`
|
|
273
280
|
|
|
274
281
|
### Extractors
|
|
275
282
|
|
|
276
283
|
- **`dig(*identifiers, indifferent: false)`** - Extract values from hashes, objects, or nested structures
|
|
277
|
-
- **`method(method_name,
|
|
278
|
-
- **`key(
|
|
279
|
-
- **`field(
|
|
280
|
-
- **`attribute(method_name,
|
|
284
|
+
- **`method(method_name, *args, **kwargs)`** - Call methods on objects with arguments
|
|
285
|
+
- **`key(*identifiers, **opts)`** - Alias for `dig` (semantic clarity for hash keys)
|
|
286
|
+
- **`field(*identifiers, **opts)`** - Alias for `dig` (semantic clarity for object fields)
|
|
287
|
+
- **`attribute(method_name, *args, **kwargs)`** - Alias for `method` (semantic clarity)
|
|
281
288
|
|
|
282
289
|
### Modifiers
|
|
283
290
|
|
|
284
291
|
- **`downcase`** - Convert extracted strings to lowercase for comparison
|
|
285
292
|
- **`upcase`** - Convert extracted strings to uppercase for comparison
|
|
286
293
|
- **`insensitive`** - Alias for `downcase` (semantic clarity)
|
|
287
|
-
- **`case_insensitive`** - Alias for `downcase` (explicit case handling)
|
|
294
|
+
- **`case_insensitive`** - Alias for `downcase` (explicit case handling)
|
|
295
|
+
|
|
296
|
+
### Nil Handling (NEW!)
|
|
297
|
+
|
|
298
|
+
- **`nil_first`** - Position nil values at the beginning of results
|
|
299
|
+
- **`nil_last`** - Position nil values at the end of results (default)
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
users = [
|
|
303
|
+
{ name: "Bob" },
|
|
304
|
+
{ name: nil },
|
|
305
|
+
{ name: "Alice" }
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
# Default: nils last
|
|
309
|
+
users.sort_by.dig(:name).sort
|
|
310
|
+
# => [{ name: "Alice" }, { name: "Bob" }, { name: nil }]
|
|
311
|
+
|
|
312
|
+
# Nils first
|
|
313
|
+
users.sort_by.dig(:name).nil_first.sort
|
|
314
|
+
# => [{ name: nil }, { name: "Alice" }, { name: "Bob" }]
|
|
315
|
+
|
|
316
|
+
# Nil positioning is independent of sort direction
|
|
317
|
+
users.sort_by.dig(:name).nil_first.desc.sort
|
|
318
|
+
# => [{ name: nil }, { name: "Bob" }, { name: "Alice" }]
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Note**: Nil positioning is independent of `asc`/`desc` modifiers. `nil_first` always places nils at the beginning, and `nil_last` always places them at the end, regardless of whether the non-nil values are sorted ascending or descending.
|
|
288
322
|
|
|
289
323
|
### Ordering
|
|
290
324
|
|
|
@@ -296,11 +330,11 @@ api_response.sort_by.dig(:name, indifferent: true).sort
|
|
|
296
330
|
- **`sort`** - Execute sort and return new array
|
|
297
331
|
- **`sort!`** - Execute sort and mutate original array
|
|
298
332
|
- **`to_a`** - Alias for `sort`
|
|
299
|
-
- **`to_a!`** - Alias for `sort!`
|
|
333
|
+
- **`to_a!`** - Alias for `sort!`
|
|
300
334
|
- **`reverse`** - Shorthand for `desc.sort`
|
|
301
335
|
- **`reverse!`** - Shorthand for `desc.sort!`
|
|
302
336
|
|
|
303
|
-
### Delegated Enumerable Methods
|
|
337
|
+
### Delegated Enumerable Methods
|
|
304
338
|
|
|
305
339
|
The following methods execute the sort and delegate to the resulting array:
|
|
306
340
|
|
|
@@ -345,6 +379,31 @@ bundle install
|
|
|
345
379
|
bundle exec rake test
|
|
346
380
|
```
|
|
347
381
|
|
|
382
|
+
### Benchmarks
|
|
383
|
+
|
|
384
|
+
Run performance benchmarks to compare Sortsmith with native Ruby:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
# Quick benchmark
|
|
388
|
+
bundle exec rake benchmark
|
|
389
|
+
|
|
390
|
+
# Full benchmark suite
|
|
391
|
+
bundle exec rake benchmark:full
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### Performance
|
|
395
|
+
|
|
396
|
+
Sortsmith prioritizes developer ergonomics and code readability over raw performance. Benchmarks show approximately **5.7x-6.5x slower** than native Ruby `sort_by` for typical use cases:
|
|
397
|
+
|
|
398
|
+
- Basic sorting: **5.75x slower**
|
|
399
|
+
- Case-insensitive + descending: **5.65x slower**
|
|
400
|
+
- With nil values: **6.06x slower**
|
|
401
|
+
- Top N selection: **6.50x slower**
|
|
402
|
+
|
|
403
|
+
*Benchmarked on Ruby 3.2.9, AMD Ryzen 7 3700X 8-Core Processor, 32GB RAM*
|
|
404
|
+
|
|
405
|
+
The performance overhead is typically measured in microseconds for small to medium datasets, making it negligible for most real-world applications where code clarity and maintainability are more important than micro-optimizations.
|
|
406
|
+
|
|
348
407
|
### Code Style
|
|
349
408
|
|
|
350
409
|
This project uses StandardRB. To check your code:
|
data/Rakefile
CHANGED
|
@@ -8,3 +8,18 @@ Minitest::TestTask.create
|
|
|
8
8
|
require "standard/rake"
|
|
9
9
|
|
|
10
10
|
task default: %i[test standard]
|
|
11
|
+
|
|
12
|
+
namespace :benchmark do
|
|
13
|
+
desc "Run quick benchmark"
|
|
14
|
+
task :quick do
|
|
15
|
+
ruby "benchmark/quick_benchmark.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Run full benchmark suite"
|
|
19
|
+
task :full do
|
|
20
|
+
ruby "benchmark/sorting_benchmark.rb"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "Run quick benchmark"
|
|
25
|
+
task benchmark: "benchmark:quick"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "sortsmith"
|
|
6
|
+
require "benchmark/ips"
|
|
7
|
+
|
|
8
|
+
# Quick comparison of common scenarios
|
|
9
|
+
puts "Quick Performance Comparison"
|
|
10
|
+
puts "=" * 60
|
|
11
|
+
|
|
12
|
+
users = Array.new(1000) do |i|
|
|
13
|
+
{
|
|
14
|
+
name: ["Alice", "bob", "Charlie", "DIANA"].sample,
|
|
15
|
+
score: rand(0..100),
|
|
16
|
+
age: i.even? ? rand(18..65) : nil
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
puts "\n1. Basic sorting"
|
|
21
|
+
Benchmark.ips do |x|
|
|
22
|
+
x.report("native") { users.sort_by { |u| u[:name] } }
|
|
23
|
+
x.report("sortsmith") { users.sort_by.dig(:name).sort }
|
|
24
|
+
x.compare!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
puts "\n2. Case-insensitive + descending"
|
|
28
|
+
Benchmark.ips do |x|
|
|
29
|
+
x.report("native") { users.sort_by { |u| u[:name].to_s.downcase }.reverse }
|
|
30
|
+
x.report("sortsmith") { users.sort_by.dig(:name).insensitive.desc.sort }
|
|
31
|
+
x.compare!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts "\n3. With nil values"
|
|
35
|
+
Benchmark.ips do |x|
|
|
36
|
+
x.report("native") { users.sort_by { |u| u[:age] || 0 } }
|
|
37
|
+
x.report("sortsmith") { users.sort_by.dig(:age).sort }
|
|
38
|
+
x.compare!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
puts "\n4. Top 5 by score"
|
|
42
|
+
Benchmark.ips do |x|
|
|
43
|
+
x.report("native") { users.sort_by { |u| u[:score] }.last(5).reverse }
|
|
44
|
+
x.report("sortsmith") { users.sort_by.dig(:score).desc.first(5) }
|
|
45
|
+
x.compare!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
puts "\n" + "=" * 60
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "sortsmith"
|
|
6
|
+
require "benchmark/ips"
|
|
7
|
+
|
|
8
|
+
# Sample data generators
|
|
9
|
+
def generate_users(count)
|
|
10
|
+
Array.new(count) do |i|
|
|
11
|
+
{
|
|
12
|
+
name: ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"].sample,
|
|
13
|
+
age: rand(18..65),
|
|
14
|
+
score: rand(0..100),
|
|
15
|
+
email: "user#{i}@example.com"
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def generate_mixed_nil_users(count)
|
|
21
|
+
Array.new(count) do |i|
|
|
22
|
+
{
|
|
23
|
+
name: i.even? ? ["Alice", "Bob", "Charlie"].sample : nil,
|
|
24
|
+
age: rand(18..65),
|
|
25
|
+
score: i.odd? ? rand(0..100) : nil
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def generate_mixed_case_names(count)
|
|
31
|
+
names = ["alice", "BOB", "Charlie", "DIANA", "eve", "Frank"]
|
|
32
|
+
Array.new(count) { names.sample }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
puts "=" * 80
|
|
36
|
+
puts "Sortsmith Performance Benchmarks"
|
|
37
|
+
puts "=" * 80
|
|
38
|
+
puts
|
|
39
|
+
|
|
40
|
+
# Benchmark 1: Basic hash key sorting
|
|
41
|
+
puts "Benchmark 1: Basic hash key sorting (1000 items)"
|
|
42
|
+
puts "-" * 80
|
|
43
|
+
users = generate_users(1000)
|
|
44
|
+
|
|
45
|
+
Benchmark.ips do |x|
|
|
46
|
+
x.config(time: 5, warmup: 2)
|
|
47
|
+
|
|
48
|
+
x.report("native sort_by") do
|
|
49
|
+
users.sort_by { |u| u[:name] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
x.report("sortsmith .dig") do
|
|
53
|
+
users.sort_by.dig(:name).sort
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
x.report("sortsmith direct") do
|
|
57
|
+
users.sort_by(:name).sort
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
x.compare!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
puts "\n"
|
|
64
|
+
|
|
65
|
+
# Benchmark 2: Case-insensitive sorting
|
|
66
|
+
puts "Benchmark 2: Case-insensitive sorting (1000 items)"
|
|
67
|
+
puts "-" * 80
|
|
68
|
+
names = generate_mixed_case_names(1000)
|
|
69
|
+
|
|
70
|
+
Benchmark.ips do |x|
|
|
71
|
+
x.config(time: 5, warmup: 2)
|
|
72
|
+
|
|
73
|
+
x.report("native downcase") do
|
|
74
|
+
names.sort_by { |n| n.downcase }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
x.report("sortsmith .insensitive") do
|
|
78
|
+
names.sort_by.insensitive.sort
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
x.compare!
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
puts "\n"
|
|
85
|
+
|
|
86
|
+
# Benchmark 3: Descending order
|
|
87
|
+
puts "Benchmark 3: Descending order (1000 items)"
|
|
88
|
+
puts "-" * 80
|
|
89
|
+
|
|
90
|
+
Benchmark.ips do |x|
|
|
91
|
+
x.config(time: 5, warmup: 2)
|
|
92
|
+
|
|
93
|
+
x.report("native reverse") do
|
|
94
|
+
users.sort_by { |u| u[:score] }.reverse
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
x.report("sortsmith .desc") do
|
|
98
|
+
users.sort_by.dig(:score).desc.sort
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
x.report("sortsmith .reverse") do
|
|
102
|
+
users.sort_by.dig(:score).reverse
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
x.compare!
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
puts "\n"
|
|
109
|
+
|
|
110
|
+
# Benchmark 4: Complex chaining
|
|
111
|
+
puts "Benchmark 4: Complex chaining - case-insensitive descending (1000 items)"
|
|
112
|
+
puts "-" * 80
|
|
113
|
+
|
|
114
|
+
Benchmark.ips do |x|
|
|
115
|
+
x.config(time: 5, warmup: 2)
|
|
116
|
+
|
|
117
|
+
x.report("native") do
|
|
118
|
+
users.sort_by { |u| u[:name].to_s.downcase }.reverse
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
x.report("sortsmith") do
|
|
122
|
+
users.sort_by.dig(:name).insensitive.desc.sort
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
x.compare!
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
puts "\n"
|
|
129
|
+
|
|
130
|
+
# Benchmark 5: Nil handling
|
|
131
|
+
puts "Benchmark 5: Nil handling (1000 items, ~50% nils)"
|
|
132
|
+
puts "-" * 80
|
|
133
|
+
mixed_users = generate_mixed_nil_users(1000)
|
|
134
|
+
|
|
135
|
+
Benchmark.ips do |x|
|
|
136
|
+
x.config(time: 5, warmup: 2)
|
|
137
|
+
|
|
138
|
+
x.report("native with || fallback") do
|
|
139
|
+
mixed_users.sort_by { |u| u[:name] || "" }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
x.report("sortsmith nil_last (default)") do
|
|
143
|
+
mixed_users.sort_by.dig(:name).sort
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
x.report("sortsmith nil_first") do
|
|
147
|
+
mixed_users.sort_by.dig(:name).nil_first.sort
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
x.compare!
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
puts "\n"
|
|
154
|
+
|
|
155
|
+
# Benchmark 6: Small vs Large datasets
|
|
156
|
+
puts "Benchmark 6: Scaling - Small vs Large datasets"
|
|
157
|
+
puts "-" * 80
|
|
158
|
+
|
|
159
|
+
[10, 100, 1_000, 10_000].each do |size|
|
|
160
|
+
puts "\n#{size} items:"
|
|
161
|
+
data = generate_users(size)
|
|
162
|
+
|
|
163
|
+
Benchmark.ips do |x|
|
|
164
|
+
x.config(time: 3, warmup: 1)
|
|
165
|
+
|
|
166
|
+
x.report("native") do
|
|
167
|
+
data.sort_by { |u| u[:name] }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
x.report("sortsmith") do
|
|
171
|
+
data.sort_by.dig(:name).sort
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
x.compare!
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
puts "\n"
|
|
179
|
+
|
|
180
|
+
# Benchmark 7: Delegated methods
|
|
181
|
+
puts "Benchmark 7: Delegated enumerable methods (1000 items)"
|
|
182
|
+
puts "-" * 80
|
|
183
|
+
|
|
184
|
+
Benchmark.ips do |x|
|
|
185
|
+
x.config(time: 5, warmup: 2)
|
|
186
|
+
|
|
187
|
+
x.report("native .first(3)") do
|
|
188
|
+
users.sort_by { |u| u[:score] }.last(3).reverse
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
x.report("sortsmith .first(3)") do
|
|
192
|
+
users.sort_by.dig(:score).desc.first(3)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
x.report("native .each") do
|
|
196
|
+
users.sort_by { |u| u[:name] }.each { |u| u[:name] }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
x.report("sortsmith .each") do
|
|
200
|
+
users.sort_by.dig(:name).each { |u| u[:name] }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
x.compare!
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
puts "\n" + "=" * 60
|
data/flake.lock
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"nodes": {
|
|
3
|
+
"flake-compat": {
|
|
4
|
+
"flake": false,
|
|
5
|
+
"locked": {
|
|
6
|
+
"lastModified": 1747046372,
|
|
7
|
+
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
|
8
|
+
"owner": "edolstra",
|
|
9
|
+
"repo": "flake-compat",
|
|
10
|
+
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
|
11
|
+
"type": "github"
|
|
12
|
+
},
|
|
13
|
+
"original": {
|
|
14
|
+
"owner": "edolstra",
|
|
15
|
+
"repo": "flake-compat",
|
|
16
|
+
"type": "github"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
3
19
|
"flake-utils": {
|
|
4
20
|
"inputs": {
|
|
5
21
|
"systems": "systems"
|
|
@@ -18,13 +34,31 @@
|
|
|
18
34
|
"type": "github"
|
|
19
35
|
}
|
|
20
36
|
},
|
|
37
|
+
"flake-utils_2": {
|
|
38
|
+
"inputs": {
|
|
39
|
+
"systems": "systems_2"
|
|
40
|
+
},
|
|
41
|
+
"locked": {
|
|
42
|
+
"lastModified": 1731533236,
|
|
43
|
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
44
|
+
"owner": "numtide",
|
|
45
|
+
"repo": "flake-utils",
|
|
46
|
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
47
|
+
"type": "github"
|
|
48
|
+
},
|
|
49
|
+
"original": {
|
|
50
|
+
"owner": "numtide",
|
|
51
|
+
"repo": "flake-utils",
|
|
52
|
+
"type": "github"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
21
55
|
"nixpkgs": {
|
|
22
56
|
"locked": {
|
|
23
|
-
"lastModified":
|
|
24
|
-
"narHash": "sha256-
|
|
57
|
+
"lastModified": 1771008912,
|
|
58
|
+
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
|
25
59
|
"owner": "NixOS",
|
|
26
60
|
"repo": "nixpkgs",
|
|
27
|
-
"rev": "
|
|
61
|
+
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
|
28
62
|
"type": "github"
|
|
29
63
|
},
|
|
30
64
|
"original": {
|
|
@@ -34,10 +68,33 @@
|
|
|
34
68
|
"type": "github"
|
|
35
69
|
}
|
|
36
70
|
},
|
|
71
|
+
"nixpkgs-ruby": {
|
|
72
|
+
"inputs": {
|
|
73
|
+
"flake-compat": "flake-compat",
|
|
74
|
+
"flake-utils": "flake-utils_2",
|
|
75
|
+
"nixpkgs": [
|
|
76
|
+
"nixpkgs"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"locked": {
|
|
80
|
+
"lastModified": 1770273217,
|
|
81
|
+
"narHash": "sha256-biMRh5KRKwtDbyHaBTmdrQxB0Ua2SlMEF+krGH1tPt0=",
|
|
82
|
+
"owner": "bobvanderlinden",
|
|
83
|
+
"repo": "nixpkgs-ruby",
|
|
84
|
+
"rev": "8b840328105a5d6d5c0aac9d3d12d7e2213aac33",
|
|
85
|
+
"type": "github"
|
|
86
|
+
},
|
|
87
|
+
"original": {
|
|
88
|
+
"owner": "bobvanderlinden",
|
|
89
|
+
"repo": "nixpkgs-ruby",
|
|
90
|
+
"type": "github"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
37
93
|
"root": {
|
|
38
94
|
"inputs": {
|
|
39
95
|
"flake-utils": "flake-utils",
|
|
40
|
-
"nixpkgs": "nixpkgs"
|
|
96
|
+
"nixpkgs": "nixpkgs",
|
|
97
|
+
"nixpkgs-ruby": "nixpkgs-ruby"
|
|
41
98
|
}
|
|
42
99
|
},
|
|
43
100
|
"systems": {
|
|
@@ -54,6 +111,21 @@
|
|
|
54
111
|
"repo": "default",
|
|
55
112
|
"type": "github"
|
|
56
113
|
}
|
|
114
|
+
},
|
|
115
|
+
"systems_2": {
|
|
116
|
+
"locked": {
|
|
117
|
+
"lastModified": 1681028828,
|
|
118
|
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
119
|
+
"owner": "nix-systems",
|
|
120
|
+
"repo": "default",
|
|
121
|
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
122
|
+
"type": "github"
|
|
123
|
+
},
|
|
124
|
+
"original": {
|
|
125
|
+
"owner": "nix-systems",
|
|
126
|
+
"repo": "default",
|
|
127
|
+
"type": "github"
|
|
128
|
+
}
|
|
57
129
|
}
|
|
58
130
|
},
|
|
59
131
|
"root": "root",
|
data/flake.nix
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
inputs = {
|
|
5
5
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
6
6
|
flake-utils.url = "github:numtide/flake-utils";
|
|
7
|
+
nixpkgs-ruby.url = "github:bobvanderlinden/nixpkgs-ruby";
|
|
8
|
+
nixpkgs-ruby.inputs.nixpkgs.follows = "nixpkgs";
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
outputs =
|
|
@@ -11,19 +13,19 @@
|
|
|
11
13
|
self,
|
|
12
14
|
nixpkgs,
|
|
13
15
|
flake-utils,
|
|
16
|
+
nixpkgs-ruby,
|
|
14
17
|
}:
|
|
15
18
|
flake-utils.lib.eachDefaultSystem (
|
|
16
19
|
system:
|
|
17
20
|
let
|
|
18
21
|
pkgs = nixpkgs.legacyPackages.${system};
|
|
22
|
+
ruby = nixpkgs-ruby.packages.${system}."ruby-3.2.9";
|
|
19
23
|
in
|
|
20
24
|
{
|
|
21
25
|
devShells.default = pkgs.mkShell {
|
|
22
|
-
buildInputs =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
docSupport = false;
|
|
26
|
-
})
|
|
26
|
+
buildInputs = [
|
|
27
|
+
ruby
|
|
28
|
+
] ++ (with pkgs; [
|
|
27
29
|
|
|
28
30
|
# Dependencies for native gems
|
|
29
31
|
pkg-config
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
readline
|
|
32
34
|
zstd
|
|
33
35
|
libyaml
|
|
34
|
-
];
|
|
36
|
+
]);
|
|
35
37
|
|
|
36
38
|
shellHook = ''
|
|
37
39
|
export GEM_HOME="$PWD/vendor/bundle"
|
data/lib/sortsmith/sorter.rb
CHANGED
|
@@ -32,6 +32,14 @@ module Sortsmith
|
|
|
32
32
|
# users.sort_by.method(:missing_email).sort
|
|
33
33
|
# # => preserves original order when method doesn't exist
|
|
34
34
|
#
|
|
35
|
+
# @example Nil value handling
|
|
36
|
+
# users = [{name: "Bob"}, {name: nil}, {name: "Alice"}]
|
|
37
|
+
# users.sort_by.dig(:name).nil_first.sort
|
|
38
|
+
# # => [{name: nil}, {name: "Alice"}, {name: "Bob"}]
|
|
39
|
+
#
|
|
40
|
+
# users.sort_by.dig(:name).nil_last.desc.sort
|
|
41
|
+
# # => [{name: "Bob"}, {name: "Alice"}, {name: nil}]
|
|
42
|
+
#
|
|
35
43
|
# @see Enumerable#sort_by The enhanced sort_by method
|
|
36
44
|
# @since 0.9.0
|
|
37
45
|
#
|
|
@@ -91,7 +99,8 @@ module Sortsmith
|
|
|
91
99
|
@input = input
|
|
92
100
|
@extractors = []
|
|
93
101
|
@modifiers = []
|
|
94
|
-
@
|
|
102
|
+
@descending = false
|
|
103
|
+
@nil_first = false
|
|
95
104
|
end
|
|
96
105
|
|
|
97
106
|
############################################################################
|
|
@@ -393,7 +402,7 @@ module Sortsmith
|
|
|
393
402
|
# users.sort_by.dig(:name).desc.asc.sort # ends up ascending
|
|
394
403
|
#
|
|
395
404
|
def asc
|
|
396
|
-
@
|
|
405
|
+
@descending = false
|
|
397
406
|
self
|
|
398
407
|
end
|
|
399
408
|
|
|
@@ -414,7 +423,79 @@ module Sortsmith
|
|
|
414
423
|
# # => Case-insensitive, reverse alphabetical
|
|
415
424
|
#
|
|
416
425
|
def desc
|
|
417
|
-
@
|
|
426
|
+
@descending = true
|
|
427
|
+
self
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
##
|
|
431
|
+
# Position nil values at the beginning of sort results.
|
|
432
|
+
#
|
|
433
|
+
# By default, nil values sort last (matching SQL/database conventions).
|
|
434
|
+
# This modifier overrides that behavior to place nils first, regardless
|
|
435
|
+
# of sort direction (asc/desc).
|
|
436
|
+
#
|
|
437
|
+
# The nil positioning is independent of asc/desc modifiers, meaning
|
|
438
|
+
# nil_first with desc will show nils first, then non-nil values in
|
|
439
|
+
# descending order.
|
|
440
|
+
#
|
|
441
|
+
# @return [Sorter] Returns self for method chaining
|
|
442
|
+
#
|
|
443
|
+
# @example Basic nil_first usage
|
|
444
|
+
# users = [
|
|
445
|
+
# {name: "Bob"},
|
|
446
|
+
# {name: nil},
|
|
447
|
+
# {name: "Alice"}
|
|
448
|
+
# ]
|
|
449
|
+
# users.sort_by.dig(:name).nil_first.sort
|
|
450
|
+
# # => [{name: nil}, {name: "Alice"}, {name: "Bob"}]
|
|
451
|
+
#
|
|
452
|
+
# @example Combining with desc
|
|
453
|
+
# users.sort_by.dig(:name).nil_first.desc.sort
|
|
454
|
+
# # => [{name: nil}, {name: "Bob"}, {name: "Alice"}]
|
|
455
|
+
# # Nils first, then descending order
|
|
456
|
+
#
|
|
457
|
+
# @example With case modifiers
|
|
458
|
+
# users.sort_by.dig(:name).insensitive.nil_first.sort
|
|
459
|
+
# # => Case-insensitive sort with nils first
|
|
460
|
+
#
|
|
461
|
+
# @see #nil_last To explicitly position nils at the end
|
|
462
|
+
#
|
|
463
|
+
def nil_first
|
|
464
|
+
@nil_first = true
|
|
465
|
+
self
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
##
|
|
469
|
+
# Position nil values at the end of sort results (explicit default).
|
|
470
|
+
#
|
|
471
|
+
# This is the default behavior, but can be used for explicitness or to
|
|
472
|
+
# override a previous nil_first call in a chain. Nil values will appear
|
|
473
|
+
# last regardless of sort direction (asc/desc).
|
|
474
|
+
#
|
|
475
|
+
# @return [Sorter] Returns self for method chaining
|
|
476
|
+
#
|
|
477
|
+
# @example Explicit nil_last
|
|
478
|
+
# users = [
|
|
479
|
+
# {name: "Bob"},
|
|
480
|
+
# {name: nil},
|
|
481
|
+
# {name: "Alice"}
|
|
482
|
+
# ]
|
|
483
|
+
# users.sort_by.dig(:name).nil_last.sort
|
|
484
|
+
# # => [{name: "Alice"}, {name: "Bob"}, {name: nil}]
|
|
485
|
+
#
|
|
486
|
+
# @example Overriding nil_first
|
|
487
|
+
# users.sort_by.dig(:name).nil_first.nil_last.sort
|
|
488
|
+
# # => Last setting wins: nils appear last
|
|
489
|
+
#
|
|
490
|
+
# @example With desc
|
|
491
|
+
# users.sort_by.dig(:name).nil_last.desc.sort
|
|
492
|
+
# # => [{name: "Bob"}, {name: "Alice"}, {name: nil}]
|
|
493
|
+
# # Descending order, then nils last
|
|
494
|
+
#
|
|
495
|
+
# @see #nil_first To position nils at the beginning
|
|
496
|
+
#
|
|
497
|
+
def nil_last
|
|
498
|
+
@nil_first = false
|
|
418
499
|
self
|
|
419
500
|
end
|
|
420
501
|
|
|
@@ -439,11 +520,53 @@ module Sortsmith
|
|
|
439
520
|
# result = users.sort_by.dig(:name, indifferent: true).insensitive.desc.sort
|
|
440
521
|
#
|
|
441
522
|
def sort
|
|
442
|
-
#
|
|
443
|
-
|
|
523
|
+
# Schwartzian Transform: extract once (O(n)), sort (O(n log n)), map back (O(n))
|
|
524
|
+
|
|
525
|
+
# Step 1: Pair each item with its extracted sort value
|
|
526
|
+
pairs = @input.map { |item| [extract_and_transform(item), item] }
|
|
527
|
+
|
|
528
|
+
# Step 2: Partition nils so we can use C-level sort_by on non-nil values
|
|
529
|
+
nil_pairs, non_nil_pairs = pairs.partition { |v, _| v.nil? }
|
|
530
|
+
|
|
531
|
+
# Step 3: Sort non-nil values using Ruby's native sort_by (runs in C)
|
|
532
|
+
begin
|
|
533
|
+
non_nil_pairs.sort_by! { |v, _| v }
|
|
534
|
+
rescue ArgumentError
|
|
535
|
+
# Re-raise with a more helpful error message
|
|
536
|
+
# Find the first pair of incomparable values for the message
|
|
537
|
+
non_nil_pairs.each_cons(2) do |(val_a, _), (val_b, _)|
|
|
538
|
+
result = val_a <=> val_b
|
|
539
|
+
next unless result.nil?
|
|
540
|
+
|
|
541
|
+
# <=> returned nil - incomparable types
|
|
542
|
+
raise ArgumentError, <<~ERROR
|
|
543
|
+
Cannot compare values during sort - the values are incomparable types.
|
|
544
|
+
This usually means your extraction returned mixed types or you're missing an extraction method.
|
|
545
|
+
Comparing:
|
|
546
|
+
#{val_a.inspect} (#{val_a.class})
|
|
547
|
+
<=>
|
|
548
|
+
#{val_b.inspect} (#{val_b.class})
|
|
549
|
+
ERROR
|
|
550
|
+
rescue ArgumentError
|
|
551
|
+
# <=> raised instead of returning nil
|
|
552
|
+
raise ArgumentError, <<~ERROR
|
|
553
|
+
Cannot compare values during sort - the <=> operator raised an exception.
|
|
554
|
+
This usually means the class doesn't implement <=>, has a buggy implementation, or you're missing an extraction method.
|
|
555
|
+
Comparing:
|
|
556
|
+
#{val_a.inspect} (#{val_a.class})
|
|
557
|
+
<=>
|
|
558
|
+
#{val_b.inspect} (#{val_b.class})
|
|
559
|
+
ERROR
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
non_nil_pairs.reverse! if @descending
|
|
564
|
+
|
|
565
|
+
# Step 4: Combine based on nil positioning
|
|
566
|
+
result = @nil_first ? nil_pairs.concat(non_nil_pairs) : non_nil_pairs.concat(nil_pairs)
|
|
444
567
|
|
|
445
|
-
#
|
|
446
|
-
|
|
568
|
+
# Step 5: Strip the sort values, keeping only the original items
|
|
569
|
+
result.map! { |_, item| item }
|
|
447
570
|
end
|
|
448
571
|
|
|
449
572
|
##
|
|
@@ -478,11 +601,8 @@ module Sortsmith
|
|
|
478
601
|
# result = users.sort_by.dig(:name).sort!.first(10)
|
|
479
602
|
#
|
|
480
603
|
def sort!
|
|
481
|
-
|
|
482
|
-
@input.
|
|
483
|
-
|
|
484
|
-
# Apply any ordering transformations
|
|
485
|
-
apply_ordering(@input)
|
|
604
|
+
sorted = sort
|
|
605
|
+
@input.replace(sorted)
|
|
486
606
|
end
|
|
487
607
|
|
|
488
608
|
##
|
|
@@ -626,27 +746,48 @@ module Sortsmith
|
|
|
626
746
|
private
|
|
627
747
|
|
|
628
748
|
##
|
|
629
|
-
#
|
|
749
|
+
# Extract and transform a single item through the full pipeline.
|
|
630
750
|
#
|
|
631
|
-
#
|
|
632
|
-
#
|
|
633
|
-
# that will be compared using Ruby's spaceship operator.
|
|
751
|
+
# Applies all extractors and modifiers to produce the final value
|
|
752
|
+
# that will be used for sorting comparisons.
|
|
634
753
|
#
|
|
635
|
-
# @param
|
|
636
|
-
# @
|
|
637
|
-
# @return [Integer] Comparison result (-1, 0, 1)
|
|
754
|
+
# @param item [Object] The item to process
|
|
755
|
+
# @return [Object] The extracted and transformed value
|
|
638
756
|
#
|
|
639
757
|
# @api private
|
|
640
758
|
#
|
|
641
|
-
def
|
|
759
|
+
def extract_and_transform(item)
|
|
760
|
+
value = item
|
|
761
|
+
|
|
642
762
|
@extractors.each do |extractor|
|
|
643
|
-
|
|
763
|
+
value = extract_value(value, **extractor)
|
|
644
764
|
end
|
|
645
765
|
|
|
646
766
|
@modifiers.each do |modifier|
|
|
647
|
-
|
|
767
|
+
value = modify_value(value, **modifier)
|
|
648
768
|
end
|
|
649
769
|
|
|
770
|
+
value
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
##
|
|
774
|
+
# Compare two extracted values according to nil positioning and sort direction.
|
|
775
|
+
#
|
|
776
|
+
# Handles nil values specially and applies the @descending flag to non-nil comparisons.
|
|
777
|
+
#
|
|
778
|
+
# @param item_a [Object] First value to compare
|
|
779
|
+
# @param item_b [Object] Second value to compare
|
|
780
|
+
# @return [Integer] Comparison result (-1, 0, 1)
|
|
781
|
+
#
|
|
782
|
+
# @api private
|
|
783
|
+
#
|
|
784
|
+
def compare_values(item_a, item_b)
|
|
785
|
+
# Handle nil values specially before comparison
|
|
786
|
+
# nil_first/nil_last positioning is absolute and not affected by asc/desc
|
|
787
|
+
return 0 if item_a.nil? && item_b.nil?
|
|
788
|
+
return @nil_first ? -1 : 1 if item_a.nil?
|
|
789
|
+
return @nil_first ? 1 : -1 if item_b.nil?
|
|
790
|
+
|
|
650
791
|
# Final comparison using Ruby's spaceship operator
|
|
651
792
|
result = item_a <=> item_b
|
|
652
793
|
|
|
@@ -661,46 +802,7 @@ module Sortsmith
|
|
|
661
802
|
ERROR
|
|
662
803
|
end
|
|
663
804
|
|
|
664
|
-
result
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
##
|
|
668
|
-
# Apply ordering transformations to the sorted array.
|
|
669
|
-
#
|
|
670
|
-
# Executes any ordering steps (like desc) that affect the final
|
|
671
|
-
# arrangement of the sorted results. This happens after the sort
|
|
672
|
-
# comparison is complete.
|
|
673
|
-
#
|
|
674
|
-
# @param sorted [Array] The array to apply ordering to
|
|
675
|
-
# @return [Array] The array with ordering applied
|
|
676
|
-
#
|
|
677
|
-
# @api private
|
|
678
|
-
#
|
|
679
|
-
def apply_ordering(sorted)
|
|
680
|
-
@ordering.each { |step| sorted.public_send(step[:method]) }
|
|
681
|
-
|
|
682
|
-
sorted
|
|
683
|
-
end
|
|
684
|
-
|
|
685
|
-
##
|
|
686
|
-
# Apply an extraction step to both comparison items.
|
|
687
|
-
#
|
|
688
|
-
# Extraction steps pull values out of objects (like hash keys or method calls)
|
|
689
|
-
# that will be used for comparison. When extraction fails, returns empty
|
|
690
|
-
# strings to preserve original ordering.
|
|
691
|
-
#
|
|
692
|
-
# @param extractor [Hash] Extraction step configuration
|
|
693
|
-
# @param item_a [Object] First item to extract from
|
|
694
|
-
# @param item_b [Object] Second item to extract from
|
|
695
|
-
# @return [Array<Object, Object>] Extracted values for comparison
|
|
696
|
-
#
|
|
697
|
-
# @api private
|
|
698
|
-
#
|
|
699
|
-
def apply_extractor(extractor, item_a, item_b)
|
|
700
|
-
item_a = extract_value(item_a, **extractor)
|
|
701
|
-
item_b = extract_value(item_b, **extractor)
|
|
702
|
-
|
|
703
|
-
[item_a, item_b]
|
|
805
|
+
@descending ? -result : result
|
|
704
806
|
end
|
|
705
807
|
|
|
706
808
|
##
|
|
@@ -720,31 +822,10 @@ module Sortsmith
|
|
|
720
822
|
# @api private
|
|
721
823
|
#
|
|
722
824
|
def extract_value(item, method:, positional: [], keyword: {}, before_extract: nil)
|
|
723
|
-
return "" unless item.respond_to?(method)
|
|
724
|
-
|
|
725
825
|
item = before_extract.call(item) if before_extract
|
|
726
826
|
item.public_send(method, *positional, **keyword)
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
##
|
|
730
|
-
# Apply a modification step to both comparison items.
|
|
731
|
-
#
|
|
732
|
-
# Modification steps transform values for comparison (like case changes).
|
|
733
|
-
# Both items are processed with the same transformation to ensure
|
|
734
|
-
# consistent comparison behavior.
|
|
735
|
-
#
|
|
736
|
-
# @param modifier [Hash] Modification step configuration
|
|
737
|
-
# @param item_a [Object] First item to modify
|
|
738
|
-
# @param item_b [Object] Second item to modify
|
|
739
|
-
# @return [Array<Object, Object>] Modified values for comparison
|
|
740
|
-
#
|
|
741
|
-
# @api private
|
|
742
|
-
#
|
|
743
|
-
def apply_modifier(modifier, item_a, item_b)
|
|
744
|
-
item_a = modify_value(item_a, **modifier)
|
|
745
|
-
item_b = modify_value(item_b, **modifier)
|
|
746
|
-
|
|
747
|
-
[item_a, item_b]
|
|
827
|
+
rescue NoMethodError
|
|
828
|
+
""
|
|
748
829
|
end
|
|
749
830
|
|
|
750
831
|
##
|
|
@@ -763,9 +844,9 @@ module Sortsmith
|
|
|
763
844
|
# @api private
|
|
764
845
|
#
|
|
765
846
|
def modify_value(item, method:, positional: [], keyword: {})
|
|
766
|
-
return item.to_s unless item.respond_to?(method)
|
|
767
|
-
|
|
768
847
|
item.public_send(method, *positional, **keyword)
|
|
848
|
+
rescue NoMethodError
|
|
849
|
+
item.to_s
|
|
769
850
|
end
|
|
770
851
|
end
|
|
771
852
|
end
|
data/lib/sortsmith/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sortsmith
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bryan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-02-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Sortsmith provides a flexible, chainable API for sorting Ruby collections.
|
|
14
14
|
It supports sorting by object keys, methods, case sensitivity, and custom transformations.
|
|
@@ -25,6 +25,8 @@ files:
|
|
|
25
25
|
- LICENSE.txt
|
|
26
26
|
- README.md
|
|
27
27
|
- Rakefile
|
|
28
|
+
- benchmark/quick_benchmark.rb
|
|
29
|
+
- benchmark/sorting_benchmark.rb
|
|
28
30
|
- flake.lock
|
|
29
31
|
- flake.nix
|
|
30
32
|
- lib/sortsmith.rb
|