In Ruby, what do Hash#transform_keys and transform_values return, and what happens when two old keys map to the same new key?
answer
- new hash, same order
- transform_values sees only values
- a later duplicate overwrites
- a mapping hash renames some keys
- bang versions return self
basics
~20 stransform_keys returns a new hash with each key replaced by the block's result and the values kept; transform_values keeps the keys and replaces each value. If two old keys map to the same new key, the later entry silently overwrites the earlier.
solid answer
~40 sBoth return a **new hash** in the original order and leave the receiver alone; `transform_keys!` and `transform_values!` edit it in place and return `self`. `transform_keys(&:to_sym)` turns string keys from a form into symbols. It also takes a **mapping hash**, `transform_keys(stars: :rating)`, which renames listed keys, keeps unlisted ones and ignores unused entries; with a mapping and a block, the block handles the keys the mapping does not list. The block of `transform_values` receives only the value. The trap is collisions: `{"Plot" => 2, "plot" => 5}.transform_keys(&:downcase)` returns `{"plot" => 5}` with no error, so normalising keys can silently drop data. The new hashes also do not carry over the receiver's default value or default proc.
code
ruby · 13 linesrow = {"title" => "Dune", "stars" => "5"}
row.transform_keys(&:to_sym)
# => {title: "Dune", stars: "5"}
row.transform_keys("stars" => "rating")
# => {"title" => "Dune", "rating" => "5"}
counts = {"great" => 2, "plot" => 3}
counts.transform_values { |n| n * 10 }
# => {"great" => 20, "plot" => 30}
{"Plot" => 2, "plot" => 5}.transform_keys(&:downcase)
# => {"plot" => 5}, the earlier count is lostgo deeper
Recall that transform_keys changes keys and transform_values changes values, both returning a new hash in the same order.
Explain the mapping-hash form, how the block covers unmapped keys, what the bang versions return, and that defaults are not carried over.
Treat key normalisation of external data as a possible merge, detect collisions by size, and combine values deliberately when they happen.
Decide where key normalisation belongs at a system boundary so collisions are resolved once, with a documented rule, rather than per call site.
## What each method returns `Hash#transform_keys` and `Hash#transform_values` build a **new hash** from an existing one, changing one side of every entry: - **`transform_keys { |key| ... }`** replaces each key with the block's return value and keeps the value. - **`transform_values { |value| ... }`** replaces each value and keeps the key. Its block receives **only the value**. - Both keep the receiver's **entry order**. - Both leave the receiver unchanged. Without a block they return an `Enumerator`. A detail worth knowing: the new hash does **not** inherit the receiver's default value or default proc. `Hash.new(0).merge(a: 1).transform_values { |v| v * 2 }[:missing]` returns `nil`, not `0`. ## Three ways to name the new keys | Call | Keys listed in the mapping | Other keys | |---|---|---| | `transform_keys { \|k\| ... }` | n/a | block's return value | | `transform_keys(stars: :rating)` | renamed per the mapping | kept unchanged | | `transform_keys(stars: :rating) { \|k\| ... }` | renamed per the mapping, block not called | block's return value | Entries in the mapping that match no key are ignored. The mapping form is the concise way to rename a few fields from an external payload without touching the rest. ## Collisions overwrite silently If two old keys produce the same new key, the one processed **later** wins, because building the result is a series of assignments into the new hash: ```ruby {"Plot" => 2, "plot" => 5}.transform_keys(&:downcase) # => {"plot" => 5} ``` No exception or warning is raised. When keys come from users or external systems, normalising case, whitespace or type can merge entries. Guard against it: 1. Compare sizes: `result.size == source.size` detects a collision. 2. Merge deliberately instead, for example by building the result with `each_with_object` and summing counts for colliding words. ## The bang versions `transform_keys!` and `transform_values!` modify the receiver and return `self`, including when nothing changed; they do not follow the self-or-nil convention of some `Array` bang methods. `transform_keys!` also accepts the mapping hash and the block. Because it rewrites keys in place, it has the same collision behaviour: a duplicate new key overwrites. ## Only the top level changes Both methods are **shallow**. `transform_keys(&:to_sym)` on a parsed review such as `{"title" => "Dune", "author" => {"name" => "Ann"}}` symbolises `title` and `author`, but the nested hash under `author` keeps its string key `"name"`. Converting a whole tree needs a recursive helper that calls `transform_keys` on every nested hash and walks arrays, or conversion at parse time by the parser that produced the data. Likewise, `transform_values` passes nested hashes to its block as whole values; the block decides whether to descend. ## Changing keys and values together - Chain them: `h.transform_keys(&:to_sym).transform_values(&:to_i)`, which allocates one intermediate hash. - Use `to_h` with a block that returns a pair: `h.to_h { |k, v| [k.to_sym, v.to_i] }`, which builds one hash. ## Example: normalising review word counts A word-frequency table built from raw review text may contain `"Plot"` and `"plot"` as separate keys. `transform_keys(&:downcase)` would keep only one count. Summing into a fresh hash keeps both: ```ruby normalised = raw.each_with_object(Hash.new(0)) { |(w, n), acc| acc[w.downcase] += n } ``` Then `normalised.transform_values { |n| (n * 100.0 / total).round(1) }` turns counts into percentages while keeping the keys and their order.
- What does transform_keys do with keys that its mapping hash does not mention?Without a block it keeps them unchanged, so `{title: 1, stars: 5}.transform_keys(stars: :rating)` gives `{title: 1, rating: 5}`. With a block as well, the block is called for exactly those unmapped keys. Mapping entries that match no key are ignored.
- How do you change keys and values in one pass?Use `Hash#to_h` with a block that returns a two-element array: `h.to_h { |k, v| [k.to_sym, v.to_i] }`. Chaining `transform_keys` and `transform_values` gives the same result but builds an intermediate hash.
saying these in an interview costs you the question
- transform_keys modifies the receiver in place
- transform_keys raises an error when two keys collide
- A mapping hash drops the keys it does not mention
- transform_values passes both the key and the value to its block
- transform_keys! returns nil when no key changed
- The transformed hash keeps the receiver's default proc