3
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?

More than 5 years have passed since last update.

自分なりの便利なArrayRefinementsたち

Last updated at Posted at 2017-04-05

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

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

  • Array#average
  • Array#percentage
  • Array#select_index

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

メソッドの紹介

Array#average

その名の通り配列の平均値を求めます。

using Util::ArrayUtil

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

Array#percentage

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

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というメソッドがありこれを使うと特定の要素あるいは条件を満たす配列のインデックスを取得できます。

['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なので複数個マッチする値があっても最初の要素しか返しません。

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

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

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の適用範囲はレキシカルスコープで決まるらしく以下のようにspecファイル内ならスコープゲートを切らない限りどこでもいいんですがusingを書くことでトップレベルにrefinementsが当たってテストがしやすかったです。)
※ コメント欄にあるとおり、#percentage, #select_indexについてscivolaさんにご教示いただいた実装を参考にしています。ありがとうございます。

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

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
3
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?