Ruby – 优雅地比较两个枚举器
我在Ruby(1.9.2)中有两个来自两个不同来源(二进制数据)的长数据流。
这两个源以两个枚举器的forms封装。
我想检查两个流是否完全相同。
我有几个解决方案,但两者看起来都很不优雅。
第一个简单地将两者转换为数组:
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
那么,有更简单,更优雅的方式吗?
假设您的流不包含元素: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) begin h[k] = s.next rescue StopIteration end 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
exception推入单独的函数并使用哈希中缺少键是一种相当方便的方法。 如果您的流包含false
或nil
只检查vals[:s1]
将导致问题,但检查是否存在密钥可以解决该问题。
这是通过为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
在评论中讨论之后,这里是基于zip的解决方案,首先在Enumerator
包装zip
版块,然后使用它来比较相应的元素。
它有效,但是已经提到了边缘情况:如果第一个流比另一个流短,则另一个流中的剩余元素将被丢弃(参见下面的示例)。
我已将此答案标记为社区维基,因为其他成员可以改进它。
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
这是使用光纤/协同例程的双源示例。 这有点啰嗦,但它的行为非常明确,这很好。
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