Cara memetakan dan menghapus nilai nil di Ruby

361

Saya punya mapyang bisa mengubah nilai atau menyetelnya menjadi nol. Saya kemudian ingin menghapus entri nil dari daftar. Daftar tidak perlu disimpan.

Inilah yang saat ini saya miliki:

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

Saya sadar saya hanya bisa melakukan loop dan secara kondisional mengumpulkan di array lain seperti ini:

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

Tapi sepertinya itu tidak idiomatis. Apakah ada cara yang bagus untuk memetakan fungsi pada daftar, menghapus / mengecualikan nil saat Anda pergi?

Pete Hamilton
sumber
3
Ruby 2.7 memperkenalkan filter_map, yang tampaknya sempurna untuk ini. Menghemat kebutuhan untuk memproses ulang array, alih-alih mendapatkannya sesuai keinginan pertama kali. Info lebih lanjut di sini.
SRack

Jawaban:

21

Ruby 2.7+

Ada sekarang!

Ruby 2.7 memperkenalkan filter_mapuntuk tujuan yang tepat ini. Ini idiomatis dan pemain, dan saya berharap itu akan menjadi norma segera.

Sebagai contoh:

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

Dalam kasus Anda, ketika blok mengevaluasi ke falsey, cukup:

items.filter_map { |x| process_x url }

" Ruby 2.7 menambahkan Enumerable # filter_map " adalah bacaan yang bagus tentang subjek, dengan beberapa tolok ukur kinerja terhadap beberapa pendekatan sebelumnya untuk masalah ini:

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
sumber
1
Bagus! Terima kasih atas pembaruannya :) Setelah Ruby 2.7.0 dirilis, saya pikir mungkin masuk akal untuk mengalihkan jawaban yang diterima ke yang ini. Saya tidak yakin etiket apa yang ada di sini, apakah Anda biasanya memberi tanggapan diterima yang ada kesempatan untuk memperbarui? Saya berpendapat ini adalah jawaban pertama yang merujuk pada pendekatan baru di 2.7, jadi harus menjadi yang diterima. @ the-tin-man Anda setuju dengan pengambilan ini?
Pete Hamilton
Terima kasih @PeterHamilton - hargai umpan baliknya, dan harap ini akan bermanfaat bagi banyak orang. Saya senang dengan keputusan Anda, meskipun jelas saya menyukai argumen yang Anda buat :)
SRack
Ya, itu hal yang menyenangkan tentang bahasa yang memiliki tim inti yang mendengarkan.
Manusia Timah
Ini adalah isyarat yang baik untuk merekomendasikan jawaban yang dipilih diubah, tetapi itu jarang terjadi. SO tidak memberikan tickler untuk mengingatkan orang dan orang biasanya tidak mengunjungi kembali pertanyaan lama yang telah mereka tanyakan kecuali SO mengatakan telah ada kegiatan. Sebagai bilah sisi, saya sarankan untuk melihat Fruity sebagai tolok ukur karena jauh lebih sedikit dan lebih mudah untuk membuat tes yang masuk akal.
the Tin Man
930

Anda bisa menggunakan compact:

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

Saya ingin mengingatkan orang bahwa jika Anda mendapatkan array yang berisi nils sebagai output dari sebuah mapblok, dan blok itu mencoba mengembalikan nilai secara kondisional, maka Anda memiliki kode bau dan perlu memikirkan kembali logika Anda.

Misalnya, jika Anda melakukan sesuatu yang melakukan ini:

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

Kalau begitu jangan. Alih-alih, sebelum map, rejecthal-hal yang tidak Anda inginkan atau selectapa yang Anda inginkan:

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

Saya menganggap menggunakan compactuntuk membersihkan kekacauan sebagai upaya terakhir untuk menyingkirkan hal-hal yang tidak kami tangani dengan benar, biasanya karena kami tidak tahu apa yang akan terjadi pada kami. Kita harus selalu tahu data seperti apa yang dilemparkan ke dalam program kita; Data yang tidak terduga / tidak dikenal buruk. Setiap kali saya melihat nils dalam array yang sedang saya kerjakan, saya menggali mengapa mereka ada, dan melihat apakah saya dapat meningkatkan kode yang menghasilkan array, daripada membiarkan Ruby membuang-buang waktu dan memori yang menghasilkan nil kemudian memilah-milah array untuk menghapus mereka nanti.

'Just my $%0.2f.' % [2.to_f/100]
Manusia Timah
sumber
29
Nah, itu ruby-esque!
Christophe Marois
4
Kenapa harus begitu? OP perlu menghapus nilentri, bukan string kosong. BTW, niltidak sama dengan string kosong.
the Tin Man
9
Kedua solusi tersebut dilakukan dua kali dalam koleksi ... mengapa tidak digunakan reduceatau inject?
Ziggy
4
Tidak terdengar seperti Anda membaca pertanyaan OP atau jawabannya. Pertanyaannya adalah, bagaimana cara menghapus nil dari array. compactlebih cepat tetapi sebenarnya menulis kode dengan benar di awal menghilangkan kebutuhan untuk menangani nils sepenuhnya.
the Tin Man
3
Saya tidak setuju! Pertanyaannya adalah "Peta dan hapus nilai nil". Nah, memetakan dan menghapus nilai nil adalah mengurangi. Dalam contoh mereka, OP memetakan dan kemudian memilih nil. Memanggil peta dan kemudian memadatkan, atau memilih dan kemudian memetakan, sama dengan membuat kesalahan yang sama: seperti yang Anda tunjukkan dalam jawaban Anda, itu adalah bau kode.
Ziggy
96

Coba gunakan reduceatau inject.

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

  memo
}

Saya setuju dengan jawaban yang diterima bahwa kita tidak boleh mapdan compact, tetapi tidak untuk alasan yang sama.

Saya merasa jauh di lubuk hati yang mapkemudian compactsetara dengan selectitu map. Pertimbangkan: mapadalah fungsi satu-ke-satu. Jika Anda memetakan dari beberapa set nilai, dan Anda map, maka Anda ingin satu nilai dalam set output untuk setiap nilai dalam set input. Jika Anda harus selectsebelumnya, maka Anda mungkin tidak ingin mapdi set. Jika Anda harus selectsetelah itu (atau compact) maka Anda mungkin tidak ingin mapdi set. Dalam kedua kasus Anda mengulangi dua kali seluruh set, ketika reducehanya perlu pergi sekali.

Juga, dalam bahasa Inggris, Anda mencoba "mengurangi satu set bilangan bulat menjadi satu set bilangan bulat genap".

Ziggy
sumber
4
Kasihan Ziggy, tidak ada cinta untuk saran Anda. lol. ditambah satu, orang lain memiliki ratusan upvotes!
DDDD
2
Saya percaya bahwa suatu hari, dengan bantuan Anda, jawaban ini akan melampaui yang diterima pada. ^ o ^ //
Ziggy
2
Memberi +1 jawaban yang saat ini diterima tidak memungkinkan Anda untuk menggunakan hasil operasi yang Anda lakukan selama fase pilih
chees
1
iterating lebih dari enumerable struktur data dua kali jika hanya diperlukan diperlukan seperti dalam jawaban yang diterima tampaknya sia-sia. Jadi, kurangi jumlah lintasan dengan menggunakan kurangi! Terima kasih @ Ziggy
sebisnow
Itu benar! Tetapi melakukan dua melewati koleksi n elemen masih O (n). Kecuali jika koleksi Anda sangat besar sehingga tidak muat di cache Anda, melakukan dua lintasan mungkin baik-baik saja (saya hanya berpikir ini lebih elegan, ekspresif, dan lebih kecil kemungkinannya mengarah ke bug di masa depan ketika, katakanlah, loop jatuh tidak sinkron). Jika Anda suka melakukan hal-hal sekaligus, Anda mungkin tertarik mempelajari transduser! github.com/cognitect-labs/transducers-ruby
Ziggy
33

Dalam contoh Anda:

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

itu tidak terlihat seperti nilai telah berubah selain diganti dengan nil. Jika itu masalahnya, maka:

items.select{|x| process_x url}

akan cukup.

sawa
sumber
27

Jika Anda ingin kriteria yang lebih longgar untuk penolakan, misalnya, untuk menolak string kosong dan nol, Anda dapat menggunakan:

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

Jika Anda ingin melangkah lebih jauh dan menolak nilai nol (atau menerapkan logika yang lebih kompleks untuk proses tersebut), Anda bisa meneruskan blok untuk menolak:

[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
sumber
5
.kosong? hanya tersedia di rel.
ewalk
Untuk referensi di masa mendatang, karena blank?hanya tersedia di rel, kita bisa menggunakan items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]yang tidak digabungkan ke rel. (tidak akan mengecualikan string atau 0s kosong)
Fotis
27

Jelas compactmerupakan pendekatan terbaik untuk menyelesaikan tugas ini. Namun, kami dapat mencapai hasil yang sama hanya dengan pengurangan sederhana:

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]
Evgenia Manolova
sumber
4
Ya, atur pengurangan akan berhasil, tetapi sekitar setengahnya lebih cepat karena overhead-nya.
the Tin Man
4

each_with_object mungkin cara paling bersih untuk pergi ke sini:

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

Menurut pendapat saya, each_with_objectlebih baik daripada inject/ reducedalam kasus bersyarat karena Anda tidak perlu khawatir tentang nilai pengembalian blok.

pnomolos
sumber
0

Satu cara lagi untuk mencapainya adalah seperti yang ditunjukkan di bawah ini. Di sini, kami menggunakan Enumerable#each_with_objectuntuk mengumpulkan nilai-nilai, dan memanfaatkan Object#tapuntuk menyingkirkan variabel sementara yang jika tidak diperlukan untuk nilmemeriksa hasil process_xmetode.

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

Contoh lengkap untuk ilustrasi:

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

Pendekatan alternatif:

Dengan melihat metode yang Anda panggil process_x url, tidak jelas apa tujuan input xdalam metode itu. Jika saya berasumsi bahwa Anda akan memproses nilai xdengan melewatkannya urldan menentukan mana dari xs yang benar-benar diproses menjadi hasil non-nil yang valid - maka, mungkin Enumerabble.group_bymerupakan opsi yang lebih baik daripada Enumerable#map.

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

h["Good"]
#=> [3,4,5]
Pembuat Tongkat
sumber