Reputation: 2351
I'm looking for a more elegant way of blending together two SQL resultsets with a given ratio. Within each of them I want them to be worked through in the same order they come in, but I want to interleave the processing to achieve a desired blend.
I realised this can be made into a very generic method working with two enums and yielding items to process, so I've written this method which I'm simultaneously quite proud of (nice generic solution) and quite ashamed of.
def combine_enums_with_ratio(enum_a, enum_b, desired_ratio)
a_count = 1
b_count = 1
a_finished = false
b_finished = false
loop do
ratio_so_far = a_count / b_count.to_f
if !a_finished && (b_finished || ratio_so_far <= desired_ratio)
begin
yield enum_a.next
a_count += 1
rescue StopIteration
a_finished = true
end
end
if !b_finished && (a_finished || ratio_so_far > desired_ratio)
begin
yield enum_b.next
b_count += 1
rescue StopIteration
b_finished = true
end
end
break if a_finished && b_finished
end
end
Ashamed because it's clearly written in a very imperative style. Not looking very rubyish. Maybe there's a way of using one of ruby's nice declarative looping methods, except they don't seem to work holding open two enums like this. So then I believe I'm left having to rescue an exception as part of control flow like this, which feels very dirty. I'm missing java's hasNext()
method.
Is there a better way?
I did find a similar question about comparing enums: Ruby - Compare two Enumerators elegantly . Some compact answers, but not particularly solving it, and my problem involving unequal lengths and unequal yielding seems trickier.
Upvotes: 2
Views: 198
Reputation: 114228
Here's a shorter and more general approach:
def combine_enums_with_ratio(ratios)
return enum_for(__method__, ratios) unless block_given?
counts = ratios.transform_values { |value| Rational(1, value) }
until counts.empty?
begin
enum, _ = counts.min_by(&:last)
yield enum.next
counts[enum] += Rational(1, ratios[enum])
rescue StopIteration
counts.delete(enum)
end
end
end
Instead of two enums, it takes a hash of enum => ratio
pairs.
At first, it creates a counts
hash using the ratio's reciprocal, i.e. enum_a => 3, enum_b => 2
becomes:
counts = { enum_a => 1/3r, enum_b => 1/2r }
Then, within a loop, it fetches the hash's minimum value, which is enum_a
in the above example. It yields its next
value and increment its counts
ratio value:
counts[enum_a] += 1/3r
counts #=> {:enum_a=>(2/3), :enum_b=>(1/2)}
On the next iteration, enum_b
has the smallest value, so its next
value will be yielded and its ratio be incremented:
counts[enum_b] += 1/2r
counts #=> {:enum_a=>(2/3), :enum_b=>(1/1)}
If you keep incrementing enum_a
by (1/3)
and enum_b
by (1/2)
, the yield ratio of their elements will be 3:2.
Finally, the rescue
clause handles enums running out of elements. If this happens, that enum is removed from the counts
hash.
Once the counts
hash is empty, the loop stops.
Example usage with 3 enums:
enum_a = (1..10).each
enum_b = ('a'..'f').each
enum_c = %i[foo bar baz].each
combine_enums_with_ratio(enum_a => 3, enum_b => 2, enum_c => 1).to_a
#=> [1, "a", 2, 3, "b", :foo, 4, "c", 5, 6, "d", :bar, 7, "e", 8, 9, "f", :baz, 10]
# <---------------------> <---------------------> <--------------------->
# 3:2:1 3:2:1 3:2:1
Upvotes: 3