web-dev-qa-db-de.com

Zuordnen und Entfernen von Nullwerten in Ruby

Ich habe eine Map, die 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.

Das habe ich aktuell:

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

Mir ist bewusst, dass ich einfach eine Schleife erstellen und in einem anderen Array wie dem folgenden unter bestimmten Bedingungen 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?

331
Peter Hamilton

Sie könnten compact verwenden:

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

Ich möchte die Leute daran erinnern, dass, wenn Sie ein Array mit nils als Ausgabe eines map-Blocks erhalten und dieser Block versucht, Werte bedingt zurückzugeben, Sie Code-Geruch haben und Ihre Logik überdenken müssen.

Wenn Sie beispielsweise Folgendes tun:

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

Dann tu es nicht. Stattdessen map, reject das, was Sie nicht wollen oder select, was Sie wollen:

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

Ich überlege, compact zu verwenden, um ein Durcheinander aufzuräumen, um Dinge loszuwerden, die wir nicht richtig gehandhabt haben, normalerweise, weil wir nicht wussten, was auf uns zukam. Wir sollten immer wissen, welche Art von Daten in unserem Programm herumgeworfen werden. Unerwartete/unbekannte Daten sind fehlerhaft. Jedes Mal, wenn ich nils in einem Array sehe, an dem ich arbeite, beschäftige ich mich mit der Frage, warum sie existieren, und ob ich den Code zum Generieren des Arrays verbessern kann, anstatt Ruby Zeit und Speicher zu verschwenden und dann nils zu generieren Durchsuchen Sie das Array, um sie später zu entfernen.

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

Versuchen Sie es mit #reduce oder #inject!

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

  memo
}

Ich stimme der akzeptierten Antwort zu, dass wir nicht mappen und komprimieren sollten, aber nicht aus den gleichen Gründen!

Ich fühle mich tief in dieser Karte-dann-kompakt gleichbedeutend mit Auswahl-dann-Karte. Bedenken Sie: Eine Karte ist eine Eins-zu-Eins-Funktion. Wenn Sie aus einer Reihe von Werten mappen und mappen, dann wollen ein Wert in der Ausgabemenge für jeden Wert in der Eingabemenge. Wenn Sie vorher auswählen müssen, möchten Sie wahrscheinlich keine Karte am Set haben. Wenn Sie danach auswählen (oder komprimieren) müssen, möchten Sie wahrscheinlich keine Karte am Set haben. In beiden Fällen iterieren Sie zweimal über den gesamten Satz, wenn eine Reduzierung nur einmal ausgeführt werden muss.

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

87
Ziggy

In deinem 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 nil ersetzt wurden. Wenn das der Fall ist, dann:

items.select{|x| process_x url}

wird genügen.

33
sawa

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

@ the Tin Man, Nice - ich kenne diese Methode nicht. Nun, definitiv kompakt ist der beste Weg, kann aber auch mit einfacher Subtraktion durchgeführt werden:

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

each_with_object ist wahrscheinlich der sauberste Weg, hierher zu kommen:

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

Meiner Meinung nach ist each_with_object in bedingten Fällen besser als inject/reduce, da Sie sich keine Gedanken über den Rückgabewert des Blocks machen müssen.

3
pnomolos

Ruby 2.7 +

Es gibt jetzt!

Ruby 2.7 führt zu diesem Zweck filter_map ein. 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 als falsch ausgewertet wird, einfach:

items.filter_map { |x| process_x url }

Hier ist ein gute Lektüre zum Thema mit einigen Leistungsbenchmarks gegen einige der früheren Lösungsansä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)

Hoffe, das ist nützlich für jemanden!

2
SRack

Ein weiterer Weg, dies zu erreichen, ist der unten gezeigte. Hier verwenden wir Enumerable#each_with_object , um Werte zu sammeln, und Object#tap , um temporäre Variablen zu entfernen, die ansonsten für die Überprüfung von nil benötigt werden Ergebnis der Methode _process_x_.

_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, die Sie _process_x url_ aufrufen, ist nicht klar, wozu die Eingabe x in dieser Methode dient. Wenn ich annehme, dass Sie den Wert von x verarbeiten werden, indem Sie ihn mit url übergeben, und feststellen, welche der xs wirklich zu gültigen Nicht-Null-Ergebnissen verarbeitet werden - dann kann Enumerabble.group_by sein ist eine 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]
_
0
Wand Maker