Ruby: ¿la forma más fácil de filtrar claves de hash?

225

Tengo un hash que se parece a esto:

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

Y quiero una manera simple de rechazar todas las claves que no son choice + int. Podría ser la opción1 o la opción1 a la opción10. Varía.

¿Cómo selecciono las teclas con solo la elección de palabras y un dígito o dígitos después de ellas?

Prima:

Convierta el hash en una cadena con la pestaña (\ t) como delimitador. Hice esto, pero tomó varias líneas de código. Por lo general, los maestros rubíes pueden hacerlo en una o más líneas.

Derek
fuente
En términos de la pregunta adicional, ¿puede aclarar cómo le gustaría que se vea la cadena?
mikej
Claro, el hash de ejemplo anterior arrojaría: "Oh, mira, otro \ tEve más cadenas \ tPero espera" (sin \ t al final de la cadena, solo entre ellas)
Derek
Su cadena de ejemplo ha sido cortada en el comentario. Puede usar el enlace de edición para editar su pregunta y agregar el ejemplo allí. También puede publicar el rubí que haya creado hasta ahora como un ejemplo de lo que desea lograr.
mikej
2
posible duplicado del filtro Ruby Hash
jbochi

Respuestas:

281

Editar a la respuesta original : aunque esta es la respuesta seleccionada (al momento de este comentario) es la respuesta seleccionada, la versión original de esta respuesta está desactualizada.

Estoy agregando una actualización aquí para ayudar a otros a evitar ser desviados por esta respuesta como lo hice yo.

Como menciona la otra respuesta, Ruby> = 2.5 agregó el Hash#slicemétodo que anteriormente solo estaba disponible en Rails.

Ejemplo:

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

Fin de la edición. Lo que sigue es la respuesta original que supongo que será útil si estás en Ruby <2.5 sin Rails, aunque imagino que ese caso es bastante poco común en este momento.


Si usa Ruby, puede usar el selectmétodo. Tendrá que convertir la clave de un Símbolo a una Cadena para hacer la coincidencia de expresiones regulares. Esto le dará un nuevo Hash con solo las opciones que contiene.

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

o puede usar delete_ify modificar el hash existente, por ejemplo

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

o si solo son las claves y no los valores que desea, puede hacer:

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

y esto le dará solo una matriz de teclas, por ejemplo [:choice1, :choice2, :choice3]

mikej
fuente
1
¡Excelente! Una vez que lo veo, ¡esto es muy simple! Muchas gracias, y gracias a los demás que se tomaron el tiempo de responder también.
Derek
2
Los símbolos se pueden analizar con expresiones regulares hoy en día IIRC.
Andrew Grimm
@ Andrew, creo que tienes razón. Estaba en 1.8.7 cuando estaba probando la respuesta a esta pregunta.
mikej
1
La contraparte de .selectes .reject, si eso hace que su código sea más idiomático.
Joe Atzberger
La #sliceforma funciona para mí en 2.5.3 incluso sin soporte activo.
graywolf
305

En Ruby, Hash # select es una opción correcta. Si trabaja con Rails, puede usar Hash # slice y Hash # slice !. por ejemplo (rieles 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
fuente
12
cuidado con las claves en cadena..h1 = {'a' => 1,: b => 2} solo devolverá {: b => 2} con h1.slice (: a,: b)
davidcollom
Impresionante solución. Utilicé esto para devolver el resultado en rspec, donde quería analizar un valor de hash dentro de un hash. `` `test = {: a => 1,: b => 2, c => {: A => 1,: B => 2}} solución ==> test.c.slice (: B)` ` `
PriyankaK
Reemplace Rails por ActiveSupport y esto es perfecto;)
Geoffroy
55
sin embargo, esto no responde la pregunta, ya que quiere una forma de extraer los valores para las claves que son "elección + int". Su solución solo puede extraer claves conocidas de antemano.
metakungfu
46

La forma más fácil es incluir el gem 'activesupport'(o gem 'active_support').

Entonces, en tu clase solo necesitas

require 'active_support/core_ext/hash/slice'

y llamar

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

Creo que no vale la pena declarar otras funciones que pueden tener errores, y es mejor usar un método que se haya modificado durante los últimos años.

Nuno Costa
fuente
ActiveRecord no es necesario para esto al menos en 2.5.
graywolf
13

La forma más fácil es incluir la gema 'soporte activo' (o gema 'soporte_activo').

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

翟英昌
fuente
44
Agregue más detalles a su respuesta.
Mohit Jain
13

Si trabaja con rieles y tiene las claves en una lista separada, puede usar la *notación:

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

Como se indicó en otras respuestas, también puede usar slice!para modificar el hash en su lugar (y devolver la clave / valores borrados).

Robert
fuente
9

Esta es una línea para resolver la pregunta original completa:

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

Pero la mayoría de las soluciones anteriores están resolviendo un caso en el que necesita conocer las claves con anticipación, utilizando sliceo regexp simple.

Aquí hay otro enfoque que funciona para casos de uso simples y más complejos, que se puede intercambiar en tiempo de ejecución

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

Ahora, no solo esto permite una lógica más compleja al hacer coincidir las claves o los valores, sino que también es más fácil de probar y puede intercambiar la lógica de coincidencia en tiempo de ejecución.

Ex para resolver el 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
fuente
1
Me gusta mucho el partido
Calin
7

Con Hash::select:

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

Si quieres el hash restante:

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

o si solo quieres las llaves:

params.keys.delete_if {|k| ! k.match(/choice[0-9]+/)}
ayckoster
fuente
Dependiendo de cuántas opcionesX e irrelevantesX tenga, seleccione O eliminar_si podría ser mejor. Si tienes más opciones X, delete_if es mejor.
ayckoster
6

Pon esto en un 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

Entonces puedes hacer

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

o

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

En cuanto a la pregunta extra:

  1. Si tiene salida de un #selectmétodo como este (lista de matrices de 2 elementos):

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

    luego simplemente tome este resultado y ejecute:

    filtered_params.join("\t")
    # or if you want only values instead of pairs key-value
    filtered_params.map(&:last).join("\t")
  2. Si tiene salida de un #delete_ifmétodo como este (hash):

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

    luego:

    filtered_params.to_a.join("\t")
    # or
    filtered_params.values.join("\t")
MBO
fuente
Grandes soluciones Una pregunta: ¿Filter_params.map (&: last) .join ("\ t") es la abreviatura de filter_params.map (| i | i.last) .join ("\ t")? Si es así, ¿qué es 'último'? ¿podría usar &: value para obtener el valor del hash?
Derek
maptoma bloque como argumento. Puede crear bloques sobre la marcha, con &:methodconstrucción, lo que crearía un bloque {|v| v.method }. En mi caso &:lastse invocó el argumento array (matriz de 2 elementos). Si quiere jugar con él, primero verifique qué argumento está recibiendo en el bloque (obtenga 1 argumento, ¡no lo deconstruya en múltiples argumentos!) E intente encontrar 1 método (con el que no puede encadenar métodos &:method) que le devolvería lo que querer. Si es posible, puede usar el acceso directo, si no, entonces necesita el bloqueo 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
fuente
0

Tuve un problema similar, en mi caso la solución fue un trazador de líneas que funciona incluso si las teclas no son símbolos, pero debe tener las claves de criterios en una matriz

criteria_array = [:choice1, :choice2]

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

Otro ejemplo

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
fuente