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
Другие вопросы по тегам