In Ruby, how do you rank hotels by rating descending and then price ascending using sort_by or sort?
answer
- an Array as the sort key
- Array#<=> compares element by element
- negate numeric keys for descending
- String has no negation that reverses
- nonzero? chains comparator results
basics
~20 sReturn 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 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.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
Recall that sort_by accepts an array as its key and that putting a minus sign in front of a number flips its direction.
Explain Array#<=> as element-by-element comparison, why reverse flips every key, and how nonzero? chains comparator results for non-numeric descending keys.
Anticipate nil keys raising ArgumentError in production data and choose where missing values sort, rather than letting a report crash.
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