Jak mapować i usuwać wartości zerowe w Ruby


361

Mam wartość, mapktóra albo zmienia wartość, albo ustawia ją na zero. Następnie chcę usunąć z listy wpisy zerowe. Lista nie musi być przechowywana.

Oto, co obecnie mam:

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

Wiem, że mógłbym po prostu wykonać pętlę i warunkowo zebrać w innej tablicy takiej jak ta:

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

Ale to nie wydaje się takie idiomatyczne. Czy istnieje dobry sposób na mapowanie funkcji na liście, usuwając / wykluczając nils w miarę przemieszczania się?


3
Ruby 2.7 wprowadza filter_map, co wydaje się idealne do tego. Oszczędza potrzebę ponownego przetwarzania tablicy, zamiast tego wykonując ją zgodnie z oczekiwaniami za pierwszym razem. Więcej informacji tutaj.
SRack

Odpowiedzi:


21

Ruby 2.7+

Jest teraz!

Ruby 2.7 wprowadza właśnie filter_mapw tym celu. Jest idiomatyczny i skuteczny i spodziewałbym się, że wkrótce stanie się normą.

Na przykład:

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

W twoim przypadku, gdy blok zmienia się w falsey, po prostu:

items.filter_map { |x| process_x url }

Ruby 2.7 dodaje Enumerable # filter_map ” to dobra lektura na ten temat, z pewnymi testami wydajności w stosunku do niektórych wcześniejszych podejść do tego problemu:

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)

1
Miły! Dzięki za aktualizację :) Po wydaniu Ruby 2.7.0 myślę, że prawdopodobnie sensowne jest przełączyć zaakceptowaną odpowiedź na tę. Nie jestem jednak pewien, jaka jest tutaj etykieta, czy ogólnie dajesz istniejącej zaakceptowanej odpowiedzi szansę na aktualizację? Twierdzę, że jest to pierwsza odpowiedź odnosząca się do nowego podejścia w wersji 2.7, więc powinna zostać przyjęta. @ the-tin-man Czy zgadzasz się z tym ujęciem?
Pete Hamilton,

Dzięki @PeterHamilton - doceniamy opinie i mam nadzieję, że okażą się przydatne dla wielu osób. Cieszę się z twojej decyzji, choć oczywiście podoba mi się twój argument :)
SRack

Tak, to miła rzecz w językach, w których główne zespoły słuchają.
Tin Man

Miło jest polecić zmianę wybranych odpowiedzi, ale rzadko się to zdarza. SO nie zapewnia łaskotki przypominającej ludziom, a ludzie zwykle nie odwiedzają starych zadawanych pytań, chyba że SO mówi, że była aktywność. Jako pasek boczny polecam sprawdzenie Fruity pod kątem testów porównawczych, ponieważ jest to znacznie mniej kłopotliwe i ułatwia wykonywanie rozsądnych testów.
Tin Man,

930

Możesz użyć compact:

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

Chciałbym przypomnieć ludziom, że jeśli otrzymujesz tablicę zawierającą wartości zerowe jako wynik mapbloku, a ten blok próbuje warunkowo zwrócić wartości, to masz zapach kodu i musisz przemyśleć swoją logikę.

Na przykład, jeśli robisz coś, co to robi:

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

Więc nie rób tego. Zamiast tego, przed kontrolą map, rejectrzeczy nie chcesz lub selectczego chcą:

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

Rozważam użycie compactsprzątania bałaganu jako ostatniego kroku do pozbycia się rzeczy, z którymi nie radziliśmy sobie poprawnie, zwykle dlatego, że nie wiedzieliśmy, co nas czeka. Zawsze powinniśmy wiedzieć, jakie dane są przekazywane w naszym programie; Nieoczekiwane / nieznane dane są złe. Za każdym razem, gdy widzę zero w tablicy, nad którą pracuję, kopię, dlaczego istnieją, i sprawdzam, czy mogę poprawić kod generujący tablicę, zamiast pozwolić Ruby marnować czas i generować pamięć, a następnie przeszukiwać tablicę, aby usunąć je później.

'Just my $%0.2f.' % [2.to_f/100]

29
Teraz to jest rubinowy!
Christophe Marois

4
Dlaczego to powinno? OP musi usunąć nilwpisy, a nie puste ciągi. BTW, nilto nie to samo, co pusty ciąg.
Tin Man

9
Oba rozwiązania powtarzają się dwukrotnie w całej kolekcji ... dlaczego nie użyć reducelub inject?
Ziggy

4
Nie brzmi to tak, jakbyś czytał pytanie OP lub odpowiedź. Pytanie brzmi: jak usunąć zero z tablicy. compactjest najszybszy, ale tak naprawdę napisanie kodu na początku eliminuje potrzebę całkowitego radzenia sobie z zerami.
Tin Man

3
Nie zgadzam się! Pytanie brzmi „Mapuj i usuwaj wartości zerowe”. Cóż, mapowanie i usuwanie wartości zerowych oznacza zmniejszenie. W ich przykładzie OP mapuje, a następnie wybiera zero. Wywołanie mapy, a następnie kompaktowania lub wybrania, a następnie mapy, jest równoznaczne z popełnieniem tego samego błędu: jak wskazano w odpowiedzi, jest to zapach kodu.
Ziggy,

96

Spróbuj użyć reducelub inject.

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

  memo
}

Zgadzam się z przyjętymi odpowiedź, że nie powinniśmy mapi compact, ale nie z tych samych powodów.

Czuję się głęboko wewnątrz, które mapnastępnie compactjest równoważne selectwtedy map. Zastanów się: mapto funkcja jeden do jednego. Jeśli odwzorowujesz z jakiegoś zestawu wartości, a ty map, to potrzebujesz jednej wartości w zestawie wyjściowym dla każdej wartości w zestawie wejściowym. Jeśli musisz z selectwyprzedzeniem, prawdopodobnie nie chcesz mieć mapna planie. Jeśli musisz selectpóźniej (lub compact), prawdopodobnie nie chcesz mieć mapna planie. W obu przypadkach iterujesz dwa razy przez cały zestaw, gdy wystarczy reducetylko raz.

Również w języku angielskim próbujesz „zredukować zestaw liczb całkowitych do zestawu parzystych liczb całkowitych”.


4
Biedny Ziggy, brak miłości do twoich sugestii. lol. plus jeden, ktoś inny ma setki pozytywnych opinii!
DDDD,

2
Wierzę, że któregoś dnia, z twoją pomocą, odpowiedź ta przekroczy przyjęte. ^ o ^ //
Ziggy

2
+1 obecnie akceptowana odpowiedź nie pozwala na wykorzystanie wyników operacji wykonanych podczas wybranej fazy
chees

1
dwukrotne powtarzanie iteracyjnych struktur danych, jeśli tylko przejście jest potrzebne, tak jak w przyjętej odpowiedzi, wydaje się marnotrawstwem. W ten sposób zmniejsz liczbę przejść, używając zmniejszania! Dzięki @Ziggy
sebisnow,

To prawda! Ale wykonanie dwóch przejść w zbiorze n elementów to nadal O (n). O ile twoja kolekcja nie jest tak duża, że ​​nie mieści się w twojej pamięci podręcznej, wykonanie dwóch przejść jest prawdopodobnie w porządku (myślę, że jest to bardziej eleganckie, wyraziste i rzadziej prowadzi do błędów w przyszłości, gdy powiedzmy, że pętle opadają brak synchronizacji). Jeśli lubisz robić rzeczy w jednym przejściu, być może zainteresuje Cię nauka o przetwornikach! github.com/cognitect-labs/transducers-ruby
Ziggy

33

W twoim przykładzie:

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

nie wygląda na to, że wartości uległy zmianie poza zastąpieniem nil. W takim przypadku:

items.select{|x| process_x url}

wystarczy.


27

Jeśli chcesz luźniejszego kryterium odrzucenia, na przykład, aby odrzucić puste ciągi, a także zero, możesz użyć:

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

Jeśli chcesz pójść dalej i odrzucić wartości zerowe (lub zastosować bardziej złożoną logikę do procesu), możesz przekazać blok, aby odrzucić:

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

5
.pusty? jest dostępny tylko w szynach.
ewalk

Na przyszłość, ponieważ blank?jest on dostępny tylko w szynach, moglibyśmy użyć tego, items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]który nie jest połączony z szynami. (nie wyklucza jednak pustych ciągów lub zer)
Fotis

27

Zdecydowanie compactjest to najlepsze podejście do rozwiązania tego zadania. Możemy jednak osiągnąć ten sam wynik po prostu odejmując:

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

4
Tak, ustawianie odejmowania będzie działać, ale jest o około połowę szybsze z powodu narzutu.
Tin Man

4

each_with_object jest prawdopodobnie najczystszym sposobem na przejście tutaj:

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

Moim zdaniem each_with_objectjest lepszy niż inject/ reducew przypadkach warunkowych, ponieważ nie musisz się martwić o wartość zwracaną bloku.


0

Jeszcze jeden sposób na osiągnięcie tego będzie taki, jak pokazano poniżej. Tutaj używamy Enumerable#each_with_objectdo zbierania wartości i wykorzystujemy Object#tapdo pozbycia się zmiennej tymczasowej, która w przeciwnym razie byłaby potrzebna do nilsprawdzenia wyniku process_xmetody.

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

Kompletny przykład dla ilustracji:

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

Alternatywne podejście:

Patrząc na metodę, którą wywołujesz process_x url, nie jest jasne, jaki jest cel wprowadzania xtej metody. Jeśli założę, że zamierzasz przetworzyć wartość xprzekazania jej urli określić, które z nich xnaprawdę zostaną przetworzone na prawidłowe wyniki inne niż zero - wtedy może być Enumerabble.group_bylepszym rozwiązaniem niż Enumerable#map.

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

h["Good"]
#=> [3,4,5]
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.