Come mappare e rimuovere i valori zero in Ruby


361

Ho un valore mapche modifica un valore o lo imposta su zero. Voglio quindi rimuovere le voci zero dall'elenco. Non è necessario conservare l'elenco.

Questo è quello che ho attualmente:

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

Sono consapevole che potrei semplicemente fare un ciclo e raccogliere condizionatamente in un altro array come questo:

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

Ma non sembra così idiomatico. C'è un bel modo per mappare una funzione su un elenco, rimuovendo / escludendo gli zero mentre procedi?


3
Ruby 2.7 introduce filter_map, che sembra essere perfetto per questo. Salva la necessità di rielaborare l'array, ottenendolo invece come desiderato per la prima volta. Maggiori informazioni qui.
SRack

Risposte:


21

Rubino 2.7+

C'è adesso!

Ruby 2.7 sta introducendo filter_mapper questo preciso scopo. È idiomatico e performante, e mi aspetto che diventi presto la norma.

Per esempio:

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

Nel tuo caso, poiché il blocco viene valutato come falso, semplicemente:

items.filter_map { |x| process_x url }

" Ruby 2.7 aggiunge Enumerable # filter_map " è una buona lettura sull'argomento, con alcuni benchmark delle prestazioni rispetto ad alcuni dei precedenti approcci a questo problema:

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
Bello! Grazie per l'aggiornamento :) Una volta rilasciato Ruby 2.7.0, penso che probabilmente abbia senso cambiare la risposta accettata a questa. Non sono sicuro di cosa sia l'etichetta qui, se in genere dai la possibilità alla risposta accettata esistente di aggiornare? Direi che questa è la prima risposta che fa riferimento al nuovo approccio in 2.7, quindi dovrebbe diventare quella accettata. @ the-tin-man sei d'accordo con questa interpretazione?
Pete Hamilton,

Grazie @PeterHamilton - apprezza il feedback e spero che possa rivelarsi utile a molte persone. Sono felice di prendere la tua decisione, anche se ovviamente mi piace l'argomento che hai fatto :)
SRack

Sì, questa è la cosa bella delle lingue che hanno un core team che ascolta.
Tin Man,

È un bel gesto raccomandare di modificare le risposte selezionate, ma succede raramente. SO non fornisce un tickler per ricordare alle persone e le persone di solito non riesaminano le vecchie domande che hanno posto a meno che SO non dica che c'è stata attività. Come barra laterale, consiglio di guardare Fruity per i benchmark perché è molto meno complicato e rende più facile fare test sensati.
Tin Man,

930

Puoi usare compact:

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

Vorrei ricordare alla gente che se stai ottenendo un array contenente nils come output di un mapblocco, e quel blocco cerca di restituire condizionalmente valori, allora hai odore di codice e devi ripensare la tua logica.

Ad esempio, se stai facendo qualcosa che fa questo:

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

Allora no. Invece, prima di map, rejectle cose che non vuoi o selectche cosa vuoi:

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

Considero l'uso compactdi ripulire un casino come uno sforzo disperato per sbarazzarsi di cose che non abbiamo gestito correttamente, di solito perché non sapevamo cosa ci stava succedendo. Dovremmo sempre sapere che tipo di dati vengono lanciati nel nostro programma; I dati imprevisti / sconosciuti sono errati. Ogni volta che vedo zero in un array su cui sto lavorando, scavo nel motivo per cui esistono e vedo se riesco a migliorare il codice che genera l'array, piuttosto che consentire a Ruby di perdere tempo e memoria generando zero quindi setacciare l'array per rimuovere più tardi.

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

29
Questo è ruby-esque!
Christophe Marois,

4
Perché dovrebbe? L'OP deve eliminare le nilvoci, non stringhe vuote. A proposito, nilnon è lo stesso di una stringa vuota.
Tin Man,

9
Entrambe le soluzioni ripetono due volte la collezione ... perché non usare reduceo inject?
Ziggy

4
Non sembra che tu abbia letto la domanda dei PO o la risposta. La domanda è: come rimuovere nils da un array. compactè più veloce ma in realtà scrivere correttamente il codice all'inizio rimuove la necessità di gestire completamente nils.
Tin Man

3
Non sono d'accordo! La domanda è "Mappa e rimuovi i valori zero". Bene, mappare e rimuovere valori nulli è ridurre. Nel loro esempio, l'OP mappa e quindi seleziona gli zero. Chiamare mappa e quindi compattare, o selezionare e quindi mappare, equivale a fare lo stesso errore: come fai notare nella tua risposta, è un odore di codice.
Ziggy,

96

Prova a usare reduceo inject.

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

  memo
}

Concordo con la risposta accettata che non dovremmo mape compact, ma non per gli stessi motivi.

Mi sento profondamente dentro che mapallora compactequivale a selectallora map. Considera: mapè una funzione uno a uno. Se esegui il mapping da un determinato set di valori e tu map, allora vuoi un valore nel set di output per ciascun valore nel set di input. Se devi farlo selectin anticipo, probabilmente non vorrai un mapset sul set. Se dopo dovrai select(o compact), probabilmente non vorrai un mapset sul set. In entrambi i casi stai iterando due volte sull'intero set, quando devi reducesolo andare una volta.

Inoltre, in inglese, stai cercando di "ridurre un set di numeri interi in un set di numeri pari".


4
Povero Ziggy, nessun amore per il tuo suggerimento. lol. più uno, qualcun altro ha centinaia di voti!
DDDD,

2
Credo che un giorno, con il tuo aiuto, questa risposta supererà quella accettata. ^ o ^ //
Ziggy

2
+1 la risposta attualmente accettata non ti consente di utilizzare i risultati delle operazioni eseguite durante la fase di selezione
chees

1
iterare su numerosissime strutture dati due volte se è necessario solo un passaggio come nella risposta accettata sembra dispendioso. Riduci così il numero di passaggi usando riduci! Grazie @Ziggy
sebisnow il

È vero! Ma fare due passaggi su una raccolta di n elementi è ancora O (n). A meno che la tua raccolta non sia così grande da non rientrare nella tua cache, fare due passaggi probabilmente va bene (penso solo che sia più elegante, espressivo e meno probabile che porti a bug in futuro quando, diciamo, i loop cadono fuori sincrono). Se ti piace fare anche le cose in una sola passata, potresti essere interessato a conoscere i trasduttori! github.com/cognitect-labs/transducers-ruby
Ziggy

33

Nel tuo esempio:

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

non sembra che i valori siano cambiati oltre a essere sostituiti con nil. In tal caso, quindi:

items.select{|x| process_x url}

sarà sufficiente.


27

Se si desidera un criterio più flessibile per il rifiuto, ad esempio, per rifiutare stringhe vuote e zero, è possibile utilizzare:

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

Se si desidera andare oltre e rifiutare i valori zero (o applicare una logica più complessa al processo), è possibile passare un blocco per rifiutare:

[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
.blank? è disponibile solo su binari.
ewalk

Per riferimento futuro, poiché blank?è disponibile solo nelle rotaie, potremmo utilizzare ciò items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]che non è accoppiato alle rotaie. (non escluderebbe stringhe vuote o 0s però)
Fotis

27

Sicuramente compactè l'approccio migliore per risolvere questo compito. Tuttavia, possiamo ottenere lo stesso risultato solo con una semplice sottrazione:

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

4
Sì, la sottrazione impostata funzionerà, ma è circa la metà più veloce a causa del suo sovraccarico.
Tin Man,

4

each_with_object è probabilmente il modo più pulito per andare qui:

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

A mio avviso, each_with_objectè meglio di inject/ reducein casi condizionali perché non devi preoccuparti del valore di ritorno del blocco.


0

Un altro modo per realizzarlo sarà come mostrato di seguito. Qui, utilizziamo Enumerable#each_with_objectper raccogliere valori e utilizziamo Object#tapper sbarazzarci della variabile temporanea che è altrimenti necessaria per nilverificare il risultato del process_xmetodo.

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

Esempio completo per l'illustrazione:

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

Approccio alternativo:

Osservando il metodo che stai chiamando process_x url, non è chiaro quale sia lo scopo dell'input xin quel metodo. Se suppongo che stai per elaborare il valore di xpassandolo un po ' urle determinare quale dei due viene xrealmente trasformato in risultati validi non nulli - allora, potrebbe essere Enumerabble.group_byun'opzione migliore di Enumerable#map.

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

h["Good"]
#=> [3,4,5]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.