Ruby - сравни элегантно два счетчика
У меня есть два длинных потока чисел из двух разных источников (двоичные данные) в Ruby (1.9.2).
Два источника инкапсулированы в виде двух перечислителей.
Я хочу проверить, что два потока точно равны.
Я пришел с парой решений, но оба кажутся совсем не элегантными.
Первый просто преобразует оба в массив:
def equal_streams?(s1, s2)
s1.to_a == s2.to_a
end
Это работает, но не очень производительно, с точки зрения памяти, особенно если потоки имеют много информации.
Другой вариант... тьфу.
def equal_streams?(s1, s2)
s1.each do |e1|
begin
e2 = s2.next
return false unless e1 == e2 # Different element found
rescue StopIteration
return false # s2 has run out of items before s1
end
end
begin
s2.next
rescue StopIteration
# s1 and s2 have run out of elements at the same time; they are equal
return true
end
return false
end
Итак, есть ли более простой и элегантный способ сделать это?
5 ответов
Просто небольшой рефакторинг вашего кода, предполагая, что ваши потоки не содержат элемент :eof
,
def equal_streams?(s1, s2)
loop do
e1 = s1.next rescue :eof
e2 = s2.next rescue :eof
return false unless e1 == e2
return true if e1 == :eof
end
end
Используя ключевое слово как loop
должно быть быстрее, чем при использовании такого метода, как each
,
Сравнение их по одному элементу за раз, вероятно, лучшее, что вы сможете сделать, но вы можете сделать это лучше, чем ваше "тьфу" решение:
def grab_next(h, k, s)
h[k] = s.next
rescue StopIteration
end
def equal_streams?(s1, s2)
loop do
vals = { }
grab_next(vals, :s1, s1)
grab_next(vals, :s2, s2)
return true if(vals.keys.length == 0) # Both of them ran out.
return false if(vals.keys.length == 1) # One of them ran out early.
return false if(vals[:s1] != vals[:s2]) # Found a mismatch.
end
end
Сложная часть состоит в том, чтобы различать только один поток, истекающий и оба заканчивающиеся. Толкая StopIteration
исключение в отдельную функцию и использование отсутствия ключа в хэше - довольно удобный способ сделать это. Просто проверяю vals[:s1]
вызовет проблемы, если ваш поток содержит false
или же nil
но проверка на наличие ключа решает эту проблему.
Вот пример того, как сделать это, создав альтернативу Enumerable#zip
, который работает лениво и не создает целый массив. Это объединяет мою реализацию Closure's interleave
и два других ответа здесь (используя значение часового, чтобы указать конец Enumerable
был достигнут - факт, вызывающий проблему в том, что next
перематывает Enumerable
как только дело дошло до конца).
Это решение поддерживает несколько параметров, поэтому вы можете сравнивать n структур одновременно.
module Enumerable
# this should be just a unique sentinel value (any ideas for more elegant solution?)
END_REACHED = Object.new
def lazy_zip *others
sources = ([self] + others).map(&:to_enum)
Enumerator.new do |yielder|
loop do
sources, values = sources.map{|s|
[s, s.next] rescue [nil, END_REACHED]
}.transpose
raise StopIteration if values.all?{|v| v == END_REACHED}
yielder.yield values.map{|v| v == END_REACHED ? nil : v}
end
end
end
end
Итак, когда у вас есть вариант zip
который работает лениво и не останавливает итерацию, когда первое перечисляемое достигает конца, вы можете использовать all?
или же any?
на самом деле проверить соответствующие элементы на равенство.
# zip would fail here, as it would return just [[1,1],[2,2],[3,3]]:
p [1,2,3].lazy_zip([1,2,3,4]).all?{|l,r| l == r}
#=> false
# this is ok
p [1,2,3,4].lazy_zip([1,2,3,4]).all?{|l,r| l == r}
#=> true
# comparing more than two input streams:
p [1,2,3,4].lazy_zip([1,2,3,4],[1,2,3]).all?{|vals|
# check for equality by checking length of the uniqued array
vals.uniq.length == 1
}
#=> false
После обсуждения в комментариях приведено решение на основе zip, версия первого блока упаковки zip
в пределах Enumerator
, а затем использовать его для сравнения соответствующих элементов.
Это работает, но уже упоминается крайний случай: если первый поток короче другого, остальные элементы другого будут отбрасываться (см. Пример ниже).
Я пометил этот ответ как вики сообщества, так как другие участники могли улучшить его.
def zip_lazy *enums
Enumerator.new do |yielder|
head, *tail = enums
head.zip(*tail) do |values|
yielder.yield values
end
end
end
p zip_lazy(1..3, 1..4).all?{|l,r| l == r}
#=> true
p zip_lazy(1..3, 1..3).all?{|l,r| l == r}
#=> true
p zip_lazy(1..4, 1..3).all?{|l,r| l == r}
#=> false
Вот пример с 2 источниками, использующий оптоволокно / сопрограмму. Это немного скучно, но очень четко описывает свое поведение, что приятно.
def zip_verbose(enum1, enum2)
e2_fiber = Fiber.new do
enum2.each{|e2| Fiber.yield true, e2 }
Fiber.yield false, nil
end
e2_has_value, e2_val = true, nil
enum1.each do |e1_val|
e2_has_value, e2_val = e2_fiber.resume if e2_has_value
yield [true, e1_val], [e2_has_value, e2_val]
end
return unless e2_has_value
loop do
e2_has_value, e2_val = e2_fiber.resume
break unless e2_has_value
yield [false, nil], [e2_has_value, e2_val]
end
end
def zip(enum1, enum2)
zip_verbose(enum1, enum2) {|e1, e2| yield e1[1], e2[1] }
end
def self.equal?(enum1, enum2)
zip_verbose(enum1, enum2) do |e1,e2|
return false unless e1 == e2
end
return true
end