/commands/ruby/sort-array-of-hashes-by-value
ruby sort array of hashes by value (sort_by, min_by, max_by)
- tool
- Ruby
- versions
- Ruby 1.9+, Ruby 3.4
- os
- any
people.sort_by { |h| h[:age] } sorts an array of hashes by the value of one key and returns a new array. Use an array as the key to sort by several fields (sort_by { |h| [h[:team], h[:name]] }), .reverse or a negated number for descending order, and sort_by! to sort in place. min_by and max_by return a single element.
Examples
The example runs CRuby 3.4 compiled to WebAssembly (ruby.wasm, the official Ruby project build) with Node.js:
npm init -y >/dev/null
npm install --silent --no-audit --no-fund @ruby/3.4-wasm-wasi@2.8.1 @ruby/wasm-wasi@2.8.1 >/dev/null 2>&1
cat > ruby.mjs <<'EOF'
import fs from "node:fs/promises";
import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/node";
const wasm = await WebAssembly.compile(await fs.readFile("node_modules/@ruby/3.4-wasm-wasi/dist/ruby+stdlib.wasm"));
const { vm } = await DefaultRubyVM(wasm);
vm.eval(await fs.readFile(process.argv[2], "utf8") + "\n$stdout.flush");
EOF
cat > sort.rb <<'EOF'
people = [
{ name: "zoe", age: 31, team: "ops" },
{ name: "Adam", age: 25, team: "dev" },
{ name: "bea", age: 31, team: "dev" },
{ name: "Carl", age: 19, team: "ops" },
]
names = ->(list) { list.map { |h| h[:name] }.join(", ") }
puts "age: " + names.(people.sort_by { |h| h[:age] })
puts "age desc: " + names.(people.sort_by { |h| -h[:age] })
puts "team, then age: " + names.(people.sort_by { |h| [h[:team], h[:age]] })
puts "name: " + names.(people.sort_by { |h| h[:name] })
puts "name, any case: " + names.(people.sort_by { |h| h[:name].downcase })
puts "oldest: " + people.max_by { |h| h[:age] }[:name]
puts "original: " + names.(people)
begin
[{ age: 3 }, { age: nil }].sort_by { |h| h[:age] }
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
EOF
node --no-warnings ruby.mjs sort.rbOutput
age: Carl, Adam, zoe, bea age desc: zoe, bea, Adam, Carl team, then age: Adam, bea, Carl, zoe name: Adam, Carl, bea, zoe name, any case: Adam, bea, Carl, zoe oldest: zoe original: zoe, Adam, bea, Carl ArgumentError: comparison of Integer with nil failed
Strings sort by byte value, so capitalised names come before lowercase ones; downcase in the key gives alphabetical order. A nil among the values cannot be compared with an Integer and raises ArgumentError.
Patterns
| Code | Result |
|---|---|
| arr.sort_by { |h| h[:age] } | Ascending by one key |
| arr.sort_by { |h| -h[:age] } | Descending, numeric keys only |
| arr.sort_by { |h| h[:name] }.reverse | Descending for any key type |
| arr.sort_by { |h| [h[:team], h[:age]] } | Several keys; arrays compare element by element |
| arr.sort { |a, b| [a[:team], b[:age]] <=> [b[:team], a[:age]] } | Team ascending, age descending |
| arr.sort_by { |h| h[:age] || 0 } | Treat missing values as 0 |
| arr.min_by / arr.max_by { |h| h[:age] } | Only the smallest or largest element |
| arr.sort_by!(&:size) | In place, with a method name as key |
Pitfalls
- String vs Symbol keys. Hashes from
JSON.parsehave String keys;h[:age]is thennilfor every element, so nothing is really sorted (and no error is raised). When only some values arenil, the sort raisescomparison of Integer with nil failed, as in the example. Useh["age"]or parse withsymbolize_names: true(see parse JSON). - Mixed types. Integers and Strings cannot be compared with each other (
comparison of String with Integer failed); convert first. - Missing elements. Calling a method on a missing value inside the block, such as
h[:name].downcase, raises undefined method for nil. Useh[:name].to_s.downcase. - Stability. Elements with equal keys may change order; add the index as a final key if order matters.
Confirm it worked
Map the sorted result to the key (sorted.map { |h| h[:age] }) and check it is in order; for long lists, sorted.each_cons(2).all? { |a, b| a[:age] <= b[:age] } returns true.
FAQ
How do I sort in descending order?
Negate a numeric key (sort_by { |h| -h[:age] }), call .reverse on the result, or use sort { |a, b| b[:age] <=> a[:age] }. For strings, reverse or the sort block are the options.
Is sort_by stable in Ruby?
No, the documentation states that the result is not guaranteed to be stable. Add a tie-breaker, for example sort_by.with_index { |h, i| [h[:age], i] }, to keep the original order of equal keys.
Does sort_by change the original array?
No, it returns a new array. sort_by! sorts the array in place.