Cách ánh xạ và xóa các giá trị không trong Ruby


361

tôi có một map trong đó thay đổi một giá trị hoặc đặt nó thành không. Sau đó tôi muốn xóa các mục không trong danh sách. Danh sách này không cần phải lưu giữ.

Đây là những gì tôi hiện có:

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

Tôi biết tôi chỉ có thể thực hiện một vòng lặp và thu thập có điều kiện trong một mảng khác như thế này:

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

Nhưng nó không có vẻ thành ngữ. Có một cách hay để ánh xạ một chức năng qua một danh sách, loại bỏ / loại trừ các nils khi bạn đi không?


3
Ruby 2.7 giới thiệu filter_map, dường như là hoàn hảo cho việc này. Tiết kiệm nhu cầu xử lý lại mảng, thay vào đó là đạt được như mong muốn lần đầu tiên. Thêm thông tin ở đây.
SRack

Câu trả lời:


21

Ruby 2.7 trở lên

Có ngay bây giờ!

Ruby 2.7 đang giới thiệu filter_map cho mục đích chính xác này. Đó là thành ngữ và biểu diễn, và tôi hy vọng nó sẽ sớm trở thành chuẩn mực.

Ví dụ:

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

Trong trường hợp của bạn, khi khối đánh giá falsey, chỉ cần:

items.filter_map { |x| process_x url }

" Ruby 2.7 bổ sung Số lượng # filter_map " là một bài đọc tốt về chủ đề này, với một số điểm chuẩn hiệu suất chống lại một số cách tiếp cận trước đây cho vấn đề này:

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
Đẹp! Cảm ơn đã cập nhật :) Một khi Ruby 2.7.0 được phát hành, tôi nghĩ có lẽ nên chuyển câu trả lời được chấp nhận sang câu trả lời này. Tôi không chắc chắn những gì nghi thức ở đây mặc dù, nói chung bạn có cho phép phản hồi được chấp nhận hiện tại một cơ hội để cập nhật không? Tôi cho rằng đây là câu trả lời đầu tiên đề cập đến cách tiếp cận mới trong 2.7, vì vậy nên trở thành câu trả lời được chấp nhận. @ the-tin-man bạn có đồng ý với việc này không?
Pete Hamilton

Cảm ơn @Peter Hamilton - đánh giá cao phản hồi và hy vọng nó sẽ hữu ích với nhiều người. Tôi rất vui khi đi đến quyết định của bạn, mặc dù rõ ràng tôi thích lập luận bạn đã đưa ra :)
SRack

Vâng, đó là điều tốt đẹp về các ngôn ngữ có các nhóm cốt lõi lắng nghe.
Tin Man

Đó là một cử chỉ tốt đẹp để đề nghị các câu trả lời được chọn được thay đổi, nhưng nó hiếm khi xảy ra. SO không cung cấp một công cụ kiểm tra để nhắc nhở mọi người và mọi người thường không xem lại những câu hỏi cũ mà họ đã hỏi trừ khi SO nói có hoạt động. Là một thanh bên, tôi khuyên bạn nên xem Fruity để biết điểm chuẩn vì nó ít khó khăn hơn và giúp thực hiện các bài kiểm tra hợp lý dễ dàng hơn.
Tin Man

930

Bạn có thể sử dụng compact:

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

Tôi muốn nhắc mọi người rằng nếu bạn nhận được một mảng chứa nils là đầu ra của một mapkhối và khối đó cố gắng trả về các giá trị một cách có điều kiện, thì bạn đã có mùi mã và cần suy nghĩ lại về logic của mình.

Ví dụ: nếu bạn đang làm một cái gì đó làm điều này:

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

Vậy thì đừng. Thay vào đó, trước đó map, rejectnhững thứ bạn không muốn hoặc selectnhững gì bạn muốn:

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

Tôi xem xét việc sử dụng compactđể dọn dẹp mớ hỗn độn như một nỗ lực cuối cùng để loại bỏ những thứ chúng ta không xử lý chính xác, thường là vì chúng ta không biết điều gì đang đến với mình. Chúng ta nên luôn luôn biết loại dữ liệu nào đang bị ném xung quanh trong chương trình của chúng ta; Dữ liệu bất ngờ / không xác định là xấu. Bất cứ khi nào tôi thấy nils trong một mảng tôi đang làm việc, tôi sẽ tìm hiểu lý do tại sao chúng tồn tại và xem liệu tôi có thể cải thiện mã tạo ra mảng hay không, thay vì cho phép Ruby lãng phí thời gian và bộ nhớ tạo ra các nils sau đó lọc qua mảng để loại bỏ chúng sau này

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

29
Bây giờ đó là ruby-esque!
Barshe Marois

4
Tại sao nên làm thế? OP cần loại bỏ nilcác mục, không phải chuỗi trống. BTW, nilkhông giống như một chuỗi rỗng.
Tin Man

9
Cả hai giải pháp lặp lại hai lần trong bộ sưu tập ... tại sao không sử dụng reducehoặc inject?
Ziggy

4
Nó không giống như bạn đọc câu hỏi OP hoặc câu trả lời. Câu hỏi là, làm thế nào để loại bỏ nils khỏi một mảng. compactlà nhanh nhất nhưng thực sự viết mã chính xác khi bắt đầu loại bỏ sự cần thiết phải xử lý hoàn toàn nils.
Tin Man

3
Tôi không đồng ý! Câu hỏi là "Bản đồ và loại bỏ các giá trị không". Vâng, để ánh xạ và loại bỏ các giá trị không là giảm. Trong ví dụ của họ, các bản đồ OP và sau đó chọn ra các nils. Gọi bản đồ và sau đó thu gọn, hoặc chọn và sau đó ánh xạ, sẽ gây ra lỗi tương tự: như bạn chỉ ra trong câu trả lời của mình, đó là mùi mã.
Ziggy

96

Hãy thử sử dụng reducehoặc inject.

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

  memo
}

Tôi đồng ý với câu trả lời được chấp nhận rằng chúng ta không nên mapcompact, nhưng không phải vì những lý do tương tự.

Tôi cảm thấy sâu bên trong mapđó compactlà tương đương với selectsau đó map. Xem xét: maplà một chức năng một-một. Nếu bạn đang ánh xạ từ một số bộ giá trị và bạn map, thì bạn muốn một giá trị trong bộ đầu ra cho mỗi giá trị trong bộ đầu vào. Nếu bạn phải xử lý selecttrước, thì có lẽ bạn không muốn có một bộ maptrên phim. Nếu bạn phải selectsau đó (hoặc compact) thì có lẽ bạn không muốn có maptrên phim trường. Trong cả hai trường hợp, bạn đang lặp lại hai lần trên toàn bộ tập hợp, khi reducechỉ cần đi một lần.

Ngoài ra, trong tiếng Anh, bạn đang cố gắng "giảm một bộ số nguyên thành một bộ số nguyên chẵn".


4
Ziggy đáng thương, không có tình yêu cho đề nghị của bạn. cười lớn. cộng với một, người khác có hàng trăm upvote!
DDDĐ

2
Tôi tin rằng một ngày nào đó, với sự giúp đỡ của bạn, câu trả lời này sẽ vượt qua được chấp nhận. ^ o ^ //
Ziggy

2
+1 câu trả lời hiện được chấp nhận không cho phép bạn sử dụng kết quả của các thao tác bạn đã thực hiện trong giai đoạn chọn
chees

1
lặp đi lặp lại qua vô số cơ sở dữ liệu hai lần nếu chỉ cần vượt qua như trong câu trả lời được chấp nhận có vẻ lãng phí. Do đó giảm số lượng chuyền bằng cách sử dụng giảm! Cảm ơn @Ziggy
sebisnow

Đúng! Nhưng thực hiện hai lần vượt qua một tập hợp n phần tử vẫn là O (n). Trừ khi bộ sưu tập của bạn quá lớn đến nỗi nó không vừa với bộ nhớ cache của bạn, thực hiện hai lần vượt qua có lẽ là tốt (tôi chỉ nghĩ rằng điều này thanh lịch hơn, biểu cảm hơn và ít có khả năng dẫn đến lỗi trong tương lai khi, nói, các vòng lặp rơi Không đồng bộ). Nếu bạn cũng thích làm mọi thứ trong một lần, bạn có thể thích tìm hiểu về đầu dò! github.com/cognitect-labs/transducers-ruby
Ziggy

33

Trong ví dụ của bạn:

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

không có vẻ như các giá trị đã thay đổi ngoài việc được thay thế bằng nil. Nếu đó là trường hợp, thì:

items.select{|x| process_x url}

sẽ đủ.


27

Nếu bạn muốn một tiêu chí lỏng lẻo hơn để từ chối, ví dụ, để từ chối các chuỗi trống cũng như không, bạn có thể sử dụng:

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

Nếu bạn muốn đi xa hơn và từ chối các giá trị 0 (hoặc áp dụng logic phức tạp hơn cho quy trình), bạn có thể vượt qua một khối để từ chối:

[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
.chỗ trống? chỉ có sẵn trong đường ray.
ewalk

Để tham khảo trong tương lai, vì blank?chỉ có sẵn trong đường ray, chúng tôi có thể sử dụng items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]không được ghép nối với đường ray. (sẽ không loại trừ các chuỗi trống hoặc 0 mặc dù)
Fotis

27

Chắc chắn compactlà cách tiếp cận tốt nhất để giải quyết nhiệm vụ này. Tuy nhiên, chúng ta có thể đạt được kết quả tương tự chỉ với một phép trừ đơn giản:

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

4
Có, thiết lập phép trừ sẽ hoạt động, nhưng nó nhanh bằng một nửa do chi phí hoạt động.
Tin Man

4

each_with_object có lẽ là cách sạch nhất để đi đến đây:

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

Theo tôi, each_with_objecttốt hơn inject/ reducetrong các trường hợp có điều kiện vì bạn không phải lo lắng về giá trị trả về của khối.


0

Một cách nữa để thực hiện nó sẽ được hiển thị dưới đây. Ở đây, chúng tôi sử dụng Enumerable#each_with_objectđể thu thập các giá trị và sử dụng Object#tapđể loại bỏ biến tạm thời cần thiết để nilkiểm tra kết quả của process_xphương thức.

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

Ví dụ hoàn chỉnh để minh họa:

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

Phương pháp thay thế:

Bằng cách nhìn vào phương thức bạn đang gọi process_x url, không rõ mục đích của đầu vào xtrong phương thức đó là gì. Nếu tôi giả định rằng bạn sẽ xử lý giá trị xbằng cách chuyển nó một số urlvà xác định xem cái nào xthực sự được xử lý thành kết quả không hợp lệ - thì, có thể Enumerabble.group_bylà một lựa chọn tốt hơn Enumerable#map.

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

h["Good"]
#=> [3,4,5]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.