Cómo mapear y eliminar valores nulos en Ruby

361

Tengo uno mapque cambia un valor o lo establece en cero. Luego quiero eliminar las entradas nulas de la lista. La lista no necesita ser mantenida.

Esto es lo que tengo actualmente:

# A simple example function, which returns a value or nil
def transform(n)
  rand > 0.5 ? n * 10 : nil }
end

items.map! { |x| transform(x) } # [1, 2, 3, 4, 5] => [10, nil, 30, 40, nil]
items.reject! { |x| x.nil? } # [10, nil, 30, 40, nil] => [10, 30, 40]

Soy consciente de que podría hacer un bucle y recopilar condicionalmente en otra matriz como esta:

new_items = []
items.each do |x|
    x = transform(x)
    new_items.append(x) unless x.nil?
end
items = new_items

Pero no parece tan idiomático. ¿Hay una buena manera de mapear una función sobre una lista, eliminando / excluyendo los nulos a medida que avanza?

Pete Hamilton
fuente
3
Ruby 2.7 presenta filter_map, que parece ser perfecto para esto. Ahorra la necesidad de volver a procesar la matriz, en lugar de obtenerla como desee la primera vez. Más información aquí.
SRack

Respuestas:

21

Ruby 2.7+

Hay ahora!

Ruby 2.7 se presenta filter_mappara este propósito exacto. Es idiomático y eficaz, y espero que se convierta en la norma muy pronto.

Por ejemplo:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

En su caso, a medida que el bloque se evalúa como falsey, simplemente:

items.filter_map { |x| process_x url }

" Ruby 2.7 agrega Enumerable # filter_map " es una buena lectura sobre el tema, con algunos puntos de referencia de rendimiento contra algunos de los enfoques anteriores de este problema:

N = 1_00_000
enum = 1.upto(1_000)
Benchmark.bmbm do |x|
  x.report("select + map")  { N.times { enum.select { |i| i.even? }.map{|i| i + 1} } }
  x.report("map + compact") { N.times { enum.map { |i| i + 1 if i.even? }.compact } }
  x.report("filter_map")    { N.times { enum.filter_map { |i| i + 1 if i.even? } } }
end

# Rehearsal -------------------------------------------------
# select + map    8.569651   0.051319   8.620970 (  8.632449)
# map + compact   7.392666   0.133964   7.526630 (  7.538013)
# filter_map      6.923772   0.022314   6.946086 (  6.956135)
# --------------------------------------- total: 23.093686sec
# 
#                     user     system      total        real
# select + map    8.550637   0.033190   8.583827 (  8.597627)
# map + compact   7.263667   0.131180   7.394847 (  7.405570)
# filter_map      6.761388   0.018223   6.779611 (  6.790559)
SRack
fuente
1
¡Agradable! Gracias por la actualización :) Una vez que se lanza Ruby 2.7.0, creo que probablemente tenga sentido cambiar la respuesta aceptada a esta. Sin embargo, no estoy seguro de cuál es la etiqueta, si generalmente le da a la respuesta aceptada existente la oportunidad de actualizar. Yo diría que esta es la primera respuesta que hace referencia al nuevo enfoque en 2.7, por lo que debería convertirse en la aceptada. @ the-tin-man ¿estás de acuerdo con esta toma?
Pete Hamilton el
Gracias @PeterHamilton: aprecio los comentarios y espero que sea útil para muchas personas. Estoy feliz de seguir con tu decisión, aunque obviamente me gusta el argumento que has hecho :)
SRack
Sí, eso es lo bueno de los idiomas que tienen equipos centrales que escuchan.
El hombre de hojalata
Es un buen gesto recomendar que se cambien las respuestas seleccionadas, pero rara vez sucede. SO no proporciona un tickler para recordarle a las personas y las personas no suelen volver a consultar las viejas preguntas que han hecho a menos que SO indique que ha habido actividad. Como barra lateral, recomiendo mirar Fruity para obtener puntos de referencia porque es mucho menos complicado y hace que sea más fácil hacer pruebas sensatas.
The Tin Man
930

Podrías usar compact:

[1, nil, 3, nil, nil].compact
=> [1, 3] 

Me gustaría recordarle a la gente que si obtiene una matriz que contiene nils como la salida de un mapbloque, y ese bloque intenta devolver valores condicionalmente, entonces tiene olor a código y necesita repensar su lógica.

Por ejemplo, si estás haciendo algo que hace esto:

[1,2,3].map{ |i|
  if i % 2 == 0
    i
  end
}
# => [nil, 2, nil]

Entonces no lo hagas. En cambio, antes del map, rejectlas cosas que no quieres o selectlo que quieres:

[1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
  i
}
# => [2]

Considero usar compactpara limpiar un desastre como un último esfuerzo para deshacernos de las cosas que no manejamos correctamente, generalmente porque no sabíamos lo que nos esperaba. Siempre debemos saber qué tipo de datos se arrojan en nuestro programa; Los datos inesperados / desconocidos son malos. Cada vez que veo nils en una matriz en la que estoy trabajando, profundizo en por qué existen y veo si puedo mejorar el código que genera la matriz, en lugar de permitir que Ruby pierda tiempo y memoria generando nils y luego revisando la matriz para eliminar ellos luego.

'Just my $%0.2f.' % [2.to_f/100]
el hombre de hojalata
fuente
29
Ahora que es rubí-esque!
Christophe Marois
44
¿Por qué debería hacerlo? El OP necesita eliminar nilentradas, no cadenas vacías. Por cierto, nilno es lo mismo que una cadena vacía.
The Tin Man
99
Ambas soluciones iteran dos veces sobre la colección ... ¿por qué no usar reduceo inject?
Ziggy
44
No parece que hayas leído la pregunta o la respuesta de los OP. La pregunta es cómo eliminar nils de una matriz. compactes más rápido, pero en realidad escribir el código correctamente al principio elimina la necesidad de tratar con nils por completo
The Tin Man
3
¡Estoy en desacuerdo! La pregunta es "Asignar y eliminar valores nulos". Bueno, mapear y eliminar valores nulos es reducir. En su ejemplo, el OP asigna y luego selecciona los nulos. Llamar mapa y luego compactar, o seleccionar y luego mapa, equivale a cometer el mismo error: como señala en su respuesta, es un olor a código.
Ziggy
96

Intenta usar reduceo inject.

[1, 2, 3].reduce([]) { |memo, i|
  if i % 2 == 0
    memo << i
  end

  memo
}

Estoy de acuerdo con la respuesta aceptada que no debemos mapy compact, pero no por las mismas razones.

Siento en el fondo que mapeso compactes equivalente a selectentonces map. Considere: mapes una función uno a uno. Si está mapeando desde un conjunto de valores, y usted map, entonces desea un valor en el conjunto de salida para cada valor en el conjunto de entrada. Si tiene que hacerlo selectde antemano, entonces probablemente no quiera un mapen el set. Si tiene que hacerlo selectdespués (o compact), entonces probablemente no quiera un mapen el set. En cualquier caso, está iterando dos veces en todo el conjunto, cuando reducesolo necesita ir una vez.

Además, en inglés, está tratando de "reducir un conjunto de enteros en un conjunto de enteros pares".

Ziggy
fuente
44
Pobre Ziggy, no me encanta tu sugerencia. jajaja ¡más uno, alguien más tiene cientos de votos a favor!
DDDD
2
Creo que un día, con su ayuda, esta respuesta superará la aceptada. ^ o ^ //
Ziggy
2
+1 la respuesta actualmente aceptada no le permite usar los resultados de las operaciones que realizó durante la fase de selección
chees
1
iterar sobre estructuras de datos enumerables dos veces si solo se necesita pasar como en la respuesta aceptada parece un desperdicio. Por lo tanto, reduzca el número de pases utilizando reduce! Gracias @Ziggy
sebisnow
¡Es verdad! Pero hacer dos pases sobre una colección de n elementos sigue siendo O (n). A menos que su colección sea tan grande que no quepa en su caché, hacer dos pases probablemente esté bien (solo creo que esto es más elegante, expresivo y menos probable que genere errores en el futuro cuando, por ejemplo, los bucles caen fuera de sincronización). Si también te gusta hacer las cosas de una vez, ¡podrías estar interesado en aprender sobre los transductores! github.com/cognitect-labs/transducers-ruby
Ziggy
33

En tu ejemplo:

items.map! { |x| process_x url } # [1, 2, 3, 4, 5] => [1, nil, 3, nil, nil]

no parece que los valores hayan cambiado más que ser reemplazados por nil. Si ese es el caso, entonces:

items.select{|x| process_x url}

Será suficiente.

sawa
fuente
27

Si desea un criterio más flexible para el rechazo, por ejemplo, para rechazar cadenas vacías y nulas, puede usar:

[1, nil, 3, 0, ''].reject(&:blank?)
 => [1, 3, 0] 

Si desea ir más allá y rechazar valores cero (o aplicar una lógica más compleja al proceso), puede pasar un bloque para rechazar:

[1, nil, 3, 0, ''].reject do |value| value.blank? || value==0 end
 => [1, 3]

[1, nil, 3, 0, '', 1000].reject do |value| value.blank? || value==0 || value>10 end
 => [1, 3]
Fred Willmore
fuente
55
.¿blanco? solo está disponible en rieles.
paseo el
Para referencia futura, dado blank?que solo está disponible en rieles, podríamos usar el items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]que no está acoplado a los rieles. (sin embargo, no excluiría cadenas vacías o ceros)
Fotis
27

Definitivamente compactes el mejor enfoque para resolver esta tarea. Sin embargo, podemos lograr el mismo resultado solo con una simple resta:

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]
Evgenia Manolova
fuente
44
Sí, la resta establecida funcionará, pero es aproximadamente la mitad de rápido debido a su sobrecarga.
The Tin Man
4

each_with_object es probablemente la forma más limpia de ir aquí:

new_items = items.each_with_object([]) do |x, memo|
    ret = process_x(x)
    memo << ret unless ret.nil?
end

En mi opinión, each_with_objectes mejor que inject/ reduceen casos condicionales porque no tiene que preocuparse por el valor de retorno del bloque.

pnomolos
fuente
0

Una forma más de lograrlo será como se muestra a continuación. Aquí, utilizamos Enumerable#each_with_objectpara recopilar valores, y utilizamos Object#tappara deshacernos de la variable temporal que de otro modo sería necesaria para nilverificar el resultado del process_xmétodo.

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

Ejemplo completo para ilustración:

items = [1,2,3,4,5]
def process x
    rand(10) > 5 ? nil : x
end

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

Enfoque alternativo:

Al observar el método que está llamando process_x url, no está claro cuál es el propósito de la entrada xen ese método. Si supongo que va a procesar el valor de xpasándolo un poco urly determinar cuál de los xs realmente se procesa en resultados válidos no nulos, entonces, puede ser Enumerabble.group_byuna mejor opción que Enumerable#map.

h = items.group_by {|x| (process x).nil? ? "Bad" : "Good"}
#=> {"Bad"=>[1, 2], "Good"=>[3, 4, 5]}

h["Good"]
#=> [3,4,5]
Fabricante de varita
fuente