ようへいの日々精進XP

よかろうもん

Ruby の Enumerable#chunk メソッドを使ってみた

この記事は

YAMAP エンジニア Advent Calendar 2022 の第三日目の記事です。

qiita.com

経緯

チームメンバーに以下のような質問をもらって即答出来ず、夜な夜な試行錯誤していてたら、RubyEnumerable#chunk メソッドに出会った話です。

(質問)

以下のような空行で区切られた数字の羅列があって、区切られた数字毎に合計を計算して、合計の最も大きい数値を表示したいのですが、どのように実装すればエレガントでしょうか?

(数字の羅列)
123
456

789

998
999

本記事で利用する環境は以下の通りです。

root@fbb0a6b38299:/# ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]

最初の試み

最初に、心のままに書いてみたコードは以下のようなコードでした。

numbers = <<'EOS'
123
456

789

998
999
EOS

datas = numbers.split("\n")

# 数値の合計を入れる配列
_result = []
# 数値を入れる配列
_array = []

datas.each do |d|
  # 配列の要素が empty であれば...
  if d.empty?
    # 数値が入っている配列内の数値を合計して、合計用の配列に突っ込む
    _result << _array.sum
    # 数値を入れる配列を初期化
    _array = []
  else
    # 数値を配列に突っ込む
    _array << d.to_i
  end
end

# 各合計が入っている配列をソートして最後の数値 (最も大きい値) をダンプ
p _result.max

実行してみると、以下のような結果となりました。

root@a68e3293a424:/work# ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
root@a68e3293a424:/work# ruby test.rb
789

あれ、なんかおかしい。。。998 + 999 の結果が最大値になりそうですが、789 が最大値として出力されたので意図しない結果です。

空白を区切りとして各要素を足していく戦法は悪く無さそうですが、変数 datas の中身を覗くと以下のような内容となっていて、最後の空白以降の 998 と 998 の足し算が行われていないことが解りました。

["123", "456", "", "789", "", "998", "999"]

次の試み

空白を区切りにするアプローチにこだわって改修してみると、以下のようなコードになりました。

numbers = <<'EOS'
123
456

789

998
999
EOS

datas = numbers.split("\n")

# 最後に空白を追加する
datas.push("")

# 数値の合計を入れる配列
_result = []
# 数値を入れる配列
_array = []

datas.each do |d|
  # 配列の要素が empty であれば...
  if d.empty?
    # 数値が入っている配列内の数値を合計して、合計用の配列に突っ込む
    _result << _array.sum
    # 数値を入れる配列を初期化
    _array = []
  else
    # 数値を配列に突っ込む
    _array << d.to_i
  end
end

# 各合計が入っている配列をソートして最後の数値 (最も大きい値) をダンプ
p _result.max

配列 datas の最後に空白を追加するので、必ず datas の中身は以下のような状態 (配列の最後の要素は "") となります。

["123", "456", "", "789", "", "998", "999", ""]

これで、期待した動作をしてくれそうです。

root@a68e3293a424:/work# ruby test.rb
1997

一旦、これで、チームメンバーに対する面目が立ちそうですが、なんかエレガントさに欠けますね... (汗

そこで Enumerable#chunk を使ってみる

Enumerable#chunk とは

ドキュメントを引用させて頂くと…

> 要素を前から順にブロックで評価し、その結果によって要素をチャンクに分けた(グループ化した)要素を持つ **[Enumerator](https://docs.ruby-lang.org/ja/latest/class/Enumerator.html)** を返します。

とあります。ドキュメントのサンプルコードを見るとなんとなく理解出来ました (なんとなくで恐縮です)

irb(main):001:1* [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].chunk {|n|
irb(main):002:1*   n.even?
irb(main):003:1* }.each {|even, ary|
irb(main):004:1*   p [even, ary]
irb(main):005:0> }
[false, [3, 1]]
[true, [4]]
[false, [1, 5, 9]]
[true, [2, 6]]
[false, [5, 3, 5]]
=> nil

n.even? の even? は、n が偶数であれば true を返し、奇数であれば false を返す Integer クラスのメソッドで、結果を見ると、n.even? の結果 (true or false) で出力が分割されていることが解ります。

ということは…

chunk メソッドを利用して、数字の羅列を空行 (空白) を区切ってグループ化出来そうです。ということで、以下のように書けそうです。

numbers = <<'EOS'
123
456

789

998
999
EOS

datas = numbers.split("\n")

p datas.chunk {|x| x != "" || nil}.map {|x| x.last }

実行してみると…

root@a68e3293a424:/work# ruby test.rb
[["123", "456"], ["789"], ["998", "999"]]

お、いい感じでチャンクされているように見えます。

あとは各要素を計算するだけ

あとは、チャンクされた各要素の中の数値を合計するコードを書いていきます。

numbers = <<'EOS'
123
456

789

998
999
EOS

datas = numbers.split("\n")

groups = datas.chunk {|x| x != "" || nil}.map {|x| x.last }
p groups

result = []
groups.each do |g|
  result << g.map(&:to_i).sum
end

p result.max

実行してみると…

root@a68e3293a424:/work# ruby test.rb
[["123", "456"], ["789"], ["998", "999"]]
1997

おお、意図した結果となりました!

以上

Ruby の奥深さと楽しさを改めて学ぶことが出来ました。

現場からは以上です。

参考

docs.ruby-lang.org