われプログラミングする、ゆえにバグあり

私だって価値を創造してみたいのです

要素が足りない場合に効率的に追加するEnumerable

Ruby on Railsでコード書いてて、以下の様なケースがありました。
 
Hogeからnameを取得し、limit件数に満たなければ、さらにFooからtitleを取得して追加する。

limit = 10
elems = Hoge.where(:status => 1)
                .first(limit)
                .pluck(:name)
if elems.count < limit
  elems += Foo.where(:flag => true)
                    .first(limit - elems.count)
                    .pluck(:title)
end

Hoge と Foo は ActiveRecord::Base を継承しています。
場合によってはSQLのUnionを使うようにして解決できますが、わりと面倒(ソート順のカラムを合わせたりなどなど)だったり。

 
いろいろとコードがめんどくさい感じになっていて不便なので、クラスを作りました。

作ったクラス
class MultiEnumerable
  include Enumerable

  def initialize(*args)
    if args.last.is_a? Hash
      @collections = args[0...-1]
      @options = args.last
    else
      @collections = args
      @options = {}
    end
  end

  def each &block
    cnt = 0
    fst = @options[:first]
    @collections.each do |c|
      f = fst ? fst - cnt : nil
      break if not f.nil? and f <= 0
      if c.is_a? Proc
        if c.arity == 0
          c = c.call
        else
          c = c.call(f)
        end
      end
      c = c.first(f) if f
      c.each do |i|
        block.call(i)
      end
      cnt += c.count
    end
  end

end


使い方はこんな感じです。

mul = MultiEnumerable.new(
  -> { [1,2,3] },
  -> (c) { c ? [4,5,6][0...c] : [1,2,3] }, # この場合、ここは実行されない
  {:first => 2}
)
mul.each do |item|↲
  # item => 1, 2 の2回、ここが実行されます
end

mul = MultiEnumerable.new(
  -> { [1,2,3] },
  -> (c) { c ? [4,5,6][0...c] : [1,2,3] }, # この場合、ここは c  => 1 で実行される
  {:first => 4}
)
mul.each do |item|↲
  # item => 1, 2, 3, 4 の4回、ここが実行されます
end

 

最初のコードを置き換えるとこうなります
MultiEnumerable.new(
  -> (c) { Hoge.where(:status => 1).first(c).pluck(:name) },
  -> (c) { Foo.where(:flag => true).first(c).pluck(:title) },
  {:first => 10}
)

簡潔になりましたね。

 

まとめ

車輪の再発明だと悲しい。