web-dev-qa-db-fra.com

Mapper et supprimer des valeurs nulles dans Ruby

J'ai une carte qui change une valeur ou la met à zéro. Je veux ensuite supprimer les entrées nil de la liste. La liste n'a pas besoin d'être conservée.

C'est ce que j'ai actuellement:

# 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]

Je suis conscient que je pourrais juste faire une boucle et collecter conditionnellement dans un autre tableau comme ceci:

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

Mais cela ne semble pas si idiomatique. Existe-t-il un bon moyen de mapper une fonction sur une liste, en supprimant/excluant les nils au fur et à mesure?

331
Peter Hamilton

Vous pouvez utiliser compact :

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

J'aimerais rappeler aux gens que si vous obtenez un tableau contenant nils en tant que sortie d'un bloc map et que ce bloc tente de renvoyer des valeurs de manière conditionnelle, vous avez une odeur de code et devez repenser votre logique.

Par exemple, si vous faites quelque chose qui fait ceci:

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

Alors ne le fais pas. Avant le map, reject, ce que vous ne voulez pas ou ce que vous voulez select:

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

J'envisage d'utiliser compact pour nettoyer un gâchis comme un dernier effort pour se débarrasser de choses que nous ne gérons pas correctement, habituellement parce que nous ne savions pas ce qui nous attendait. Nous devrions toujours savoir quelle sorte de données est projetée dans notre programme; Les données inattendues/inconnues sont incorrectes. À chaque fois que je vois des nils dans un tableau sur lequel je travaille, je m'interroge sur leur existence et cherche à améliorer le code générant ce tableau, plutôt que de permettre à Ruby de perdre du temps et de la mémoire, générant alors nils. tamiser à travers le tableau pour les supprimer plus tard.

'Just my $%0.2f.' % [2.to_f/100]
848
the Tin Man

Essayez d’utiliser #reduce ou #inject!

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

  memo
}

Je suis d'accord avec la réponse acceptée selon laquelle nous ne devrions pas cartographier et compacter, mais pas pour les mêmes raisons!

Je sens profondément que map-then-compact équivaut à select-then-map. Considérez: une carte est une fonction un-à-un. Si vous mappez à partir d'un ensemble de valeurs et que vous mappez, vous souhaitez une valeur dans le jeu de résultats pour chaque valeur du jeu d'entrées. Si vous devez sélectionner au préalable, vous ne voudrez probablement pas de carte sur le plateau. Si vous devez sélectionner après (ou compact), alors vous ne voulez probablement pas une carte sur l'ensemble. Dans les deux cas, vous effectuez une itération deux fois sur l'ensemble du jeu, lorsqu'une réduction ne doit être appliquée qu'une seule fois.

De plus, en anglais, vous essayez de "réduire un ensemble d’entiers en un ensemble d’entiers pairs".

87
Ziggy

Dans votre exemple:

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

il ne semble pas que les valeurs aient changé autrement que d'être remplacées par nil. Si tel est le cas, alors:

items.select{|x| process_x url}

suffira.

33
sawa

Si vous vouliez un critère plus vague pour le rejet, par exemple, pour rejeter les chaînes vides aussi bien que nil, vous pouvez utiliser:

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

Si vous voulez aller plus loin et rejeter les valeurs nulles (ou appliquer une logique plus complexe au processus), vous pouvez passer un bloc à rejeter:

[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]
26
Fred Willmore

@ the Tin Man, Nice - Je ne connais pas cette méthode. Eh bien, définitivement compact est la meilleure façon, mais peut toujours être fait avec une simple soustraction:

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]
21
Evgenia Manolova

each_with_object est probablement le moyen le plus propre d'aller ici:

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

À mon avis, each_with_object est meilleur que inject/reduce dans les cas conditionnels, car vous n'avez pas à vous soucier de la valeur de retour du bloc.

3
pnomolos

Ruby 2.7 +

Il y a maintenant!

Ruby 2.7 introduit filter_map dans ce but précis. C'est idiomatique et performant, et je m'attends à ce qu'il devienne la norme très bientôt.

Par exemple:

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

Dans votre cas, alors que le bloc est évalué à falsey, simplement:

items.filter_map { |x| process_x url }

Voici un bonne lecture sur le sujet , avec quelques points de repère de performance par rapport à certaines des approches précédentes de ce problème:

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)

J'espère que c'est utile à quelqu'un!

2
SRack

Une autre façon de le faire sera comme indiqué ci-dessous. Ici, nous utilisons Enumerable#each_with_object pour collecter des valeurs, et nous utilisons Object#tap pour nous débarrasser de la variable temporaire qui est par ailleurs nécessaire pour nil vérification du résultat de la méthode _process_x_.

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

Exemple complet pour illustration:

_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?}}
_

Autre approche:

En regardant la méthode que vous appelez _process_x url_, vous ne savez pas quel est le but de l'entrée x dans cette méthode. Si je suppose que vous allez traiter la valeur de x en lui passant quelques url et déterminer lequel des xs est réellement traité en résultats non nuls valides - alors, peut être Enumerabble.group_by est une meilleure option 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]
_
0
Wand Maker