sortsmith 0.9.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 +82 -1
- data/README.md +168 -89
- 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/core_ext/enumerable.rb +53 -19
- data/lib/sortsmith/sorter.rb +640 -105
- data/lib/sortsmith/version.rb +1 -1
- metadata +5 -3
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
|
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
<!--
|
|
8
9
|
## [Unreleased]
|
|
9
10
|
|
|
10
11
|
### Added
|
|
@@ -12,6 +13,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
12
13
|
### Changed
|
|
13
14
|
|
|
14
15
|
### Removed
|
|
16
|
+
-->
|
|
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
|
+
|
|
46
|
+
## [1.0.0] - 12025-08-03
|
|
47
|
+
|
|
48
|
+
### 🎉 API Stability Milestone
|
|
49
|
+
|
|
50
|
+
Sortsmith has reached 1.0.0! After evolving through several major API redesigns and proving itself in real projects, the core interface is now stable and ready for broader adoption.
|
|
51
|
+
|
|
52
|
+
**What 1.0.0 means:**
|
|
53
|
+
- **Stable API**: Method signatures and behavior are locked for semver compatibility
|
|
54
|
+
- **Complete Vision**: From the verbose early days to today's clean `collection.sort_by(:name).insensitive.desc.sort`, the API finally feels right
|
|
55
|
+
- **Battle Tested**: Handles the weird edge cases and mixed data types you actually encounter in real Ruby apps
|
|
56
|
+
|
|
57
|
+
This represents the sorting library I always wished Ruby had built-in. Simple things are simple, complex things are possible, and it all reads like English.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
#### Universal Field Extraction
|
|
62
|
+
|
|
63
|
+
- **Direct syntax**: `sort_by(field, **opts)` - Sort by any field/method without explicit chaining
|
|
64
|
+
- **Object method extraction**: `method(*args, **kwargs)` - Call methods on objects with full argument support
|
|
65
|
+
- **Mixed key handling**: Enhanced `indifferent: true` support across all extractors
|
|
66
|
+
|
|
67
|
+
#### Semantic Aliases for Better Readability
|
|
68
|
+
|
|
69
|
+
- `key` - Alias for `dig` (emphasizes hash key extraction)
|
|
70
|
+
- `field` - Alias for `dig` (emphasizes object field access)
|
|
71
|
+
- `attribute` - Alias for `method` (emphasizes object attribute access)
|
|
72
|
+
- `case_insensitive` - Alias for `insensitive`/`downcase` (explicit case handling)
|
|
73
|
+
|
|
74
|
+
#### Seamless Enumerable Integration
|
|
75
|
+
|
|
76
|
+
- **Delegated methods**: `first`, `last`, `take`, `drop`, `each`, `map`, `select`, `[]`, `size`, `count`, `length`
|
|
77
|
+
- **Fluent chaining**: Sort operations flow directly into array operations without breaking
|
|
78
|
+
- **Full argument support**: All delegated methods support blocks, arguments, and return appropriate types
|
|
79
|
+
|
|
80
|
+
#### Enhanced Terminators
|
|
81
|
+
|
|
82
|
+
- `to_a!` - Mutating version of `to_a` for consistency with `sort!`
|
|
15
83
|
|
|
16
84
|
## [0.9.0] - 12025-07-06
|
|
17
85
|
|
|
@@ -24,23 +92,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
24
92
|
### Added
|
|
25
93
|
|
|
26
94
|
#### Core API Transformation
|
|
95
|
+
|
|
27
96
|
- **Fluent API**: Direct extension of `Enumerable#sort_by` for natural Ruby integration
|
|
28
97
|
- **Chainable Interface**: Method chaining that reads like English: `users.sort_by.dig(:name).downcase.desc.sort`
|
|
29
98
|
- **Universal `dig` Method**: Single extraction method that works with hashes, objects, and nested structures
|
|
30
99
|
- **Indifferent Key Access**: Handle mixed symbol/string hash keys with `dig(:name, indifferent: true)`
|
|
31
100
|
|
|
32
101
|
#### New Extraction Methods
|
|
102
|
+
|
|
33
103
|
- `dig(*identifiers, indifferent: false)` - Extract values from hashes, objects, or nested structures
|
|
34
104
|
- Support for nested extraction: `dig(:user, :profile, :email)`
|
|
35
105
|
- Automatic fallback to method calls for non-hash objects
|
|
36
106
|
|
|
37
107
|
#### Enhanced Modifiers
|
|
108
|
+
|
|
38
109
|
- `downcase` / `upcase` - Case transformations with automatic type checking
|
|
39
110
|
- `insensitive` - Alias for `downcase` for semantic clarity
|
|
40
111
|
- `asc` / `desc` - Explicit sort direction control
|
|
41
112
|
- Smart modifier chaining that only affects compatible data types
|
|
42
113
|
|
|
43
114
|
#### Multiple Terminators
|
|
115
|
+
|
|
44
116
|
- `sort` - Returns new sorted array (non-mutating)
|
|
45
117
|
- `sort!` - Mutates original array in place
|
|
46
118
|
- `reverse` - Shorthand for `desc.sort`
|
|
@@ -48,6 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
48
120
|
- `to_a` - Alias for `sort` for semantic clarity
|
|
49
121
|
|
|
50
122
|
#### Backward Compatibility
|
|
123
|
+
|
|
51
124
|
- `sort_by` with block maintains original Ruby behavior
|
|
52
125
|
- `sort_by` without block returns Sortsmith::Sorter instance
|
|
53
126
|
- Zero breaking changes for existing Ruby code
|
|
@@ -55,16 +128,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
55
128
|
### Changed
|
|
56
129
|
|
|
57
130
|
#### API Design Philosophy
|
|
131
|
+
|
|
58
132
|
- **Before**: `Sortsmith::Sorter.new(collection).by_key(:name).case_insensitive.desc.sort`
|
|
59
133
|
- **After**: `collection.sort_by.dig(:name).insensitive.desc.sort`
|
|
60
134
|
|
|
61
135
|
#### Improved Ergonomics
|
|
136
|
+
|
|
62
137
|
- No more explicit `Sorter.new()` instantiation required
|
|
63
138
|
- Tab-completable method discovery
|
|
64
139
|
- Natural language flow in method chains
|
|
65
140
|
- Unified `dig` method replaces separate `by_key`/`by_method` methods
|
|
66
141
|
|
|
67
142
|
#### Enhanced Ruby Version Support
|
|
143
|
+
|
|
68
144
|
- **Restored Ruby 3.0 and 3.1 compatibility** - Previously removed in v0.2.0, now supported again
|
|
69
145
|
- Full compatibility matrix: Ruby 3.0.7, 3.1.7, 3.2.8, 3.3.8, 3.4.4
|
|
70
146
|
- Expanded from Ruby 3.2+ requirement back to Ruby 3.0+ for broader accessibility
|
|
@@ -72,6 +148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
72
148
|
### Removed
|
|
73
149
|
|
|
74
150
|
#### Legacy API (Breaking Changes)
|
|
151
|
+
|
|
75
152
|
- `by_key` method (replaced by `dig`)
|
|
76
153
|
- `by_method`/`by_attribute` methods (replaced by `dig`)
|
|
77
154
|
- `case_insensitive` method (replaced by `insensitive`/`downcase`)
|
|
@@ -122,11 +199,13 @@ objects.sort_by.dig(:calculate_score).sort
|
|
|
122
199
|
## [0.1.1] - 12025-01-15
|
|
123
200
|
|
|
124
201
|
### Changed
|
|
202
|
+
|
|
125
203
|
- Improved handling of non-string objects when sorting
|
|
126
204
|
|
|
127
205
|
## [0.1.0] - 12025-01-14
|
|
128
206
|
|
|
129
207
|
### Added
|
|
208
|
+
|
|
130
209
|
- Initial implementation of `Sortsmith::Sorter`
|
|
131
210
|
- Support for case-insensitive sorting
|
|
132
211
|
- Support for sorting by hash keys and object methods
|
|
@@ -134,7 +213,9 @@ objects.sort_by.dig(:calculate_score).sort
|
|
|
134
213
|
- Type checking with Steep/RBS
|
|
135
214
|
- GitHub Actions workflow for automated testing and type checking
|
|
136
215
|
|
|
137
|
-
[unreleased]: https://github.com/itsthedevman/sortsmith/compare/
|
|
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
|
|
218
|
+
[1.0.0]: https://github.com/itsthedevman/sortsmith/compare/v0.9.0...v1.0.0
|
|
138
219
|
[0.9.0]: https://github.com/itsthedevman/sortsmith/compare/v0.2.0...v0.9.0
|
|
139
220
|
[0.2.0]: https://github.com/itsthedevman/sortsmith/compare/v0.1.1...v0.2.0
|
|
140
221
|
[0.1.1]: https://github.com/itsthedevman/sortsmith/compare/v0.1.0...v0.1.1
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Sortsmith
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/sortsmith)
|
|
4
|
-

|
|
5
5
|
[](https://github.com/itsthedevman/sortsmith/actions/workflows/main.yml)
|
|
6
6
|
|
|
7
7
|
**Sortsmith** makes sorting Ruby collections feel natural and fun. Instead of wrestling with verbose blocks or complex comparisons, just chain together what you want in plain English.
|
|
@@ -11,10 +11,10 @@
|
|
|
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
|
-
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
|
|
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.
|
|
18
18
|
|
|
19
19
|
## Table of Contents
|
|
20
20
|
|
|
@@ -46,21 +46,28 @@ 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:**
|
|
65
|
+
|
|
58
66
|
- **Fluent chaining** - Reads like English
|
|
59
67
|
- **Universal extraction** - Works with hashes, objects, and nested data
|
|
60
68
|
- **Indifferent key access** - Handles mixed symbol/string keys automatically
|
|
61
|
-
- **Nil-safe** - Graceful handling of missing data
|
|
69
|
+
- **Nil-safe** - Graceful handling of missing data with control over nil positioning
|
|
62
70
|
- **Minimal overhead** - Extends existing Ruby methods without breaking compatibility
|
|
63
|
-
- **Tab-completable** - Discoverable API through your editor
|
|
64
71
|
|
|
65
72
|
## Installation
|
|
66
73
|
|
|
@@ -84,17 +91,17 @@ $ gem install sortsmith
|
|
|
84
91
|
|
|
85
92
|
## Quick Start
|
|
86
93
|
|
|
87
|
-
Sortsmith extends Ruby's `sort_by` method.
|
|
94
|
+
Sortsmith extends Ruby's `sort_by` method with a fluent, chainable API. Use it with or without a block for maximum flexibility:
|
|
88
95
|
|
|
89
96
|
```ruby
|
|
90
97
|
require "sortsmith"
|
|
91
98
|
|
|
92
|
-
#
|
|
99
|
+
# Direct syntax for simple cases
|
|
93
100
|
names = ["charlie", "Alice", "bob"]
|
|
94
|
-
names.sort_by.insensitive.sort
|
|
101
|
+
names.sort_by(:name).insensitive.sort
|
|
95
102
|
# => ["Alice", "bob", "charlie"]
|
|
96
103
|
|
|
97
|
-
#
|
|
104
|
+
# Or use the chainable API for complex scenarios
|
|
98
105
|
users = [
|
|
99
106
|
{ name: "Charlie", age: 25 },
|
|
100
107
|
{ name: "Alice", age: 30 },
|
|
@@ -104,6 +111,10 @@ 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
|
|
|
114
|
+
# Seamless integration with enumerable methods
|
|
115
|
+
users.sort_by(:age).desc.first(2)
|
|
116
|
+
# => [{ name: "Alice", age: 30 }, { name: "Charlie", age: 25 }]
|
|
117
|
+
|
|
107
118
|
# The original sort_by with blocks still works exactly the same!
|
|
108
119
|
users.sort_by { |u| u[:age] }
|
|
109
120
|
# => [{ name: "Bob", age: 20 }, { name: "Charlie", age: 25 }, { name: "Alice", age: 30 }]
|
|
@@ -113,10 +124,10 @@ users.sort_by { |u| u[:age] }
|
|
|
113
124
|
|
|
114
125
|
Sortsmith uses a simple pipeline concept where each step is **optional** except for the terminator:
|
|
115
126
|
|
|
116
|
-
1. **Extract** - Get the value to sort by (`dig`) -
|
|
117
|
-
2. **Transform** - Modify the value for comparison (`downcase`, `upcase
|
|
118
|
-
3. **Order** - Choose sort direction (`asc`, `desc`) -
|
|
119
|
-
4. **Execute** - Run the sort (`sort`, `sort!`, `reverse`) - **required**
|
|
127
|
+
1. **Extract** - Get the value to sort by (`dig`, `method`, etc.) - _optional_
|
|
128
|
+
2. **Transform** - Modify the value for comparison (`downcase`, `upcase`, etc.) - _optional_
|
|
129
|
+
3. **Order** - Choose sort direction (`asc`, `desc`) - _optional_
|
|
130
|
+
4. **Execute** - Run the sort (`sort`, `sort!`, `reverse`, `to_a`, etc.) - **required**
|
|
120
131
|
|
|
121
132
|
```ruby
|
|
122
133
|
collection.sort_by.dig(:field).downcase.desc.sort
|
|
@@ -126,6 +137,7 @@ collection.sort_by.dig(:field).downcase.desc.sort
|
|
|
126
137
|
```
|
|
127
138
|
|
|
128
139
|
**Minimal example:**
|
|
140
|
+
|
|
129
141
|
```ruby
|
|
130
142
|
# This works! (though not particularly useful)
|
|
131
143
|
users.sort_by.sort # Same as users.sort
|
|
@@ -141,59 +153,29 @@ Each step builds on the previous ones, so you can mix and match based on what yo
|
|
|
141
153
|
|
|
142
154
|
## Usage Examples
|
|
143
155
|
|
|
144
|
-
###
|
|
156
|
+
### Simple Direct Syntax
|
|
145
157
|
|
|
146
158
|
```ruby
|
|
147
|
-
#
|
|
148
|
-
words = ["
|
|
149
|
-
words.sort_by.sort
|
|
150
|
-
# => ["
|
|
151
|
-
|
|
152
|
-
# Case-insensitive
|
|
153
|
-
words.sort_by.insensitive.sort
|
|
154
|
-
# => ["Apple", "banana", "cherry"]
|
|
155
|
-
|
|
156
|
-
# Descending order
|
|
157
|
-
words.sort_by.downcase.desc.sort
|
|
158
|
-
# => ["cherry", "banana", "Apple"]
|
|
159
|
-
|
|
160
|
-
# In-place mutation
|
|
161
|
-
words.sort_by.insensitive.sort!
|
|
162
|
-
# Modifies the original array
|
|
163
|
-
```
|
|
159
|
+
# Clean and direct for common operations
|
|
160
|
+
words = ["elephant", "cat", "butterfly"]
|
|
161
|
+
words.sort_by(:length).desc.sort
|
|
162
|
+
# => ["butterfly", "elephant", "cat"]
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
164
|
+
# Works great with hashes
|
|
168
165
|
users = [
|
|
169
|
-
{ name: "
|
|
170
|
-
{ name: "
|
|
171
|
-
{ name: "
|
|
166
|
+
{ name: "Cat", score: 99 },
|
|
167
|
+
{ name: "Charlie", score: 85 },
|
|
168
|
+
{ name: "karen", score: 50 },
|
|
169
|
+
{ name: "Alice", score: 92 },
|
|
170
|
+
{ name: "bob", score: 78 },
|
|
172
171
|
]
|
|
173
172
|
|
|
174
|
-
#
|
|
175
|
-
users.sort_by
|
|
176
|
-
# => [{ name: "
|
|
177
|
-
|
|
178
|
-
# Sort by name (case-insensitive)
|
|
179
|
-
users.sort_by.dig(:name).insensitive.sort
|
|
180
|
-
# => [{ name: "Alice" }, { name: "bob" }, { name: "Charlie" }]
|
|
181
|
-
|
|
182
|
-
# Sort by score (descending)
|
|
183
|
-
users.sort_by.dig(:score).desc.sort
|
|
184
|
-
# => [{ score: 92 }, { score: 85 }, { score: 78 }]
|
|
185
|
-
|
|
186
|
-
# Multiple field extraction (nested digging)
|
|
187
|
-
data = [
|
|
188
|
-
{ user: { profile: { name: "Charlie" } } },
|
|
189
|
-
{ user: { profile: { name: "Alice" } } }
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
data.sort_by.dig(:user, :profile, :name).sort
|
|
193
|
-
# => [{ user: { profile: { name: "Alice" } } }, ...]
|
|
173
|
+
# Get top 3 by score
|
|
174
|
+
users.sort_by(:score).desc.first(3)
|
|
175
|
+
# => [{ name: "Cat", score: 99 }, { name: "Alice", score: 92 }, { name: "Charlie", score: 85 }]
|
|
194
176
|
```
|
|
195
177
|
|
|
196
|
-
### Object
|
|
178
|
+
### Object Method Sorting
|
|
197
179
|
|
|
198
180
|
```ruby
|
|
199
181
|
User = Struct.new(:name, :age, :city)
|
|
@@ -204,20 +186,59 @@ users = [
|
|
|
204
186
|
User.new("bob", 20, "Chicago")
|
|
205
187
|
]
|
|
206
188
|
|
|
207
|
-
# Sort by any method
|
|
208
|
-
users.sort_by.
|
|
189
|
+
# Sort by any method or attribute
|
|
190
|
+
users.sort_by.method(:name).insensitive.sort
|
|
209
191
|
# => [User.new("Alice"), User.new("bob"), User.new("Charlie")]
|
|
210
192
|
|
|
211
|
-
|
|
212
|
-
|
|
193
|
+
# Or use the semantic alias
|
|
194
|
+
users.sort_by.attribute(:age).desc.first
|
|
195
|
+
# => User.new("Alice", 30, "LA")
|
|
213
196
|
|
|
214
|
-
#
|
|
215
|
-
class
|
|
216
|
-
def
|
|
197
|
+
# Methods with arguments work too
|
|
198
|
+
class Product
|
|
199
|
+
def price_in(currency)
|
|
200
|
+
# calculation logic
|
|
201
|
+
end
|
|
217
202
|
end
|
|
218
203
|
|
|
219
|
-
|
|
220
|
-
|
|
204
|
+
products.sort_by.method(:price_in, "USD").sort
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Hash Collections with Multiple Access Patterns
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
users = [
|
|
211
|
+
{ name: "Charlie", score: 85, team: "red" },
|
|
212
|
+
{ name: "Alice", score: 92, team: "blue" },
|
|
213
|
+
{ name: "bob", score: 78, team: "red" }
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
# Multiple semantic ways to express extraction
|
|
217
|
+
users.sort_by.key(:name).insensitive.sort # Emphasizes hash keys
|
|
218
|
+
users.sort_by.field(:score).desc.sort # Emphasizes data fields
|
|
219
|
+
users.sort_by.dig(:team, :name).sort # Nested access
|
|
220
|
+
|
|
221
|
+
# Case handling with explicit naming
|
|
222
|
+
users.sort_by(:name).case_insensitive.reverse
|
|
223
|
+
# => [{ name: "bob" }, { name: "Charlie" }, { name: "Alice" }]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Seamless Enumerable Integration
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# Chain directly into enumerable methods - no .to_a needed!
|
|
230
|
+
users.sort_by(:score).desc.first(2) # Top 2 performers
|
|
231
|
+
users.sort_by(:name).each { |u| puts u } # Iterate in order
|
|
232
|
+
users.sort_by(:team).map(&:name) # Transform sorted results
|
|
233
|
+
users.sort_by(:score).select { |u| u[:active] } # Filter sorted results
|
|
234
|
+
|
|
235
|
+
# Array access works too
|
|
236
|
+
users.sort_by(:score).desc[0] # Best performer
|
|
237
|
+
users.sort_by(:name)[1..3] # Users 2-4 alphabetically
|
|
238
|
+
|
|
239
|
+
# Quick stats
|
|
240
|
+
users.sort_by(:score).count # Total count
|
|
241
|
+
users.sort_by(:team).count { |u| u[:active] } # Conditional count
|
|
221
242
|
```
|
|
222
243
|
|
|
223
244
|
### Mixed Key Types
|
|
@@ -251,17 +272,53 @@ api_response.sort_by.dig(:name, indifferent: true).sort
|
|
|
251
272
|
|
|
252
273
|
## API Reference
|
|
253
274
|
|
|
275
|
+
### Universal Extraction
|
|
276
|
+
|
|
277
|
+
- **`sort_by(field, **opts)`** - Direct field extraction
|
|
278
|
+
- Works with hashes, objects, and any method name
|
|
279
|
+
- Supports all the same options as `dig` and `method`
|
|
280
|
+
|
|
254
281
|
### Extractors
|
|
255
282
|
|
|
256
283
|
- **`dig(*identifiers, indifferent: false)`** - Extract values from hashes, objects, or nested structures
|
|
257
|
-
|
|
258
|
-
|
|
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)
|
|
259
288
|
|
|
260
289
|
### Modifiers
|
|
261
290
|
|
|
262
291
|
- **`downcase`** - Convert extracted strings to lowercase for comparison
|
|
263
292
|
- **`upcase`** - Convert extracted strings to uppercase for comparison
|
|
264
293
|
- **`insensitive`** - Alias for `downcase` (semantic clarity)
|
|
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.
|
|
265
322
|
|
|
266
323
|
### Ordering
|
|
267
324
|
|
|
@@ -273,33 +330,28 @@ api_response.sort_by.dig(:name, indifferent: true).sort
|
|
|
273
330
|
- **`sort`** - Execute sort and return new array
|
|
274
331
|
- **`sort!`** - Execute sort and mutate original array
|
|
275
332
|
- **`to_a`** - Alias for `sort`
|
|
333
|
+
- **`to_a!`** - Alias for `sort!`
|
|
276
334
|
- **`reverse`** - Shorthand for `desc.sort`
|
|
277
335
|
- **`reverse!`** - Shorthand for `desc.sort!`
|
|
278
336
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
The v0.3.x API is more concise and intuitive:
|
|
337
|
+
### Delegated Enumerable Methods
|
|
282
338
|
|
|
283
|
-
|
|
284
|
-
# v0.2.x (OLD - no longer works)
|
|
285
|
-
Sortsmith::Sorter.new(users).by_key(:name).case_insensitive.desc.sort
|
|
339
|
+
The following methods execute the sort and delegate to the resulting array:
|
|
286
340
|
|
|
287
|
-
|
|
288
|
-
|
|
341
|
+
- **`first(n=1)`**, **`last(n=1)`** - Get first/last n elements
|
|
342
|
+
- **`take(n)`**, **`drop(n)`** - Take/drop n elements
|
|
343
|
+
- **`each`**, **`map`**, **`select`** - Standard enumerable operations
|
|
344
|
+
- **`[](index)`** - Array access by index or range
|
|
345
|
+
- **`size`**, **`count`**, **`length`** - Size information
|
|
289
346
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
347
|
+
```ruby
|
|
348
|
+
# All of these execute the sort first, then apply the operation
|
|
349
|
+
users.sort_by(:score).desc.first(3) # Get top 3
|
|
350
|
+
users.sort_by(:name).take(5) # Take first 5 alphabetically
|
|
351
|
+
users.sort_by(:team)[0] # First by team name
|
|
352
|
+
users.sort_by(:score).size # Total size after sorting
|
|
295
353
|
```
|
|
296
354
|
|
|
297
|
-
**Key Changes:**
|
|
298
|
-
- `by_key` → `dig`
|
|
299
|
-
- `by_method`/`by_attribute` → `dig`
|
|
300
|
-
- `case_insensitive` → `insensitive` or `downcase`
|
|
301
|
-
- No more manual `Sorter.new()` - just call `sort_by` without a block
|
|
302
|
-
|
|
303
355
|
## Development
|
|
304
356
|
|
|
305
357
|
### Prerequisites
|
|
@@ -310,11 +362,13 @@ objects.sort_by.dig(:calculate_score).sort
|
|
|
310
362
|
### Setting Up the Development Environment
|
|
311
363
|
|
|
312
364
|
With Nix:
|
|
365
|
+
|
|
313
366
|
```bash
|
|
314
367
|
direnv allow
|
|
315
368
|
```
|
|
316
369
|
|
|
317
370
|
Without Nix:
|
|
371
|
+
|
|
318
372
|
```bash
|
|
319
373
|
bundle install
|
|
320
374
|
```
|
|
@@ -325,6 +379,31 @@ bundle install
|
|
|
325
379
|
bundle exec rake test
|
|
326
380
|
```
|
|
327
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
|
+
|
|
328
407
|
### Code Style
|
|
329
408
|
|
|
330
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
|