Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to lazy concat Ruby ranges?

I have a very large range to iterate through and find the first element that satisfies specific constraints. That can be done efficiently in Ruby already.

# Runs until memory is exhausted _without_ lazy!
(1..).lazy.select { |i| i > 5 }.first
# => 6

In my use-case however, I want to begin iteration at a random interval of the range and, if no element passes the check when reaching the end of the range, continue from the start of the range (up until the random interval is reached again, if need be). With Combining two different 'ranges' to one in ruby as reference I came to...

letter = ('b'..'y').to_a.sample
[*letter..'z', *'a'...letter].map { |c| c.capitalize }.join
# => "FGHIJKLMNOPQRSTUVWXYZABCDE"

Of course, I don't have the alphabet as range to iterate through, this is just the small-scale example, which fails for my use-case.

  • the * (splat) operator is not lazy
  • map is not lazy

With some more googling and experimentation, I came to the following constructs:

# lazy version of previous alphabet example
[(letter..'z'), ('a'...letter)].lazy.flat_map { |r| r.each.lazy }.map { |c| c.capitalize }.force.join
=> "FGHIJKLMNOPQRSTUVWXYZABCDE"

# Comparable to what I want
start = rand(2**64)
# => 15282219649142738977
[(start..2**64), (0...start)].lazy.flat_map { |r| r.each.lazy }.select { |i| i % 7 == 0 }.first(5)
# => [15282219649142738978, 15282219649142738985, 15282219649142738992, 15282219649142738999, 15282219649142739006]
iter = [(start..2**64), (0...start)].lazy.flat_map { |r| r.each.lazy }.select { |i| i % 7 == 0 }
# => #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: [15282219649142738977..18446744073709551616, 0...15282219649142738977]>:flat_map>:select>
iter.next
# => 15282219649142738978
iter.next
# => 15282219649142738985

That does look overly complicated to me and maybe someone has a better idea?

Thank you for your time,
Xavier.

like image 236
Xavier Mol Avatar asked Nov 01 '25 14:11

Xavier Mol


1 Answers

How to lazy concat Ruby ranges?

You can concatenate enumerators via +. A range is not an enumerator, but you can retrieve one via Range#each, for example:

enum = (-3..0).each + (1..)

The combined enumerator will iterate each of the concatenated enumerators:

enum.take(10)
#=> [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6]

I have a very large range to iterate through and find the first element that satisfies specific constraints

Ruby has a dedicated method Enumerable#find which does exactly this. It iterates the collection and returns the first element for which the block returns a truthy result (without iterating any further), e.g.

enum.find { |i| i > 5 }
#=> 6
like image 106
Stefan Avatar answered Nov 03 '25 17:11

Stefan