Enumerable#max_by
The max_by method returns the element that would be largest according to the criteria you define in a block. Unlike max which compares elements directly, max_by lets you specify how to compare them.
how it works
max_by evaluates the block for each element and returns the element that produced the largest value:
collection.max_by { |element| block_return_value }
The element itself is returned, not the block’s return value.
The block is only the scoring rule. That makes max_by a clean way to ask for the best candidate without sorting the whole collection first.
basic usage
Find the longest string in an array:
words = ["cat", "elephant", "dog", "hippopotamus"]
longest = words.max_by { |word| word.length }
# => "hippopotamus"
The method compares elements by their block return values. When you pass a block that extracts a numeric property, max_by ranks every element by that property and returns the one with the highest score. This works with any object that can be scored:
people = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
]
oldest = people.max_by { |person| person[:age] }
# => {:name=>"Charlie", :age=>35}
The structure mirrors min_by, but the direction changes from smallest to largest. That symmetry makes it easy to remember the method once you have used either one of them.
Because max_by scores items with a block rather than comparing them directly, you can express ranking logic that has nothing to do with the natural sort order of the objects themselves.
with nested comparisons
When multiple elements produce the same maximum value, max_by returns the first one encountered:
words = ["aa", "bb", "cc", "dd"]
result = words.max_by { |w| w[0] }
# => "aa" (all have the same value, first one wins)
When there is a tie, the first matching element wins. That gives you deterministic output even when multiple items score the same way. Because max_by scans the collection in order and keeps the first element that ties for the maximum score, the result is stable across multiple calls on the same data. This stability is useful when you are processing records that share a score but differ in other attributes, since the output does not flip randomly between tied candidates.
finding multiple maximum elements
Use max_by with an argument to get multiple maximum elements:
numbers = [5, 2, 8, 1, 9, 3, 7, 4, 6]
largest_three = numbers.max_by(3) { |n| n }
# => [9, 8, 7]
This is useful when you want a short ranking rather than a single winner. It is common in leaderboards, reports, and “top N” style summaries. The integer argument tells Ruby how many candidates to return, and the results appear in decreasing score order. When no argument is given, max_by returns a single element, but when you pass an integer, it returns an array of that many elements sorted from highest score to lowest. This small change in the return type is worth keeping in mind when you are chaining methods after the call.
Passing a count to max_by is also useful for finding top performers by a specific metric. When the data is a collection of hashes or model objects, the block can extract whichever attribute matters most for the ranking. The block stays clean because the scoring logic and the top-N selection are both handled by the method itself:
products = [
{ name: "Laptop", price: 999 },
{ name: "Phone", price: 699 },
{ name: "Tablet", price: 449 },
{ name: "Watch", price: 299 },
{ name: "Headphones", price: 199 }
]
expensive_three = products.max_by(3) { |p| p[:price] }
# => [{:name=>"Laptop", :price=>999}, {:name=>"Phone", :price=>699}, {:name=>"Tablet", :price=>449}]
The method keeps the ranking logic in one place and returns the highest-scoring items directly. That is usually easier to follow than sorting the whole list and then slicing it afterward. The block is a scoring function, so you can adjust the ranking logic without touching the data.
with arrays of arrays
pairs = [[1, 5], [3, 2], [2, 8], [4, 1]]
# Find pair with largest first element
by_first = pairs.max_by { |pair| pair[0] }
# => [3, 2]
# Find pair with largest second element
by_second = pairs.max_by { |pair| pair[1] }
# => [2, 8]
Arrays of arrays are a nice fit for examples because the comparison key is obvious. In real code, the key can be a nested field, an attribute accessed with a method call, or a more complex derived score that depends on multiple factors.
practical examples
finding the longest method name
methods = [:to_s, :inspect, :class, :object_id, :nil?]
longest = methods.max_by { |m| m.to_s.length }
# => :object_id (9 characters)
This shows the advantage of max_by clearly: the data stays in its original shape, and the block explains how to judge it. The method separates the scoring rule from the data itself, so the collection elements can be anything that the block knows how to evaluate.
finding the fastest shipping option
shipping_options = [
{ carrier: "USPS", weight: 5, distance: 100 },
{ carrier: "FedEx", weight: 5, distance: 50 },
{ carrier: "UPS", weight: 5, distance: 75 }
]
fastest = shipping_options.max_by { |opt| 1.0 / (opt[:weight] * opt[:distance]) }
# => {:carrier=>"FedEx", :weight=>5, :distance=>50}
The scoring expression can be tuned to match whatever “best” means for your application. The method does not care whether the score is simple or derived, as long as the block returns something comparable. For distance-based comparisons, you can compute a Manhattan metric, Euclidean distance, or any custom heuristic directly inside the block, and max_by handles the ranking automatically.
finding the furthest location
current_location = { x: 10, y: 10 }
locations = [
{ name: "Office", x: 5, y: 5 },
{ name: "Home", x: 15, y: 15 },
{ name: "Store", x: 8, y: 12 }
]
furthest = locations.max_by { |loc|
(loc[:x] - current_location[:x]).abs + (loc[:y] - current_location[:y]).abs
}
# => {:name=>"Home", :x=>15, :y=>15}
This pattern is useful when the comparison is based on distance, size, or any other numeric metric. A custom block keeps that logic explicit and avoids a separate sort. Since max_by only needs to find the element with the highest score, it does less work than sort_by followed by last.
comparison with max
| Method | Use When |
|---|---|
max | You want natural/built-in comparison |
max_by | You need custom comparison logic |
numbers = [5, 2, 8, 1, 9]
numbers.max # => 9 (natural comparison)
numbers.max_by { |n| n } # => 9 (same result)
# With custom logic, max_by shines
numbers.max_by { |n| -n } # => 1 (finds minimum via negative)
If you are already thinking in terms of a ranking function, max_by is usually the clearest expression of that idea. It reads like a direct request: give me the element with the biggest score.
performance notes
max_byuses the spaceship operator (<=>) internally for comparisons- For enumerables that respond to
#<=>, this is efficient - When no block is given, Ruby 3.0+ returns an enumerator
# Ruby 3.0+ - lazy evaluation
enum = [5, 2, 8, 1, 9].max_by
enum.each { |n| puts n } # Still evaluates, but deferred
The enumerator form exists mostly for composition. In ordinary code, the direct form is easier to read and keeps the comparison in the same place as the call.
Return Value
Returns the element that maximizes the block’s return value, or nil if the collection is empty:
[].max_by { |x| x } # => nil
When you need more than one result, pass an integer argument to get the top N elements sorted by score in descending order. This is the same approach used in the earlier ranking examples, but worth calling out separately because the method signature changes in a subtle way when the argument is present:
[3, 1, 4, 1, 5, 9, 2, 6].max_by(2)
# => [9, 6]
When you ask for several results, Ruby returns them in descending order. That makes it easy to treat the answer as a ranked shortlist. The descending order is consistent with the idea of “maximum first,” so you can use the first element of the result array with confidence.
edge cases
Empty collections return nil:
[].max_by(&:to_s) # => nil
When a collection has only one element, there is no comparison to perform, so max_by returns that single element directly. The block still runs but its return value is only used to confirm there is a valid score; the ranking step is skipped entirely. This is the same behaviour you would expect from max on a single-element array:
[42].max_by { |n| n } # => 42
Boundary cases stay predictable with max_by, which makes it a good fit for general-purpose collection processing. Empty input gives nil, and one-item input gives the only item available without raising an error or requiring special handling.
See Also
Enumerable#min_by— find the minimum element by a criteriaEnumerable#inject— accumulate values with a blockEnumerable#each_with_object— iterate while building an accumulator objectArray#map— transform each element into a new array