自分なりの便利なArrayRefinementsたち

最近機械学習を使ったサービスをpython(Django)とruby(Rails)を使って作っていて配列周りでpythonだとあるのにrubyだと意外と無いんだなというメソッドをRefinementsで作ったのでご紹介します。(Refinementsについては[こちら](http://blog.fenrir-inc.com/jp/2014/07/refine.html))
そういうライブラリもあるかもしれませんが、これだけ使いたいみたいなときは自分で書いてしまっても勉強にもなるしいいかなと個人的には思っています。

今回作成したのはこの3つです

 * Array#average
 * Array#percentage
 * Array#select_index

メソッドの詳細を紹介したうえでソースコードも下にあるので良ければ利用してみてください。
あるいはこんな便利なものを作って使っていますとかあれば教えていただきたいです！
追加した場合は随時更新していきます。

# メソッドの紹介
## Array#average
その名の通り配列の平均値を求めます。

```ruby
using Util::ArrayUtil

[1, 2, 3, 4, 5].average #=> 3.0
```

## Array#percentage
これまたその名の通りある配列の中である条件を満たす要素の割合を％で求めます。ブロックを渡すこともできます。

```ruby
using Util::ArrayUtil

[1, 2, 3, 3, 3, 4, 5, 6, 7, 4].percentage(3) #=> 30.0

['hoge', 'baz', 'baz', 'foo'].percentage('foo') #=> 25.0

[1, 2, 3, 3, 3, 6, 7, 8, 9, 10].percentage { |i| i > 3 } # => 50.0
```

## Array#select_index
これだけは少し説明が必要で、Arrayには[find_index](https://apidock.com/ruby/Array/find_index)というメソッドがありこれを使うと特定の要素あるいは条件を満たす配列のインデックスを取得できます。

```ruby
['hoge', 'baz', 'foo'].find_index('baz') #=> 1

[1, 2, 3].find_index{|num| num % 3 == 0} #=> 2

['hoge', 'baz', 'foo'].find_index('nothing_val') #=> 0(存在しない場合は0が返る)

[1, 2, 3].find_index(1){|num| num % 3 == 0} 
# warning: given block not used
#=> 0 (引数とブロックを両方渡した場合は警告を出した上で引数が優先される)

[1, 2, 3].find_index
#=> #<Enumerator: [1, 2, 3]:find_index>
# (何も渡さないとEnumeratorが返る)
```

ただしこれはあくまでもfindなので複数個マッチする値があっても最初の要素しか返しません。

```ruby
['hoge', 'baz', 'baz', 'foo'].find_index('baz') #=> 1
```

そこでArray#select_indexメソッドでは複数個マッチする値があった場合にマッチした要素の全てのindexを配列で返します。

```ruby
using Util::ArrayUtil

[1, 2, 3, 1, 2, 3, 1, 2, 3].select_index(2) #=> [1, 4, 7]

[1, 2, 3, 1, 2, 3, 1, 2, 3].select_index { |i| i >= 2 } #=> [1, 2, 4, 5, 7, 8]

[1, 2, 3, 1, 2, 3, 1, 2, 3].select_index(2){ |i| i >= 2 }
# warning: given block not used
#=> [1, 4, 7]

[1, 2, 3, 1, 2, 3, 1, 2, 3].select_index(0)
# => [] #ない場合は空配列を返す。ここだけはfind_indexと異なる挙動
```

# ソース
ソースコードは以下になります。厳密ではないですが、specも合わせて書いています。
(このテストを書いて初めて知ったのですが[refinementsの適用範囲はレキシカルスコープで決まる](https://ruby-doc.org/core-2.1.1/doc/syntax/refinements_rdoc.html#label-Scope)らしく以下のようにspecファイル内ならスコープゲートを切らない限りどこでもいいんですがusingを書くことでトップレベルにrefinementsが当たってテストがしやすかったです。）
※ コメント欄にあるとおり、#percentage, #select_indexについて[scivola](http://qiita.com/scivola)さんにご教示いただいた実装を参考にしています。ありがとうございます。

```ruby:util/array_util.rb
module Util::ArrayUtil
  refine Array do

    def average
      sum / size.to_f
    end

    def percentage(*args, &block)
      case args.length
        when 0
          if block_given?
            count(&block).fdiv(size) * 100
          else
            return to_enum(__method__)
          end
        when 1
          warn "warning: given block not used" if block_given?
          count(args.first).fdiv(size) * 100
        else
          raise ArgumentError, "arguments more than 1 given"
      end
    end

    def select_index(*args)
      indexes = []
      case args.length
        when 0
          if block_given?
            select.with_index do |item, idx|
              indexes << idx if yield(item)
            end
          else
            return to_enum(__method__)
          end
        when 1
          warn "warning: given block not used" if block_given?
          select.with_index do |item, idx|
            indexes << idx if args.first == item
          end
        else
          raise ArgumentError, "arguments more than 1 given"
      end
      indexes
    end
  end
end
```
```ruby:util/array_util_spec.rb
require 'rails_helper'
RSpec.describe Util::ArrayUtil do
  using Util::ArrayUtil

  describe '#average' do
    context '[1, 2, 3, 4, 5]' do
      it 'return 3' do
        result = [1, 2, 3, 4, 5].average
        expect(result).to eq(3.0)
      end
    end
  end

  describe '#percentage' do
    context '[1, 2, 3, 3, 3, 6, 7, 8, 9, 10]' do
      context 'target given' do
        it 'return 30.0' do
          expect([1, 2, 3, 3, 3, 6, 7, 8, 9, 10].percentage(3)).to eq(30.0)
        end
      end

      context 'block({>3}) given' do
        it 'return 50.0' do
          expect([1, 2, 3, 3, 3, 6, 7, 8, 9, 10].percentage { |i| i > 3 }).to eq(50.0)
        end
      end

      context 'arg & block given' do
        it 'return 10.0 (prioritize arg)' do
          expect([1, 2, 3, 3, 3, 6, 7, 8, 9, 10].percentage(1) { |i| i > 3 }).to eq(10.0)

        end
      end

      context 'no args & block given' do
        it 'return Enumerator' do
          expect([1, 2, 3, 3, 3, 6, 7, 8, 9, 10].percentage).to be_a(Enumerator)
        end
      end

      context 'target is nil' do
        it 'return correct percentage' do
          expect([1, nil, nil, nil, 3, 6, 7, 8, 9, 10].percentage(nil)).to eq(30.0)
        end
      end

      context 'too many args' do
        it 'raise argment error' do
          expect{[1, nil, nil, nil, 3, 6, 7, 8, 9, 10].percentage(1, 2)}.to raise_error(ArgumentError, "arguments more than 1 given")
        end
      end
    end
  end

  describe '#select_index' do
    context '[1, 2, 3, 1, 2, 3, 1, 2, 3]' do
      context 'target given' do
        it 'return [1, 4, 7]' do
          expect([1, 2, 3, 1, 2, 3, 1, 2, 3].select_index(2)).to eq([1, 4, 7])
        end

      end

      context 'block({>=2}) given' do
        it 'return [1, 2, 4, 5, 7, 8]' do
          expect([1, 2, 3, 1, 2, 3, 1, 2, 3].select_index { |i| i >= 2 }).to eq([1, 2, 4, 5, 7, 8])
        end
      end

      context 'arg & block given' do
        it 'return 10.0 (prioritize arg)' do
          expect([1, 2, 3, 1, 2, 3, 1, 2, 3].select_index(2){ |i| i >= 2 }).to eq([1, 4, 7])
        end
      end

      context 'no args & block given' do
        it 'return Enumerator' do
          expect([1, 2, 3, 1, 2, 3, 1, 2, 3].select_index).to be_a(Enumerator)
        end
      end

      context 'no match' do
        it 'return []' do
          expect([1, 2, 3, 1, 2, 3, 1, 2, 3].select_index(0)).to eq([])
        end
      end

      context 'target is nil' do
        it 'return correct percentage' do
          expect([1, nil, nil, nil, 3, 6, 7, 8, 9, 10].select_index(nil)).to eq([1, 2, 3])
        end
      end 

      context 'too many args' do
        it 'raise argment error' do
          expect{[1, nil, nil, nil, 3, 6, 7, 8, 9, 10].select_index(1, 2)}.to raise_error(ArgumentError, "arguments more than 1 given")
        end
      end
    end
  end
end
```