Alex Rivera | Logout

Ruby - Compare two Enumerators elegantly

Asked 2011-06-26T19:10:56.917
9

I've got two long streams of numbers coming from two different sources (binary data) in Ruby (1.9.2).

The two sources are encapsulated in the form of two Enumerators.

I want to check that the two streams are exactly equal.

I've come with a couple solutions, but both seem quite inelegant.

The first one simply transforms both into an array:

def equal_streams?(s1, s2)
  s1.to_a == s2.to_a
end

This works, but it is not very performant, memory-wise, specially if the streams have lots of information.

The other option is... ugh.

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

So, is there a simpler, more elegant way of doing this?

Edit
Report

1 Answer

2

Here's a shot of doing it by creating an alternative for Enumerable#zip, which works lazily and doesn't create an entire array. It's combining my implementation of Closure's interleave and other two answers here (using sentinel value to indicate end of the Enumerable has been reached - the fact causing the problem is that next rewinds the Enumerable once it reached the end).

This solution supports multiple parameters, so you can compare n structures at once.

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

So, when you have variant of zip which works lazily and doesn't stop iteration when the first enumerable reaches the end, you can use all? or any? to actually check corresponding elements for equality.

# 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
answered 2011-06-27T07:27:00.793

Your Answer