Ruby'de sıfır değerleri nasıl eşlenir ve kaldırılır


361

Ben mapbir değeri değiştirir veya nil ayarlar. Sonra nil girişleri listeden kaldırmak istiyorum. Listenin tutulması gerekmez.

Şu anda sahip olduğum şey bu:

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

Ben sadece bir döngü yapmak ve şartlı olarak böyle bir dizide toplamak farkındayım:

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

Ama bu kadar deyimsel görünmüyor. Bir işlevi listenin üzerinde eşlemek, gittikçe sıfırları kaldırmak / hariç tutmak için güzel bir yol var mı?


3
Ruby 2.7 filter_map, bunun için mükemmel gibi görünüyor. Dizinin yeniden işlenmesi ihtiyacını kaydeder, bunun yerine diziyi ilk kez istediğiniz gibi alır. Daha fazla bilgi burada.
SRack

Yanıtlar:


22

Yakut 2.7+

Şimdi var!

Ruby 2.7 filter_maptam da bu amacı ortaya koyuyor. Deyimsel ve icracı ve çok yakında norm haline gelmesini beklerdim.

Örneğin:

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

Sizin durumunuzda, blok falsey olarak değerlendirildiğinde, basitçe:

items.filter_map { |x| process_x url }

" Ruby 2.7 Enumerable # filter_map ekliyor " konuyla ilgili iyi bir okuma ve bu soruna daha önceki yaklaşımlardan bazılarına ilişkin bazı performans karşılaştırmaları:

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
Güzel! Güncelleme için teşekkürler :) Ruby 2.7.0 piyasaya sürüldükten sonra, kabul edilen cevabı buna çevirmenin muhtemelen mantıklı olduğunu düşünüyorum. Genellikle mevcut kabul edilen yanıtı güncelleme şansı verip vermediğinizden emin olmak için burada görgü kurallarının ne olduğundan emin değilim? Bu, 2.7'deki yeni yaklaşıma atıfta bulunan ilk cevaptır, bu yüzden kabul edilen cevap olmalıdır. @ the-tin-man bu kabulü kabul ediyor musunuz?
Pete Hamilton

Teşekkürler @PeterHamilton - geri bildirimi takdir edin ve umarım bol insan için yararlı olacağını kanıtlar. Kararınıza katıldığım için mutluyum, ama açıkçası yaptığınız argümanı seviyorum :)
SRack

Evet, bu, çekirdek ekipleri dinleyen diller hakkında hoş bir şey.
Tin Man

Seçilen cevapların değiştirilmesini tavsiye etmek güzel bir jest, ancak nadiren olur. SO, insanlara hatırlatmak için bir gıdıklama sağlamaz ve SO, etkinlik olmadığını söylemedikçe, genellikle sordukları eski soruları tekrar ziyaret etmez. Kenar çubuğu olarak, kıyaslamalar için Fruity'ye bakmanızı tavsiye ederim, çünkü çok daha az uğraştır ve mantıklı testler yapmayı kolaylaştırır.
Tin Man

931

Şunları kullanabilirsiniz compact:

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

İnsanlara, bir mapbloğun çıktısı olarak nils içeren bir dizi alıyorsanız ve bu bloğun koşullu olarak değer döndürmeye çalıştığını, kod kokusuna sahip olduğunuzu ve mantığınızı yeniden düşünmeniz gerektiğini hatırlatmak isterim .

Örneğin, bunu yapan bir şey yapıyorsanız:

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

Öyleyse yapma. Bunun yerine, önce map, rejectistediğiniz veya yok şeyler selectne istiyorsun:

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

compactDoğru bir şekilde başa çıkmadığımız şeylerden kurtulmak için son bir hendek temizliği olarak bir karışıklığı temizlemek için kullanmayı düşünüyorum , çünkü genellikle bize ne geldiğini bilmiyorduk. Programımızda ne tür verilerin atıldığını daima bilmeliyiz; Beklenmeyen / bilinmeyen veriler kötü. Her zaman üzerinde çalıştığım bir dizide nils görüyorum, neden var olduklarını araştırıyorum ve Ruby'nin zaman ve bellek üreten nils'i boşa harcamasına izin vermek yerine diziyi üreten kodu geliştirip geliştiremeyeceğimi görüyorum. daha sonra.

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

29
Şimdi bu yakut esque!
Christophe Marois

4
Neden olsun ki? OP'nin nilboş dizeleri değil, girişleri çıkarması gerekir . BTW, nilboş bir dize ile aynı değildir.
Tin Man

9
Her iki çözüm de koleksiyon üzerinde iki kez yineleniyor ... neden kullanmıyorsunuz reduceveya inject?
Ziggy

4
OP sorusunu veya cevabını okuduğunuz gibi görünmüyor. Soru, bir diziden nils nasıl kaldırılacağıdır. compacten hızlıdır, ancak aslında başlangıçta kodu doğru yazmak, nils ile tamamen başa çıkma ihtiyacını ortadan kaldırır.
Tin Man

3
Katılmıyorum! Soru "Sıfır değerlerini eşle ve kaldır". Eh, nil değerlerini haritalamak ve kaldırmak azaltmaktır. Örneklerinde OP eşlenir ve ardından sıfırları seçer. Haritayı çağırmak ve sonra kompaktlaştırmak veya seçmek ve sonra eşlemek aynı hatayı yapmak anlamına gelir: Cevabınızda işaret ettiğiniz gibi, bu bir kod kokusudur.
Ziggy

96

reduceVeya tuşunu kullanmayı deneyin inject.

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

  memo
}

Kabul etmemiz gereken mapve compactaynı nedenlerden ötürü kabul etmeyeceğimiz kabul edilen cevaba katılıyorum .

O derin içini hissetmek mapsonra compacteşdeğerdir selectsonra map. Düşünmek:map bire bir işlevdir. Bazı değer kümelerinden eşleme yapıyorsanız ve siz map, giriş kümesindeki her değer için çıktı kümesinde bir değer olmasını istersiniz . Size yaşıyorsanız selectönce elden, sonra muhtemelen bir istemiyoruz mapsette. Daha selectsonra (veya compact) yapmak zorundaysanız, muhtemelen mapsette bir istemezsiniz . Her iki durumda da, reducesadece bir kez gitmesi gerektiğinde , tüm set üzerinde iki kez yinelenirsiniz .

Ayrıca, İngilizce olarak, "bir tam sayı kümesini bir çift tam sayı kümesine indirgeme" deniyorsunuz.


4
Zavallı Ziggy, öneriniz için sevgi yok. lol. artı bir, başka birinin yüzlerce upvotes var!
DDDD

2
Bir gün, sizin yardımınızla, bu cevabın kabul edileni geçeceğine inanıyorum. ^ o ^ //
Ziggy

2
Şu anda kabul edilen yanıtı +
1'le

1
kabul edilen cevapta olduğu gibi sadece geçişte gerekliyse, sayılabilir veri yapıları üzerinde iki kez yineleme israfa yol açar. Böylece azaltmak kullanarak geçiş sayısını azaltın! Teşekkürler @Ziggy
sebisnow

Bu doğru! Ancak n elementlerin bir koleksiyonundan iki geçiş yapmak hala O (n) 'dir. Koleksiyonunuz önbelleğinize sığmayacak kadar büyük olmadığı sürece, iki geçiş yapmak muhtemelen iyidir (bence bunun daha zarif, etkileyici ve gelecekte döngüler düştüğünde hatalara yol açma olasılığı daha düşüktür) senkronizasyon dışı). Bir geçişte de bir şeyler yapmak isterseniz, dönüştürücüler hakkında bilgi edinmek isteyebilirsiniz! github.com/cognitect-labs/transducers-ruby
Ziggy

33

Örneğinizde:

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

değerlerin yerine başka değerlerin değiştiği görülmez nil. Bu durumda, o zaman:

items.select{|x| process_x url}

yeterli olacaktır.


27

Örneğin, boş dizeleri nil yanı sıra reddetmek için reddetme için daha gevşek bir ölçüt istiyorsanız, şunları kullanabilirsiniz:

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

Daha ileri gitmek ve sıfır değerleri reddetmek (veya sürece daha karmaşık bir mantık uygulamak) istiyorsanız, reddetmek için bir blok geçirebilirsiniz:

[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
.boş? sadece raylarda mevcuttur.
ewalk

İleride başvurmak için blank?, sadece raylarda mevcut olduğundan, raylara items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]bağlı olmayan kullanabiliriz . (boş dizeleri veya 0'ları hariç tutmaz)
Fotis

27

Kesinlikle compact bu görevi çözmek için en iyi yaklaşımdır. Bununla birlikte, aynı sonucu basit bir çıkarma ile elde edebiliriz:

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

4
Evet, set çıkarma işe yarayacaktır, ancak yükü nedeniyle yaklaşık yarısı kadar hızlıdır.
Tin Man

4

each_with_object muhtemelen buraya gitmenin en temiz yoludur:

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

Benim düşünceme göre, koşullu durumlarda each_with_objectdaha iyidir inject/ reduceçünkü bloğun dönüş değeri hakkında endişelenmenize gerek yoktur.


0

Bunu başarmanın bir yolu aşağıda gösterildiği gibi olacaktır. Burada Enumerable#each_with_objectdeğer toplamak ve yöntem sonucunu kontrol etmek için Object#tapgerekli olan geçici değişkenlerden kurtulmak için kullanırız .nilprocess_x

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

İllüstrasyon için tam örnek:

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

Alternatif yaklaşım:

Aradığınız yönteme bakarak, bu yöntemdeki process_x urlgirdinin amacının ne olduğu açık değildir x. Eğer değerini xgeçerek işleyeceğinizi varsayarsanız urlve hangisinin xgerçekten geçerli nil olmayan sonuçlara işleneceğini belirlerseniz - o zaman, Enumerabble.group_bydaha iyi bir seçenek olabilir Enumerable#map.

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

h["Good"]
#=> [3,4,5]
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.