最近機械学習を使ったサービスを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さんにご教示いただいた実装を参考にしています。ありがとうございます。
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
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