Ruby: A maneira mais fácil de filtrar chaves de hash?

225

Eu tenho um hash que se parece com isso:

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

E eu quero uma maneira simples de rejeitar todas as chaves que não são escolha + int. Pode ser escolha1 ou escolha1 através da escolha10. Varia.

Como separar as teclas com apenas a escolha da palavra e um dígito ou dígitos após elas?

Bônus:

Transforme o hash em uma string com tab (\ t) como delimitador. Eu fiz isso, mas foram necessárias várias linhas de código. Normalmente, os mestres de Rubicistas podem fazer isso em uma ou mais linhas.

Derek
fonte
Em termos da questão do bônus, você pode esclarecer como deseja que a string seja.
Mikej
Claro, o exemplo de hash acima renderia: "Oh olhar, um outro \ TEVEN mais cordas \ tBut esperar" (com não \ t no final da cadeia, apenas entre eles)
Derek
Sua sequência de exemplo foi cortada no comentário. Você pode usar o link editar para editar sua pergunta e adicionar o exemplo lá. Você também pode postar o ruby ​​que você criou, até agora, como um exemplo do que você deseja alcançar.
MikeJ
2
possível duplicada de Ruby Hash Filter
jbochi 15/10

Respostas:

281

Editar na resposta original : mesmo que essa seja a resposta selecionada (no momento deste comentário), a versão original dessa resposta está desatualizada.

Estou adicionando uma atualização aqui para ajudar outras pessoas a evitar serem desviadas por esta resposta, como eu fiz.

Como a outra resposta menciona, Ruby> = 2.5 adicionou o Hash#slicemétodo que anteriormente estava disponível apenas no Rails.

Exemplo:

> { one: 1, two: 2, three: 3 }.slice(:one, :two)
=> {:one=>1, :two=>2}

Fim da edição. O que se segue é a resposta original, que eu acho que será útil se você estiver usando Ruby <2.5 sem Rails, embora eu imagine que esse caso seja bastante incomum neste momento.


Se você estiver usando Ruby, poderá usar o selectmétodo Você precisará converter a chave de um símbolo em uma string para fazer a correspondência regexp. Isso lhe dará um novo Hash com apenas as opções nele.

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

ou você pode usar delete_ife modificar o Hash existente, por exemplo

params.delete_if { |key, value| !key.to_s.match(/choice\d+/) }

ou se são apenas as chaves e não os valores desejados, você pode fazer:

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

e isso dará apenas uma matriz das chaves, por exemplo [:choice1, :choice2, :choice3]

mikej
fonte
1
Excelente! Uma vez que eu vejo, isso é tão simples! Muito obrigado e obrigado aos outros que também responderam.
Derek
2
Os símbolos podem ser analisados ​​com expressões regulares hoje em dia IIRC.
Andrew Grimm
@ Andrew, acho que você está certo. Eu estava no 1.8.7 quando estava testando a resposta para esta pergunta.
MikeJ
1
A contrapartida .selecté .rejectse isso torna seu código mais idiomático.
precisa saber é o seguinte
O #slicecaminho funciona para mim no 2.5.3, mesmo sem o suporte ativo.
GrayWolf
305

No Ruby, o Hash # select é uma opção correta. Se você trabalha com Rails, pode usar Hash # slice e Hash # slice !. por exemplo (trilhos 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}
lfx_cool
fonte
12
cuidado com as chaves com strings..h1 = {'a' => 1,: b => 2} retornará apenas {: b => 2} com h1.slice (: a
,:
Solução impressionante. Eu usei isso no retorno de resultado no rspec, onde eu queria analisar o valor de um hash dentro de um hash. `` teste = {: a => 1,: b => 2, c => {: A => 1,: B => 2}} solução ==> test.c.slice (: B) `` `
PriyankaK
Substitua Rails por ActiveSupport e isso é perfeito;)
Geoffroy
5
isso não responde à pergunta, pois ele deseja uma maneira de extrair os valores para chaves que são "choice + int". Sua solução pode extrair apenas as chaves conhecidas antecipadamente.
7186 metakungfu
46

A maneira mais fácil é incluir o gem 'activesupport'(ou gem 'active_support').

Então, na sua turma, você só precisa

require 'active_support/core_ext/hash/slice'

e ligar

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

Acredito que não vale a pena declarar outras funções que podem ter bugs, e é melhor usar um método que foi aprimorado nos últimos anos.

Nuno Costa
fonte
Activerecord não é necessário para isso, pelo menos em 2.5.
GrayWolf
13

A maneira mais fácil é incluir a gema 'activeesupport' (ou gema 'active_support').

params.slice(:choice1, :choice2, :choice3)

翟英昌
fonte
4
Adicione mais detalhes à sua resposta.
precisa saber é o seguinte
13

Se você trabalha com trilhos e possui as chaves em uma lista separada, pode usar a *notação:

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

Como outras respostas indicadas, você também pode usar slice!para modificar o hash no lugar (e retornar a chave / valores apagados).

Robert
fonte
9

Esta é uma linha para resolver a questão original completa:

params.select { |k,_| k[/choice/]}.values.join('\t')

Mas a maioria das soluções acima está resolvendo um caso em que você precisa conhecer as chaves com antecedência, usando sliceou uma simples expressão regular.

Aqui está outra abordagem que funciona para casos de uso simples e mais complexos, que é permutável em tempo de execução

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

Agora, não apenas isso permite uma lógica mais complexa na correspondência das chaves ou dos valores, mas também é mais fácil de testar, e você pode trocar a lógica correspondente no tempo de execução.

Ex para resolver o problema original:

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"
metakungfu
fonte
1
Eu realmente gosto da correspondência
Calin
7

Com Hash::select:

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

Se você deseja o hash restante:

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

ou se você apenas quer as chaves:

params.keys.delete_if {|k| ! k.match(/choice[0-9]+/)}
ayckoster
fonte
Dependendo de quantas opçõesX e X irrelevantes você tiver selecionado OU delete_if pode ser melhor. Se você tiver mais opçõesX delete_if é melhor.
ayckoster 15/09
6

Coloque isso em um inicializador

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

Então você pode fazer

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

ou

{a: "1", b: "b", c: "c", d: "d"}.filter(/^a/)  # =>  {a: "1"}
montrealmike
fonte
5
params.select{ |k,v| k =~ /choice\d/ }.map{ |k,v| v}.join("\t")
Puhlze
fonte
4

Quanto à pergunta bônus:

  1. Se você tiver uma saída do #selectmétodo como este (lista de matrizes de 2 elementos):

    [[:choice1, "Oh look, another one"], [:choice2, "Even more strings"], [:choice3, "But wait"]]

    então simplesmente pegue esse resultado e execute:

    filtered_params.join("\t")
    # or if you want only values instead of pairs key-value
    filtered_params.map(&:last).join("\t")
  2. Se você tiver saída do #delete_ifmétodo como este (hash):

    {:choice1=>"Oh look, another one", :choice2=>"Even more strings", :choice3=>"But wait"}

    então:

    filtered_params.to_a.join("\t")
    # or
    filtered_params.values.join("\t")
MBO
fonte
Ótimas soluções. Uma pergunta: filtrado_params.map (&: last) .join ("\ t") é a abreviação de filtrado_params.map (| i | i.last) .join ("\ t")? Se sim, o que é 'último'? eu poderia usar &: value para obter o valor do hash?
Derek
mapleva bloco como é argumento. Você pode criar um bloco rapidamente, com &:methodconstrução, o que criaria um bloco {|v| v.method }. No meu caso, &:lastfoi chamado no argumento de matriz (matriz de 2 elementos). Se você quiser brincar com ele, primeiro verifique qual argumento você está recebendo no bloco (obtenha 1 argumento, não o desconstrua em vários argumentos!) E tente encontrar 1 método (com o qual você não pode encadear métodos &:method) que retornaria o que você quer. Se for possível, você poderá usar o atalho, caso contrário, precisará de um bloco completo
MBO
2
params = { :irrelevant => "A String",
           :choice1 => "Oh look, another one",
           :choice2 => "Even more strings",
           :choice3 => "But wait",
           :irrelevant2 => "The last string" }

choices = params.select { |key, value| key.to_s[/^choice\d+/] }
#=> {:choice1=>"Oh look, another one", :choice2=>"Even more strings", :choice3=>"But wait"}
Arup Rakshit
fonte
0

Eu tive um problema semelhante, no meu caso, a solução era um liner que funciona mesmo que as chaves não sejam símbolos, mas você precisa ter as chaves de critério em uma matriz

criteria_array = [:choice1, :choice2]

params.select { |k,v| criteria_array.include?(k) } #=> { :choice1 => "Oh look another one",
                                                         :choice2 => "Even more strings" }

Outro exemplo

criteria_array = [1, 2, 3]

params = { 1 => "A String",
           17 => "Oh look, another one",
           25 => "Even more strings",
           49 => "But wait",
           105 => "The last string" }

params.select { |k,v| criteria_array.include?(k) } #=> { 1 => "A String"}
Jose Paez
fonte