sortsmith 0.1.1 → 0.9.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 +4 -4
- data/CHANGELOG.md +110 -2
- data/README.md +249 -60
- data/flake.nix +11 -5
- data/lib/sortsmith/core_ext/enumerable.rb +75 -0
- data/lib/sortsmith/sorter.rb +247 -108
- data/lib/sortsmith/version.rb +4 -1
- data/lib/sortsmith.rb +49 -2
- metadata +4 -9
- data/Steepfile +0 -8
- data/lib/sortsmith/step.rb +0 -17
- data/sig/sortsmith/sorter.rbs +0 -107
- data/sig/sortsmith/step.rbs +0 -28
- data/sig/sortsmith/version.rbs +0 -3
- data/sig/sortsmith.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2145a4f96e357b7c580dd3bbd5c672120788c422d76737fcf7015539ace75c7
|
4
|
+
data.tar.gz: 80376b95b0cb7cee7a0b741a69d90a91307384623007b63384a1551871fdb2d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5de79e4da81b5dbf9096a9c0cbf21dd6c00500a89b8fcec679b3116b324864b3b70bf8d8e4759069412dc0979eeadbe5e3a05e8da28016ea2638de7ab8fbc54a
|
7
|
+
data.tar.gz: 1e7270386a18f165954b41810143d89920f66195f311826a19f4465a60c8865cbf3dd9e0de29cc7408d9bd41cbac43f8803030aa5724bc64e0e63f96e077f456
|
data/CHANGELOG.md
CHANGED
@@ -13,6 +13,112 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
13
13
|
|
14
14
|
### Removed
|
15
15
|
|
16
|
+
## [0.9.0] - 12025-07-06
|
17
|
+
|
18
|
+
### 🎉 MAJOR REWRITE: Fluent Chainable API
|
19
|
+
|
20
|
+
**BREAKING CHANGES**: Complete API redesign introducing a fluent, chainable interface. See migration guide below.
|
21
|
+
|
22
|
+
**Pre-1.0 Notice**: This release represents our new stable API design, but we're seeking community feedback before locking in 1.0.0 compatibility guarantees.
|
23
|
+
|
24
|
+
### Added
|
25
|
+
|
26
|
+
#### Core API Transformation
|
27
|
+
- **Fluent API**: Direct extension of `Enumerable#sort_by` for natural Ruby integration
|
28
|
+
- **Chainable Interface**: Method chaining that reads like English: `users.sort_by.dig(:name).downcase.desc.sort`
|
29
|
+
- **Universal `dig` Method**: Single extraction method that works with hashes, objects, and nested structures
|
30
|
+
- **Indifferent Key Access**: Handle mixed symbol/string hash keys with `dig(:name, indifferent: true)`
|
31
|
+
|
32
|
+
#### New Extraction Methods
|
33
|
+
- `dig(*identifiers, indifferent: false)` - Extract values from hashes, objects, or nested structures
|
34
|
+
- Support for nested extraction: `dig(:user, :profile, :email)`
|
35
|
+
- Automatic fallback to method calls for non-hash objects
|
36
|
+
|
37
|
+
#### Enhanced Modifiers
|
38
|
+
- `downcase` / `upcase` - Case transformations with automatic type checking
|
39
|
+
- `insensitive` - Alias for `downcase` for semantic clarity
|
40
|
+
- `asc` / `desc` - Explicit sort direction control
|
41
|
+
- Smart modifier chaining that only affects compatible data types
|
42
|
+
|
43
|
+
#### Multiple Terminators
|
44
|
+
- `sort` - Returns new sorted array (non-mutating)
|
45
|
+
- `sort!` - Mutates original array in place
|
46
|
+
- `reverse` - Shorthand for `desc.sort`
|
47
|
+
- `reverse!` - Shorthand for `desc.sort!`
|
48
|
+
- `to_a` - Alias for `sort` for semantic clarity
|
49
|
+
|
50
|
+
#### Backward Compatibility
|
51
|
+
- `sort_by` with block maintains original Ruby behavior
|
52
|
+
- `sort_by` without block returns Sortsmith::Sorter instance
|
53
|
+
- Zero breaking changes for existing Ruby code
|
54
|
+
|
55
|
+
### Changed
|
56
|
+
|
57
|
+
#### API Design Philosophy
|
58
|
+
- **Before**: `Sortsmith::Sorter.new(collection).by_key(:name).case_insensitive.desc.sort`
|
59
|
+
- **After**: `collection.sort_by.dig(:name).insensitive.desc.sort`
|
60
|
+
|
61
|
+
#### Improved Ergonomics
|
62
|
+
- No more explicit `Sorter.new()` instantiation required
|
63
|
+
- Tab-completable method discovery
|
64
|
+
- Natural language flow in method chains
|
65
|
+
- Unified `dig` method replaces separate `by_key`/`by_method` methods
|
66
|
+
|
67
|
+
#### Enhanced Ruby Version Support
|
68
|
+
- **Restored Ruby 3.0 and 3.1 compatibility** - Previously removed in v0.2.0, now supported again
|
69
|
+
- Full compatibility matrix: Ruby 3.0.7, 3.1.7, 3.2.8, 3.3.8, 3.4.4
|
70
|
+
- Expanded from Ruby 3.2+ requirement back to Ruby 3.0+ for broader accessibility
|
71
|
+
|
72
|
+
### Removed
|
73
|
+
|
74
|
+
#### Legacy API (Breaking Changes)
|
75
|
+
- `by_key` method (replaced by `dig`)
|
76
|
+
- `by_method`/`by_attribute` methods (replaced by `dig`)
|
77
|
+
- `case_insensitive` method (replaced by `insensitive`/`downcase`)
|
78
|
+
- `Sortsmith::Step` class (internal restructure)
|
79
|
+
- Manual `Sorter.new()` instantiation requirement
|
80
|
+
- `rbs` and `steep` type checking
|
81
|
+
|
82
|
+
### Migration Guide
|
83
|
+
|
84
|
+
The new API is more concise and intuitive, but requires updating existing code:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# v0.2.x (OLD)
|
88
|
+
Sortsmith::Sorter.new(users).by_key(:name).case_insensitive.desc.sort
|
89
|
+
|
90
|
+
# v0.9.x (NEW)
|
91
|
+
users.sort_by.dig(:name).insensitive.desc.sort
|
92
|
+
|
93
|
+
# v0.2.x (OLD)
|
94
|
+
Sortsmith::Sorter.new(objects).by_method(:calculate_score).sort
|
95
|
+
|
96
|
+
# v0.9.x (NEW)
|
97
|
+
objects.sort_by.dig(:calculate_score).sort
|
98
|
+
```
|
99
|
+
|
100
|
+
### Technical Improvements
|
101
|
+
|
102
|
+
- Complete test suite rewrite with comprehensive edge case coverage
|
103
|
+
- Enhanced error handling for mixed data types
|
104
|
+
- Improved performance through reduced object allocations
|
105
|
+
- Cleaner internal architecture with separation of concerns
|
106
|
+
- Better documentation with extensive API examples
|
107
|
+
|
108
|
+
## [0.2.0] - 12025-02-17
|
109
|
+
|
110
|
+
### Added
|
111
|
+
|
112
|
+
- Added Ruby version test matrix
|
113
|
+
|
114
|
+
### Changed
|
115
|
+
|
116
|
+
- Updated `flake.nix` to use Ruby 3.4
|
117
|
+
|
118
|
+
### Removed
|
119
|
+
|
120
|
+
- Removed Ruby 3.0 and 3.1 support
|
121
|
+
|
16
122
|
## [0.1.1] - 12025-01-15
|
17
123
|
|
18
124
|
### Changed
|
@@ -28,6 +134,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
28
134
|
- Type checking with Steep/RBS
|
29
135
|
- GitHub Actions workflow for automated testing and type checking
|
30
136
|
|
31
|
-
[unreleased]: https://github.com/itsthedevman/sortsmith/compare/v0.
|
32
|
-
[0.
|
137
|
+
[unreleased]: https://github.com/itsthedevman/sortsmith/compare/v0.9.0...HEAD
|
138
|
+
[0.9.0]: https://github.com/itsthedevman/sortsmith/compare/v0.2.0...v0.9.0
|
139
|
+
[0.2.0]: https://github.com/itsthedevman/sortsmith/compare/v0.1.1...v0.2.0
|
140
|
+
[0.1.1]: https://github.com/itsthedevman/sortsmith/compare/v0.1.0...v0.1.1
|
33
141
|
[0.1.0]: https://github.com/itsthedevman/sortsmith/compare/ac357965a1bc641d187333a5b032c5a423020ae9...v0.1.0
|
data/README.md
CHANGED
@@ -1,17 +1,66 @@
|
|
1
1
|
# Sortsmith
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/sortsmith)
|
4
|
+

|
4
5
|
[](https://github.com/itsthedevman/sortsmith/actions/workflows/main.yml)
|
5
|
-

|
6
6
|
|
7
|
-
Sortsmith
|
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.
|
8
8
|
|
9
|
-
|
9
|
+
```ruby
|
10
|
+
# Instead of this...
|
11
|
+
users.sort_by { |user| user[:name].downcase }.reverse
|
12
|
+
|
13
|
+
# Write this!
|
14
|
+
users.sort_by.dig(:name).downcase.reverse
|
15
|
+
```
|
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 *what* you want sorted, not *how* to sort it.
|
18
|
+
|
19
|
+
## Table of Contents
|
20
|
+
|
21
|
+
- [Why Sortsmith?](#why-sortsmith)
|
22
|
+
- [Installation](#installation)
|
23
|
+
- [Quick Start](#quick-start)
|
24
|
+
- [Core Concepts](#core-concepts)
|
25
|
+
- [Usage Examples](#usage-examples)
|
26
|
+
- [Array Sorting](#array-sorting)
|
27
|
+
- [Hash Collections](#hash-collections)
|
28
|
+
- [Object Collections](#object-collections)
|
29
|
+
- [Mixed Key Types](#mixed-key-types)
|
30
|
+
- [API Reference](#api-reference)
|
31
|
+
- [Migration from v0.2.x](#migration-from-v02x)
|
32
|
+
- [Development](#development)
|
33
|
+
- [Contributing](#contributing)
|
34
|
+
- [License](#license)
|
35
|
+
|
36
|
+
## Why Sortsmith?
|
37
|
+
|
38
|
+
Ruby's `sort_by` is powerful, but real-world sorting often gets messy:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# Sorting users by name, case-insensitive, descending
|
42
|
+
users.sort_by { |u| u[:name].to_s.downcase }.reverse
|
10
43
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
44
|
+
# What if some names are nil?
|
45
|
+
users.sort_by { |u| (u[:name] || "").downcase }.reverse
|
46
|
+
|
47
|
+
# What about mixed string/symbol keys?
|
48
|
+
users.sort_by { |u| (u[:name] || u["name"] || "").downcase }.reverse
|
49
|
+
```
|
50
|
+
|
51
|
+
Sortsmith handles all the edge cases and gives you a clean, readable API:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
users.sort_by.dig(:name, indifferent: true).insensitive.desc.sort
|
55
|
+
```
|
56
|
+
|
57
|
+
**Features:**
|
58
|
+
- **Fluent chaining** - Reads like English
|
59
|
+
- **Universal extraction** - Works with hashes, objects, and nested data
|
60
|
+
- **Indifferent key access** - Handles mixed symbol/string keys automatically
|
61
|
+
- **Nil-safe** - Graceful handling of missing data
|
62
|
+
- **Minimal overhead** - Extends existing Ruby methods without breaking compatibility
|
63
|
+
- **Tab-completable** - Discoverable API through your editor
|
15
64
|
|
16
65
|
## Installation
|
17
66
|
|
@@ -33,89 +82,229 @@ Or install it yourself as:
|
|
33
82
|
$ gem install sortsmith
|
34
83
|
```
|
35
84
|
|
36
|
-
##
|
85
|
+
## Quick Start
|
37
86
|
|
38
|
-
|
87
|
+
Sortsmith extends Ruby's `sort_by` method. When called without a block, it returns a chainable sorter:
|
39
88
|
|
40
89
|
```ruby
|
41
|
-
|
42
|
-
users = ["Bob", "Alice", "Carol"]
|
90
|
+
require "sortsmith"
|
43
91
|
|
44
|
-
|
92
|
+
# Basic string sorting
|
93
|
+
names = ["charlie", "Alice", "bob"]
|
94
|
+
names.sort_by.insensitive.sort
|
95
|
+
# => ["Alice", "bob", "charlie"]
|
45
96
|
|
46
|
-
#
|
97
|
+
# Hash sorting
|
98
|
+
users = [
|
99
|
+
{ name: "Charlie", age: 25 },
|
100
|
+
{ name: "Alice", age: 30 },
|
101
|
+
{ name: "Bob", age: 20 }
|
102
|
+
]
|
103
|
+
|
104
|
+
users.sort_by.dig(:name).sort
|
105
|
+
# => [{ name: "Alice", age: 30 }, { name: "Bob", age: 20 }, { name: "Charlie", age: 25 }]
|
106
|
+
|
107
|
+
# The original sort_by with blocks still works exactly the same!
|
108
|
+
users.sort_by { |u| u[:age] }
|
109
|
+
# => [{ name: "Bob", age: 20 }, { name: "Charlie", age: 25 }, { name: "Alice", age: 30 }]
|
110
|
+
```
|
111
|
+
|
112
|
+
## Core Concepts
|
113
|
+
|
114
|
+
Sortsmith uses a simple pipeline concept where each step is **optional** except for the terminator:
|
115
|
+
|
116
|
+
1. **Extract** - Get the value to sort by (`dig`) - *optional*
|
117
|
+
2. **Transform** - Modify the value for comparison (`downcase`, `upcase`) - *optional*
|
118
|
+
3. **Order** - Choose sort direction (`asc`, `desc`) - *optional*
|
119
|
+
4. **Execute** - Run the sort (`sort`, `sort!`, `reverse`) - **required**
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
collection.sort_by.dig(:field).downcase.desc.sort
|
123
|
+
# ↑ ↑ ↑ ↑ ↑
|
124
|
+
# | extract transform order execute
|
125
|
+
# chainable (opt) (opt) (opt) (required)
|
126
|
+
```
|
127
|
+
|
128
|
+
**Minimal example:**
|
129
|
+
```ruby
|
130
|
+
# This works! (though not particularly useful)
|
131
|
+
users.sort_by.sort # Same as users.sort
|
132
|
+
|
133
|
+
# More practical examples
|
134
|
+
users.sort_by.dig(:name).sort # Extract only
|
135
|
+
users.sort_by.downcase.sort # Transform only
|
136
|
+
users.sort_by.desc.sort # Order only
|
137
|
+
users.sort_by.dig(:name).desc.sort # Extract + order
|
47
138
|
```
|
48
139
|
|
49
|
-
|
140
|
+
Each step builds on the previous ones, so you can mix and match based on what your data needs. The only requirement is ending with a terminator to actually execute the sort.
|
141
|
+
|
142
|
+
## Usage Examples
|
143
|
+
|
144
|
+
### Array Sorting
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
# Basic sorting
|
148
|
+
words = ["banana", "Apple", "cherry"]
|
149
|
+
words.sort_by.sort
|
150
|
+
# => ["Apple", "banana", "cherry"]
|
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
|
+
```
|
164
|
+
|
165
|
+
### Hash Collections
|
50
166
|
|
51
167
|
```ruby
|
52
|
-
# Sort an array of hashes by a key
|
53
168
|
users = [
|
54
|
-
{ name: "
|
55
|
-
{ name: "
|
56
|
-
{ name: "
|
169
|
+
{ name: "Charlie", score: 85, team: "red" },
|
170
|
+
{ name: "Alice", score: 92, team: "blue" },
|
171
|
+
{ name: "bob", score: 78, team: "red" }
|
57
172
|
]
|
58
173
|
|
59
|
-
|
60
|
-
|
61
|
-
|
174
|
+
# Sort by name (case-sensitive)
|
175
|
+
users.sort_by.dig(:name).sort
|
176
|
+
# => [{ name: "Alice" }, { name: "Charlie" }, { name: "bob" }]
|
177
|
+
|
178
|
+
# Sort by name (case-insensitive)
|
179
|
+
users.sort_by.dig(:name).insensitive.sort
|
180
|
+
# => [{ name: "Alice" }, { name: "bob" }, { name: "Charlie" }]
|
62
181
|
|
63
|
-
#
|
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" } } }, ...]
|
64
194
|
```
|
65
195
|
|
66
|
-
### Object
|
196
|
+
### Object Collections
|
67
197
|
|
68
198
|
```ruby
|
69
|
-
|
70
|
-
|
71
|
-
|
199
|
+
User = Struct.new(:name, :age, :city)
|
200
|
+
|
201
|
+
users = [
|
202
|
+
User.new("Charlie", 25, "NYC"),
|
203
|
+
User.new("Alice", 30, "LA"),
|
204
|
+
User.new("bob", 20, "Chicago")
|
205
|
+
]
|
72
206
|
|
73
|
-
|
207
|
+
# Sort by any method/attribute
|
208
|
+
users.sort_by.dig(:name).insensitive.sort
|
209
|
+
# => [User.new("Alice"), User.new("bob"), User.new("Charlie")]
|
74
210
|
|
75
|
-
|
76
|
-
|
77
|
-
.sort
|
211
|
+
users.sort_by.dig(:age).reverse
|
212
|
+
# => [User.new("Alice", 30), User.new("Charlie", 25), User.new("bob", 20)]
|
78
213
|
|
79
|
-
#
|
214
|
+
# Works with any object that responds to the method
|
215
|
+
class Score
|
216
|
+
def calculate; rand(100); end
|
217
|
+
end
|
218
|
+
|
219
|
+
scores = [Score.new, Score.new, Score.new]
|
220
|
+
scores.sort_by.dig(:calculate).desc.sort
|
80
221
|
```
|
81
222
|
|
82
|
-
###
|
223
|
+
### Mixed Key Types
|
224
|
+
|
225
|
+
Real-world data often has inconsistent key types. Sortsmith handles this gracefully:
|
83
226
|
|
84
227
|
```ruby
|
85
|
-
|
86
|
-
{
|
87
|
-
{"name" => "
|
88
|
-
{
|
89
|
-
{"name" => "
|
90
|
-
{"name" => "Cassidy"},
|
91
|
-
{"name" => "alex"}
|
228
|
+
mixed_users = [
|
229
|
+
{ name: "Charlie" }, # symbol key
|
230
|
+
{ "name" => "Alice" }, # string key
|
231
|
+
{ :name => "Bob" }, # symbol key again
|
232
|
+
{ "name" => "Diana" } # string key again
|
92
233
|
]
|
93
234
|
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
.case_insensitive
|
98
|
-
.by_key("name")
|
99
|
-
.sort
|
235
|
+
# The indifferent option handles both key types
|
236
|
+
mixed_users.sort_by.dig(:name, indifferent: true).sort
|
237
|
+
# => [{ "name" => "Alice" }, { :name => "Bob" }, { name: "Charlie" }, { "name" => "Diana" }]
|
100
238
|
|
101
|
-
#
|
239
|
+
# Without indifferent access, you'd get sorting failures or unexpected results
|
102
240
|
```
|
103
241
|
|
104
|
-
|
242
|
+
**Performance Note**: Indifferent key access adds modest overhead (~2x slower depending on the machine) but operates in microseconds and is typically worth the convenience for mixed-key scenarios.
|
105
243
|
|
106
244
|
```ruby
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
245
|
+
# Rails users can also normalize keys upfront for better performance
|
246
|
+
mixed_users.map(&:symbolize_keys).sort_by.dig(:name).sort
|
247
|
+
|
248
|
+
# But indifferent access is handy when you can't control the data source
|
249
|
+
api_response.sort_by.dig(:name, indifferent: true).sort
|
112
250
|
```
|
113
251
|
|
252
|
+
## API Reference
|
253
|
+
|
254
|
+
### Extractors
|
255
|
+
|
256
|
+
- **`dig(*identifiers, indifferent: false)`** - Extract values from hashes, objects, or nested structures
|
257
|
+
- Works with hash keys, object methods, and nested paths
|
258
|
+
- `indifferent: true` normalizes hash keys for consistent lookup across string/symbol keys
|
259
|
+
|
260
|
+
### Modifiers
|
261
|
+
|
262
|
+
- **`downcase`** - Convert extracted strings to lowercase for comparison
|
263
|
+
- **`upcase`** - Convert extracted strings to uppercase for comparison
|
264
|
+
- **`insensitive`** - Alias for `downcase` (semantic clarity)
|
265
|
+
|
266
|
+
### Ordering
|
267
|
+
|
268
|
+
- **`asc`** - Sort in ascending order (default)
|
269
|
+
- **`desc`** - Sort in descending order
|
270
|
+
|
271
|
+
### Terminators
|
272
|
+
|
273
|
+
- **`sort`** - Execute sort and return new array
|
274
|
+
- **`sort!`** - Execute sort and mutate original array
|
275
|
+
- **`to_a`** - Alias for `sort`
|
276
|
+
- **`reverse`** - Shorthand for `desc.sort`
|
277
|
+
- **`reverse!`** - Shorthand for `desc.sort!`
|
278
|
+
|
279
|
+
## Migration from v0.2.x
|
280
|
+
|
281
|
+
The v0.3.x API is more concise and intuitive:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
# v0.2.x (OLD - no longer works)
|
285
|
+
Sortsmith::Sorter.new(users).by_key(:name).case_insensitive.desc.sort
|
286
|
+
|
287
|
+
# v0.3.x (NEW)
|
288
|
+
users.sort_by.dig(:name).insensitive.desc.sort
|
289
|
+
|
290
|
+
# v0.2.x (OLD)
|
291
|
+
Sortsmith::Sorter.new(objects).by_method(:calculate_score).sort
|
292
|
+
|
293
|
+
# v0.3.x (NEW)
|
294
|
+
objects.sort_by.dig(:calculate_score).sort
|
295
|
+
```
|
296
|
+
|
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
|
+
|
114
303
|
## Development
|
115
304
|
|
116
305
|
### Prerequisites
|
117
306
|
|
118
|
-
- Ruby 3.
|
307
|
+
- Ruby 3.0+
|
119
308
|
- Nix with Direnv (optional, but recommended)
|
120
309
|
|
121
310
|
### Setting Up the Development Environment
|
@@ -136,12 +325,6 @@ bundle install
|
|
136
325
|
bundle exec rake test
|
137
326
|
```
|
138
327
|
|
139
|
-
### Type Checking
|
140
|
-
|
141
|
-
```bash
|
142
|
-
bundle exec steep check
|
143
|
-
```
|
144
|
-
|
145
328
|
### Code Style
|
146
329
|
|
147
330
|
This project uses StandardRB. To check your code:
|
@@ -168,7 +351,7 @@ Please note that this project is released with a [Contributor Code of Conduct](C
|
|
168
351
|
|
169
352
|
## License
|
170
353
|
|
171
|
-
The gem is available as open source under the terms of the [MIT License](LICENSE.
|
354
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
172
355
|
|
173
356
|
## Changelog
|
174
357
|
|
@@ -177,3 +360,9 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
|
|
177
360
|
## Credits
|
178
361
|
|
179
362
|
- Author: Bryan "itsthedevman"
|
363
|
+
|
364
|
+
## Looking for a Software Engineer?
|
365
|
+
|
366
|
+
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!
|
367
|
+
|
368
|
+
[bryan@itsthedevman.com](mailto:bryan@itsthedevman.com)
|
data/flake.nix
CHANGED
@@ -1,21 +1,27 @@
|
|
1
1
|
{
|
2
|
-
description = "Ruby 3.
|
2
|
+
description = "Ruby 3.2 development environment";
|
3
3
|
|
4
4
|
inputs = {
|
5
5
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
6
6
|
flake-utils.url = "github:numtide/flake-utils";
|
7
7
|
};
|
8
8
|
|
9
|
-
outputs =
|
10
|
-
|
9
|
+
outputs =
|
10
|
+
{
|
11
|
+
self,
|
12
|
+
nixpkgs,
|
13
|
+
flake-utils,
|
14
|
+
}:
|
15
|
+
flake-utils.lib.eachDefaultSystem (
|
16
|
+
system:
|
11
17
|
let
|
12
18
|
pkgs = nixpkgs.legacyPackages.${system};
|
13
19
|
in
|
14
20
|
{
|
15
21
|
devShells.default = pkgs.mkShell {
|
16
22
|
buildInputs = with pkgs; [
|
17
|
-
(
|
18
|
-
jemallocSupport =
|
23
|
+
(ruby_3_2.override {
|
24
|
+
jemallocSupport = false;
|
19
25
|
docSupport = false;
|
20
26
|
})
|
21
27
|
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# Extensions to Ruby's built-in Enumerable module.
|
5
|
+
#
|
6
|
+
# Sortsmith extends {Enumerable} to provide enhanced sorting capabilities
|
7
|
+
# while preserving the original behavior when used with blocks.
|
8
|
+
#
|
9
|
+
# The key enhancement is allowing `sort_by` to be called without a block,
|
10
|
+
# which returns a {Sortsmith::Sorter} instance for method chaining.
|
11
|
+
#
|
12
|
+
# @example Original behavior (unchanged)
|
13
|
+
# [1, 2, 3].sort_by { |n| -n }
|
14
|
+
# # => [3, 2, 1]
|
15
|
+
#
|
16
|
+
# @example New chainable behavior
|
17
|
+
# users.sort_by.dig(:name).downcase.sort
|
18
|
+
# # => Returns Sorter instance for chaining
|
19
|
+
#
|
20
|
+
# @see Sortsmith::Sorter The chainable sorting interface
|
21
|
+
#
|
22
|
+
module Enumerable
|
23
|
+
##
|
24
|
+
# Stores the original sort_by method before extension.
|
25
|
+
#
|
26
|
+
# This alias preserves Ruby's original `sort_by` behavior, allowing
|
27
|
+
# Sortsmith to enhance the method while maintaining full backward
|
28
|
+
# compatibility when blocks are provided.
|
29
|
+
#
|
30
|
+
# @see #sort_by The enhanced version
|
31
|
+
# @api private
|
32
|
+
#
|
33
|
+
alias_method :og_sort_by, :sort_by
|
34
|
+
|
35
|
+
##
|
36
|
+
# Enhanced sort_by that supports both traditional block usage and chainable API.
|
37
|
+
#
|
38
|
+
# When called with a block, behaves exactly like Ruby's original `sort_by`.
|
39
|
+
# When called without a block, returns a {Sortsmith::Sorter} instance that
|
40
|
+
# provides a chainable interface for complex sorting operations.
|
41
|
+
#
|
42
|
+
# This dual behavior ensures complete backward compatibility while unlocking
|
43
|
+
# powerful new sorting capabilities.
|
44
|
+
#
|
45
|
+
# @param block [Proc, nil] Optional block for traditional sort_by behavior
|
46
|
+
# @return [Array, Sortsmith::Sorter] Array when block given, Sorter when block nil
|
47
|
+
#
|
48
|
+
# @example Traditional usage (unchanged)
|
49
|
+
# users.sort_by { |user| user.name.downcase }
|
50
|
+
# # => sorted array
|
51
|
+
#
|
52
|
+
# @example Chainable usage (new)
|
53
|
+
# users.sort_by.dig(:name).downcase.desc.sort
|
54
|
+
# # => sorted array via method chaining
|
55
|
+
#
|
56
|
+
# @example Mixed key types with indifferent access
|
57
|
+
# mixed_data = [
|
58
|
+
# { name: "Bob" }, # symbol key
|
59
|
+
# { "name" => "Alice" } # string key
|
60
|
+
# ]
|
61
|
+
# mixed_data.sort_by.dig(:name, indifferent: true).sort
|
62
|
+
# # => handles both key types gracefully
|
63
|
+
#
|
64
|
+
# @see Sortsmith::Sorter#dig
|
65
|
+
# @see Sortsmith::Sorter#sort
|
66
|
+
# @see #og_sort_by Original sort_by behavior
|
67
|
+
#
|
68
|
+
# @since 0.1.0
|
69
|
+
#
|
70
|
+
def sort_by(&block)
|
71
|
+
return Sortsmith::Sorter.new(self) if block.nil?
|
72
|
+
|
73
|
+
og_sort_by(&block)
|
74
|
+
end
|
75
|
+
end
|
data/lib/sortsmith/sorter.rb
CHANGED
@@ -1,178 +1,317 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Sortsmith
|
4
|
+
##
|
5
|
+
# A chainable sorting interface that provides a fluent API for complex sorting operations.
|
6
|
+
#
|
7
|
+
# The Sorter class allows you to build sorting pipelines by chaining extractors,
|
8
|
+
# modifiers, and ordering methods before executing the sort with a terminator method.
|
9
|
+
#
|
10
|
+
# @example Basic usage
|
11
|
+
# users.sort_by.dig(:name).sort
|
12
|
+
# # => sorted array by name
|
13
|
+
#
|
14
|
+
# @example Complex chaining
|
15
|
+
# users.sort_by.dig(:name, indifferent: true).downcase.desc.sort
|
16
|
+
# # => sorted by name (case-insensitive, descending, with indifferent key access)
|
17
|
+
#
|
18
|
+
# @example Mixed key types
|
19
|
+
# mixed_data = [
|
20
|
+
# {name: "Bob"}, # symbol key
|
21
|
+
# {"name" => "Alice"} # string key
|
22
|
+
# ]
|
23
|
+
# mixed_data.sort_by.dig(:name, indifferent: true).sort
|
24
|
+
# # => handles both key types gracefully
|
25
|
+
#
|
4
26
|
class Sorter
|
27
|
+
##
|
28
|
+
# Initialize a new Sorter instance
|
5
29
|
#
|
6
|
-
#
|
30
|
+
# @param input [Array, Enumerable] The collection to be sorted
|
31
|
+
def initialize(input)
|
32
|
+
@input = input
|
33
|
+
@extractors = []
|
34
|
+
@modifiers = []
|
35
|
+
@ordering = []
|
36
|
+
end
|
37
|
+
|
38
|
+
############################################################################
|
39
|
+
# Extractors
|
40
|
+
############################################################################
|
41
|
+
|
42
|
+
##
|
43
|
+
# Extract values from objects using hash keys or object methods
|
44
|
+
#
|
45
|
+
# Works with hashes, structs, and any object that responds to the given identifiers.
|
46
|
+
# Supports nested digging with multiple arguments.
|
47
|
+
#
|
48
|
+
# @param identifiers [Array<Symbol, String, Integer>] Keys, method names, or indices to extract
|
49
|
+
# @param indifferent [Boolean] When true, normalizes hash keys to symbols for consistent lookup
|
50
|
+
#
|
51
|
+
# @return [Sorter] Returns self for method chaining
|
52
|
+
#
|
53
|
+
# @example Hash extraction
|
54
|
+
# users.sort_by.dig(:name).sort
|
55
|
+
#
|
56
|
+
# @example Nested extraction
|
57
|
+
# users.sort_by.dig(:profile, :email).sort
|
7
58
|
#
|
8
|
-
# @
|
59
|
+
# @example Mixed key types
|
60
|
+
# users.sort_by.dig(:name, indifferent: true).sort
|
9
61
|
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
62
|
+
# @example Object method calls
|
63
|
+
# objects.sort_by.dig(:calculate_score).sort
|
64
|
+
#
|
65
|
+
def dig(*identifiers, indifferent: false)
|
66
|
+
@extractors << {method: :dig, positional: identifiers, indifferent: indifferent}
|
67
|
+
self
|
14
68
|
end
|
15
69
|
|
70
|
+
############################################################################
|
71
|
+
# Modifiers
|
72
|
+
############################################################################
|
73
|
+
|
74
|
+
##
|
75
|
+
# Transform extracted values to lowercase for comparison
|
16
76
|
#
|
17
|
-
#
|
77
|
+
# Only affects values that respond to #downcase (typically strings).
|
78
|
+
# Non-string values pass through unchanged.
|
18
79
|
#
|
19
|
-
# @return [
|
80
|
+
# @return [Sorter] Returns self for method chaining
|
20
81
|
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
left_priority = type_priority(left)
|
33
|
-
right_priority = type_priority(right)
|
34
|
-
|
35
|
-
# Apply the transformation pipeline only for same-type comparisons
|
36
|
-
if left_priority == right_priority
|
37
|
-
left = apply_transformations(transformation_steps, left)
|
38
|
-
right = apply_transformations(transformation_steps, right)
|
39
|
-
|
40
|
-
left <=> right
|
41
|
-
else
|
42
|
-
left_priority <=> right_priority
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
(@direction == :asc) ? result : result.reverse
|
82
|
+
# @example Case-insensitive string sorting
|
83
|
+
# names.sort_by.downcase.sort
|
84
|
+
#
|
85
|
+
# @example With hash extraction
|
86
|
+
# users.sort_by.dig(:name).downcase.sort
|
87
|
+
#
|
88
|
+
def downcase
|
89
|
+
@modifiers << {method: :downcase}
|
90
|
+
self
|
47
91
|
end
|
48
92
|
|
93
|
+
##
|
94
|
+
# Alias for #downcase - provides case-insensitive sorting
|
95
|
+
#
|
96
|
+
# @return [Sorter] Returns self for method chaining
|
97
|
+
#
|
98
|
+
alias_method :insensitive, :downcase
|
99
|
+
|
100
|
+
##
|
101
|
+
# Transform extracted values to uppercase for comparison
|
49
102
|
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# These are performed before transformation steps
|
103
|
+
# Only affects values that respond to #upcase (typically strings).
|
104
|
+
# Non-string values pass through unchanged.
|
53
105
|
#
|
54
|
-
# @
|
106
|
+
# @return [Sorter] Returns self for method chaining
|
55
107
|
#
|
56
|
-
# @
|
108
|
+
# @example Uppercase sorting
|
109
|
+
# names.sort_by.upcase.sort
|
57
110
|
#
|
58
|
-
def
|
59
|
-
|
111
|
+
def upcase
|
112
|
+
@modifiers << {method: :upcase}
|
113
|
+
self
|
60
114
|
end
|
61
115
|
|
116
|
+
############################################################################
|
117
|
+
# Ordering
|
118
|
+
############################################################################
|
119
|
+
|
120
|
+
##
|
121
|
+
# Sort in ascending order (default behavior)
|
62
122
|
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# These are performed after filter steps
|
66
|
-
#
|
67
|
-
# @param & [Proc] The block to execute
|
123
|
+
# This is typically unnecessary as ascending is the default,
|
124
|
+
# but can be useful for explicit clarity or resetting after desc.
|
68
125
|
#
|
69
|
-
# @return [
|
126
|
+
# @return [Sorter] Returns self for method chaining
|
70
127
|
#
|
71
|
-
def
|
72
|
-
|
128
|
+
def asc
|
129
|
+
@ordering << {method: :sort!}
|
130
|
+
self
|
73
131
|
end
|
74
132
|
|
133
|
+
##
|
134
|
+
# Sort in descending order
|
75
135
|
#
|
76
|
-
#
|
136
|
+
# Reverses the final sort order after all comparisons are complete.
|
77
137
|
#
|
78
|
-
# @
|
138
|
+
# @return [Sorter] Returns self for method chaining
|
79
139
|
#
|
80
|
-
# @
|
140
|
+
# @example Descending sort
|
141
|
+
# users.sort_by.dig(:age).desc.sort
|
81
142
|
#
|
82
|
-
def
|
83
|
-
|
143
|
+
def desc
|
144
|
+
@ordering << {method: :reverse!}
|
84
145
|
self
|
85
146
|
end
|
86
147
|
|
148
|
+
############################################################################
|
149
|
+
# Terminators
|
150
|
+
############################################################################
|
151
|
+
|
152
|
+
##
|
153
|
+
# Execute the sort pipeline and return a new sorted array
|
87
154
|
#
|
88
|
-
#
|
155
|
+
# Applies all chained extraction, transformation, and ordering steps
|
156
|
+
# to produce the final sorted result. The original collection is unchanged.
|
89
157
|
#
|
90
|
-
# @
|
158
|
+
# @return [Array] A new array containing the sorted elements
|
91
159
|
#
|
92
|
-
# @
|
160
|
+
# @example Basic termination
|
161
|
+
# sorted_users = users.sort_by.dig(:name).sort
|
93
162
|
#
|
94
|
-
def
|
95
|
-
|
163
|
+
def sort
|
164
|
+
# Apply all extraction and transformation steps during comparison
|
165
|
+
sorted = @input.sort do |item_a, item_b|
|
166
|
+
apply_steps(item_a, item_b)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Apply any ordering transformations (like desc)
|
170
|
+
apply_ordering_steps(sorted)
|
96
171
|
end
|
97
172
|
|
98
|
-
|
173
|
+
##
|
174
|
+
# Alias for #sort - returns a new sorted array
|
175
|
+
#
|
176
|
+
# @return [Array] A new array containing the sorted elements
|
177
|
+
#
|
178
|
+
# @see #sort
|
179
|
+
#
|
180
|
+
alias_method :to_a, :sort
|
99
181
|
|
182
|
+
##
|
183
|
+
# Execute the sort pipeline and mutate the original array in place
|
184
|
+
#
|
185
|
+
# Same as #sort but modifies the original array instead of creating a new one.
|
186
|
+
# Returns the mutated array for chaining.
|
100
187
|
#
|
101
|
-
#
|
102
|
-
# This will prioritize capital letters first, followed by their lowercase counterparts
|
188
|
+
# @return [Array] The original array, now sorted
|
103
189
|
#
|
104
|
-
# @
|
190
|
+
# @example In-place sorting
|
191
|
+
# users.sort_by.dig(:name).sort!
|
192
|
+
# # users array is now modified
|
105
193
|
#
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
item.chars.flat_map { |c| [c.downcase, c] }
|
111
|
-
else
|
112
|
-
item
|
113
|
-
end
|
194
|
+
def sort!
|
195
|
+
# Sort the original array in place
|
196
|
+
@input.sort! do |item_a, item_b|
|
197
|
+
apply_steps(item_a, item_b)
|
114
198
|
end
|
199
|
+
|
200
|
+
# Apply any ordering transformations
|
201
|
+
apply_ordering_steps(@input)
|
115
202
|
end
|
116
203
|
|
204
|
+
##
|
205
|
+
# Shorthand for adding desc and executing sort
|
117
206
|
#
|
118
|
-
#
|
207
|
+
# Equivalent to calling .desc.sort but more concise.
|
119
208
|
#
|
120
|
-
# @return [
|
209
|
+
# @return [Array] A new array sorted in descending order
|
121
210
|
#
|
122
|
-
|
123
|
-
|
124
|
-
|
211
|
+
# @example Reverse sorting
|
212
|
+
# users.sort_by.dig(:name).reverse
|
213
|
+
#
|
214
|
+
def reverse
|
215
|
+
desc.sort
|
125
216
|
end
|
126
217
|
|
127
|
-
|
128
|
-
|
218
|
+
##
|
219
|
+
# Shorthand for adding desc and executing sort!
|
129
220
|
#
|
130
|
-
#
|
221
|
+
# Equivalent to calling .desc.sort! but more concise.
|
131
222
|
#
|
132
|
-
# @return [
|
223
|
+
# @return [Array] The original array, sorted in descending order
|
133
224
|
#
|
134
|
-
def
|
135
|
-
|
136
|
-
self
|
225
|
+
def reverse!
|
226
|
+
desc.sort!
|
137
227
|
end
|
138
228
|
|
139
|
-
alias_method :reverse, :desc
|
140
|
-
|
141
229
|
private
|
142
230
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
231
|
+
##
|
232
|
+
# Apply the complete pipeline of steps to two items for comparison
|
233
|
+
#
|
234
|
+
# Iterates through all extraction and transformation steps,
|
235
|
+
# applying each one to both items in sequence.
|
236
|
+
#
|
237
|
+
# @param item_a [Object] First item to compare
|
238
|
+
# @param item_b [Object] Second item to compare
|
239
|
+
# @return [Integer] Comparison result (-1, 0, 1)
|
240
|
+
#
|
241
|
+
def apply_steps(item_a, item_b)
|
242
|
+
@extractors.each do |step|
|
243
|
+
item_a, item_b = apply_step(step, item_a, item_b)
|
244
|
+
end
|
147
245
|
|
148
|
-
|
149
|
-
|
150
|
-
|
246
|
+
@modifiers.each do |step|
|
247
|
+
item_a, item_b = apply_step(step, item_a, item_b)
|
248
|
+
end
|
151
249
|
|
152
|
-
|
153
|
-
|
250
|
+
# Final comparison using Ruby's spaceship operator
|
251
|
+
item_a <=> item_b
|
154
252
|
end
|
155
253
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
254
|
+
##
|
255
|
+
# Apply ordering transformations to the sorted array
|
256
|
+
#
|
257
|
+
# Executes any ordering steps (like desc) that affect the final
|
258
|
+
# arrangement of the sorted results.
|
259
|
+
#
|
260
|
+
# @param sorted [Array] The array to apply ordering to
|
261
|
+
# @return [Array] The array with ordering applied
|
262
|
+
#
|
263
|
+
def apply_ordering_steps(sorted)
|
264
|
+
@ordering.each do |step|
|
265
|
+
sorted.public_send(step[:method])
|
165
266
|
end
|
267
|
+
|
268
|
+
sorted
|
166
269
|
end
|
167
270
|
|
168
|
-
|
169
|
-
|
271
|
+
##
|
272
|
+
# Apply a single step to both items in the comparison
|
273
|
+
#
|
274
|
+
# Handles different step types and safely manages method calls,
|
275
|
+
# falling back to string conversion for non-responsive objects.
|
276
|
+
#
|
277
|
+
# @param step [Hash] Step configuration containing method and arguments
|
278
|
+
# @param item_a [Object] First item to transform
|
279
|
+
# @param item_b [Object] Second item to transform
|
280
|
+
# @return [Array<Object, Object>] Transformed items
|
281
|
+
#
|
282
|
+
def apply_step(step, item_a, item_b)
|
283
|
+
method = step[:method]
|
284
|
+
positional = step[:positional] || []
|
285
|
+
indifferent = step[:indifferent] || false
|
170
286
|
|
171
|
-
|
172
|
-
|
287
|
+
# For indifferent key access, normalize all positional args to symbols
|
288
|
+
if indifferent
|
289
|
+
positional = positional.map { |i| i.respond_to?(:to_sym) ? i.to_sym : i }
|
173
290
|
end
|
174
291
|
|
175
|
-
|
292
|
+
item_a = extract_value_from(item_a, method, positional, indifferent)
|
293
|
+
item_b = extract_value_from(item_b, method, positional, indifferent)
|
294
|
+
|
295
|
+
[item_a, item_b]
|
296
|
+
end
|
297
|
+
|
298
|
+
##
|
299
|
+
# Extracts a value from an object using the specified method and parameters.
|
300
|
+
#
|
301
|
+
# @param item [Object] the object to extract a value from
|
302
|
+
# @param method [Symbol, String] the method name to call on the object
|
303
|
+
# @param positional [Array] positional arguments to pass to the method
|
304
|
+
# @param indifferent [Boolean] whether to normalize hash keys to symbols for indifferent access
|
305
|
+
#
|
306
|
+
# @return [Object] the extracted value, or the string representation of the item
|
307
|
+
#
|
308
|
+
def extract_value_from(item, method, positional, indifferent)
|
309
|
+
return item.to_s unless item.respond_to?(method)
|
310
|
+
|
311
|
+
# For hash objects with indifferent access, normalize keys to symbols
|
312
|
+
item = item.transform_keys(&:to_sym) if indifferent
|
313
|
+
|
314
|
+
item.public_send(method, *positional)
|
176
315
|
end
|
177
316
|
end
|
178
317
|
end
|
data/lib/sortsmith/version.rb
CHANGED
data/lib/sortsmith.rb
CHANGED
@@ -1,9 +1,56 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "sortsmith/sorter"
|
4
|
-
require_relative "sortsmith/step"
|
5
3
|
require_relative "sortsmith/version"
|
4
|
+
require_relative "sortsmith/core_ext/enumerable"
|
5
|
+
require_relative "sortsmith/sorter"
|
6
6
|
|
7
|
+
##
|
8
|
+
# Sortsmith provides a flexible, chainable API for sorting Ruby collections.
|
9
|
+
#
|
10
|
+
# The gem extends Ruby's built-in {Enumerable} module to provide an intuitive
|
11
|
+
# sorting interface that reads like natural language and supports complex
|
12
|
+
# sorting operations through method chaining.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# users = ["Charlie", "alice", "Bob"]
|
16
|
+
# users.sort_by.downcase.sort
|
17
|
+
# # => ["alice", "Bob", "Charlie"]
|
18
|
+
#
|
19
|
+
# @example Hash sorting
|
20
|
+
# users = [
|
21
|
+
# { name: "Charlie", age: 25 },
|
22
|
+
# { name: "alice", age: 30 },
|
23
|
+
# { name: "Bob", age: 20 }
|
24
|
+
# ]
|
25
|
+
# users.sort_by.dig(:name).insensitive.sort
|
26
|
+
# # => sorted by name, case-insensitive
|
27
|
+
#
|
28
|
+
# @example Complex chaining
|
29
|
+
# users.sort_by.dig(:name, indifferent: true).downcase.desc.sort
|
30
|
+
# # => Extract name (works with both string/symbol keys), downcase, descending
|
31
|
+
#
|
32
|
+
# @see Sortsmith::Sorter The main sorting interface
|
33
|
+
# @see Enumerable#sort_by The extended sort_by method
|
34
|
+
#
|
35
|
+
# @author Bryan "itsthedevman"
|
36
|
+
# @since 0.1.0
|
37
|
+
#
|
7
38
|
module Sortsmith
|
39
|
+
##
|
40
|
+
# Base error class for all Sortsmith-related exceptions.
|
41
|
+
#
|
42
|
+
# This provides a namespace for any custom errors that may be raised
|
43
|
+
# during sorting operations, making it easier to rescue Sortsmith-specific
|
44
|
+
# issues without catching unrelated StandardError instances.
|
45
|
+
#
|
46
|
+
# @example Rescuing Sortsmith errors
|
47
|
+
# begin
|
48
|
+
# complex_sort_operation
|
49
|
+
# rescue Sortsmith::Error => e
|
50
|
+
# handle_sorting_error(e)
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @since 0.1.0
|
54
|
+
#
|
8
55
|
class Error < StandardError; end
|
9
56
|
end
|
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: 0.
|
4
|
+
version: 0.9.0
|
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: 2025-07-07 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,17 +25,12 @@ files:
|
|
25
25
|
- LICENSE.txt
|
26
26
|
- README.md
|
27
27
|
- Rakefile
|
28
|
-
- Steepfile
|
29
28
|
- flake.lock
|
30
29
|
- flake.nix
|
31
30
|
- lib/sortsmith.rb
|
31
|
+
- lib/sortsmith/core_ext/enumerable.rb
|
32
32
|
- lib/sortsmith/sorter.rb
|
33
|
-
- lib/sortsmith/step.rb
|
34
33
|
- lib/sortsmith/version.rb
|
35
|
-
- sig/sortsmith.rbs
|
36
|
-
- sig/sortsmith/sorter.rbs
|
37
|
-
- sig/sortsmith/step.rbs
|
38
|
-
- sig/sortsmith/version.rbs
|
39
34
|
homepage: https://github.com/itsthedevman/sortsmith
|
40
35
|
licenses:
|
41
36
|
- MIT
|
@@ -53,7 +48,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
48
|
requirements:
|
54
49
|
- - ">="
|
55
50
|
- !ruby/object:Gem::Version
|
56
|
-
version: 3.0
|
51
|
+
version: '3.0'
|
57
52
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
53
|
requirements:
|
59
54
|
- - ">="
|
data/Steepfile
DELETED
data/lib/sortsmith/step.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sortsmith
|
4
|
-
#
|
5
|
-
# Represents a step in the sorting process
|
6
|
-
#
|
7
|
-
class Step < Data.define(:type, :block)
|
8
|
-
TYPES = [
|
9
|
-
TRANSFORMATION = :transformation,
|
10
|
-
FILTER = :filter
|
11
|
-
].freeze
|
12
|
-
|
13
|
-
def perform(item)
|
14
|
-
block.call(item)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
data/sig/sortsmith/sorter.rbs
DELETED
@@ -1,107 +0,0 @@
|
|
1
|
-
module Sortsmith
|
2
|
-
interface _Sortable
|
3
|
-
def to_s: () -> String
|
4
|
-
def <=>: (self) -> Integer
|
5
|
-
end
|
6
|
-
|
7
|
-
class Sorter[Elem < _Sortable]
|
8
|
-
@enumerable: Enumerable[Elem]
|
9
|
-
|
10
|
-
@pipeline: Array[Step]
|
11
|
-
|
12
|
-
@direction: Symbol
|
13
|
-
|
14
|
-
#
|
15
|
-
# Creates a Sorter builder instance
|
16
|
-
#
|
17
|
-
# @param enumerable [Enumerable] The enumerable (Array, Hash) to sort
|
18
|
-
#
|
19
|
-
def initialize: (Enumerable[Elem] enumerable) -> void
|
20
|
-
|
21
|
-
#
|
22
|
-
# Finalizes the Sorter instance and sorts the enumerable
|
23
|
-
#
|
24
|
-
# @return [Enumerable] The sorted enumerable
|
25
|
-
#
|
26
|
-
def sort: () -> Enumerable[Elem]
|
27
|
-
|
28
|
-
#
|
29
|
-
# Adds a "filter" step to the sort pipeline.
|
30
|
-
# Filter steps are used to get data from the current item being sorted
|
31
|
-
# These are performed before transformation steps
|
32
|
-
#
|
33
|
-
# @param & [Proc] The block to execute
|
34
|
-
#
|
35
|
-
# @return [Self] The sorter instance
|
36
|
-
#
|
37
|
-
def add_filter: () { (untyped) -> untyped } -> self
|
38
|
-
|
39
|
-
#
|
40
|
-
# Adds a "transformation" step to the sort pipeline
|
41
|
-
# Transformation steps are used to transform data.
|
42
|
-
# These are performed after filter steps
|
43
|
-
#
|
44
|
-
# @param & [Proc] The block to execute
|
45
|
-
#
|
46
|
-
# @return [Self] The sorter instance
|
47
|
-
#
|
48
|
-
def add_transformation: () { (untyped) -> untyped } -> self
|
49
|
-
|
50
|
-
#
|
51
|
-
# Instructs the sorter to perform a fetch by key on the Hash being sorted
|
52
|
-
#
|
53
|
-
# @param key [String, Symbol, Any] The hash key to fetch
|
54
|
-
#
|
55
|
-
# @return [Self] The sorter instance
|
56
|
-
#
|
57
|
-
def by_key: ((String | Symbol | untyped) key) -> self
|
58
|
-
|
59
|
-
#
|
60
|
-
# Instructs the sorter to perform a method call on the object being sorted
|
61
|
-
#
|
62
|
-
# @param method [String, Symbol] The method name to call
|
63
|
-
#
|
64
|
-
# @return [Self] The sorter instance
|
65
|
-
#
|
66
|
-
def by_method: ((String | Symbol) method) -> self
|
67
|
-
|
68
|
-
alias by_attribute by_method
|
69
|
-
|
70
|
-
#
|
71
|
-
# Instructs the sorter to sort by a case insensitive value
|
72
|
-
#
|
73
|
-
# @return [Self] The sorter instance
|
74
|
-
#
|
75
|
-
def case_insensitive: () -> self
|
76
|
-
|
77
|
-
#
|
78
|
-
# Controls which direction the array will be sorted
|
79
|
-
#
|
80
|
-
# @return [Self] The sorter instance
|
81
|
-
#
|
82
|
-
def asc: () -> self
|
83
|
-
|
84
|
-
alias forward asc
|
85
|
-
|
86
|
-
#
|
87
|
-
# Controls which direction the array will be sorted
|
88
|
-
#
|
89
|
-
# @return [Self] The sorter instance
|
90
|
-
#
|
91
|
-
def desc: () -> self
|
92
|
-
|
93
|
-
alias reverse desc
|
94
|
-
|
95
|
-
private
|
96
|
-
|
97
|
-
def add_step: (type: Symbol) { (untyped) -> untyped } -> self
|
98
|
-
|
99
|
-
def select_filter_steps: () -> Array[Step]
|
100
|
-
|
101
|
-
def select_transformation_steps: () -> Array[Step]
|
102
|
-
|
103
|
-
def type_priority: (untyped value) -> Integer
|
104
|
-
|
105
|
-
def apply_transformations: (Array[Step] steps, untyped value) -> untyped
|
106
|
-
end
|
107
|
-
end
|
data/sig/sortsmith/step.rbs
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
module Sortsmith
|
2
|
-
#
|
3
|
-
# Represents a step in the sorting process
|
4
|
-
#
|
5
|
-
class Step < ::Data
|
6
|
-
TYPES: ::Array[Symbol]
|
7
|
-
|
8
|
-
FILTER: Symbol
|
9
|
-
|
10
|
-
TRANSFORMATION: Symbol
|
11
|
-
|
12
|
-
attr_reader type: Symbol
|
13
|
-
|
14
|
-
attr_reader block: ^(untyped) -> untyped
|
15
|
-
|
16
|
-
def self.new: (Symbol type, untyped block) -> instance
|
17
|
-
| (type: Symbol, block: untyped) -> instance
|
18
|
-
|
19
|
-
def self.[]: (Symbol type, untyped block) -> instance
|
20
|
-
| (type: Symbol, block: untyped) -> instance
|
21
|
-
|
22
|
-
def self.members: () -> [ :type, :block ]
|
23
|
-
|
24
|
-
def members: () -> [ :type, :block ]
|
25
|
-
|
26
|
-
def perform: (untyped item) -> untyped
|
27
|
-
end
|
28
|
-
end
|
data/sig/sortsmith/version.rbs
DELETED
data/sig/sortsmith.rbs
DELETED