Ruby:过滤哈希键的最简单方法是什么?

288

我有一个哈希表,看起来像这样:

params = { :irrelevant => "A String",
           :choice1 => "Oh look, another one",
           :choice2 => "Even more strings",
           :choice3 => "But wait",
           :irrelevant2 => "The last string" }

我希望有一种简单的方式来拒绝所有不是choice+int的键。它可以是choice1,也可以是choice1到choice10。它是变化的。

如何仅挑选出单词choice和数字或数字后面的键?

奖励:

将哈希转换为以制表符(\t)作为分隔符的字符串。我做到了,但需要多行代码。通常,高级Rubicians可以在一行或几行代码中完成。


就奖励问题而言,您能澄清一下您想要字符串看起来像什么吗? - mikej
当然,上面的哈希示例将产生:"哦,看,又来了一个\t更多的字符串\t但等等"(字符串末尾没有\t,只在它们之间)。 - Derek
你的示例字符串在评论中被截断了。你可以使用编辑链接编辑你的问题并将示例添加到其中。你也可以发布你已经想出来的 Ruby 代码作为你想要实现的示例。 - mikej
2
可能是 Ruby Hash Filter 的重复问题。 - jbochi
14个回答

389
编辑原答案:尽管这个答案(截至本评论时)是被选中的答案,但原始版本已经过时。
我在这里添加一个更新,以帮助其他人避免像我一样被这个答案分心。
正如其他答案所提到的,Ruby >= 2.5 添加了Hash#slice方法,该方法之前只在Rails中可用。
示例:
> { one: 1, two: 2, three: 3 }.slice(:one, :two)
=> {:one=>1, :two=>2}

编辑结束。 接下来是原始答案,如果你使用的是Ruby < 2.5且没有Rails,那么我想这个答案可能会有用,尽管我认为这种情况在目前已经相当少见了。


如果你正在使用Ruby,可以使用select方法。你需要将键从Symbol转换为String以进行正则表达式匹配。这将给你一个只包含所选项的新哈希表。

choices = params.select { |key, value| key.to_s.match(/^choice\d+/) }

或者你可以使用delete_if并修改现有的哈希表,例如:
params.delete_if { |key, value| !key.to_s.match(/choice\d+/) }

如果你只需要键而不是值,可以使用以下方式:

params.keys.select { |key| key.to_s.match(/^choice\d+/) }

这将为您提供一个键的数组,例如[:choice1, :choice2, :choice3]


1
太好了!一看就觉得很简单!非常感谢,也感谢其他抽出时间回答的人。 - Derek
2
现在可以使用正则表达式解析符号,如果我没记错的话。 - Andrew Grimm
2
.select的对应方法是.reject,如果这样可以让你的代码更符合惯用语。 - Joe Atzberger
在我的2.5.3版本上也起作用了。只需要记住正确的键格式,是字符串还是符号! - Pysis
1
同样适合使用splat的好选择。 keys = [:one, :two] { one: 1, two: 2, three: 3 }.slice(*keys) - Jared Menard
显示剩余2条评论

313

在Ruby中,Hash#select是一个不错的选择。如果你在使用Rails,你可以使用 Hash#slice 和 Hash#slice!。例如:(rails 3.2.13)

h1 = {:a => 1, :b => 2, :c => 3, :d => 4}

h1.slice(:a, :b)         # return {:a=>1, :b=>2}, but h1 is not changed

h2 = h1.slice!(:a, :b)   # h1 = {:a=>1, :b=>2}, h2 = {:c => 3, :d => 4}

15
注意字符串化键名的问题。使用 h1.slice(:a,:b) 时,由于键名 'a' 被字符串化了,只会返回 {:b=>2}。 - davidcollom
真是个神奇的解决方案。我在 Rspec 中使用它来返回结果,其中我想解析哈希内嵌套的哈希值。test = {:a=>1, :b=>2, c=>{:A=>1, :B=>2}} solution==> test.c.slice(:B) - PriyankaK
1
用ActiveSupport替换Rails,这就完美了 ;) - Geoffroy
5
然而,这并没有回答问题,因为他想要一种提取“choice + int”键的值的方法。你的解决方案只能提取预先知道的键。 - magicgregz

49
最简单的方法是包含 gem 'activesupport'(或者gem 'active_support')。
然后,在你的类中,你只需要
require 'active_support/core_ext/hash/slice'

并呼叫

params.slice(:choice1, :choice2, :choice3) # => {:choice1=>"Oh look, another one", :choice2=>"Even more strings", :choice3=>"But wait"}

我认为声明其他可能存在漏洞的函数是不值得的,最好使用经过上几年调整的方法。


至少在2.5上,这并不需要使用Activerecord。 - graywolf

22
如果你使用Rails并且你的密钥在一个单独的列表中,你可以使用*符号:

如果您使用 Rails 并且将密钥存储在单独的列表中,则可以使用 * 符号:

keys = [:foo, :bar]
hash1 = {foo: 1, bar:2, baz: 3}
hash2 = hash1.slice(*keys)
=> {foo: 1, bar:2}

正如其他答案所述,您还可以使用 slice! 在原地修改哈希表(并返回已删除的键/值对)。


17

使用哈希切片

{ a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
# => {:a=>1, :b=>2}

# If you have an array of keys you want to limit to, you should splat them:
valid_keys = [:mass, :velocity, :time]
search(options.slice(*valid_keys))


13
这是一行代码来解决完整的原始问题:
params.select { |k,_| k[/choice/]}.values.join('\t')

但是,上述大多数解决方案都是解决需要事先知道键的情况,使用slice或简单的正则表达式。

以下是另一种方法,适用于简单和更复杂的用例,并且可以在运行时进行交换。

data = {}
matcher = ->(key,value) { COMPLEX LOGIC HERE }
data.select(&matcher)

现在,这不仅允许更复杂的逻辑来匹配键或值,而且更容易测试,并且您可以在运行时交换匹配逻辑。

例如,解决原始问题:

def some_method(hash, matcher) 
  hash.select(&matcher).values.join('\t')
end

params = { :irrelevant => "A String",
           :choice1 => "Oh look, another one",
           :choice2 => "Even more strings",
           :choice3 => "But wait",
           :irrelevant2 => "The last string" }

some_method(params, ->(k,_) { k[/choice/]}) # => "Oh look, another one\\tEven more strings\\tBut wait"
some_method(params, ->(_,v) { v[/string/]}) # => "Even more strings\\tThe last string"

1
我非常喜欢这个匹配器。 - Calin

13
最简单的方法是引入gem 'activesupport'(或gem 'active_support')。 params.slice(:choice1, :choice2, :choice3) 可以截取参数中的指定键。

4
请在您的回答中添加更多细节。 - Mohit Jain

9

使用 Hash::select

params = params.select { |key, value| /^choice\d+$/.match(key.to_s) }

6
如果您需要剩余的哈希值:
params.delete_if {|k, v| ! k.match(/choice[0-9]+/)}

或者如果你只需要键:

params.keys.delete_if {|k| ! k.match(/choice[0-9]+/)}

根据您选择的choiceX和irrelevantX的数量,选择select或delete_if可能更好。如果您有更多的choiceX,则delete_if更好。 - ayckoster

5

将以下内容放入初始化器中

class Hash
  def filter(*args)
    return nil if args.try(:empty?)
    if args.size == 1
      args[0] = args[0].to_s if args[0].is_a?(Symbol)
      self.select {|key| key.to_s.match(args.first) }
    else
      self.select {|key| args.include?(key)}
    end
  end
end

那么你可以这样做:
{a: "1", b: "b", c: "c", d: "d"}.filter(:a, :b) # =>  {a: "1", b: "b"}

或者

{a: "1", b: "b", c: "c", d: "d"}.filter(/^a/)  # =>  {a: "1"}

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