49

How do I change all the keys of a hash by a new set of given keys?

Is there a way to do that elegantly?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
JCLL
  • 5,379
  • 5
  • 44
  • 64

7 Answers7

79

Assuming you have a Hash which maps old keys to new keys, you could do something like

hsh.transform_keys(&key_map.method(:[]))
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
18

Ruby 2.5 has Hash#transform_keys! method. Example using a map of keys

h = {a: 1, b: 2, c: 3}
key_map = {a: 'A', b: 'B', c: 'C'}

h.transform_keys! {|k| key_map[k]}
# => {"A"=>1, "B"=>2, "C"=>3} 

You can also use symbol#toproc shortcut with transform_keys Eg:

h.transform_keys! &:upcase
# => {"A"=>1, "B"=>2, "C"=>3}
Santhosh
  • 28,097
  • 9
  • 82
  • 87
4

i assume you want to change the hash keys without changing the values:

hash = {
   "nr"=>"123",
   "name"=>"Herrmann Hofreiter",
   "pferd"=>"010 000 777",
   "land"=>"hight land"
}
header = ["aa", "bb", "cc", "dd"]
new_hash = header.zip(hash.values).to_h

Result:

{
   "aa"=>"123",
   "bb"=>"Herrmann Hofreiter",
   "cc"=>"010 000 777",
   "dd"=>"high land"
}
ali
  • 846
  • 2
  • 18
  • 34
Kiry Meas
  • 1,200
  • 13
  • 26
  • 1
    A moment of self promotion ☺️, but just to leave it here: if anything a bit more complex needed (like select particular keys at the same time or coerce values' types and so on), I put together a tiny lib: https://github.com/smileart/hash_remapper – smileart Dec 05 '17 at 20:49
  • You take advantage of the fact that order is preserved in hashes (since 2.0 I think) and by assumption (not given in my question) that new keys are also given in the same order as in the initial hash. – JCLL Mar 31 '18 at 19:55
3

Another way to do it is:

hash = {
  'foo' => 1,
  'bar' => 2
}

new_keys = {
  'foo' => 'foozle',
  'bar' => 'barzle'
}

new_keys.values.zip(hash.values_at(*new_keys.keys)).to_h 
# => {"foozle"=>1, "barzle"=>2}

Breaking it down:

new_keys
.values # => ["foozle", "barzle"]
.zip(
  hash.values_at(*new_keys.keys) # => [1, 2]
) # => [["foozle", 1], ["barzle", 2]]
.to_h 
# => {"foozle"=>1, "barzle"=>2}

It's benchmark time...

While I like the simplicity of Jörn's answer, I'm wasn't sure it was as fast as it should be, then I saw selvamani's comment:

require 'fruity'

HASH = {
  'foo' => 1,
  'bar' => 2
}

NEW_KEYS = {
  'foo' => 'foozle',
  'bar' => 'barzle'
}

compare do
  mittag    { HASH.dup.map {|k, v| [NEW_KEYS[k], v] }.to_h }
  ttm       { h = HASH.dup; NEW_KEYS.values.zip(h.values_at(*NEW_KEYS.keys)).to_h }
  selvamani { h = HASH.dup; h.keys.each { |key| h[NEW_KEYS[key]] = h.delete(key)}; h }
end

# >> Running each test 2048 times. Test will take about 1 second.
# >> selvamani is faster than ttm by 39.99999999999999% ± 10.0%
# >> ttm is faster than mittag by 10.000000000000009% ± 10.0%

These are running very close together speed wise, so any will do, but 39% pays off over time so consider that. A couple answers were not included because there are potential flaws where they'd return bad results.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
2

The exact solution would depend on the format that you have the new keys in (or if you can derive the new key from the old key.)

Assuming you have a hash h whose keys you want to modify and a hash new_keys that maps the current keys to the new keys you could do:

h.keys.each do |key|
  h[new_keys[key]] = h[key] # add entry for new key
  k.delete(key)             # remove old key
end
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
mikej
  • 65,295
  • 17
  • 152
  • 131
  • 1
    This will remove a value for `key` when `new_keys` accidentally happens to return `key` itself for some `key`. barbolos's answer to this question: http://stackoverflow.com/questions/4137824 overcomes this problem. – sawa Oct 24 '12 at 11:06
  • 3
    instead of this you can use `h.keys.each { |key| h[new_keys[key]] = h.delete(key)}` – Selvamani Oct 16 '15 at 05:16
  • @Selvamani, see my answer, and create an answer from your comment. – the Tin Man Mar 28 '16 at 22:58
0

If you also worry about performance, this is faster:

hsh.keys.each { |k| hsh[ key_map[k] ] = hsh.delete(k) if key_map[k] }

You don't create a new Hash and you rename only the necessary keys. That gives you better performance.

You can find more details in "How to elegantly rename all keys in a hash in Ruby?"

Community
  • 1
  • 1
barbolo
  • 3,807
  • 1
  • 31
  • 31
0
h = { 'foo'=>1, 'bar'=>2 }
key_map = { 'foo'=>'foozle', 'bar'=>'barzle' }

h.each_with_object({}) { |(k,v),g| g[key_map[k]]=v }
  #=> {"foozle"=>1, "barzle"=>2}

or

h.reduce({}) { |g,(k,v)| g.merge(key_map[k]=>v) }
  #=> {"foozle"=>1, "barzle"=>2} 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • I think calling merge for each key will be comparatively slow – Josh Oct 24 '17 at 04:38
  • @Josh, you are correct. I re-ran @theTinMan's benchmark with my two methods added and obtained the following results: "selvamani is faster than ttm by 19.99% ± 1.0%; ttm is similar to caryewo (uses `each_with_object`); caryewo is similar to mittag; mittag is faster than caryred (uses `reduce`) by 70.0% ± 10.0%". – Cary Swoveland Oct 24 '17 at 05:29