0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby で何かが最小・最大である要素を見つけるのは意外に面倒

0
Posted at

はじめに

Ruby で,数値の配列から最小のもの,最大のものを見つけるのは簡単だ。専用のメソッドがあるから:

numbers =  [3, 6, 0, 4, 7, 0, 9, 6, 2, 4]

p numbers.min    # => 0
p numbers.max    # => 9
p numbers.minmax # => [0, 9]

では,文字列の配列から最も短い文字列,最も長い文字列を見つけるには?

〈各要素に対し何らかの評価式を計算して,その値によって大小を決めたうえで,それが最小になるもの,最大になるもの〉を得るには Enumerable#min_by, Enumerable#max_by が使える。
両方いっぺんに得る Enumerable#minmax_by もある。

languages = ["Ruby", "R", "Python"]

p languages.min_by{ _1.length }    # => "R"
p languages.max_by{ _1.length }    # => "Python"
p languages.minmax_by{ _1.length } # => ["R", "Python"]

うむ,これも簡単だ。

……でいいのか? 本当に?
以下の例を見よう:

languages = ["Ruby", "Pascal", "Rust", "Python"]

p languages.min_by{ _1.length } # => "Ruby"
p languages.max_by{ _1.length } # => "Pascal"

たしかにこの四つのなかで "Ruby" は最も短いし,"Pascal" は最も長い。
しかし最短は "Rust" もそうだし,最長は "Python" もそうだ。

目的によるだろうが,多くの場合に,

  • 最も短いものすべて
  • 最も長いものすべて

が欲しいのではないだろうか?

Ruby の Enumerable モジュールには,「あるとよさそうなメソッド」はたいがいある。「こんなメソッドあるんじゃね?」と思ったらだいたい実際にある。

ところが,上記の目的に使えるメソッドは,なぜか無いんである。

なければ作ろう

Enumerable モジュールに以下の二つのメソッドを追加しよう。

  • minima_by1
    • min_by と似ているが,評価値が最小の要素すべてを配列で返す
    • そのような要素が一つしかなかった場合は,もちろん長さ 1 の配列を返す
    • 空配列に対しては空配列を返す
  • maxima_by2
    • max_by と似ているが,評価値が最大の要素すべてを配列で返す
    • 他は minima_by と同じ

「Enumerable にメソッドを追加する」といっても,組込みのモジュールである Enumerable をグローバルに変えてしまうのはお行儀の悪いやり方。
refinements という仕組みを使うと,モジュールを「特定のスコープでのみ,そのメソッドが追加された状態にする」ということができる。

さっそくコードを:

add_minima_by_and_maxima_by_to_enumerable.rb
module AddMinimaByAndMaximaByToEnumerable
  refine Enumerable do
    def minima_by
      elems_by_value = group_by{ yield _1 }
      elems_by_value[elems_by_value.keys.min]
    end

    def maxima_by
      elems_by_value = group_by{ yield _1 }
      elems_by_value[elems_by_value.keys.max]
    end
  end
end

使うときは以下のようにする:

require_relative "add_minima_by_and_maxima_by_to_enumerable"

# メソッドを追加したいスコープで `using` する
using AddMinimaByAndMaximaByToEnumerable

languages = %w[C Ruby Pascal R Rust Python]

p languages.minima_by{ _1.length }
# => ["C", "R"]

p languages.maxima_by{ _1.length }
# => ["Pascal", "Python"]

期待通りに動作した。

ぷち解説

まず,refinements のやり方をご存知ない方には何を書いているのか非常に分かりにくいと思う。
本記事では,その説明は省くが,Enumerable をグローバルに改変してしまう以下のやり方に読み替えてもらっても構わない:

module Enumerable
  def minima_by
    elems_by_value = group_by{ yield _1 }
    elems_by_value[elems_by_value.keys.min]
  end

  def maxima_by
    elems_by_value = group_by{ yield _1 }
    elems_by_value[elems_by_value.keys.max]
  end
end

この場合,もちろん使う側のコードで

using AddMinimaByAndMaximaByToEnumerable

は書かない。

なお,minima_by メソッドの動作は,以下のコードを見れば理解しやすいと思う:

languages = %w[C Ruby Pascal R Rust Python]

elems_by_value = languages.group_by{ _1.length }
p elems_by_value
# => 
# {
#   1 => ["C", "R"],
#   4 => ["Ruby", "Rust"],
#   6 => ["Pascal", "Python"]
# }

min_value = elems_by_value.keys.min
p min_value
# => 1

p elems_by_value[min_value]
# => ["C", "R"]

改良:上位 n 位まで取れるように

ところで,Enumerable の min, max, min_by, max_by とかには,非負整数の引数を与えることができる。
引数 n を与えると,「上位最大 n 個まで」の要素を配列で返すようになる。
つまり

ary = [2, 7, 3, 1, 2, 3]
p ary.min(4)
# => [1, 2, 2, 3]

といった具合。
この例では,「小ささで上位 4 要素まで」なので,二つある 3 のうち一つしか入っていないことに注意。

我らが minima_by, maxima_by も引数 n が受け取れるようにしよう。
しかし,もちろん n は「上位何位までか」を意味する。

こんな感じに,二重配列を返す:

languages = %w[C Ruby Pascal R Rust Python]

p languages.minima_by(2){ _1.length }
# => [["C", "R"], ["Ruby", "Rust"]]

上位 1 位は ["C", "R"] であり,2 位は ["Ruby", "Rust"] なのでこうなる。

コードは以下のとおり:

add_minima_by_and_maxima_by_to_enumerable
module AddMinimaByAndMaximaByToEnumerable
  refine Enumerable do
    def minima_by(n = nil)
      elems_by_value = group_by{ yield _1 }
      if n
        elems_by_value.values_at(*elems_by_value.keys.min(n))
      else
        elems_by_value[elems_by_value.keys.min]
      end
    end

    def maxima_by(n = nil)
      elems_by_value = group_by{ yield _1 }
      if n
        elems_by_value.values_at(*elems_by_value.keys.max(n))
      else
        elems_by_value[elems_by_value.keys.max]
      end
    end
  end
end
  1. minima は minimum の複数形。

  2. maxima は maximum の複数形。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?