10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RubyAdvent Calendar 2024

Day 1

知ってた? Ruby の Enumerable で意外と使えるメソッドたち

Last updated at Posted at 2024-11-09

こんにちは、とまだです。

Ruby アドベントカレンダー 1 日目の記事をお届けします!

みなさんは普段から Ruby を使っていると思いますが、eachmap以外の Enumerable メソッドってあまり使ったことないんじゃないでしょうか?
(自分も最初はeachmapしか使ってなかったので、偉そうなことは言えませんが...)

実はEnumerableモジュールには、めちゃくちゃ便利なメソッドがたくさん隠れています。

今回は、使える × 面白いの二軸で選んだメソッドをご紹介します!

1. 意外と演算子だったメソッドたち

まずは、数学っぽい演算をしてくれるメソッドを紹介します。

chain でコレクションを繋げる

よく、配列を結合したいときがありますよね。
そんなとき、chainを使うと便利です。

[1, 2].chain([3, 4]) #=> Enumerator: [1, 2, 3, 4]

chainは、複数のコレクションを繋げて一つのEnumeratorを返してくれます。

一方、chain を使わない場合は、以下のように書くことができます。

# 配列の結合
[1, 2] + [3, 4]

# もしくは
[1, 2].concat([3, 4])

「あれ?+concatと同じじゃない?」と思った方、鋭いですね!

実はchainEnumeratorを返すので、必要になるまで実際の配列は作られません。

# chainを使うと
result = [1, 2].chain([3, 4])
# この時点では新しい配列は作られていない

# 実際に使うときに初めて配列が作られる
result.to_a #=> [1, 2, 3, 4]

言い換えると、chainを使うとメモリ効率がよくなるということです。

大きなデータを扱うときには、chainを使うといいかもしれませんね!

product で直積を取る

では次に、productメソッドを紹介します。

[1, 2].product(['a', 'b'])
#=> [[1, "a"], [1, "b"], [2, "a"], [2, "b"]]

これは、組み合わせを全部作ってくれるメソッドです。

たとえば、商品の色とサイズの全組み合わせを作るときに使えます。

productを使わない場合は、以下のように書くことができます。

colors = ['Red', 'Blue']
sizes = ['S', 'M', 'L']
combinations = []
colors.each do |color|
  sizes.each do |size|
    combinations << [color, size]
  end
end
#=> [["Red", "S"], ["Red", "M"], ["Red", "L"], ["Blue", "S"], ["Blue", "M"], ["Blue", "L"]]

# もしくは
colors.flat_map {|color|
  sizes.map {|size| [color, size]}
}

上記のように、eachmapを使っても同じ結果が得られますが、productを使うともっと簡潔に書けます。

# 商品の色とサイズの全組み合わせを作る
colors = ['Red', 'Blue']
sizes = ['S', 'M', 'L']
colors.product(sizes)

同じ結果を得られるなら、短くすっきり書ける方がいいですね!

2. ゲーム開発で使えそうなメソッドたち

次は、ゲーム開発で使えそうなメソッドを紹介します。

repeated_permutation で同じ要素を使った順列

repeat_permutationは、同じ要素を使って順列を作ってくれるメソッドです。

「順列」とは、要素の順番が重要な組み合わせのことです。

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

たとえば、サイコロを 3 回振るパターンを全部列挙したいときに使えます。

普通なら、こうでしょうか。

dice = [1, 2, 3, 4, 5, 6]
patterns = []
dice.each do |d1|
  dice.each do |d2|
    dice.each do |d3|
      patterns << [d1, d2, d3]
    end
  end
end

ネストが深くなってしまいますね。

これもrepeated_permutationを使えば、一行で書けます。

[1, 2, 3, 4, 5, 6].repeated_permutation(3)

slice_when で連続する要素をグループ化

[1,1,2,2,2,3,4,4,4].slice_when {|a,b| a != b}
#=> [[1,1], [2,2,2], [3], [4,4,4]]

これは連続する同じ要素をグループ化してくれるメソッドです。

たとえば、マッチ 3 系のゲームで同じ色のブロックが何個連続しているか数えるときに使えます。

あえて slice_when を使わない場合は、以下のように書くことができます。

numbers = [1,1,2,2,2,3,4,4,4]
result = []
current_group = [numbers.first]

numbers[1..-1].each do |n|
  if current_group.last == n
    current_group << n
  else
    result << current_group
    current_group = [n]
  end
end
result << current_group

かなり長くなってしまいますね。

slice_whenを使えば、一行で同じことができます。

[1,1,2,2,2,3,4,4,4].slice_when {|a,b| a != b}

3. データ分析で重宝するメソッドたち

次は、データ分析で使えるメソッドを紹介します。

tally で要素をカウント

tallyは、各要素の出現回数をカウントしてくれるメソッドです。

Python を使っている人なら、collections.Counterに似ているかもしれません。

['a', 'b', 'a', 'c', 'a'].tally
#=> {"a"=>3, "b"=>1, "c"=>1}

これは各要素の出現回数をカウントしてくれるメソッドです。
(Ruby 2.7 から使えるようになりました。)

もちろん、eachtransform_valuesを使っても同じことができます。

array = ['a', 'b', 'a', 'c', 'a']
counts = Hash.new(0)
array.each { |element| counts[element] += 1 }

# もしくは
array.group_by(&:itself).transform_values(&:size)

# もしくは
array.each_with_object(Hash.new(0)) { |element, counts|
  counts[element] += 1
}

ただ、それなりに自分で書く必要があります。
一方、tallyを使えばこれが一行で書けます。

['a', 'b', 'a', 'c', 'a'].tally

chunk で連続する要素をまとめる

続いて、chunkメソッドを紹介します。

chunkは、連続する同じ要素をまとめてくれるメソッドです。

[1, 2, 2, 3, 3, 3].chunk(&:itself)
#=> [[1, [1]], [2, [2, 2]], [3, [3, 3, 3]]]

これは連続する同じ要素をまとめてくれるメソッドです。

たとえば、株価の上昇・下降トレンドを分析する場面を想像してみましょう。

普通なら、こうでしょうか。
ここでは、連続する同じ要素をまとめる処理を自分で書いています。

prices = [100, 110, 120, 115, 105, 95, 100, 110]
trends = []
current_trend = nil
current_values = []

prices.each_cons(2) do |a, b|
  trend = a < b ? :up : :down

  if current_trend == trend
    current_values << [a, b]
  else
    trends << [current_trend, current_values] if current_trend
    current_trend = trend
    current_values = [[a, b]]
  end
end
trends << [current_trend, current_values]

これを、chunkを使えばもっとすっきり書けます。

prices = [100, 110, 120, 115, 105, 95, 100, 110]
prices.each_cons(2).chunk { |a, b| a < b ? :up : :down }

4. コードゴルフが捗るメソッドたち

コードをより短く書きたい人におすすめのメソッドを紹介します。

「コードゴルフ」とは、プログラムをできるだけ短く書くことを指します。

sum は文字列連結もできる

よく合計値を求めるために使うsumメソッドですが、実は文字列連結にも使えます。

['Hello', ' ', 'World'].sum("")
#=> "Hello World"

文字列を連結するメソッドとして使えます。

普通なら、こうでしょうか。

['Hello', ' ', 'World'].join
# もしくは
['Hello', ' ', 'World'].reduce(:+)

sumを使うと、初期値を指定して文字列連結ができます。

# 空文字列を初期値として指定
['Hello', ' ', 'World'].sum("")

flat_map は map して flatten するのと同じ

次に、flat_mapメソッドを紹介します。

flat_mapは、mapしてからflattenするのと同じことができます。

[[1,2], [3,4]].flat_map {|a| a.map {|n| n * 2}}
#=> [2, 4, 6, 8]

これは配列をフラットにしながら map する操作です。

もちろん、mapしてからflattenすることもできます。

# mapしてからflatten
[[1,2], [3,4]].map {|a| a.map {|n| n * 2}}.flatten

# もしくは
result = []
[[1,2], [3,4]].each do |arr|
  arr.each do |n|
    result << n * 2
  end
end

flat_mapを使えば一回のイテレーションで済むので、パフォーマンス的にも有利です。

[[1,2], [3,4]].flat_map {|a| a.map {|n| n * 2}}

5. 実務で使える便利メソッドたち

最後に、実務でよく使う(であろう)メソッドを紹介します。

grep_v は条件に合わないものを抽出

よく、特定の条件に合わない要素を抽出したいときがありますよね。

そんなときに使えるのが、grep_vメソッドです。

numbers = [1, 2, 3, 4, 5]
numbers.grep_v(1..3) #=> [4, 5]

これは条件に「マッチしない」要素を抽出するメソッドです。

たとえば、1 から 3 までの数値を除外したいときに使えます。

numbers = [1, 2, 3, 4, 5]
numbers.reject { |n| (1..3).include?(n) }

# もしくは
numbers.select { |n| !(1..3).include?(n) }

grep_vを使うと、より意図が明確になります。

# 無効なメールアドレスを見つける
emails = ["test@example.com", "invalid", "user@host"]
emails.grep_v(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)

group_by でハッシュにグループ化

これもよく使うメソッドです。

[1, 2, 3, 4, 5, 6].group_by { |n| n % 3 }
#=> {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}

これは要素をグループ化してハッシュにしてくれるメソッドです。

たとえば、ユーザーを年齢層でグループ分けしたいときに使えます。

numbers = [1, 2, 3, 4, 5, 6]
result = Hash.new { |h, k| h[k] = [] }
numbers.each do |n|
  result[n % 3] << n
end

group_byを使えば、一行でグループ化できます。

# ユーザーを年齢層でグループ分け
users = [
  OpenStruct.new(name: "Alice", age: 25),
  OpenStruct.new(name: "Bob", age: 31),
  OpenStruct.new(name: "Charlie", age: 28)
]
users.group_by { |user| user.age / 10 * 10 }

まとめ

いかがでしたか?

普段使っているeachmapでも実現できる処理が、Enumerable の便利メソッドを使うともっと簡潔に書けることがわかりましたね。

まだまだ紹介しきれていない面白いメソッドがたくさんあります。

使えると思ったメソッドがあれば、ぜひ実際のコードで試してみてください!

他にもアドベントカレンダー記事を書いています!

他にも、2024 年のアドベントカレンダーに参加しています。

以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!

10
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?