如何优雅地重命名Ruby哈希中的所有键?

102

我有一个 Ruby 哈希表:

ages = { "Bruce" => 32,
         "Clark" => 28
       }
假设我有另一个替换名称的哈希表,是否有一种简洁的方法可以重命名所有键,以便最终得到:
ages = { "Bruce Wayne" => 32,
         "Clark Kent" => 28
       }
11个回答

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

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

谢谢,这太棒了!现在,如果我只想更改一些键名,有没有一种方法可以测试该键是否存在映射? - Chanpory
29
只需要在上述代码中使用 mappings[k] || k,而不是 mappings[k],它会使不在映射中的键保持原样。 - Mladen Jablanović
我注意到ages.map!似乎不起作用,所以不得不这样做ages = Hash[ages.map {|k, v| [mappings[k] || k, v] }]才能再次使用映射的变量。 - Chanpory
1
map 返回一个数组的数组,你可以通过使用 ages.map {...}.to_h 转换回哈希表。 - caesarsol
2
虽然 to_h 仅适用于 Ruby 2.0 及以上版本。在 Ruby 1.9.3 中,我通过将整个内容包装在 Hash[...] 中来完成它。 - digitig
有人能解释一下这部分吗?&mappings.method(:[]) 这行代码是做什么的? - buncis

53

我很喜欢Jörg W Mittag的回答,但是如果你想重命名当前哈希表的键,并且而不是创建一个新的具有重命名键的哈希表,那么以下代码片段正好可以实现:

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

另外还有一个优点,就是仅对必要的键进行重命名。

性能考虑:

根据The Tin Man的回答,相比于Jörg W Mittag的方案,我的方案在仅有两个键的哈希表中约快20%。如果哈希表中包含许多键,特别是只需要重命名少量键时,其性能可能会更高。


1
@peterept 你可以尝试使用 options.with_indifferent_access.merge(:methods => [:blah])。这将使 options 可以访问字符串或符号作为键。 - barbolo
喜欢这个答案...但我不明白它是如何工作的。每个集合上的值是如何设置的? - Clayton Selby
你好,@ClaytonSelby。你能更好地解释一下什么让你感到困惑吗? - barbolo
我知道问题中说的是“所有键”,但如果你想让这个过程更快,你应该遍历映射而不是哈希重命名。最坏情况下,速度是一样的。 - Ryan Taylor
虽然你提到了我的基准测试很好,但是如果mappings[k]的结果为nil值,这就会失败。 - the Tin Man
显示剩余3条评论

15

Ruby 中也有未被充分利用的 each_with_object 方法:

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

ages.each_with_object({}) { |(k, v), memo| memo[mappings[k]] = v }

1
each_with_object 被低估了,比 inject 更清晰易记。当它被引入时,它是一个受欢迎的补充。 - the Tin Man
我认为这是最好的答案。您还可以使用 || k 来处理映射中没有相应键的情况:ages.each_with_object({}) { |(k, v), memo| memo[mappings[k] || k] = v } - coisnepe

8

只是为了看看哪个更快:

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

这将输出:

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%

注意:barbell的解决方案使用了if mappings[k],如果mappings[k]结果为nil值,则会导致生成的哈希值错误。


回复:“注意:”-我不确定是否应该认为它是“错误的”,它只是在mappings有替换内容时替换键,所有其他解决方案仅在未找到两个键时返回{nil=>28}。这取决于您的要求。我不确定对基准测试的影响,我会把这留给其他人。如果您想要与其他人相同的行为,请删除提供的if mappings[k],或者如果您只想要mappings中匹配的结果,则认为这将具有更清晰的结果:ages.keys.each { |k| ages.delete(k) if mappings[k].nil? || ages[ mappings[k] ] = ages[k] } - webaholik

5

我对这个类进行了猴子补丁,以处理嵌套的哈希和数组:

   #  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

非常有帮助。我根据自己的需求将驼峰式键转换为下划线风格。 - idStar
不错!检查.responds_to?(:rename_keys)而不是.kind_of?(Hash)可能更灵活,对于Array也是如此,你觉得呢? - caesarsol

3
如果映射哈希比数据哈希小,则应迭代映射而不是数据。这在对大型哈希进行少量字段重命名时很有用:
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

2
“Facets”宝石提供了一个“rekey”方法,正好可以满足您的需求。只要您愿意依赖于“Facets”宝石,就可以将映射哈希传递给“rekey”方法,它将返回一个具有新键的新哈希表:
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}

如果您想原地修改年龄哈希表,可以使用rekey!版本:
ages.rekey!(mappings)
ages
=> {"Bruce Wayne"=>32, "Clark Kent"=>28}

2

您可能希望使用Object#tap来避免在键被修改后需要返回ages

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}

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

年龄 = mappings.inject({}) {|memo, (旧键, 新键)| memo[新键] = 年龄[旧键]; memo} - frogstarr78

0
我使用了这个方法,将 Cucumber 表格中的“友好”名称解析为类属性,以便 Factory Girl 可以创建一个实例:
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

就其价值而言,Cucumber表格如下:

  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 |

map_to_keys 看起来像这样:

{
               "Name" => "name",
          "Subdomain" => "subdomain",
       "Phone Number" => "phone_number",
            "Address" => "address",
    "Billing Contact" => "billing_contact",
    "Billing Address" => "billing_address"
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接