Ruby - сравни элегантно два счетчика - PullRequest
9 голосов
/ 26 июня 2011

У меня есть два длинных потока чисел из двух разных источников (двоичные данные) в 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 ]

10 голосов
/ 27 июня 2011

Просто небольшой рефакторинг вашего кода, предполагая, что ваши потоки не содержат элемент :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.

.
7 голосов
/ 27 июня 2011

Сравнение их по одному элементу за раз, вероятно, лучшее, что вы сможете сделать, но вы можете сделать это лучше, чем ваше "тьфу" решение:

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, но проверка на наличие ключа решит эту проблему.

2 голосов
/ 27 июня 2011

Вот пример того, как создать альтернативу для Enumerable#zip, которая работает лениво и не создает целый массив.Он объединяет мою реализацию из Closure interleave и два других ответа здесь (с использованием значения sentinel для обозначения конца 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
1 голос
/ 27 июня 2011

После обсуждения в комментариях приведено решение на основе 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
0 голосов
/ 06 июля 2011

Вот пример с 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
...