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?
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]
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".
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.
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]
@ 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]
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.
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!
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 x
s 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]
_