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.
# Instead of this...
users.sort_by { |user| user[:name].downcase }.reverse
# Write this!
users.sort_by.dig(:name).downcase.reverseSortsmith 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.
- Why Sortsmith?
- Installation
- Quick Start
- Core Concepts
- Usage Examples
- API Reference
- Migration from v0.2.x
- Development
- Contributing
- License
Ruby's sort_by is powerful, but real-world sorting often gets messy:
# Sorting users by name, case-insensitive, descending
users.sort_by { |u| u[:name].to_s.downcase }.reverse
# What if some names are nil?
users.sort_by { |u| (u[:name] || "").downcase }.reverse
# What about mixed string/symbol keys?
users.sort_by { |u| (u[:name] || u["name"] || "").downcase }.reverseSortsmith handles all the edge cases and gives you a clean, readable API:
users.sort_by.dig(:name, indifferent: true).insensitive.desc.sortFeatures:
- Fluent chaining - Reads like English
- Universal extraction - Works with hashes, objects, and nested data
- Indifferent key access - Handles mixed symbol/string keys automatically
- Nil-safe - Graceful handling of missing data
- Minimal overhead - Extends existing Ruby methods without breaking compatibility
Add this line to your application's Gemfile:
gem "sortsmith"And then execute:
$ bundle installOr install it yourself as:
$ gem install sortsmithSortsmith extends Ruby's sort_by method with a fluent, chainable API. Use it with or without a block for maximum flexibility:
require "sortsmith"
# Direct syntax for simple cases (NEW!)
names = ["charlie", "Alice", "bob"]
names.sort_by(:name).insensitive.sort
# => ["Alice", "bob", "charlie"]
# Or use the chainable API for complex scenarios
users = [
{ name: "Charlie", age: 25 },
{ name: "Alice", age: 30 },
{ name: "Bob", age: 20 }
]
users.sort_by.dig(:name).sort
# => [{ name: "Alice", age: 30 }, { name: "Bob", age: 20 }, { name: "Charlie", age: 25 }]
# Seamless integration with enumerable methods (NEW!)
users.sort_by(:age).desc.first(2)
# => [{ name: "Alice", age: 30 }, { name: "Charlie", age: 25 }]
# The original sort_by with blocks still works exactly the same!
users.sort_by { |u| u[:age] }
# => [{ name: "Bob", age: 20 }, { name: "Charlie", age: 25 }, { name: "Alice", age: 30 }]Sortsmith uses a simple pipeline concept where each step is optional except for the terminator:
- Extract - Get the value to sort by (
dig,method, etc.) - optional - Transform - Modify the value for comparison (
downcase,upcase, etc.) - optional - Order - Choose sort direction (
asc,desc) - optional - Execute - Run the sort (
sort,sort!,reverse,to_a, etc.) - required
collection.sort_by.dig(:field).downcase.desc.sort
# ↑ ↑ ↑ ↑ ↑
# | extract transform order execute
# chainable (opt) (opt) (opt) (required)Minimal example:
# This works! (though not particularly useful)
users.sort_by.sort # Same as users.sort
# More practical examples
users.sort_by.dig(:name).sort # Extract only
users.sort_by.downcase.sort # Transform only
users.sort_by.desc.sort # Order only
users.sort_by.dig(:name).desc.sort # Extract + orderEach 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.
# Clean and direct for common operations
words = ["elephant", "cat", "butterfly"]
words.sort_by(:length).desc.sort
# => ["butterfly", "elephant", "cat"]
# Works great with hashes
users = [
{ name: "Cat", score: 99 },
{ name: "Charlie", score: 85 },
{ name: "karen", score: 50 },
{ name: "Alice", score: 92 },
{ name: "bob", score: 78 },
]
# Get top 3 by score
users.sort_by(:score).desc.first(3)
# => [{ name: "Cat", score: 99 }, { name: "Alice", score: 92 }, { name: "Charlie", score: 85 }]User = Struct.new(:name, :age, :city)
users = [
User.new("Charlie", 25, "NYC"),
User.new("Alice", 30, "LA"),
User.new("bob", 20, "Chicago")
]
# Sort by any method or attribute
users.sort_by.method(:name).insensitive.sort
# => [User.new("Alice"), User.new("bob"), User.new("Charlie")]
# Or use the semantic alias
users.sort_by.attribute(:age).desc.first
# => User.new("Alice", 30, "LA")
# Methods with arguments work too
class Product
def price_in(currency)
# calculation logic
end
end
products.sort_by.method(:price_in, "USD").sortusers = [
{ name: "Charlie", score: 85, team: "red" },
{ name: "Alice", score: 92, team: "blue" },
{ name: "bob", score: 78, team: "red" }
]
# Multiple semantic ways to express extraction
users.sort_by.key(:name).insensitive.sort # Emphasizes hash keys
users.sort_by.field(:score).desc.sort # Emphasizes data fields
users.sort_by.dig(:team, :name).sort # Nested access
# Case handling with explicit naming
users.sort_by(:name).case_insensitive.reverse
# => [{ name: "bob" }, { name: "Charlie" }, { name: "Alice" }]# Chain directly into enumerable methods - no .to_a needed!
users.sort_by(:score).desc.first(2) # Top 2 performers
users.sort_by(:name).each { |u| puts u } # Iterate in order
users.sort_by(:team).map(&:name) # Transform sorted results
users.sort_by(:score).select { |u| u[:active] } # Filter sorted results
# Array access works too
users.sort_by(:score).desc[0] # Best performer
users.sort_by(:name)[1..3] # Users 2-4 alphabetically
# Quick stats
users.sort_by(:score).count # Total count
users.sort_by(:team).count { |u| u[:active] } # Conditional countReal-world data often has inconsistent key types. Sortsmith handles this gracefully:
mixed_users = [
{ name: "Charlie" }, # symbol key
{ "name" => "Alice" }, # string key
{ :name => "Bob" }, # symbol key again
{ "name" => "Diana" } # string key again
]
# The indifferent option handles both key types
mixed_users.sort_by.dig(:name, indifferent: true).sort
# => [{ "name" => "Alice" }, { :name => "Bob" }, { name: "Charlie" }, { "name" => "Diana" }]
# Without indifferent access, you'd get sorting failures or unexpected resultsPerformance 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.
# Rails users can also normalize keys upfront for better performance
mixed_users.map(&:symbolize_keys).sort_by.dig(:name).sort
# But indifferent access is handy when you can't control the data source
api_response.sort_by.dig(:name, indifferent: true).sort- **
sort_by(field, **opts)** - Direct field extraction (NEW!)- Works with hashes, objects, and any method name
- Supports all the same options as
digandmethod
dig(*identifiers, indifferent: false)- Extract values from hashes, objects, or nested structures- **
method(method_name, \*args, **kwargs)** - Call methods on objects with arguments (NEW!) key(\*identifiers, **opts)- Alias fordig(semantic clarity for hash keys) (NEW!)field(\*identifiers, **opts)- Alias fordig(semantic clarity for object fields) (NEW!)attribute(method_name, \*args, **kwargs)- Alias formethod(semantic clarity) (NEW!)
downcase- Convert extracted strings to lowercase for comparisonupcase- Convert extracted strings to uppercase for comparisoninsensitive- Alias fordowncase(semantic clarity)case_insensitive- Alias fordowncase(explicit case handling) (NEW!)
asc- Sort in ascending order (default)desc- Sort in descending order
sort- Execute sort and return new arraysort!- Execute sort and mutate original arrayto_a- Alias forsortto_a!- Alias forsort!(NEW!)reverse- Shorthand fordesc.sortreverse!- Shorthand fordesc.sort!
The following methods execute the sort and delegate to the resulting array:
first(n=1),last(n=1)- Get first/last n elementstake(n),drop(n)- Take/drop n elementseach,map,select- Standard enumerable operations[](index)- Array access by index or rangesize,count,length- Size information
# All of these execute the sort first, then apply the operation
users.sort_by(:score).desc.first(3) # Get top 3
users.sort_by(:name).take(5) # Take first 5 alphabetically
users.sort_by(:team)[0] # First by team name
users.sort_by(:score).size # Total size after sorting- Ruby 3.0+
- Nix with Direnv (optional, but recommended)
With Nix:
direnv allowWithout Nix:
bundle installbundle exec rake testThis project uses StandardRB. To check your code:
bundle exec standardrbTo automatically fix issues:
bundle exec standardrb --fix- Fork it
- Create your feature branch (
git checkout -b feature/my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin feature/my-new-feature) - Create new Pull Request
Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
The gem is available as open source under the terms of the MIT License.
See CHANGELOG.md for a list of changes.
- Author: Bryan "itsthedevman"
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!