102

I have a Ruby hash:

ages = { "Bruce" => 32,
         "Clark" => 28
       }

Assuming I have another hash of replacement names, is there an elegant way to rename all the keys so that I end up with:

ages = { "Bruce Wayne" => 32,
         "Clark Kent" => 28
       }
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Chanpory
  • 3,015
  • 6
  • 37
  • 49

11 Answers11

198
ages = { 'Bruce' => 32, 'Clark' => 28 }
mappings = { 'Bruce' => 'Bruce Wayne', 'Clark' => 'Clark Kent' }

ages.transform_keys(&mappings.method(:[]))
#=> { 'Bruce Wayne' => 32, 'Clark Kent' => 28 }
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Thanks, this is great! Now, if I only want to change some of the key names, is there a way to test if a mapping exist for the key? – Chanpory Nov 09 '10 at 20:18
  • 29
    Just use `mappings[k] || k` instead of `mappings[k]` above and it will leave keys not in the mapping as is. – Mladen Jablanović Nov 09 '10 at 20:26
  • I noticed that `ages.map!` doesn't seem to work... so had to do `ages = Hash[ages.map {|k, v| [mappings[k] || k, v] }]` to be able to call the variable again with the mapping. – Chanpory Nov 09 '10 at 20:32
  • 1
    `map` returns an Array of Arrays, you can transform back to Hash by using `ages.map {...}.to_h` – caesarsol Sep 17 '14 at 13:49
  • 2
    Although `to_h` is only available in Ruby 2.0 onwards. In Ruby 1.9.3 I did it by wrapping the whole thing in `Hash[...]` – digitig Sep 18 '14 at 14:18
  • anyone could explain this part? `&mappings.method(:[])` what is this line do? – buncis May 19 '21 at 14:25
53

I liked Jörg W Mittag's answer, but if you want to rename the keys of your current Hash and not to create a new Hash with the renamed keys, the following snippet does exactly that:

ages = { "Bruce" => 32, "Clark" => 28 }
mappings = {"Bruce" => "Bruce Wayne", "Clark" => "Clark Kent"}

ages.keys.each { |k| ages[ mappings[k] ] = ages.delete(k) if mappings[k] }
ages

There's also the advantage of only renaming the necessary keys.

Performance considerations:

Based on the Tin Man's answer, my answer is about 20% faster than Jörg W Mittag's answer for a Hash with only two keys. It may get even higher performance for Hashes with many keys, specially if there are just a few keys to be renamed.

user664833
  • 18,397
  • 19
  • 91
  • 140
barbolo
  • 3,807
  • 1
  • 31
  • 31
  • I like this. One gotcha that hit me was I used this in as_json() call, and although the main attributes keys were converted to string, the options.merge(:methods => [:blah]) then that is a key in the map not a string. – peterept Mar 08 '13 at 01:04
  • 1
    @peterept you can try options.with_indifferent_access.merge(:methods => [:blah]). This will make options access strings or symbols as keys. – barbolo Mar 09 '13 at 15:20
  • Love the answer...but I am confused how this actually works. How is the value set on each set? – Clayton Selby Mar 10 '15 at 05:14
  • Hi, @ClaytonSelby. Can you better explain what is confusing you? – barbolo Mar 10 '15 at 13:01
  • I know the question says "all keys" but if you want to make this faster, you should probably iterate through mappings not the hash your renaming. Worst case, it's the same speed. – Ryan Taylor Mar 16 '16 at 22:29
  • While it's nice you mentioned my benchmark, this fails if `mappings[k]` results in a nil value. – the Tin Man Mar 28 '16 at 23:10
  • @theTinMan - I'm not sure I'd consider it a "failure", it simply replaces key if `mappings` has something to replace it with, all other solutions will return `{nil=>28}` only if both keys weren't found. It depends on your requirement. I'm not sure of the impact to benchmark, I'll leave that to someone else. If you want same behavior as others, simply remove the `if mappings[k]` where provided OR if you only wanted the matching results in `mappings`, I think this would have a cleaner result: `ages.keys.each { |k| ages.delete(k) if mappings[k].nil? || ages[ mappings[k] ] = ages[k] }` – webaholik Sep 15 '18 at 14:25
  • @ClaytonSelby - `ages[ mappings[k] ]` create's the new key, `ages.delete(k)` both returns the original value & deletes the original associative array (key/value pair)...hopefully you can get some sleep now...I know that's been keeping you up for the last 3.5 years ;-) – webaholik Sep 15 '18 at 14:38
15

There's the under-utilized each_with_object method in Ruby as well:

ages = { "Bruce" => 32, "Clark" => 28 }
mappings = { "Bruce" => "Bruce Wayne", "Clark" => "Clark Kent" }

ages.each_with_object({}) { |(k, v), memo| memo[mappings[k]] = v }
steel
  • 11,883
  • 7
  • 72
  • 109
  • 1
    `each_with_object` is definitely under-utilized, and is clearer and easier to remember than `inject`. It was a welcome addition when it was introduced. – the Tin Man Mar 28 '16 at 22:20
  • I think this is the best answer. You could also use `|| k` to handle the case where mappings doesn't have the corresponding key: ```ages.each_with_object({}) { |(k, v), memo| memo[mappings[k] || k] = v }``` – coisnepe Aug 18 '17 at 16:33
8

Just to see what was faster:

require 'fruity'

AGES = { "Bruce" => 32, "Clark" => 28 }
MAPPINGS = {"Bruce" => "Bruce Wayne", "Clark" => "Clark Kent"}

def jörg_w_mittag_test(ages, mappings)
  Hash[ages.map {|k, v| [mappings[k], v] }]
end

require 'facets/hash/rekey'
def tyler_rick_test(ages, mappings)
  ages.rekey(mappings)
end

def barbolo_test(ages, mappings)
  ages.keys.each { |k| ages[ mappings[k] ] = ages.delete(k) if mappings[k] }
  ages
end

class Hash
  def tfr_rekey(h)
    dup.tfr_rekey! h
  end

  def tfr_rekey!(h)
    h.each { |k, newk| store(newk, delete(k)) if has_key? k }
    self
  end
end

def tfr_test(ages, mappings)
  ages.tfr_rekey mappings
end

class Hash
  def rename_keys(mapping)
    result = {}
    self.map do |k,v|
      mapped_key = mapping[k] ? mapping[k] : k
      result[mapped_key] = v.kind_of?(Hash) ? v.rename_keys(mapping) : v
      result[mapped_key] = v.collect{ |obj| obj.rename_keys(mapping) if obj.kind_of?(Hash)} if v.kind_of?(Array)
    end
    result
  end
end

def greg_test(ages, mappings)
  ages.rename_keys(mappings)
end

compare do
  jörg_w_mittag { jörg_w_mittag_test(AGES.dup, MAPPINGS.dup) }
  tyler_rick    { tyler_rick_test(AGES.dup, MAPPINGS.dup)    }
  barbolo       { barbolo_test(AGES.dup, MAPPINGS.dup)       }
  greg          { greg_test(AGES.dup, MAPPINGS.dup)          }
end

Which outputs:

Running each test 1024 times. Test will take about 1 second.
barbolo is faster than jörg_w_mittag by 19.999999999999996% ± 10.0%
jörg_w_mittag is faster than greg by 10.000000000000009% ± 10.0%
greg is faster than tyler_rick by 30.000000000000004% ± 10.0%

Caution: barbell's solution uses if mappings[k], which will cause the resulting hash to be wrong if mappings[k] results in a nil value.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • RE: "*Caution:*" - I'm not sure I'd consider it "wrong", it simply replaces key if `mappings` has something to replace it with, all other solutions will return `{nil=>28}` only if both keys weren't found. It depends on your requirement. I'm not sure of the impact to benchmark, I'll leave that to someone else. If you want same behavior as others, simply remove the `if mappings[k]` where provided OR if you only wanted the matching results in `mappings`, I think this would have a cleaner result: `ages.keys.each { |k| ages.delete(k) if mappings[k].nil? || ages[ mappings[k] ] = ages[k] }` – webaholik Sep 15 '18 at 14:27
5

I monkey-patched the class to handle nested Hashes and Arrays:

   #  Netsted Hash:
   # 
   #  str_hash = {
   #                "a"  => "a val", 
   #                "b"  => "b val",
   #                "c" => {
   #                          "c1" => "c1 val",
   #                          "c2" => "c2 val"
   #                        }, 
   #                "d"  => "d val",
   #           }
   #           
   # mappings = {
   #              "a" => "apple",
   #              "b" => "boss",
   #              "c" => "cat",
   #              "c1" => "cat 1"
   #           }
   # => {"apple"=>"a val", "boss"=>"b val", "cat"=>{"cat 1"=>"c1 val", "c2"=>"c2 val"}, "d"=>"d val"}
   #
   class Hash
    def rename_keys(mapping)
      result = {}
      self.map do |k,v|
        mapped_key = mapping[k] ? mapping[k] : k
        result[mapped_key] = v.kind_of?(Hash) ? v.rename_keys(mapping) : v
        result[mapped_key] = v.collect{ |obj| obj.rename_keys(mapping) if obj.kind_of?(Hash)} if v.kind_of?(Array)
      end
    result
   end
  end
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Greg
  • 4,509
  • 2
  • 29
  • 22
  • Very helpful. Adapted it to my needs to make camel case keys underscore style. – idStar Mar 07 '13 at 01:45
  • nice! it could be more flexible to check for `.responds_to?(:rename_keys)` instead of `.kind_of?(Hash)`, and the equivalent for `Array`, what do you think? – caesarsol Sep 17 '14 at 15:05
3

If the mapping Hash will be smaller than the data Hash then iterate on mappings instead. This is useful for renaming a few fields in a large Hash:

class Hash
  def rekey(h)
    dup.rekey! h
  end

  def rekey!(h)
    h.each { |k, newk| store(newk, delete(k)) if has_key? k }
    self
  end
end

ages = { "Bruce" => 32, "Clark" => 28, "John" => 36 }
mappings = {"Bruce" => "Bruce Wayne", "Clark" => "Clark Kent"}
p ages.rekey! mappings
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
TFR
  • 81
  • 3
2

The Facets gem provides a rekey method that does exactly what you're wanting.

As long as you're okay with a dependency on the Facets gem, you can pass a hash of mappings to rekey and it will return a new hash with the new keys:

require 'facets/hash/rekey'
ages = { "Bruce" => 32, "Clark" => 28 }
mappings = {"Bruce" => "Bruce Wayne", "Clark" => "Clark Kent"}
ages.rekey(mappings)
=> {"Bruce Wayne"=>32, "Clark Kent"=>28}

If you want to modify ages hash in place, you can use the rekey! version:

ages.rekey!(mappings)
ages
=> {"Bruce Wayne"=>32, "Clark Kent"=>28}
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Tyler Rick
  • 9,191
  • 6
  • 60
  • 60
2

You may wish to use Object#tap to avoid the need to return ages after the keys have been modified:

ages = { "Bruce" => 32, "Clark" => 28 }
mappings = {"Bruce" => "Bruce Wayne", "Clark" => "Clark Kent"}

ages.tap {|h| h.keys.each {|k| (h[mappings[k]] = h.delete(k)) if mappings.key?(k)}}
  #=> {"Bruce Wayne"=>32, "Clark Kent"=>28}
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1
ages = { "Bruce" => 32, "Clark" => 28 }
mappings = {"Bruce" => "Bruce Wayne", "Clark" => "Clark Kent"}
ages = mappings.inject({}) {|memo, mapping| memo[mapping[1]] = ages[mapping[0]]; memo}
puts ages.inspect
dan
  • 43,914
  • 47
  • 153
  • 254
0
>> x={ :a => 'qwe', :b => 'asd'}
=> {:a=>"qwe", :b=>"asd"}
>> rename={:a=>:qwe}
=> {:a=>:qwe}
>> rename.each{|old,new| x[new] = x.delete old}
=> {:a=>:qwe}
>> x
=> {:b=>"asd", :qwe=>"qwe"}

This would loop just through renames hash.

sites
  • 21,417
  • 17
  • 87
  • 146
0

I used this to allow "friendly" names in a Cucumber table to be parsed into class attributes such that Factory Girl could create an instance:

Given(/^an organization exists with the following attributes:$/) do |table|
  # Build a mapping from the "friendly" text in the test to the lower_case actual name in the class
  map_to_keys = Hash.new
  table.transpose.hashes.first.keys.each { |x| map_to_keys[x] = x.downcase.gsub(' ', '_') }
  table.transpose.hashes.each do |obj|
    obj.keys.each { |k| obj[map_to_keys[k]] = obj.delete(k) if map_to_keys[k] }
    create(:organization, Rack::Utils.parse_nested_query(obj.to_query))
  end
end

For what it's worth, the Cucumber table looks like this:

  Background:
    And an organization exists with the following attributes:
      | Name            | Example Org                        |
      | Subdomain       | xfdc                               |
      | Phone Number    | 123-123-1234                       |
      | Address         | 123 E Walnut St, Anytown, PA 18999 |
      | Billing Contact | Alexander Hamilton                 |
      | Billing Address | 123 E Walnut St, Anytown, PA 18999 |

And map_to_keys looks like this:

{
               "Name" => "name",
          "Subdomain" => "subdomain",
       "Phone Number" => "phone_number",
            "Address" => "address",
    "Billing Contact" => "billing_contact",
    "Billing Address" => "billing_address"
}
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Jon Kern
  • 3,186
  • 32
  • 34