Ruby で ActiveSupport の in_groups_of を実装する

in_groups_of っていうのは ActiveSupport にあるメソッドで each_slice と同じように任意の要素数で配列を分割するんですが、足りない分を nil で埋めるという特性があります。

[1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]]

試しに自前で書いてみた。

using Module.new {
  refine Array do
    def in_groups_of(num, fill_with = nil, &block)
      # fill で足りない分を埋めてから each_slice する
      fill(fill_with, size, ((size + size % num) % num)).each_slice(num, &block).to_a
    end
  end
}

p (1..7).to_a.fill(nil, 7, (7 + 7 % 3) % 3).each_slice(3).to_a
# => [[1, 2, 3], [4, 5, 6], [7, nil, nil]]

p (1..6).to_a.in_groups_of(3)
# => [[1, 2, 3], [4, 5, 6]]

p (1..7).to_a.in_groups_of(3)
# => [[1, 2, 3], [4, 5, 6], [7, nil, nil]]

p (1..8).to_a.in_groups_of(3)
# => [[1, 2, 3], [4, 5, 6], [7, 8, nil]]

p (1..9).to_a.in_groups_of(3)
# => [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

上のコードだと先に fill で足りない分を埋めてから each_slice で分割しています。

ちなみに ActiveSupport の実装は以下のような感じでした。

# https://github.com/rails/rails/blob/fbe2433be6e052a1acac63c7faf287c52ed3c5ba/activesupport/lib/active_support/core_ext/array/grouping.rb#L22-L43
def in_groups_of(number, fill_with = nil)
  if number.to_i <= 0
    raise ArgumentError,
      "Group size must be a positive integer, was #{number.inspect}"
  end

  if fill_with == false
    collection = self
  else
    # size % number gives how many extra we have;
    # subtracting from number gives how many to add;
    # modulo number ensures we don't add group of just fill.
    padding = (number - size % number) % number
    collection = dup.concat(Array.new(padding, fill_with))
  end

  if block_given?
    collection.each_slice(number) { |slice| yield(slice) }
  else
    collection.each_slice(number).to_a
  end
end

ちゃんと丁寧に書いてある。
ユースケースがあれば標準にもいれれそうな気がするけどどうだろうか。