sortsmith 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +146 -1
- data/README.md +269 -60
- data/flake.lock +3 -3
- data/flake.nix +11 -5
- data/lib/sortsmith/core_ext/enumerable.rb +109 -0
- data/lib/sortsmith/sorter.rb +692 -99
- 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: 16b369873631fe1de4c59f32bbbf5fbdb383ee1b3924a864ccd11a2ac6c4f1d5
|
4
|
+
data.tar.gz: cb329eecad62b7cf05e13624258fc09dd4a21e0aab192e50774a8e2a6c6beed3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3ada898d22d88aabe5c314dc67ac35dced713ec5f7bced55ec4ca87d12f10cd62b9fe218d6acb45831d9ec62734917d72c5343dce56b15bace088b23188bc95
|
7
|
+
data.tar.gz: e362605cde0d51b9eb63ef435e69cf13d91651ff2a9ba22dfe07a8cdd1e33c9c8bb0ce695c0914bfb3579226a3306c2d4d03f029752c25f8b17dbff22133fb1d
|
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,146 @@ 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.0] - 12025-08-03
|
19
|
+
|
20
|
+
### 🎉 API Stability Milestone
|
21
|
+
|
22
|
+
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.
|
23
|
+
|
24
|
+
**What 1.0.0 means:**
|
25
|
+
- **Stable API**: Method signatures and behavior are locked for semver compatibility
|
26
|
+
- **Complete Vision**: From the verbose early days to today's clean `collection.sort_by(:name).insensitive.desc.sort`, the API finally feels right
|
27
|
+
- **Battle Tested**: Handles the weird edge cases and mixed data types you actually encounter in real Ruby apps
|
28
|
+
|
29
|
+
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.
|
30
|
+
|
31
|
+
### Added
|
32
|
+
|
33
|
+
#### Universal Field Extraction
|
34
|
+
|
35
|
+
- **Direct syntax**: `sort_by(field, **opts)` - Sort by any field/method without explicit chaining
|
36
|
+
- **Object method extraction**: `method(*args, **kwargs)` - Call methods on objects with full argument support
|
37
|
+
- **Mixed key handling**: Enhanced `indifferent: true` support across all extractors
|
38
|
+
|
39
|
+
#### Semantic Aliases for Better Readability
|
40
|
+
|
41
|
+
- `key` - Alias for `dig` (emphasizes hash key extraction)
|
42
|
+
- `field` - Alias for `dig` (emphasizes object field access)
|
43
|
+
- `attribute` - Alias for `method` (emphasizes object attribute access)
|
44
|
+
- `case_insensitive` - Alias for `insensitive`/`downcase` (explicit case handling)
|
45
|
+
|
46
|
+
#### Seamless Enumerable Integration
|
47
|
+
|
48
|
+
- **Delegated methods**: `first`, `last`, `take`, `drop`, `each`, `map`, `select`, `[]`, `size`, `count`, `length`
|
49
|
+
- **Fluent chaining**: Sort operations flow directly into array operations without breaking
|
50
|
+
- **Full argument support**: All delegated methods support blocks, arguments, and return appropriate types
|
51
|
+
|
52
|
+
#### Enhanced Terminators
|
53
|
+
|
54
|
+
- `to_a!` - Mutating version of `to_a` for consistency with `sort!`
|
55
|
+
|
56
|
+
## [0.9.0] - 12025-07-06
|
57
|
+
|
58
|
+
### 🎉 MAJOR REWRITE: Fluent Chainable API
|
59
|
+
|
60
|
+
**BREAKING CHANGES**: Complete API redesign introducing a fluent, chainable interface. See migration guide below.
|
61
|
+
|
62
|
+
**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.
|
63
|
+
|
64
|
+
### Added
|
65
|
+
|
66
|
+
#### Core API Transformation
|
67
|
+
|
68
|
+
- **Fluent API**: Direct extension of `Enumerable#sort_by` for natural Ruby integration
|
69
|
+
- **Chainable Interface**: Method chaining that reads like English: `users.sort_by.dig(:name).downcase.desc.sort`
|
70
|
+
- **Universal `dig` Method**: Single extraction method that works with hashes, objects, and nested structures
|
71
|
+
- **Indifferent Key Access**: Handle mixed symbol/string hash keys with `dig(:name, indifferent: true)`
|
72
|
+
|
73
|
+
#### New Extraction Methods
|
74
|
+
|
75
|
+
- `dig(*identifiers, indifferent: false)` - Extract values from hashes, objects, or nested structures
|
76
|
+
- Support for nested extraction: `dig(:user, :profile, :email)`
|
77
|
+
- Automatic fallback to method calls for non-hash objects
|
78
|
+
|
79
|
+
#### Enhanced Modifiers
|
80
|
+
|
81
|
+
- `downcase` / `upcase` - Case transformations with automatic type checking
|
82
|
+
- `insensitive` - Alias for `downcase` for semantic clarity
|
83
|
+
- `asc` / `desc` - Explicit sort direction control
|
84
|
+
- Smart modifier chaining that only affects compatible data types
|
85
|
+
|
86
|
+
#### Multiple Terminators
|
87
|
+
|
88
|
+
- `sort` - Returns new sorted array (non-mutating)
|
89
|
+
- `sort!` - Mutates original array in place
|
90
|
+
- `reverse` - Shorthand for `desc.sort`
|
91
|
+
- `reverse!` - Shorthand for `desc.sort!`
|
92
|
+
- `to_a` - Alias for `sort` for semantic clarity
|
93
|
+
|
94
|
+
#### Backward Compatibility
|
95
|
+
|
96
|
+
- `sort_by` with block maintains original Ruby behavior
|
97
|
+
- `sort_by` without block returns Sortsmith::Sorter instance
|
98
|
+
- Zero breaking changes for existing Ruby code
|
99
|
+
|
100
|
+
### Changed
|
101
|
+
|
102
|
+
#### API Design Philosophy
|
103
|
+
|
104
|
+
- **Before**: `Sortsmith::Sorter.new(collection).by_key(:name).case_insensitive.desc.sort`
|
105
|
+
- **After**: `collection.sort_by.dig(:name).insensitive.desc.sort`
|
106
|
+
|
107
|
+
#### Improved Ergonomics
|
108
|
+
|
109
|
+
- No more explicit `Sorter.new()` instantiation required
|
110
|
+
- Tab-completable method discovery
|
111
|
+
- Natural language flow in method chains
|
112
|
+
- Unified `dig` method replaces separate `by_key`/`by_method` methods
|
113
|
+
|
114
|
+
#### Enhanced Ruby Version Support
|
115
|
+
|
116
|
+
- **Restored Ruby 3.0 and 3.1 compatibility** - Previously removed in v0.2.0, now supported again
|
117
|
+
- Full compatibility matrix: Ruby 3.0.7, 3.1.7, 3.2.8, 3.3.8, 3.4.4
|
118
|
+
- Expanded from Ruby 3.2+ requirement back to Ruby 3.0+ for broader accessibility
|
119
|
+
|
120
|
+
### Removed
|
121
|
+
|
122
|
+
#### Legacy API (Breaking Changes)
|
123
|
+
|
124
|
+
- `by_key` method (replaced by `dig`)
|
125
|
+
- `by_method`/`by_attribute` methods (replaced by `dig`)
|
126
|
+
- `case_insensitive` method (replaced by `insensitive`/`downcase`)
|
127
|
+
- `Sortsmith::Step` class (internal restructure)
|
128
|
+
- Manual `Sorter.new()` instantiation requirement
|
129
|
+
- `rbs` and `steep` type checking
|
130
|
+
|
131
|
+
### Migration Guide
|
132
|
+
|
133
|
+
The new API is more concise and intuitive, but requires updating existing code:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
# v0.2.x (OLD)
|
137
|
+
Sortsmith::Sorter.new(users).by_key(:name).case_insensitive.desc.sort
|
138
|
+
|
139
|
+
# v0.9.x (NEW)
|
140
|
+
users.sort_by.dig(:name).insensitive.desc.sort
|
141
|
+
|
142
|
+
# v0.2.x (OLD)
|
143
|
+
Sortsmith::Sorter.new(objects).by_method(:calculate_score).sort
|
144
|
+
|
145
|
+
# v0.9.x (NEW)
|
146
|
+
objects.sort_by.dig(:calculate_score).sort
|
147
|
+
```
|
148
|
+
|
149
|
+
### Technical Improvements
|
150
|
+
|
151
|
+
- Complete test suite rewrite with comprehensive edge case coverage
|
152
|
+
- Enhanced error handling for mixed data types
|
153
|
+
- Improved performance through reduced object allocations
|
154
|
+
- Cleaner internal architecture with separation of concerns
|
155
|
+
- Better documentation with extensive API examples
|
15
156
|
|
16
157
|
## [0.2.0] - 12025-02-17
|
17
158
|
|
@@ -30,11 +171,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
30
171
|
## [0.1.1] - 12025-01-15
|
31
172
|
|
32
173
|
### Changed
|
174
|
+
|
33
175
|
- Improved handling of non-string objects when sorting
|
34
176
|
|
35
177
|
## [0.1.0] - 12025-01-14
|
36
178
|
|
37
179
|
### Added
|
180
|
+
|
38
181
|
- Initial implementation of `Sortsmith::Sorter`
|
39
182
|
- Support for case-insensitive sorting
|
40
183
|
- Support for sorting by hash keys and object methods
|
@@ -42,7 +185,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
42
185
|
- Type checking with Steep/RBS
|
43
186
|
- GitHub Actions workflow for automated testing and type checking
|
44
187
|
|
45
|
-
[unreleased]: https://github.com/itsthedevman/sortsmith/compare/
|
188
|
+
[unreleased]: https://github.com/itsthedevman/sortsmith/compare/v1.0.0...HEAD
|
189
|
+
[1.0.0]: https://github.com/itsthedevman/sortsmith/compare/v0.9.0...v1.0.0
|
190
|
+
[0.9.0]: https://github.com/itsthedevman/sortsmith/compare/v0.2.0...v0.9.0
|
46
191
|
[0.2.0]: https://github.com/itsthedevman/sortsmith/compare/v0.1.1...v0.2.0
|
47
192
|
[0.1.1]: https://github.com/itsthedevman/sortsmith/compare/v0.1.0...v0.1.1
|
48
193
|
[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)
|
10
35
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
43
|
+
|
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
|
+
|
59
|
+
- **Fluent chaining** - Reads like English
|
60
|
+
- **Universal extraction** - Works with hashes, objects, and nested data
|
61
|
+
- **Indifferent key access** - Handles mixed symbol/string keys automatically
|
62
|
+
- **Nil-safe** - Graceful handling of missing data
|
63
|
+
- **Minimal overhead** - Extends existing Ruby methods without breaking compatibility
|
15
64
|
|
16
65
|
## Installation
|
17
66
|
|
@@ -33,99 +82,259 @@ 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 with a fluent, chainable API. Use it with or without a block for maximum flexibility:
|
39
88
|
|
40
89
|
```ruby
|
41
|
-
|
42
|
-
|
90
|
+
require "sortsmith"
|
91
|
+
|
92
|
+
# Direct syntax for simple cases (NEW!)
|
93
|
+
names = ["charlie", "Alice", "bob"]
|
94
|
+
names.sort_by(:name).insensitive.sort
|
95
|
+
# => ["Alice", "bob", "charlie"]
|
43
96
|
|
44
|
-
|
97
|
+
# Or use the chainable API for complex scenarios
|
98
|
+
users = [
|
99
|
+
{ name: "Charlie", age: 25 },
|
100
|
+
{ name: "Alice", age: 30 },
|
101
|
+
{ name: "Bob", age: 20 }
|
102
|
+
]
|
45
103
|
|
46
|
-
|
104
|
+
users.sort_by.dig(:name).sort
|
105
|
+
# => [{ name: "Alice", age: 30 }, { name: "Bob", age: 20 }, { name: "Charlie", age: 25 }]
|
106
|
+
|
107
|
+
# Seamless integration with enumerable methods (NEW!)
|
108
|
+
users.sort_by(:age).desc.first(2)
|
109
|
+
# => [{ name: "Alice", age: 30 }, { name: "Charlie", age: 25 }]
|
110
|
+
|
111
|
+
# The original sort_by with blocks still works exactly the same!
|
112
|
+
users.sort_by { |u| u[:age] }
|
113
|
+
# => [{ name: "Bob", age: 20 }, { name: "Charlie", age: 25 }, { name: "Alice", age: 30 }]
|
114
|
+
```
|
115
|
+
|
116
|
+
## Core Concepts
|
117
|
+
|
118
|
+
Sortsmith uses a simple pipeline concept where each step is **optional** except for the terminator:
|
119
|
+
|
120
|
+
1. **Extract** - Get the value to sort by (`dig`, `method`, etc.) - _optional_
|
121
|
+
2. **Transform** - Modify the value for comparison (`downcase`, `upcase`, etc.) - _optional_
|
122
|
+
3. **Order** - Choose sort direction (`asc`, `desc`) - _optional_
|
123
|
+
4. **Execute** - Run the sort (`sort`, `sort!`, `reverse`, `to_a`, etc.) - **required**
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
collection.sort_by.dig(:field).downcase.desc.sort
|
127
|
+
# ↑ ↑ ↑ ↑ ↑
|
128
|
+
# | extract transform order execute
|
129
|
+
# chainable (opt) (opt) (opt) (required)
|
130
|
+
```
|
131
|
+
|
132
|
+
**Minimal example:**
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# This works! (though not particularly useful)
|
136
|
+
users.sort_by.sort # Same as users.sort
|
137
|
+
|
138
|
+
# More practical examples
|
139
|
+
users.sort_by.dig(:name).sort # Extract only
|
140
|
+
users.sort_by.downcase.sort # Transform only
|
141
|
+
users.sort_by.desc.sort # Order only
|
142
|
+
users.sort_by.dig(:name).desc.sort # Extract + order
|
47
143
|
```
|
48
144
|
|
49
|
-
|
145
|
+
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.
|
146
|
+
|
147
|
+
## Usage Examples
|
148
|
+
|
149
|
+
### Simple Direct Syntax
|
50
150
|
|
51
151
|
```ruby
|
52
|
-
#
|
152
|
+
# Clean and direct for common operations
|
153
|
+
words = ["elephant", "cat", "butterfly"]
|
154
|
+
words.sort_by(:length).desc.sort
|
155
|
+
# => ["butterfly", "elephant", "cat"]
|
156
|
+
|
157
|
+
# Works great with hashes
|
53
158
|
users = [
|
54
|
-
{ name: "
|
55
|
-
{ name: "
|
56
|
-
{ name: "
|
159
|
+
{ name: "Cat", score: 99 },
|
160
|
+
{ name: "Charlie", score: 85 },
|
161
|
+
{ name: "karen", score: 50 },
|
162
|
+
{ name: "Alice", score: 92 },
|
163
|
+
{ name: "bob", score: 78 },
|
57
164
|
]
|
58
165
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
# Result: [{ name: "Alice" }, { name: "Bob" }, { name: "Carol" }]
|
166
|
+
# Get top 3 by score
|
167
|
+
users.sort_by(:score).desc.first(3)
|
168
|
+
# => [{ name: "Cat", score: 99 }, { name: "Alice", score: 92 }, { name: "Charlie", score: 85 }]
|
64
169
|
```
|
65
170
|
|
66
|
-
### Object Sorting
|
171
|
+
### Object Method Sorting
|
67
172
|
|
68
173
|
```ruby
|
69
|
-
|
70
|
-
|
71
|
-
|
174
|
+
User = Struct.new(:name, :age, :city)
|
175
|
+
|
176
|
+
users = [
|
177
|
+
User.new("Charlie", 25, "NYC"),
|
178
|
+
User.new("Alice", 30, "LA"),
|
179
|
+
User.new("bob", 20, "Chicago")
|
180
|
+
]
|
72
181
|
|
73
|
-
|
182
|
+
# Sort by any method or attribute
|
183
|
+
users.sort_by.method(:name).insensitive.sort
|
184
|
+
# => [User.new("Alice"), User.new("bob"), User.new("Charlie")]
|
74
185
|
|
75
|
-
|
76
|
-
|
77
|
-
|
186
|
+
# Or use the semantic alias
|
187
|
+
users.sort_by.attribute(:age).desc.first
|
188
|
+
# => User.new("Alice", 30, "LA")
|
78
189
|
|
79
|
-
#
|
190
|
+
# Methods with arguments work too
|
191
|
+
class Product
|
192
|
+
def price_in(currency)
|
193
|
+
# calculation logic
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
products.sort_by.method(:price_in, "USD").sort
|
80
198
|
```
|
81
199
|
|
82
|
-
###
|
200
|
+
### Hash Collections with Multiple Access Patterns
|
83
201
|
|
84
202
|
```ruby
|
85
203
|
users = [
|
86
|
-
{
|
87
|
-
{
|
88
|
-
{
|
89
|
-
{"name" => "carol"},
|
90
|
-
{"name" => "Cassidy"},
|
91
|
-
{"name" => "alex"}
|
204
|
+
{ name: "Charlie", score: 85, team: "red" },
|
205
|
+
{ name: "Alice", score: 92, team: "blue" },
|
206
|
+
{ name: "bob", score: 78, team: "red" }
|
92
207
|
]
|
93
208
|
|
94
|
-
#
|
95
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
.by_key("name")
|
99
|
-
.sort
|
209
|
+
# Multiple semantic ways to express extraction
|
210
|
+
users.sort_by.key(:name).insensitive.sort # Emphasizes hash keys
|
211
|
+
users.sort_by.field(:score).desc.sort # Emphasizes data fields
|
212
|
+
users.sort_by.dig(:team, :name).sort # Nested access
|
100
213
|
|
101
|
-
#
|
214
|
+
# Case handling with explicit naming
|
215
|
+
users.sort_by(:name).case_insensitive.reverse
|
216
|
+
# => [{ name: "bob" }, { name: "Charlie" }, { name: "Alice" }]
|
102
217
|
```
|
103
218
|
|
104
|
-
###
|
219
|
+
### Seamless Enumerable Integration
|
105
220
|
|
106
221
|
```ruby
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
222
|
+
# Chain directly into enumerable methods - no .to_a needed!
|
223
|
+
users.sort_by(:score).desc.first(2) # Top 2 performers
|
224
|
+
users.sort_by(:name).each { |u| puts u } # Iterate in order
|
225
|
+
users.sort_by(:team).map(&:name) # Transform sorted results
|
226
|
+
users.sort_by(:score).select { |u| u[:active] } # Filter sorted results
|
227
|
+
|
228
|
+
# Array access works too
|
229
|
+
users.sort_by(:score).desc[0] # Best performer
|
230
|
+
users.sort_by(:name)[1..3] # Users 2-4 alphabetically
|
231
|
+
|
232
|
+
# Quick stats
|
233
|
+
users.sort_by(:score).count # Total count
|
234
|
+
users.sort_by(:team).count { |u| u[:active] } # Conditional count
|
235
|
+
```
|
236
|
+
|
237
|
+
### Mixed Key Types
|
238
|
+
|
239
|
+
Real-world data often has inconsistent key types. Sortsmith handles this gracefully:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
mixed_users = [
|
243
|
+
{ name: "Charlie" }, # symbol key
|
244
|
+
{ "name" => "Alice" }, # string key
|
245
|
+
{ :name => "Bob" }, # symbol key again
|
246
|
+
{ "name" => "Diana" } # string key again
|
247
|
+
]
|
248
|
+
|
249
|
+
# The indifferent option handles both key types
|
250
|
+
mixed_users.sort_by.dig(:name, indifferent: true).sort
|
251
|
+
# => [{ "name" => "Alice" }, { :name => "Bob" }, { name: "Charlie" }, { "name" => "Diana" }]
|
252
|
+
|
253
|
+
# Without indifferent access, you'd get sorting failures or unexpected results
|
254
|
+
```
|
255
|
+
|
256
|
+
**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.
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
# Rails users can also normalize keys upfront for better performance
|
260
|
+
mixed_users.map(&:symbolize_keys).sort_by.dig(:name).sort
|
261
|
+
|
262
|
+
# But indifferent access is handy when you can't control the data source
|
263
|
+
api_response.sort_by.dig(:name, indifferent: true).sort
|
264
|
+
```
|
265
|
+
|
266
|
+
## API Reference
|
267
|
+
|
268
|
+
### Universal Extraction
|
269
|
+
|
270
|
+
- **`sort_by(field, **opts)`\*\* - Direct field extraction (NEW!)
|
271
|
+
- Works with hashes, objects, and any method name
|
272
|
+
- Supports all the same options as `dig` and `method`
|
273
|
+
|
274
|
+
### Extractors
|
275
|
+
|
276
|
+
- **`dig(*identifiers, indifferent: false)`** - Extract values from hashes, objects, or nested structures
|
277
|
+
- **`method(method_name, \*args, **kwargs)`\*\* - Call methods on objects with arguments (NEW!)
|
278
|
+
- **`key(\*identifiers, **opts)`** - Alias for `dig` (semantic clarity for hash keys) (NEW!)
|
279
|
+
- **`field(\*identifiers, **opts)`** - Alias for `dig` (semantic clarity for object fields) (NEW!)
|
280
|
+
- **`attribute(method_name, \*args, **kwargs)`** - Alias for `method` (semantic clarity) (NEW!)
|
281
|
+
|
282
|
+
### Modifiers
|
283
|
+
|
284
|
+
- **`downcase`** - Convert extracted strings to lowercase for comparison
|
285
|
+
- **`upcase`** - Convert extracted strings to uppercase for comparison
|
286
|
+
- **`insensitive`** - Alias for `downcase` (semantic clarity)
|
287
|
+
- **`case_insensitive`** - Alias for `downcase` (explicit case handling) (NEW!)
|
288
|
+
|
289
|
+
### Ordering
|
290
|
+
|
291
|
+
- **`asc`** - Sort in ascending order (default)
|
292
|
+
- **`desc`** - Sort in descending order
|
293
|
+
|
294
|
+
### Terminators
|
295
|
+
|
296
|
+
- **`sort`** - Execute sort and return new array
|
297
|
+
- **`sort!`** - Execute sort and mutate original array
|
298
|
+
- **`to_a`** - Alias for `sort`
|
299
|
+
- **`to_a!`** - Alias for `sort!` (NEW!)
|
300
|
+
- **`reverse`** - Shorthand for `desc.sort`
|
301
|
+
- **`reverse!`** - Shorthand for `desc.sort!`
|
302
|
+
|
303
|
+
### Delegated Enumerable Methods (NEW!)
|
304
|
+
|
305
|
+
The following methods execute the sort and delegate to the resulting array:
|
306
|
+
|
307
|
+
- **`first(n=1)`**, **`last(n=1)`** - Get first/last n elements
|
308
|
+
- **`take(n)`**, **`drop(n)`** - Take/drop n elements
|
309
|
+
- **`each`**, **`map`**, **`select`** - Standard enumerable operations
|
310
|
+
- **`[](index)`** - Array access by index or range
|
311
|
+
- **`size`**, **`count`**, **`length`** - Size information
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
# All of these execute the sort first, then apply the operation
|
315
|
+
users.sort_by(:score).desc.first(3) # Get top 3
|
316
|
+
users.sort_by(:name).take(5) # Take first 5 alphabetically
|
317
|
+
users.sort_by(:team)[0] # First by team name
|
318
|
+
users.sort_by(:score).size # Total size after sorting
|
112
319
|
```
|
113
320
|
|
114
321
|
## Development
|
115
322
|
|
116
323
|
### Prerequisites
|
117
324
|
|
118
|
-
- Ruby 3.
|
325
|
+
- Ruby 3.0+
|
119
326
|
- Nix with Direnv (optional, but recommended)
|
120
327
|
|
121
328
|
### Setting Up the Development Environment
|
122
329
|
|
123
330
|
With Nix:
|
331
|
+
|
124
332
|
```bash
|
125
333
|
direnv allow
|
126
334
|
```
|
127
335
|
|
128
336
|
Without Nix:
|
337
|
+
|
129
338
|
```bash
|
130
339
|
bundle install
|
131
340
|
```
|
@@ -136,12 +345,6 @@ bundle install
|
|
136
345
|
bundle exec rake test
|
137
346
|
```
|
138
347
|
|
139
|
-
### Type Checking
|
140
|
-
|
141
|
-
```bash
|
142
|
-
bundle exec steep check
|
143
|
-
```
|
144
|
-
|
145
348
|
### Code Style
|
146
349
|
|
147
350
|
This project uses StandardRB. To check your code:
|
@@ -168,7 +371,7 @@ Please note that this project is released with a [Contributor Code of Conduct](C
|
|
168
371
|
|
169
372
|
## License
|
170
373
|
|
171
|
-
The gem is available as open source under the terms of the [MIT License](LICENSE.
|
374
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
172
375
|
|
173
376
|
## Changelog
|
174
377
|
|
@@ -177,3 +380,9 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
|
|
177
380
|
## Credits
|
178
381
|
|
179
382
|
- Author: Bryan "itsthedevman"
|
383
|
+
|
384
|
+
## Looking for a Software Engineer?
|
385
|
+
|
386
|
+
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!
|
387
|
+
|
388
|
+
[bryan@itsthedevman.com](mailto:bryan@itsthedevman.com)
|
data/flake.lock
CHANGED
@@ -20,11 +20,11 @@
|
|
20
20
|
},
|
21
21
|
"nixpkgs": {
|
22
22
|
"locked": {
|
23
|
-
"lastModified":
|
24
|
-
"narHash": "sha256-
|
23
|
+
"lastModified": 1751792365,
|
24
|
+
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
|
25
25
|
"owner": "NixOS",
|
26
26
|
"repo": "nixpkgs",
|
27
|
-
"rev": "
|
27
|
+
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
|
28
28
|
"type": "github"
|
29
29
|
},
|
30
30
|
"original": {
|
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
|
|