In Ruby, what do Enumerable#min_by, max_by and minmax_by return, and why prefer them over sort_by followed by first?
answer
- the element, not the key
- nil on an empty collection
- max_by(n) returns an Array
- minmax_by: [min, max] in one pass
- no full sort needed
basics
~20 smin_by and max_by return the element with the smallest or largest block value, or nil when empty; with a count n they return an Array. minmax_by returns [min, max] in one pass. None sorts the whole collection.
solid answer
~40 s`min_by { |h| h.price }` calls the block once per element and returns the **element** with the smallest key (the hotel, not its price); `max_by` does the same for the largest. On an empty collection both return `nil`. Passing a count, `max_by(3) { |h| h.rating }`, returns an **Array** of up to three elements ordered from the extreme inward, `[]` when empty. `minmax_by` returns a two-element array `[min_element, max_element]` from a single scan (`[nil, nil]` when empty). Compared with `sort_by { ... }.first` or `.last`, these avoid sorting everything: one pass for the single-element forms and a small buffer for `n`, and they say what you mean. Without a block, each returns an `Enumerator`.
code
ruby · 11 linesHotel = Data.define(:name, :rating, :price)
hotels = [
Hotel.new("Ash", 4.5, 120), Hotel.new("Birch", 4.8, 200),
Hotel.new("Cedar", 4.5, 90), Hotel.new("Dune", 4.8, 150)
]
p hotels.min_by { |h| h.price }.name # => "Cedar"
p hotels.max_by(2) { |h| h.price }.map(&:name) # => ["Birch", "Dune"]
p hotels.minmax_by { |h| h.price }.map(&:name) # => ["Cedar", "Birch"]
p hotels.max_by { |h| [h.rating, -h.price] }.name # => "Dune"
p [].max_by { |h| h.price } # => nilgo deeper
Recall that min_by and max_by take a one-argument key block and hand back the element itself, or nil when there is nothing to choose from.
Explain the return shapes with and without n, the single pass of minmax_by, and why these beat sorting just to take one element.
Guard nil results on empty inputs in production code and encode tiebreakers in the key so the chosen element is deterministic.
Favour intention-revealing extremes in shared code so reviewers see the rule, and reserve full sorts for places that genuinely need the whole order.
## The by-key extremes Ruby's `Enumerable` has two families for finding extremes: - `min`, `max` and `minmax` compare the **elements** with `<=>` or a two-argument comparator block. - `min_by`, `max_by` and `minmax_by` take a **key block** with one parameter, like `sort_by`, and compare the keys. The `_by` family is what you want when the element is an object and the ordering comes from one of its attributes, such as the cheapest hotel or the best-rated one. ## Return values | Call | Non-empty result | Empty collection | |---|---|---| | `min_by { \|h\| h.price }` | the element with the smallest key | `nil` | | `max_by { \|h\| h.rating }` | the element with the largest key | `nil` | | `min_by(2) { \|h\| h.price }` | Array of the 2 smallest, smallest first | `[]` | | `max_by(2) { \|h\| h.rating }` | Array of the 2 largest, largest first | `[]` | | `minmax_by { \|h\| h.price }` | `[min_element, max_element]` | `[nil, nil]` | | any of them without a block | an `Enumerator` | an `Enumerator` | The most common slip is expecting the **key** back. `hotels.max_by { |h| h.rating }` returns the hotel; to get the rating you call `.rating` on the result, or use `hotels.map { |h| h.rating }.max` if you truly want only the number. The `nil` on empty input is the second slip: `hotels.min_by { |h| h.price }.name` raises `NoMethodError` on an empty search result. Guard with `&.`, an early return, or an explicit empty-state branch. ## Why not sort_by and then first? ```ruby cheapest = hotels.sort_by { |h| h.price }.first # sorts everything cheapest = hotels.min_by { |h| h.price } # one pass ``` Both return the same hotel, but: 1. `sort_by` allocates a key/element array and sorts all n entries, **O(n log n)**, to keep one of them. 2. `min_by` scans once, keeping the best element so far, **O(n)** with no intermediate array. 3. `min_by(n)` and `max_by(n)` use CRuby's `rb_nmin_run` in `enum.c`, which keeps a bounded buffer and filters it as it goes, instead of sorting the whole collection. 4. `minmax_by` finds both ends in one traversal instead of two, which matters for a lazy or single-pass source such as an IO-backed enumerator. Intent matters as much as speed: `max_by` reads as "the best-rated hotel", while `sort_by(...).last` makes the reader work out that only one element is used. ## Direction and ties - To get the "top n" in descending order you do not need negation: `max_by(3) { |h| h.rating }` already returns the highest first. - For a composite rule, return an array key: `max_by { |h| [h.rating, -h.price] }` picks the best-rated hotel and, among equal ratings, the cheapest. - When several elements share the extreme key, the documentation does not promise which one you get. If the choice matters to the product, encode the tiebreaker in the key as above. ## Common interview traps - Confusing `max_by` with `max`: `max { |a, b| a.rating <=> b.rating }` also works but takes a two-argument comparator and computes both keys on every comparison. - Forgetting that `max_by(1)` returns an **array** of one element, not the element. - Assuming `minmax_by` returns keys; it returns elements. ## Summary `min_by`, `max_by` and `minmax_by` are the one-pass, intention-revealing way to pick extremes by a key. Remember their return shapes: an element or `nil`, an Array with `n`, and a two-element Array for `minmax_by`.
- How do you pick the best-rated hotel and, among equal ratings, the cheapest one?Return an array key that encodes both rules: `hotels.max_by { |h| [h.rating, -h.price] }`. `Array#<=>` compares rating first and uses the negated price only on ties, so the cheapest wins among equals.
- What does hotels.min_by(1) { |h| h.price } return compared with hotels.min_by { |h| h.price }?With a count it returns an Array, here a one-element Array holding the cheapest hotel; without a count it returns the hotel itself. On an empty list the forms return `[]` and `nil` respectively.
saying these in an interview costs you the question
- max_by returns the largest key value rather than the element
- min_by raises an error on an empty collection
- max_by(3) returns the three hotels in their original order
- minmax_by calls the collection twice, once for each end
- sort_by followed by first is just as efficient as min_by