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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2145a4f96e357b7c580dd3bbd5c672120788c422d76737fcf7015539ace75c7
4
- data.tar.gz: 80376b95b0cb7cee7a0b741a69d90a91307384623007b63384a1551871fdb2d5
3
+ metadata.gz: 594500201df14a274a22465d71f25e9471d67a9f6154047d73826974bdec2d62
4
+ data.tar.gz: cbf4a37cfeb106380825ea34aa9963c9342b2c2c8776149df7e1a1f2c9b60582
5
5
  SHA512:
6
- metadata.gz: 5de79e4da81b5dbf9096a9c0cbf21dd6c00500a89b8fcec679b3116b324864b3b70bf8d8e4759069412dc0979eeadbe5e3a05e8da28016ea2638de7ab8fbc54a
7
- data.tar.gz: 1e7270386a18f165954b41810143d89920f66195f311826a19f4465a60c8865cbf3dd9e0de29cc7408d9bd41cbac43f8803030aa5724bc64e0e63f96e077f456
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/v0.9.0...HEAD
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
  [![Gem Version](https://badge.fury.io/rb/sortsmith.svg)](https://badge.fury.io/rb/sortsmith)
4
- ![Ruby Version](https://img.shields.io/badge/ruby-3.0+-ruby)
4
+ ![Ruby Version](https://img.shields.io/badge/ruby-3.1+-ruby)
5
5
  [![Tests](https://github.com/itsthedevman/sortsmith/actions/workflows/main.yml/badge.svg)](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.dig(:name).downcase.reverse
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 *what* you want sorted, not *how* to sort it.
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, indifferent: true).insensitive.desc.sort
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. When called without a block, it returns a chainable sorter:
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
- # Basic string sorting
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
- # Hash sorting
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`) - *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**
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
- ### Array Sorting
156
+ ### Simple Direct Syntax
145
157
 
146
158
  ```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
- ```
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
- ### Hash Collections
166
-
167
- ```ruby
164
+ # Works great with hashes
168
165
  users = [
169
- { name: "Charlie", score: 85, team: "red" },
170
- { name: "Alice", score: 92, team: "blue" },
171
- { name: "bob", score: 78, team: "red" }
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
- # 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" }]
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 Collections
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/attribute
208
- users.sort_by.dig(:name).insensitive.sort
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
- users.sort_by.dig(:age).reverse
212
- # => [User.new("Alice", 30), User.new("Charlie", 25), User.new("bob", 20)]
193
+ # Or use the semantic alias
194
+ users.sort_by.attribute(:age).desc.first
195
+ # => User.new("Alice", 30, "LA")
213
196
 
214
- # Works with any object that responds to the method
215
- class Score
216
- def calculate; rand(100); end
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
- scores = [Score.new, Score.new, Score.new]
220
- scores.sort_by.dig(:calculate).desc.sort
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
- - Works with hash keys, object methods, and nested paths
258
- - `indifferent: true` normalizes hash keys for consistent lookup across string/symbol keys
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
- ## Migration from v0.2.x
280
-
281
- The v0.3.x API is more concise and intuitive:
337
+ ### Delegated Enumerable Methods
282
338
 
283
- ```ruby
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
- # v0.3.x (NEW)
288
- users.sort_by.dig(:name).insensitive.desc.sort
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
- # 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
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