Я ищу более элегантный способ смешивания двух наборов результатов SQL с заданным соотношением. Внутри каждого из них я хочу, чтобы они обрабатывались в том же порядке, в котором они появляются, но я хочу чередовать обработку для достижения желаемого сочетания.
Я понял, что это можно превратить в очень общий метод, работающий с двумя перечислениями и выдающий элементы для обработки, поэтому я написал этот метод, которым я одновременно очень горжусь (хорошее универсальное решение) и очень стыжусь.
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
Стыдно, потому что это явно написано в очень императивном стиле. Выглядит не очень рубиново. Может быть, есть способ использовать один из хороших методов декларативного цикла ruby, за исключением того, что они, похоже, не работают, удерживая открытыми два перечисления, подобные этому. Итак, я считаю, что мне осталось спасать исключение как часть потока управления, подобного этому, который кажется очень грязным. Мне не хватает метода Java hasNext()
.
Есть ли способ лучше?
Я нашел похожий вопрос о сравнении перечислений: Ruby — элегантное сравнение двух перечислителей. Некоторые компактные ответы, но не особенно решающие, и моя проблема, связанная с неравными длинами и неравным доходом, кажется более сложной.
Вот более короткий и общий подход:
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
Вместо двух перечислений требуется хэш из enum => ratio
пар.
Сначала он создает хэш counts
, используя обратное отношение, то есть enum_a => 3, enum_b => 2
становится:
counts = { enum_a => 1/3r, enum_b => 1/2r }
Затем в цикле он извлекает минимальное значение хеша, которое в приведенном выше примере равно enum_a
. Он дает свое значение next
и увеличивает значение отношения counts
:
counts[enum_a] += 1/3r
counts #=> {:enum_a=>(2/3), :enum_b=>(1/2)}
На следующей итерации enum_b
имеет наименьшее значение, поэтому его значение next
будет получено, а его отношение будет увеличено:
counts[enum_b] += 1/2r
counts #=> {:enum_a=>(2/3), :enum_b=>(1/1)}
Если вы продолжите увеличивать enum_a
на (1/3)
и enum_b
на (1/2)
, коэффициент доходности их элементов будет 3:2.
Наконец, предложение rescue
обрабатывает перечисления, в которых заканчиваются элементы. Если это произойдет, это перечисление будет удалено из хэша counts
.
Как только хэш counts
станет пустым, цикл остановится.
Пример использования с 3 перечислениями:
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
Хороший! Я полагаю, мне должно было прийти в голову, что цикл по набору входных перечислений может оказаться короче, чем весь мой код «a» и «b». Это довольно аккуратно!