5

I want to iterate through an array, each element of which is an array of two integers (e.g. `[3,5]'); for each of these elements, I want to calculate the sum of the two integers, exiting the loop when any of these sums exceeds a certain arbitrary value. The source array is quite large, and I will likely find the desired value near the beginning, so looping through all of the unneeded elements is not a good option.

I have written three loops to do this, all of which produce the desired result. My question is: which is more idiomatic Ruby? Or--better yet--is there a better way? I try not to use non-local loop variables in, but break statements look kind of hackish to my (admittedly novice) eye.

# Loop A
pairs.each do |pair|
  pair_sum = pair.inject(:+) 
  arr1 << pair_sum
  break if pair_sum > arr2.max
end

#Loop B - (just A condensed)
pairs.each { |pair| arr1.last <= arr2.max ? arr1 << pair.inject(:+) : break }

#Loop C
i = 0
pair_sum = 0
begin
  pair_sum = pairs[i].inject(:+)
  arr1 << pair_sum
  i += 1
end until pair_sum > arr2.max

A similar question was asked at escaping the .each { } iteration early in Ruby, but the responses were essentially that, while using .each or .each_with_index and exiting with break when the target index was reached would work, .take(num_elements).each is more idiomatic. In my situation, however, I don't know in advance how many elements I'll have to iterate through, presenting me with what appears to be a boundary case.

This is from a project Euler-type problem I've already solved, btw. Just wondering about the community-preferred syntax. Thanks in advance for your valuable time.

Community
  • 1
  • 1
jdburns
  • 65
  • 1
  • 7

4 Answers4

8

take and drop have a variant take_while and drop_while where instead of providing a fixed number of elements you provide a block. Ruby will accumulate values from the receiver (in the case of take_while) as long as the block returns true. Your code could be rewritten as

array.take_while {|pair| pair.sum < foo}.map(&:sum)

This does mean that you calculate the sum of some of these pairs twice.

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • Chagrined, I now see that method glaring at me from the bottom of the Array page in the docs, where I'd somehow repeatedly overlooked it. Thanks--exactly the sort of method I assumed was out there somewhere. – jdburns Apr 30 '13 at 09:20
  • Forgive me if this is obvious, but: the .sum method you used in your code is just a placeholder for some method which produces a sum, and not in some popular module I'm unaware of, right? – jdburns Apr 30 '13 at 09:24
  • @jdburns: there are several libraries, which monkeypatch a `sum` method to `Enumerable`, e.g. `ActiveSupport`. – Jörg W Mittag Apr 30 '13 at 10:20
  • @JörgWMittag: I thought as much; I monkey-patched my own `product` method to override the built-in Array method in order to make my code more clear to those unfamiliar with Ruby and `.inject` (I guess I could've just made an alias, now that I think about it...). I know that's generally a bad idea, but it was just a toy script. – jdburns Apr 30 '13 at 10:44
4

In Ruby 2.0 there's Enumerable#lazy which returns a lazy enumerator:

sums = pairs.lazy.map { |a, b| a + b }.take_while { |pair_sum| pair_sum < some_max_value }.force

This avoids calculating the sums twice.

Stefan
  • 109,145
  • 14
  • 143
  • 218
  • Ah, so THAT'S how lazy enumeration works. I knew it was out there, but hadn't yet played with it and so didn't realize it would be relevant to my situation. Thanks for putting another tool in my belt. – jdburns Apr 30 '13 at 09:39
  • Although I vaguely recall seeing something saying that lazy enumeration was rather slow in 2.0 – Frederick Cheung Apr 30 '13 at 10:55
  • @FrederickCheung In this case, `lazy` is indeed slower than calculating the sum twice. It would be more interesting for expensive operations. – Stefan Apr 30 '13 at 14:53
1
[[1, 2], [3, 4], [5, 6]].find{|x, y| x + y > 6}
# => [3, 4]
sawa
  • 165,429
  • 45
  • 277
  • 381
  • Very nice. I wasn't familiar with the `|x,y|' syntax for passing arrays to blocks. That will come in quite handy. – jdburns Apr 30 '13 at 09:17
  • You can also use `find_all` to find *all* items matching the condition instead of just the first one. – Patrick Oscity Apr 30 '13 at 09:33
  • @padde "I want to calculate the sum of the two integers, exiting the loop when any of these sums exceeds a certain arbitrary value" – sawa Apr 30 '13 at 09:44
  • @sawa I wasn't explicit about it in my question (either the site's acting up or my rep is too low to allow me to edit my post), but I do want to push each sum to another array for future use, so find_all would indeed be the method of choice here, as per padde's comment. Thanks to both of you. – jdburns Apr 30 '13 at 10:09
1
[[1, 2], [3, 4], [5, 6]].find{|x, y| x + y > 6}.inject(:+)
#=> 7
Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317
  • Combining your input with that of sawa and padde, I come up with: – jdburns Apr 30 '13 at 10:12
  • Combining your input with that of sawa and padde, I come up with: `s =[[1, 2], [3, 4], [5, 6]].find_all {|x, y| x + y > 6}.each { |z| arr << z.inject(:+) }`, which provides the functionality I'm seeking. I'm sure there's a less verbose way of doing this... – jdburns Apr 30 '13 at 10:38
  • Haha - still learning the ropes. SO cut me off from editing my response in mid-edit. Reposted in full directly above this one. – jdburns Apr 30 '13 at 10:39
  • 1
    The find block will execute for the entire collection (and return all matching elements) so this isn't quite the same – Frederick Cheung Apr 30 '13 at 10:56
  • @FrederickCheung: Ah... excellent point. I'd lost sight of the fact that I was trying to avoid iterating through the entire (massive) array, only with more idiomatic code--which, I now realize, is why I didn't use `find` in the first place. – jdburns Apr 30 '13 at 11:04