So ordnen Sie Nullwerte in Ruby zu und entfernen sie

361

Ich habe eine, mapdie entweder einen Wert ändert oder auf Null setzt. Ich möchte dann die Null-Einträge aus der Liste entfernen. Die Liste muss nicht aufbewahrt werden.

Folgendes habe ich derzeit:

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

Ich bin mir bewusst, dass ich einfach eine Schleife machen und bedingt in einem anderen Array wie diesem sammeln könnte:

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

Aber es scheint nicht so idiomatisch. Gibt es eine gute Möglichkeit, eine Funktion einer Liste zuzuordnen und dabei die Nullen zu entfernen / auszuschließen?

Pete Hamilton
quelle
3
Ruby 2.7 stellt vor filter_map, was dafür perfekt zu sein scheint. Spart die Notwendigkeit, das Array erneut zu verarbeiten, anstatt es beim ersten Mal wie gewünscht durchzuziehen. Mehr Infos hier.
SRack

Antworten:

21

Ruby 2.7+

Da ist jetzt!

Ruby 2.7 wird filter_mapgenau für diesen Zweck eingeführt. Es ist idiomatisch und performant, und ich würde erwarten, dass es sehr bald zur Norm wird.

Zum Beispiel:

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

In Ihrem Fall, wenn der Block zu Falsey ausgewertet wird, einfach:

items.filter_map { |x| process_x url }

" Ruby 2.7 fügt Enumerable # filter_map hinzu " ist eine gute Lektüre zu diesem Thema, mit einigen Leistungsbenchmarks gegen einige der früheren Ansätze für dieses Problem:

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
quelle
1
Nett! Vielen Dank für das Update :) Sobald Ruby 2.7.0 veröffentlicht ist, halte ich es wahrscheinlich für sinnvoll, die akzeptierte Antwort auf diese zu ändern. Ich bin mir jedoch nicht sicher, wie die Etikette lautet, ob Sie der vorhandenen akzeptierten Antwort im Allgemeinen eine Chance zur Aktualisierung geben. Ich würde argumentieren, dass dies die erste Antwort ist, die sich auf den neuen Ansatz in 2.7 bezieht, und daher die akzeptierte sein sollte. @ the-tin-man stimmst du dieser Einstellung zu?
Pete Hamilton
Vielen Dank an @PeterHamilton - schätzen Sie das Feedback und hoffen Sie, dass es sich für viele Menschen als nützlich erweisen wird. Ich bin glücklich, mit Ihrer Entscheidung zu gehen, obwohl ich offensichtlich das Argument mag, das Sie gemacht haben :)
SRack
Ja, das ist das Schöne an Sprachen mit Kernteams, die zuhören.
der Blechmann
Es ist eine nette Geste, zu empfehlen, ausgewählte Antworten zu ändern, aber es kommt selten vor. SO bietet keinen Tickler, um die Leute daran zu erinnern, und die Leute wiederholen normalerweise nicht die alten Fragen, die sie gestellt haben, es sei denn, SO sagt, dass es Aktivitäten gegeben hat. Als Seitenleiste empfehle ich, Fruity nach Benchmarks zu durchsuchen, da dies viel weniger umständlich ist und es einfacher macht, sinnvolle Tests durchzuführen.
der Blechmann
930

Sie könnten verwenden compact:

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

Ich möchte die Leute daran erinnern, dass wenn Sie ein Array erhalten, das nils als Ausgabe von a enthält map Blocks erhalten und dieser Block versucht, Werte bedingt zurückzugeben, Sie nach Code riechen und Ihre Logik überdenken müssen.

Zum Beispiel, wenn Sie etwas tun, das dies tut:

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

Dann nicht. Stattdessen vor dem map, rejectwas Sie nicht wollen oder selectwas Sie wollen:

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

Ich denke darüber compactnach, ein Chaos als letzten Versuch zu beseitigen, um Dinge loszuwerden, mit denen wir nicht richtig umgegangen sind, normalerweise weil wir nicht wussten, was auf uns zukommt. Wir sollten immer wissen, welche Art von Daten in unserem Programm herumgeworfen werden. Unerwartete / unbekannte Daten sind schlecht. Jedes Mal, wenn ich Nullen in einem Array sehe, an dem ich arbeite, untersuche ich, warum sie existieren, und prüfe, ob ich den Code verbessern kann, der das Array generiert, anstatt Ruby Zeit und Speicher zu verschwenden, um Nullen zu generieren, und dann das Array zu durchsuchen, um sie zu entfernen sie später.

'Just my $%0.2f.' % [2.to_f/100]
der Blechmann
quelle
29
Das ist rubinrot!
Christophe Marois
4
Warum sollte es? Das OP muss nilEinträge entfernen, keine leeren Zeichenfolgen. Übrigens, nilist nicht dasselbe wie eine leere Zeichenfolge.
der Blechmann
9
Beide Lösungen durchlaufen die Sammlung zweimal ... warum nicht verwenden reduceoder inject?
Ziggy
4
Es hört sich nicht so an, als würden Sie die Frage oder Antwort des OP lesen. Die Frage ist, wie man Nullen aus einem Array entfernt. compactist am schnellsten, aber wenn Sie den Code am Anfang richtig schreiben, müssen Sie sich nicht mehr vollständig mit nils befassen.
der Blechmann
3
Ich stimme dir nicht zu! Die Frage lautet "Nullwerte zuordnen und entfernen". Nun, Nullwerte abzubilden und zu entfernen, bedeutet zu reduzieren. In ihrem Beispiel ordnet das OP die Nullen zu und wählt sie dann aus. Das Aufrufen der Karte und das anschließende Komprimieren oder das Auswählen und anschließende Zuordnen der Karte bedeutet denselben Fehler: Wie Sie in Ihrer Antwort hervorheben, handelt es sich um einen Codegeruch.
Ziggy
96

Versuchen Sie es mit reduceoder inject.

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

  memo
}

Ich bin mit der Antwort akzeptiert , dass wir nicht mapund compact, aber nicht aus den gleichen Gründen.

Ich fühle mich tief in mir, das ist mapdann compactgleichbedeutend mit selectdann map. Bedenken Sie: mapist eine Eins-zu-Eins-Funktion. Wenn Sie eine Zuordnung von einem Wertesatz und von Ihnen mapvornehmen, möchten Sie einen Wert im Ausgabesatz für jeden Wert im Eingabesatz. Wenn Sie vorher müssen select, dann wollen Sie wahrscheinlich keine mapam Set. Wenn Sie selectdanach (oder compact) müssen, dann möchten Sie wahrscheinlich keine mapam Set. In beiden Fällen iterieren Sie zweimal über den gesamten Satz, wenn ein reducenur einmal ausgeführt werden muss.

Außerdem versuchen Sie auf Englisch, "eine Menge von ganzen Zahlen in eine Menge von geraden ganzen Zahlen zu reduzieren".

Ziggy
quelle
4
Armer Ziggy, keine Liebe für deinen Vorschlag. lol. plus eins, jemand anderes hat Hunderte von Upvotes!
DDDD
2
Ich glaube, dass diese Antwort eines Tages mit Ihrer Hilfe die akzeptierte Antwort übertreffen wird. ^ o ^ //
Ziggy
2
+1 Die aktuell akzeptierte Antwort erlaubt es Ihnen nicht, die Ergebnisse von Operationen zu verwenden, die Sie während der
Auswahlphase ausgeführt haben
1
Das zweimalige Durchlaufen von unzähligen Datenstrukturen, wenn nur ein Pass erforderlich ist, wie in der akzeptierten Antwort, erscheint verschwenderisch. Reduzieren Sie also die Anzahl der Durchgänge mit redu! Danke @Ziggy
sebisnow
Das stimmt! Zwei Durchgänge über eine Sammlung von n Elementen sind jedoch immer noch O (n). Wenn Ihre Sammlung nicht so groß ist, dass sie nicht in Ihren Cache passt, ist es wahrscheinlich in Ordnung, zwei Durchgänge durchzuführen (ich denke nur, dass dies eleganter, ausdrucksvoller ist und weniger wahrscheinlich zu Fehlern in der Zukunft führt, wenn beispielsweise die Schleifen fallen nicht mehr synchronisiert). Wenn Sie es auch mögen, Dinge in einem Durchgang zu erledigen, könnten Sie daran interessiert sein, etwas über Wandler zu lernen! github.com/cognitect-labs/transducers-ruby
Ziggy
33

In Ihrem Beispiel:

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

Es sieht nicht so aus, als hätten sich die Werte geändert, außer dass sie durch ersetzt wurden nil. Wenn dies der Fall ist, dann:

items.select{|x| process_x url}

wird genügen.

sawa
quelle
27

Wenn Sie ein lockereres Ablehnungskriterium wünschen, um beispielsweise leere Zeichenfolgen sowie Null abzulehnen, können Sie Folgendes verwenden:

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

Wenn Sie weiter gehen und Nullwerte ablehnen möchten (oder eine komplexere Logik auf den Prozess anwenden möchten), können Sie einen Block zum Ablehnen übergeben:

[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
quelle
5
.leer? ist nur in Schienen erhältlich.
Ewalk
Zum späteren Nachschlagen, da blank?es nur in Schienen erhältlich ist, könnten wir verwenden, items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]die nicht an Schienen gekoppelt sind. (würde aber leere Zeichenfolgen oder Nullen nicht ausschließen)
Fotis
27

Auf jeden Fall compactist der beste Ansatz zur Lösung dieser Aufgabe. Wir können jedoch das gleiche Ergebnis nur mit einer einfachen Subtraktion erzielen:

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]
Evgenia Manolova
quelle
4
Ja, die eingestellte Subtraktion funktioniert, ist aber aufgrund des Overheads etwa halb so schnell.
der Blechmann
4

each_with_object ist wahrscheinlich der sauberste Weg hierher:

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

Meiner Meinung nach each_with_objectist es besser als inject/ reducein bedingten Fällen, weil Sie sich nicht um den Rückgabewert des Blocks kümmern müssen.

pnomolos
quelle
0

Eine weitere Möglichkeit, dies zu erreichen, ist die unten gezeigte. Hier verwenden wir Enumerable#each_with_object, um Werte zu sammeln und um Object#taptemporäre Variablen zu entfernen, die ansonsten für die nilÜberprüfung des Ergebnisses der process_xMethode benötigt werden.

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

Vollständiges Beispiel zur Veranschaulichung:

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

Alternativer Ansatz:

Wenn Sie sich die Methode ansehen process_x url, die Sie aufrufen , ist nicht klar, welchen Zweck die Eingabe xin diese Methode hat. Wenn ich davon ausgehe, dass Sie den Wert xvon verarbeiten, indem Sie einige übergeben urlund feststellen, welche der xs wirklich zu gültigen Nicht-Null-Ergebnissen verarbeitet werden, ist dies möglicherweise Enumerabble.group_byeine bessere Option als Enumerable#map.

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

h["Good"]
#=> [3,4,5]
Zauberstabmacher
quelle