skip to content

Sorting & Ordering

sort takes a <=> block while sort_by computes one key per element, and min_by, max_by and uniq with a block choose by key. Interviewers ask which is faster, multi-key sorts and stability.

on this pageshow

explore

questions

5

In Ruby, how do you rank hotels by rating descending and then price ascending using sort_by or sort?

level: middleimportance: must knowfreq 58%

answer

  1. an Array as the sort key
  2. Array#<=> compares element by element
  3. negate numeric keys for descending
  4. String has no negation that reverses
  5. nonzero? chains comparator results

basics

~20 s

Return an Array key, hotels.sort_by { |h| [-h.rating, h.price] }, because Array#<=> compares element by element. Negation only reverses numeric keys; for a descending String key use sort with (b.x <=> a.x).nonzero? || a.y <=> b.y.

solid answer

~40 s

`sort_by` accepts any key that responds to `<=>`, and `Array#<=>` compares arrays **element by element**, stopping at the first pair that differs. So `hotels.sort_by { |h| [-h.rating, h.price] }` sorts by rating descending (the negated number), then by price ascending. Negation works only for numbers: `-"name"` is `String#-@`, which returns a frozen, deduplicated copy, so the sort stays ascending and nothing raises. For a descending String key, write a comparator: `hotels.sort { |a, b| (b.name <=> a.name).nonzero? || a.price <=> b.price }`, where `Numeric#nonzero?` returns `nil` on zero so `||` falls through to the next key. Calling `.reverse` on a finished sort flips **every** key, not only the first. A `nil` inside a key makes `Array#<=>` return `nil`, and the sort raises `ArgumentError`.

code

ruby · 14 lines
ruby
Hotel = 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.sort_by { |h| [-h.rating, h.price] }.map(&:name)
# => ["Dune", "Birch", "Cedar", "Ash"]

p hotels.sort_by { |h| [h.rating, h.price] }.reverse.map(&:name)
# => ["Birch", "Dune", "Ash", "Cedar"]  (price now descending too)

p hotels.sort_by { |h| -h.name }.map(&:name)
# => ["Ash", "Birch", "Cedar", "Dune"]  (String#-@ does not reverse)

go deeper

for a junior

Recall that sort_by accepts an array as its key and that putting a minus sign in front of a number flips its direction.

for a middle

Explain Array#<=> as element-by-element comparison, why reverse flips every key, and how nonzero? chains comparator results for non-numeric descending keys.

for a senior

Anticipate nil keys raising ArgumentError in production data and choose where missing values sort, rather than letting a report crash.

for a principal

Push sort rules into one named key method so every listing, export and API endpoint ranks hotels the same way.

## The scenario A hotel search page lists results with the best-rated hotels first and, among hotels with the same rating, the cheapest first. That is a **multi-key sort**: a primary key (rating, descending) and a secondary key (price, ascending) that only matters when the primary keys tie. ## Arrays as composite keys Ruby's `Array#<=>` is defined in `array.c` as a **lexicographic** comparison: 1. Compare `self[0] <=> other[0]`; if it is not zero, that is the answer. 2. Otherwise compare the next pair, and so on. 3. If every compared pair is zero, the shorter array sorts first. Because `sort_by` compares its keys with `<=>`, returning an array from the block gives you a composite key for free: ```ruby Hotel = Data.define(:name, :rating, :price) hotels.sort_by { |h| [-h.rating, h.price] } # rating 4.8 first; within 4.8, cheaper first ``` The key is computed once per hotel, which keeps the Schwartzian-transform advantage of `sort_by`. ## Descending order, three ways | Technique | Works for | Pitfall | |---|---|---| | Negate the key: `-h.rating` | Integer, Float, Rational | not for Strings, Symbols or Times | | Swap the operands in a comparator: `b.x <=> a.x` | anything with `<=>` | block runs per comparison | | `.reverse` after an ascending sort | a single key | reverses **all** keys and all ties | - **Negation** is the idiomatic choice for numbers. It is also the only way to mix directions inside one `sort_by` array key. - **Swapping operands** in a `sort` block is the general tool. To chain keys, use `Numeric#nonzero?`, which returns `self` when non-zero and `nil` when zero, so `||` moves to the next key: ```ruby hotels.sort do |a, b| (b.name <=> a.name).nonzero? || a.price <=> b.price end ``` - **`.reverse`** is fine for a single-key descending sort. With a composite key it turns "rating ascending, price ascending" into "rating descending, price descending", which is rarely what the product wants. ## The String negation trap A frequent bug is writing `sort_by { |h| -h.name }` to get names in descending order. It does not raise `NoMethodError`: Ruby's `String#-@` (aliased `dedup`) returns a **frozen, deduplicated** copy of the string. The keys are the same text, so the result is still ascending, and the bug is silent. The fix is a comparator with swapped operands, or `sort_by { |h| h.name }.reverse` when the name is the only key. ## nil and other incomparable keys `Array#<=>` returns `nil` as soon as a pair of elements cannot be compared, for example `nil <=> 4.5`. The sort then raises: ```ruby [Hotel.new("New", nil, 80), Hotel.new("Ash", 4.5, 120)] .sort_by { |h| [-h.rating.to_f, h.price] } # nil.to_f is 0.0: works [Hotel.new("New", nil, 80), Hotel.new("Ash", 4.5, 120)] .sort_by { |h| [h.rating, h.price] } # ArgumentError: comparison of Array with Array failed ``` Decide explicitly where unrated hotels go. Two common choices: - put a flag first in the key: `[h.rating.nil? ? 1 : 0, -(h.rating || 0), h.price]` sends unrated hotels to the end; - substitute a sentinel such as `0` or `-Float::INFINITY` when the business rule allows it. ## Why not sort twice? Some languages let you sort by price and then sort again by rating, relying on a **stable** sort to keep the price order inside each rating. Ruby's `sort` and `sort_by` do not guarantee stability, so the second pass may scramble the first. One pass with a composite key is both correct and cheaper. ## Summary checklist - Use `sort_by { [primary, secondary] }` for multi-key sorts. - Negate numeric keys to flip their direction inside the array. - Use a `sort` comparator with `nonzero?` chaining for non-numeric descending keys. - Handle `nil` in keys before sorting, or expect `ArgumentError`.

  • How would you sort hotels by name descending, then price ascending, in one pass?
    With a comparator, because a String cannot be negated: `hotels.sort { |a, b| (b.name <=> a.name).nonzero? || a.price <=> b.price }`. `nonzero?` turns a zero result into `nil`, so `||` evaluates the price comparison only when the names tie.
  • What does Array#<=> return when the arrays share a prefix but differ in length?
    When every compared pair is equal, it compares the sizes, so the shorter array is smaller: `[4.8, 150] <=> [4.8, 150, 1]` is `-1`. With fixed-length keys built by the same block this never matters.

saying these in an interview costs you the question

  • sort_by { |h| -h.name } sorts names in descending order
  • Calling reverse after a two-key sort_by flips only the first key
  • A nil rating inside the key array simply sorts first
  • Sorting by price, then again by rating, keeps price order within each rating
  • sort_by can only take a single scalar key
open as a page

In Ruby, how does Enumerable#sort with a <=> block differ from sort_by, and which one is faster?

level: middleimportance: must knowfreq 62%

basics

~20 s

sort's block compares two elements and runs once per comparison; sort_by's block maps each element to a key once, then Ruby sorts the cached keys. sort_by wins when keys are expensive; blockless sort wins when elements are their own keys.

open as a page

In Ruby, what do Enumerable#min_by, max_by and minmax_by return, and why prefer them over sort_by followed by first?

level: juniorimportance: should knowfreq 52%

basics

~20 s

min_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.

open as a page

In Ruby, how does uniq with a block decide which elements are duplicates, and which element does it keep for each key?

level: middleimportance: should knowfreq 40%

basics

~20 s

uniq with a block runs it once per element and treats elements whose block values match as Hash keys as duplicates, keeping the first one seen, in original order. Sort first to choose which element survives.

open as a page

In Ruby, are Array#sort and sort_by stable, and how do you keep tied hotels in their original order?

level: seniorimportance: should knowfreq 32%

basics

~20 s

No. Ruby documents the order of equal elements as indeterminate and possibly unstable, and it varies by platform and input. To keep input order for ties, make the index the last key: sort_by.with_index { |h, i| [-h.rating, i] }.

open as a page