วิธีแมปและลบค่าศูนย์ใน Ruby


361

ฉันมีสิ่งmapใดที่เปลี่ยนค่าหรือตั้งค่าเป็นศูนย์ ฉันต้องการลบรายการที่ไม่มีจากรายการ รายการไม่จำเป็นต้องถูกเก็บไว้

นี่คือสิ่งที่ฉันมีในปัจจุบัน:

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

ฉันรู้ว่าฉันสามารถวนรอบและรวบรวมตามเงื่อนไขในอาร์เรย์อื่นเช่นนี้:

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

แต่ดูเหมือนว่ามันจะไม่สำนวน มีวิธีที่ดีในการแมปฟังก์ชั่นเหนือรายการโดยเอาออก / ไม่รวมนิลส์หรือเปล่า?


3
Ruby 2.7 แนะนำfilter_mapซึ่งดูเหมือนว่าจะเหมาะสำหรับการนี้ บันทึกความต้องการในการประมวลผลอาร์เรย์อีกครั้งแทนที่จะทำให้ได้รับตามต้องการในครั้งแรก ข้อมูลเพิ่มเติมที่นี่
SRack

คำตอบ:


21

Ruby 2.7+

ตอนนี้!

Ruby 2.7 กำลังแนะนำfilter_mapเพื่อจุดประสงค์ที่แน่นอนนี้ มันเป็นสำนวนและนักแสดงและฉันคาดหวังว่ามันจะเป็นบรรทัดฐานในไม่ช้า

ตัวอย่างเช่น:

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

ในกรณีของคุณเมื่อบล็อกประเมินว่าเป็นเท็จเพียง:

items.filter_map { |x| process_x url }

" Ruby 2.7 เพิ่ม Enumerable # filter_map " เป็นการอ่านที่ดีเกี่ยวกับหัวเรื่องพร้อมกับการวัดประสิทธิภาพเทียบกับวิธีการก่อนหน้านี้บางส่วนของปัญหานี้:

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
ดี! ขอบคุณสำหรับการอัปเดต :) เมื่อ Ruby 2.7.0 ออกมาฉันคิดว่ามันคงสมเหตุสมผลที่จะเปลี่ยนคำตอบที่ยอมรับให้เป็นอย่างนี้ ฉันไม่แน่ใจว่ามารยาทที่นี่คืออะไรไม่ว่าคุณจะให้โอกาสในการตอบรับการยอมรับที่มีอยู่โดยทั่วไปหรือไม่? ฉันเถียงว่านี่เป็นคำตอบแรกที่อ้างอิงถึงวิธีการใหม่ใน 2.7 ดังนั้นควรเป็นที่ยอมรับ @ คุณเห็นด้วยกับสิ่งนี้ไหม?
Pete Hamilton

ขอบคุณ @PeterHamilton - ขอขอบคุณข้อเสนอแนะและหวังว่ามันจะเป็นประโยชน์ต่อผู้คนมากมาย ฉันยินดีที่จะไปกับการตัดสินใจของคุณ แต่เห็นได้ชัดว่าฉันชอบการโต้แย้งที่คุณได้ทำ :)
464

ใช่นั่นเป็นสิ่งที่ดีเกี่ยวกับภาษาที่มีทีมหลักที่รับฟัง
มนุษย์ดีบุก

มันเป็นท่าทางที่ดีที่จะแนะนำคำตอบที่เลือกไว้ซึ่งจะเปลี่ยน แต่มันไม่ค่อยเกิดขึ้น ดังนั้นผู้ให้บริการจึงไม่ได้เป็นเครื่องมือเตือนความจำให้คนทั่วไปมักจะไม่ตอบคำถามเก่า ๆ ที่พวกเขาถามเว้นแต่ว่ามีการทำกิจกรรม ในฐานะที่เป็นแถบด้านข้างฉันขอแนะนำให้ดูฟรุ๊ตตี้สำหรับการวัดประสิทธิภาพเพราะมันมีปัญหาน้อยลงและทำให้การทดสอบที่สมเหตุสมผลง่ายขึ้น
คนดีบุก

930

คุณสามารถใช้compact:

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

ฉันต้องการเตือนผู้คนว่าถ้าคุณได้รับอาร์เรย์ที่มีนิลเป็นผลลัพธ์ของmapบล็อกและบล็อกนั้นพยายามคืนค่าตามเงื่อนไขคุณก็จะได้กลิ่นรหัสและต้องคิดตรรกะของคุณใหม่

ตัวอย่างเช่นหากคุณกำลังทำสิ่งที่ทำสิ่งนี้:

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

จากนั้นทำไม่ได้ แต่ก่อนที่จะมีmap, rejectสิ่งที่คุณไม่ต้องการหรือselectสิ่งที่คุณต้องการ:

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

ฉันคิดว่าใช้compactเพื่อทำความสะอาดเป็นระเบียบเป็นความพยายามครั้งสุดท้ายเพื่อกำจัดสิ่งที่เราไม่ได้จัดการอย่างถูกต้องโดยปกติแล้วเพราะเราไม่รู้ว่ากำลังเกิดอะไรขึ้นกับเรา เราควรรู้อยู่เสมอว่าข้อมูลประเภทใดที่ถูกส่งออกไปในโปรแกรมของเรา ข้อมูลที่ไม่คาดคิด / ไม่รู้จักไม่ดี เมื่อใดก็ตามที่ฉันเห็น nils ในอาร์เรย์ที่ฉันกำลังทำงานอยู่ฉันจะค้นหาสาเหตุที่มีอยู่และดูว่าฉันสามารถปรับปรุงโค้ดที่สร้างอาร์เรย์ได้หรือไม่แทนที่จะปล่อยให้ Ruby เสียเวลาและหน่วยความจำที่สร้าง nils ก็จะลอดผ่านอาร์เรย์ พวกเขาในภายหลัง

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

29
ตอนนี้นั่นคือทับทิม!
Christophe Marois

4
ทำไมจึงเป็นเช่นนั้น? OP ต้องการดึงnilรายการไม่ใช่สตริงว่าง BTW nilไม่เหมือนกับสตริงว่าง
ชายดีบุก

9
การแก้ปัญหาทั้งสองย้ำสองครั้งคอลเลกชัน ... ทำไมไม่ใช้reduceหรือinject?
Ziggy

4
ดูเหมือนคุณอ่านคำถาม OPs หรือคำตอบ คำถามคือวิธีการลบนิลจากอาร์เรย์ compactเร็วที่สุด แต่จริง ๆ แล้วการเขียนรหัสอย่างถูกต้องในการเริ่มต้นทำให้ไม่จำเป็นต้องจัดการกับนิลทั้งหมด
Tin Man

3
ฉันไม่เห็นด้วย! คำถามคือ "แผนที่และลบค่าศูนย์" การแมปและลบค่าศูนย์คือการลด ในตัวอย่างของพวกเขา OP maps และเลือก nils แผนที่การโทรแล้วกระชับหรือเลือกจากนั้นแผนที่จำนวนที่จะทำผิดพลาดเหมือนกัน: เมื่อคุณชี้ให้เห็นในคำตอบของคุณมันเป็นกลิ่นรหัส
Ziggy

96

ลองใช้หรือreduceinject

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

  memo
}

ฉันเห็นด้วยกับคำตอบที่ยอมรับว่าเราไม่ควรmapและcompactแต่ไม่ใช่ด้วยเหตุผลเดียวกัน

ฉันรู้สึกภายในลึกที่mapแล้วcompactเทียบเท่ากับแล้วselect mapพิจารณา: mapเป็นฟังก์ชั่นหนึ่งต่อหนึ่ง หากคุณกำลังทำการแมปจากชุดของค่าบางค่าและmapคุณต้องการค่าหนึ่งค่าในชุดผลลัพธ์สำหรับแต่ละค่าในชุดอินพุต หากคุณต้องมาselectก่อนคุณอาจไม่ต้องการmapชุด หากคุณต้องselectหลังจากนั้น (หรือcompact) จากนั้นคุณอาจไม่ต้องการmapในชุด ไม่ว่าในกรณีใดคุณจะวนซ้ำทั้งชุดเมื่อreduceต้องการเพียงครั้งเดียว

นอกจากนี้ในภาษาอังกฤษคุณกำลังพยายามที่จะ "ลดชุดจำนวนเต็มเป็นชุดจำนวนเต็มคู่"


4
แย่ Ziggy ไม่ชอบคำแนะนำของคุณ ฮ่า ๆ. บวกหนึ่งคนอื่นมี upvotes หลายร้อย!
DDDD

2
ฉันเชื่อว่าวันหนึ่งด้วยความช่วยเหลือของคุณคำตอบนี้จะเกินกว่าที่ยอมรับได้ ^ o ^ //
Ziggy

2
+1 คำตอบที่ได้รับการยอมรับในปัจจุบันไม่อนุญาตให้คุณใช้ผลลัพธ์ของการดำเนินการที่คุณดำเนินการในช่วงที่เลือก
chees

1
วนซ้ำมากกว่า datastructures สองเท่าหากจำเป็นต้องผ่านเหมือนในคำตอบที่ยอมรับดูเหมือนว่าสิ้นเปลือง ดังนั้นลดจำนวนการผ่านโดยใช้การลด! ขอบคุณ @Ziggy
sebisnow

นั่นเป็นความจริง! แต่การทำสองครั้งผ่านการรวมองค์ประกอบ n ยังคงเป็น O (n) ถ้าคอลเลกชันของคุณใหญ่มากจนไม่เหมาะกับแคชของคุณการทำสองรอบนั้นอาจจะไม่เป็นไร (ฉันแค่คิดว่านี่เป็นสิ่งที่สง่างามแสดงออกและมีโอกาสน้อยกว่าที่จะนำไปสู่ข้อบกพร่องในอนาคตเมื่อพูดว่า ไม่ซิงค์) ถ้าคุณชอบทำสิ่งต่าง ๆ ในครั้งเดียวคุณอาจสนใจเรียนรู้เรื่องตัวแปลงสัญญาณ! github.com/cognitect-labs/transducers-ruby
Ziggy

33

ในตัวอย่างของคุณ:

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

มันไม่ได้มีลักษณะเหมือนค่าที่มีการเปลี่ยนแปลงอื่น ๆ nilกว่าถูกแทนที่ด้วย หากเป็นเช่นนั้นให้ทำดังนี้

items.select{|x| process_x url}

จะพอเพียง


27

หากคุณต้องการเกณฑ์ที่หลวมสำหรับการปฏิเสธตัวอย่างเช่นการปฏิเสธสตริงที่ว่างเปล่าและไม่มีคุณสามารถใช้:

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

หากคุณต้องการดำเนินการต่อไปและปฏิเสธค่าศูนย์ (หรือใช้ตรรกะที่ซับซ้อนกว่านี้กับกระบวนการ) คุณสามารถส่งบล็อกเพื่อปฏิเสธ:

[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? มีเฉพาะในรางเท่านั้น
ewalk

สำหรับการอ้างอิงในอนาคตเนื่องจากblank?มีเฉพาะในรางเท่านั้นเราสามารถใช้items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]ซึ่งไม่ได้เชื่อมต่อกับราง (จะไม่ยกเว้นสตริงว่างหรือ 0s)
Fotis

27

แน่นอนcompactเป็นวิธีที่ดีที่สุดในการแก้ปัญหางานนี้ อย่างไรก็ตามเราสามารถได้ผลลัพธ์เดียวกันโดยการลบอย่างง่าย:

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

4
ใช่การลบชุดจะใช้งานได้ แต่เร็วกว่าครึ่งเนื่องจากค่าใช้จ่าย
ชายดีบุก

4

each_with_object น่าจะเป็นวิธีที่สะอาดที่สุดที่จะไปที่นี่:

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

ในความคิดของฉันeach_with_objectดีกว่าinject/ reduceในกรณีที่มีเงื่อนไขเพราะคุณไม่ต้องกังวลเกี่ยวกับค่าตอบแทนของบล็อก


0

อีกวิธีหนึ่งในการทำให้สำเร็จจะเป็นดังที่แสดงด้านล่าง ที่นี่เราใช้Enumerable#each_with_objectเพื่อรวบรวมค่าและใช้ประโยชน์จากObject#tapการกำจัดตัวแปรชั่วคราวที่จำเป็นสำหรับการnilตรวจสอบผลลัพธ์ของprocess_xวิธีการ

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

ตัวอย่างที่สมบูรณ์สำหรับภาพประกอบ:

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

วิธีอื่น:

โดยดูที่วิธีการที่คุณโทรprocess_x urlมันไม่ชัดเจนสิ่งที่เป็นวัตถุประสงค์ของการป้อนข้อมูลxในวิธีการที่ ถ้าผมคิดว่าคุณจะไปในการประมวลผลค่าของxโดยผ่านมันบางurlและตรวจสอบว่าของxจริงๆได้รับการแปรรูปเป็นผลไม่ใช่ศูนย์ที่ถูกต้อง - แล้วอาจจะเป็นตัวเลือกที่ดีกว่าEnumerabble.group_byEnumerable#map

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

h["Good"]
#=> [3,4,5]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.