Ruby enumerable - find up to n occurrences of a matching element

I have the following array:

arr = [1, 3, 2, 5, 2, 4, 2, 2, 4, 4, 2, 2, 4, 2, 1, 5] 

I need an array containing the first three odd elements.

I know I can do this:

 arr.select(&:odd?).take(3) 

but I want to avoid iterating over the entire array, and will come back instead as soon as I find the third match.

I came up with the following solution, which I believe does what I want:

 my_arr.each_with_object([]) do |el, memo| memo << el if el.odd?; break memo if memo.size == 3 end 

But is there an easier / idiomatic way to do this?

+5
source share
2 answers

Use a lazy enumerator with Enumerated # lazy :

 arr.lazy.select(&:odd?).take(3).force # => [1, 3, 5] 

force used to make a lazy counter evaluate. Or, you can use first as he is impatient:

 arr.lazy.select(&:odd?).first(3) # => [1, 3, 5] 
+9
source

Code and example

 arr.take_while.with_object([]) do |e,a| a << e if e.odd? a.size < 3 end #=> [1, 3, 5] 

Benchmark

 require 'fruity' def compare_em(arr, m) compare( lazy: -> { arr.lazy.select(&:odd?).take(m).force }, take_while: -> { arr.take_while.with_object([]) { |e,a| a << e if e.odd?; a.size < m } } ) end n = 1e6 arr = (1..n).to_a.shuffle 

Get the first 1000 odd items:

 compare_em(arr, 1e3) # Running each test 8 times. Test will take about 1 second. # take_while is faster than lazy by 2x ± 1.0 

Get the first 10,000 odd items:

 compare_em(arr, 1e4) # Running each test once. Test will take about 1 second. # take_while is faster than lazy by 2x ± 1.0 

Get the first 100,000 odd items:

 compare_em(arr, 1e5) # Running each test once. Test will take about 3 seconds. # take_while is faster than lazy by 2x ± 0.1 

I am surprised that lazy did a good job, as it is often much slower, relatively, in tests.

+5
source

Source: https://habr.com/ru/post/1232586/