In Ruby, how does uniq with a block decide which elements are duplicates, and which element does it keep for each key?
answer
- block runs once per element
- block values compared like Hash keys
- first occurrence wins, order kept
- sort first, then uniq
- Enumerable#uniq returns an Array
basics
~20 suniq 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.
solid answer
~40 s`uniq { |x| key }` computes a key per element and stores the element in a hidden Hash only if that key is not there yet, so two elements are duplicates when their keys match as Hash keys, and **the first occurrence wins**, with the survivors in their original order. That makes "one per group, chosen by a rule" a two-step idiom: sort so the preferred element comes first, then `uniq`. For example, `offers.sort_by { |o| o.price }.uniq { |o| o.hotel_id }` keeps the cheapest offer per hotel. To keep the last occurrence instead, `reverse.uniq { ... }.reverse`. `uniq` returns a new Array; on a Hash, `Enumerable#uniq` yields `[key, value]` pairs and returns an Array of pairs, not a Hash.
code
ruby · 14 linesOffer = Data.define(:hotel_id, :source, :price)
offers = [
Offer.new(1, "direct", 140), Offer.new(2, "agency", 95),
Offer.new(1, "agency", 120), Offer.new(2, "direct", 99)
]
p offers.uniq { |o| o.hotel_id }.map(&:price)
# => [140, 95] first seen per hotel
p offers.sort_by { |o| o.price }.uniq { |o| o.hotel_id }.map(&:price)
# => [95, 120] cheapest per hotel
p offers.reverse.uniq { |o| o.hotel_id }.reverse.map(&:price)
# => [120, 99] last seen per hotelgo deeper
Recall that the block computes a key per element, duplicates share a key, and uniq returns a new Array of the original elements.
Explain the first-occurrence rule and the hidden Hash behind it, and use sort-then-uniq to choose which element survives.
Make survivor choice deterministic by adding tiebreakers to the preceding sort, and pick composite keys that match the real identity of a record.
Push deduplication rules to one place with a named key so merges from several sources agree on what counts as the same record.
## What uniq with a block does `uniq` removes duplicates and returns a **new Array**. Without a block, elements are compared directly. With a block, Ruby compares the **block values** instead and returns the original elements: ```ruby %w[Ash ash Birch].uniq { |s| s.downcase } # => ["Ash", "Birch"] ``` "Ash" and "ash" produce the same key, `"ash"`, so the second is dropped. ## How the key is used In CRuby, `Enumerable#uniq` (`enum.c`) and `Array#uniq` (`array.c`) both work through a hidden Hash: 1. The block runs **once per element**. 2. The returned value is used as a Hash key; the element is stored as the value **only if the key is not already present** (`rb_hash_add_new_element`). 3. The result is the Hash's values, in insertion order. Three consequences follow: - **Duplicates are defined by Hash-key equality** of the block values: two elements collide exactly when their keys would land on the same Hash entry (the `eql?` and `hash` pair). - **The first occurrence is kept.** The `Array#uniq` rdoc says so directly: "the first occurrence always being retained". - **Order is preserved.** Survivors appear in the order they were first seen. ## Choosing which element survives Because the first occurrence wins, you control the survivor by ordering the input first. Hotel search often merges offers from several booking sources, and the page should show one offer per hotel, the cheapest: ```ruby Offer = Data.define(:hotel_id, :source, :price) cheapest = offers.sort_by { |o| o.price }.uniq { |o| o.hotel_id } ``` The variants: | Goal | Idiom | |---|---| | cheapest offer per hotel | `sort_by { \|o\| o.price }.uniq { \|o\| o.hotel_id }` | | most recent offer per hotel | `sort_by { \|o\| -o.fetched_at.to_i }.uniq { \|o\| o.hotel_id }` | | last occurrence in input order | `reverse.uniq { \|o\| o.hotel_id }.reverse` | | first occurrence in input order | `uniq { \|o\| o.hotel_id }` | When the sort key can tie, which tied element comes first is not guaranteed (Ruby's sorts are not stable), so add a tiebreaker to the sort key if the choice must be reproducible. ## Composite and derived keys The block can return any object that works as a Hash key: - an **array** for a composite identity: `uniq { |o| [o.hotel_id, o.room_type] }`; - a **normalised string** for case-insensitive or whitespace-insensitive matching: `uniq { |h| h.name.strip.downcase }`; - a **rounded number** to collapse near-identical prices: `uniq { |o| o.price.round }`. ## uniq on other Enumerables `Enumerable#uniq` is available on any Enumerable, not only Arrays, and always returns an Array. On a Hash it iterates `[key, value]` pairs: ```ruby {a: 1, b: 1, c: 2}.uniq { |k, v| v } # => [[:a, 1], [:c, 2]] ``` The result is an Array of pairs, not a Hash; call `to_h` on it if you need a Hash back. ## Cost `uniq` is a single pass with a Hash, so it is roughly linear in the number of elements, plus the cost of the block. Combined with a `sort_by` it becomes O(n log n), dominated by the sort. For a pipeline that only needs the minimum per group, that is usually acceptable and much clearer than a hand-written loop. ## Key points - The block produces a key; duplicates share a key. - The first element per key is kept, in original order. - Sort before `uniq` to decide which element survives. - `uniq` never mutates the receiver and always returns an Array.
- How do you keep the last occurrence per key instead of the first?Reverse, deduplicate, reverse back: `offers.reverse.uniq { |o| o.hotel_id }.reverse`. The first occurrence in the reversed list is the last in the original, and the final `reverse` restores input order among the survivors.
- What does uniq with a block return when called on a Hash?An Array of `[key, value]` pairs, because `Enumerable#uniq` yields each pair and collects the surviving elements into an Array. `{a: 1, b: 1}.uniq { |k, v| v }` is `[[:a, 1]]`; call `to_h` to turn it back into a Hash.
A hotel front desk handing out one welcome pack per room number: guests arrive in line order, and the first guest to name a room gets the pack, so if you want the pack to go to the earliest booking, you line guests up by booking time first.
saying these in an interview costs you the question
- uniq with a block returns the block values rather than the elements
- uniq keeps the last element it sees for each key
- uniq with a block reorders the survivors by their keys
- Calling uniq on a Hash returns a Hash
- uniq with a block removes duplicates from the receiver in place