如何在 Ruby 中优雅地重命名散列中的所有键?

我有一个 Ruby 散列:

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

假设我有另一个替换名称的散列,是否有一种优雅的方法来重命名所有键,以便最终得到:

ages = { "Bruce Wayne" => 32,
"Clark Kent" => 28
}
60065 次浏览
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
ages = { 'Bruce' => 32, 'Clark' => 28 }
mappings = { 'Bruce' => 'Bruce Wayne', 'Clark' => 'Clark Kent' }


ages.transform_keys(&mappings.method(:[]))
#=> { 'Bruce Wayne' => 32, 'Clark Kent' => 28 }

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

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.

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 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}

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.

>> 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.

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"
}

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 }

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}