Enumerable#sort_by
The sort_by method sorts elements based on the return value of a block. It’s the flexible cousin of sort — instead of comparing elements directly, you tell Ruby how to compare them.
How it works
sort_by evaluates the block for each element and sorts based on those return values:
collection.sort_by { |element| criteria }
Unlike sort, which compares elements using their own <=> method, sort_by lets you define custom comparison logic.
That makes it easier to separate the thing you are sorting from the rule you want to apply. Instead of teaching every object how to compare itself, you can provide a sorting key at the call site.
Basic usage
Sort strings by length:
words = ["cat", "elephant", "dog", "hippopotamus"]
sorted = words.sort_by { |word| word.length }
# => ["cat", "dog", "elephant", "hippopotamus"]
The block evaluates word.length for each string, producing the sort keys 3, 8, 3, and 12. Ruby then sorts the original strings by these keys, which puts "cat" and "dog" first in their original order since they share the same key value.
Sort hashes by a specific key:
people = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
]
sorted = people.sort_by { |person| person[:age] }
# => [{:name=>"Bob", :age=>25}, {:name=>"Alice", :age=>30}, {:name=>"Charlie", :age=>35}]
These examples show the most common pattern: pick a value that describes the ordering and let Ruby sort by that value. It is a good fit for strings, hashes, and any other collection where the natural ordering is not obvious.
Sorting in descending order
Ruby doesn’t have a direct sort_by! (descending) option, but you can negate numeric values:
numbers = [5, 2, 8, 1, 9, 3]
sorted = numbers.sort_by { |n| -n }
# => [9, 8, 5, 3, 2, 1]
Negating numeric values is the most direct way to get descending order because Ruby compares the keys with <=> and negative numbers sort before positive ones. For non-numeric keys, you cannot negate them, so reversing after an ascending sort is the more general approach.
Or reverse after sorting:
words = ["cat", "elephant", "dog"]
sorted = words.sort_by(&:length).reverse
# => ["hippopotamus", "elephant", "dog", "cat"]
The negation approach is best for numeric keys, while reverse is convenient when the ascending result already exists. Both keep the block focused on the sort key rather than on the final display order.
Multiple sort criteria
Sort by multiple criteria by returning an array from the block:
products = [
{ name: "Phone", category: "electronics", price: 699 },
{ name: "Shirt", category: "clothing", price: 29 },
{ name: "Laptop", category: "electronics", price: 999 },
{ name: "Pants", category: "clothing", price: 49 }
]
# Sort by category first, then by price within each category
sorted = products.sort_by { |p| [p[:category], p[:price]] }
# => [
# {:name=>"Shirt", :category=>"clothing", :price=>29},
# {:name=>"Pants", :category=>"clothing", :price=>49},
# {:name=>"Phone", :category=>"electronics", :price=>699},
# {:name=>"Laptop", :category=>"electronics", :price=>999}
# ]
This works because arrays are compared element-by-element in order.
That array key trick is the standard way to express secondary and tertiary ordering rules in Ruby. It keeps the whole comparison in one place and makes the intended sort order easy to read.
Practical Examples
Sort files by modification time
require 'pathname'
files = Dir.glob("*").map { |f| Pathname.new(f) }
sorted = files.select(&:file?).sort_by(&:mtime)
# Most recently modified first
sorted.reverse.each { |f| puts "#{f.mtime} - #{f}" }
This pattern is useful when you want the newest files first without giving up the convenience of sort_by. The block describes the sort key, and the reverse call handles the presentation order. Together they produce a clean pipeline that reads from left to right.
Sort by string length (case-insensitive)
words = ["Apple", "banana", "Cherry", "date"]
# Case-insensitive sorting
sorted = words.sort_by { |w| w.downcase }
# => ["Apple", "banana", "Cherry", "date"]
Lowercasing inside the block keeps the comparison predictable without changing the original strings. It is a simple way to make ordering ignore case while preserving the original values in the result. The block only affects the sort key, so the output still contains the original cased strings.
Sort by multiple attributes
employees = [
{ name: "Alice", department: "Engineering", level: 2 },
{ name: "Bob", department: "Sales", level: 1 },
{ name: "Charlie", department: "Engineering", level: 1 },
{ name: "Diana", department: "Sales", level: 2 }
]
# Sort by department, then by level (descending within department)
sorted = employees.sort_by { |e| [e[:department], -e[:level]] }
Returning an array lets Ruby compare the department first and the level second. That pattern is common in reports where one column defines the primary order and another breaks ties. Negating the level number puts higher levels first within each department group.
Natural sort (like file explorers)
files = ["file1.txt", "file10.txt", "file2.txt"]
# Standard sort: file1, file10, file2
# Natural sort: file1, file2, file10
sorted = files.sort_by { |f| f.scan(/\d+/).first.to_i }
# => ["file1.txt", "file2.txt", "file10.txt"]
Natural sorting is useful when embedded numbers should be compared numerically instead of lexicographically. Without that extra step, the string order would put file10 before file2.
Comparison with sort
| Method | Use When |
|---|---|
sort | Natural comparison, simple elements |
sort_by | Custom comparison logic, complex criteria |
numbers = [5, 2, 8, 1, 9]
numbers.sort # => [1, 2, 5, 8, 9]
numbers.sort_by { |n| n } # => [1, 2, 5, 8, 9] - same result
# sort_by shines with custom logic
numbers.sort_by { |n| -n } # => [9, 8, 5, 2, 1] - descending
numbers.sort_by { |n| n.to_s } # => [1, 2, 5, 8, 9] - string sort
These variations show that sort_by is not limited to one style of comparison. You can sort numerically, alphabetically, or with any other key that makes sense for the data.
Performance notes
sort_byuses the Schwartzian transform internally — it maps elements to their sort keys once, sorts, then extracts original elements- This is more efficient than
sortwith an expensive block that gets called repeatedly - When no block is given, Ruby returns an enumerator (Ruby 1.9+)
# Lazy evaluation
enum = [5, 2, 8].sort_by
enum.each { |n| puts n } # Prints sorted values
The enumerator form is useful when you want to delay the sort or pass it into another chain. It follows the same rule as many other enumerable methods: no block means Ruby gives you an enumerator instead of doing the work immediately.
Return value
Returns a new array of sorted elements:
[3, 1, 4, 1, 5, 9, 2, 6].sort_by { |n| n }
# => [1, 1, 2, 3, 4, 5, 6, 9]
# With n argument (Ruby 3.0+)
[3, 1, 4, 1, 5].sort_by(2) { |n| n }
# => [1, 1]
The n argument (added in Ruby 3.0) limits the result to the first n elements after sorting. This is useful when you only need the top few items rather than the fully sorted list, and it avoids allocating a large intermediate array when the input is long.
Returns an empty array if the collection is empty:
[].sort_by { |x| x } # => []
The return value is always a new array, so the original collection stays untouched. That makes sort_by predictable when you want ordered output but do not want to mutate the source data. The non-destructive contract holds for every call regardless of input size.
Edge cases
Empty collections return an empty array:
[].sort_by(&:to_s) # => []
Using &:to_s as a shorthand passes each element through to_s before comparing. For an empty array, the result is still an empty array because there is nothing to sort or convert.
Collections with one element return that element sorted:
[42].sort_by { |n| n } # => [42]
A single-element collection always produces a single-element result because there is nothing to reorder. The method does not short-circuit or optimize this case away; it simply returns the element wrapped in a new array.
Nil or false block returns are handled as-is in sorting:
items = [nil, false, 1, 0, "a", ""]
items.sort_by { |x| x.nil? ? 1 : 0 } # nil values sort last
# => [1, 0, "a", "", false, nil]
Nil and false values are not special to sort_by; they are only special when your block gives them a different key. That keeps the method flexible enough for mixed collections without extra branching.
See Also
Enumerable#max_by— find the maximum element by a criteriaEnumerable#min_by— find the minimum element by a criteriaEnumerable#group_by— group elements by a criteriaArray#sort— natural sorting for arrays