rubyguides

Array#sort_by

sort_by { |obj| block } → array

What Array#sort_by does

Array#sort_by sorts an array by a key you compute from each element. The block maps an element to a sort key, and Ruby then sorts those keys with <=>. It comes from Enumerable (which Array includes), and it returns a new array — the receiver is unchanged.

["pear", "fig", "apple"].sort_by { |word| word.length }
# => ["fig", "pear", "apple"]

Without a block, sort_by returns an Enumerator, so you can chain it. The method is the built-in Schwartzian Transform: the block runs once per element to compute the key, the keys are sorted, and the original elements are reordered to match.

Basic usage

The most common shape is “sort records by some attribute.” Here, an array of hashes gets sorted by :age:

users = [
  { name: "Alice", age: 30 },
  { name: "Bob",   age: 25 },
  { name: "Carol", age: 35 }
]

users.sort_by { |u| u[:age] }
# => [{:name=>"Bob", :age=>25}, {:name=>"Alice", :age=>30}, {:name=>"Carol", :age=>35}]

The original users array is not modified. sort_by returns a fresh Array, so you can keep the input around untouched and assign the sorted copy to a different variable. Records that share an :age keep their original relative order because sort_by is stable on modern Ruby (2.3+), which is usually the behavior you want from UI lists or report output where ties should not shuffle around.

Sorting in descending order

There is no descending flag in either sort or sort_by. The idiomatic way to flip the result depends on the type of the key you are sorting by. For numbers, the cleanest trick is to negate the key inside the block, so larger values sort to the front:

[3, 1, 4, 1, 5, 9, 2, 6].sort_by { |n| -n }
# => [9, 6, 5, 4, 3, 2, 1, 1]

Negate only works cleanly when every key is a number — the negation of "pear" is not a meaningful ordering operation, and the same is true for hashes, dates, or any object that does not have a sensible negative form. For non-numeric keys like strings, lengths, or arbitrary objects, append .reverse to the sorted result. That keeps the block readable as a true “sort by this key” expression and avoids inventing a negation that does not actually exist for the type you are sorting:

["pear", "fig", "apple"].sort_by { |w| w.length }.reverse
# => ["apple", "pear", "fig"]

Sorting by multiple keys

Use a composite key: return an array of the primary and secondary keys. Array#<=> compares element-wise, so the first key breaks ties first, and the second key breaks remaining ties. Sorting is stable on modern Ruby (2.3+), so equal primary keys keep their original order in the absence of a secondary key.

pairs = [[3, :b], [1, :a], [3, :a], [1, :b]]

pairs.sort_by { |num, letter| [num, letter] }
# => [[1, :a], [1, :b], [3, :a], [3, :b]]

A common mistake is chaining two sort_by calls. That re-sorts the array at each step and is roughly quadratic — always combine keys into one tuple instead.

sort_by vs sort

Array#sort takes a block that returns -1, 0, or 1 after comparing two elements. sort_by takes a block that maps one element to a key. Use sort_by when extracting the key is more expensive than the comparison, because the key is computed once per element. Examples where sort_by is the right call:

  • sort_by { |u| Date.parse(u[:dob]) } — parsing a date once is much cheaper than comparing dates element by element.
  • sort_by { |s| s.downcase } — case-folding once is cheaper than case-folding at every comparison.
  • sort_by { |r| r.length } for strings — length is O(1), but extracting it once keeps the intent obvious.

For trivially-comparable elements (integers, symbols, single-character strings), plain sort is usually faster because sort_by allocates the [element, key] pairs.

The bang form: sort_by!

Array defines a destructive sibling that mutates the receiver in place. Enumerable has no sort_by! — only Array does.

arr = [3, 1, 2]
arr.sort_by! { |n| n }
arr
# => [1, 2, 3]

sort_by! returns self, not a copy. That means code like original = arr.sort_by! { |n| n } aliases the two variables; mutating original mutates arr too.

No block → Enumerator

Like most Enumerable methods, calling sort_by without a block returns an Enumerator. That is useful when you want to defer the actual sort or compose it with other enumerator methods.

enum = [3, 1, 2].sort_by
enum.each { |n| n }
# => [1, 2, 3]

You can also chain the enumerator with with_index and friends before the block ever fires. The next block pairs each letter with a tiny composite key: 0 for elements at even indices, 1 for elements at odd indices, then the letter itself. Because sort_by compares arrays element-wise, even-indexed letters sort ahead of odd-indexed ones while staying alphabetical within each group, so the original ordering is partially preserved instead of being a pure alphabetical sort.

["b", "a", "c"].sort_by.with_index { |letter, i| [i.even? ? 0 : 1, letter] }
# => ["a", "c", "b"]

If you expected an array and got an Enumerator, this is why. Add a block, call .to_a, or chain .each.

Common mistakes

A few things that bite people who are new to sort_by:

  • Forgetting the bang. If you want the original array changed, you need sort_by!. The non-destructive sort_by always returns a new array.
  • Returning a comparator from the block. The block returns a key, not -1/0/1. Think of it like Python’s sorted(key=...), not like a <=> callback.
  • Incomparable keys raise ArgumentError. If the block returns nil for some elements and Integer for others, the sort blows up. Coerce keys to a common comparable type first.
  • Chaining sort_by for multi-key sorts. As noted above, build an array key, don’t chain.

That ArgumentError is worth seeing once:

[nil, 1, 2].sort_by { |n| n }
# ArgumentError: comparison of Integer with nil failed

A nil key mixed with an Integer key is the most common trigger. Coerce with to_i, a default like &.something || fallback, or filter nils out before sorting.

See Also

  • Array#sort — the <=>-based sibling. Pick sort when the comparison is naturally pairwise and the key would be expensive to compute.
  • Array#min_by — same block-returns-a-key shape, but returns a single element rather than a sorted array.
  • Enumerable#sort_by — the method Array#sort_by inherits from. Worth reading if you want to use sort_by on Hashes, ranges, or your own enumerables.