Rubyでは、「select」と「map」を組み合わせたArrayメソッドはありますか?


95

いくつかの文字列値を含むRuby配列があります。する必要がある:

  1. 一部の述語に一致するすべての要素を検索します
  2. 一致する要素を変換で実行します
  3. 結果を配列として返す

現在、私のソリューションは次のようになります。

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

selectとmapを組み合わせて単一の論理ステートメントにするArrayまたはEnumerableメソッドはありますか?


5
現在のところメソッドはありませんが、Rubyに追加するための提案:bugs.ruby-lang.org/issues/5663
stefankolb

このEnumerable#grepメソッドは、要求された内容を正確に実行し、Rubyで10年以上使用されています。述語引数と変換ブロックを受け取ります。@hirolauがこの質問に対する唯一の正しい答えを提供します。
inopinatus

2
Ruby 2.7はまさにfilter_mapこの目的のために導入されています。詳細はこちら
SRack

回答:


114

私は通常、選択基準mapcompact一緒にPostfixとして使用しますifcompactネイルを取り除きます。

jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}    
 => [3, 3, 3, nil, nil, nil] 


jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}.compact
 => [3, 3, 3] 

1
ああ、私はマップブロックから返されたnilを無視する方法を見つけようとしていました。ありがとう!
Seth Petry-Johnson

問題ありません、コンパクトが大好きです。それは控えめにそこに座って、その仕事をします。この方法は非常に宣言的であるため、単純な選択基準で列挙可能な関数をチェーンするよりもこの方法を好みます。
Jed Schneider

4
場合、私はわからないしmap+はcompact本当に良くより実行することになりinject、関連スレッドに私のベンチマーク結果を掲載:stackoverflow.com/questions/310426/list-comprehension-in-ruby/...
knuton

3
これにより、すべてのnilが削除されます。元のnilと基準を満たさないnilの両方です。要注意
user1143669

1
チェーンが完全になくなるわけではありませんmapselect、それはそれだけだcompact特殊なケースであるrejectことNILSおよび実行する上で作品が幾分良好に起因C.に直接実装されていたこと
ジョーAtzberger

53

このために使用できますreduceが、パスは1つだけ必要です。

[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3] 

つまり、必要な状態(この場合は空のリストを埋める:)になるように状態を初期化[]し、元のリストの各要素(この場合は変更された要素)を変更して、必ずこの値を返すようにします。リストにプッシュされます)。

これは、1つのパス(map+ selectまたはcompact 2つのパスが必要)でです。

あなたの場合:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end

20
しないeach_with_objectもう少し理にかなって?ブロックの各反復の最後に配列を返す必要はありません。簡単にできますmy_array.each_with_object([]) { |i, a| a << i if i.condition }
henrebotha

@henrebothaおそらくそうです。私はのは、なぜ私が見つけたことを、機能的背景から来ているreduce最初の😊
アダム・リンドバーグ

33

Ruby 2.7以降

今があります!

Ruby 2.7はまさにfilter_mapこの目的のために導入されています。それは慣用的で高性能であり、すぐに標準になると思います。

例えば:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

これは主題についてのよい読書です。

それが誰かに役立つことを願っています!


1
どれだけ頻繁にアップグレードしても、クールな機能は常に次のバージョンにあります。
mlt

いいね。1つの問題は、以来かもしれないfilterselectfind_all同じように、同義であるmapcollectされ、このメソッドの名前を覚えておくのは難しいかもしれません。それはfilter_mapselect_collectfind_all_mapまたはfilter_collect
Eric Duminil、

19

これにアプローチする別の異なる方法は、新しい(この質問に関連する)を使用することEnumerator::Lazyです:

def example
  @lines.lazy
        .select { |line| line.property == requirement }
        .map    { |line| transforming_method(line) }
        .uniq
        .sort
end

この.lazyメソッドは、遅延列挙子を返します。.selectまたは.map遅延列挙子を呼び出すと、別の遅延列挙子が返されます。一度呼び出すだけ.uniqで、実際に列挙子を強制して配列を返します。それでは、効果的に起こることは、あなたである.select.map呼び出しが一つに結合されている-あなただけの反復処理@linesの両方を行うために一度.select.map

私の本能は、アダムのreduce方法が少し速くなるということですが、これははるかに読みやすいと思います。


これの主な結果は、後続のメソッド呼び出しごとに中間配列オブジェクトが作成されないことです。通常の@lines.select.map状況でselectは、によって変更された配列を返しmap、再び配列を返します。比較すると、遅延評価では配列が1回だけ作成されます。これは、最初のコレクションオブジェクトが大きい場合に役立ちます。また、無限列挙子(例:)を操作することもできますrandom_number_generator.lazy.select(&:odd?).take(10)


4
それぞれ独自に。私の種類のソリューションを使用すると、メソッド名を一目で見れば、入力データのサブセットを変換し、一意にしてソートすることがすぐにわかります。reduce「何でもやる」という変容はいつも私にとってかなり厄介なものです。
henrebotha

2
@henrebotha:私はあなたが何を意味するのか誤解をしている場合、私を許し、これは非常に重要なポイントである:それはだ正しくないと言うことは、「あなただけの反復処理@linesの両方を行うために一度.select.map」。を使用.lazyしても、遅延列挙子での連鎖操作の操作が1回の反復に「縮小」されるわけではありません。これは、コレクションに対する操作の連鎖的な遅延評価に関してよくある誤解です。(最初の例のとブロックのputs先頭にステートメントを追加してこれをテストできます。同じ数の行が出力されることがわかります)selectmap
pje

1
@henrebotha:削除する.lazyと、同じ回数印刷されます。それが私のポイントです。あなたのmapブロックとあなたのselectブロックは、遅延バージョンと熱心バージョンで同じ回数実行されます。遅延バージョンは「あなた.select.map電話を組み合わせる」ことをしません
pje

1
@pje:条件を満たさない要素はに渡されないため、実際に lazyはそれらを組み合わせます。言い換えれば、プリペンディングは交換とほぼ同等であると単一とし、「インテリジェント」製造のブロックに含めるに前提条件の結果。selectmaplazyselectmapreduce([])selectreduce
henrebotha

1
@henrebotha:遅延評価はこのアルゴリズムの時間の複雑さを変更しないため、遅延評価の一般的な誤解を招く類推だと思います。これが私のポイントです。すべての場合で、遅延選択マップは常にその意欲的なバージョンと同じ数の計算を実行します。これは何もスピードアップせず、各反復の実行順序を変更するだけです。チェーンの最後の関数は、必要に応じて前の関数から逆の順序で値を「プル」します。
pje

13

演算子()をselect使用できるがある場合、これは良い代替手段です:case===grep

p [1,2,'not_a_number',3].grep(Integer){|x| -x } #=> [-1, -2, -3]

p ['1','2','not_a_number','3'].grep(/\D/, &:upcase) #=> ["NOT_A_NUMBER"]

より複雑なロジックが必要な場合は、ラムダを作成できます。

my_favourite_numbers = [1,4,6]

is_a_favourite_number = -> x { my_favourite_numbers.include? x }

make_awesome = -> x { "***#{x}***" }

my_data = [1,2,3,4]

p my_data.grep(is_a_favourite_number, &make_awesome) #=> ["***1***", "***4***"]

それは代替手段ではありません-それは質問に対する唯一の正しい答えです。
inopinatus

@inopinatus:もうありません。しかし、これはまだ良い答えです。そうでなければ、ブロックでgrepを見たことを覚えていません。
Eric Duminil、

8

あるかわかりません。列挙モジュール追加、selectおよびはmap、いずれかが表示されません。

select_and_transformメソッドに2つのブロックを渡す必要がありますが、これは少し直感的ではありません。

もちろん、それらをチェーン化することもできます。

transformed_list = lines.select{|line| ...}.map{|line| ... }

3

簡単な答え:

あなたは、n個の記録を持っていて、したい場合selectmap、条件に基づいて、

records.map { |record| record.attribute if condition }.compact

ここで、attributeは、レコードに必要なものであり、任意のチェックを行うことができる条件です。

コンパクトは、if条件から出てきた不要なnilをフラッシュすることです


1
条件なしでも同じように使えます。私の友人が尋ねたように。
Sk。イルファン07

2

いいえ、でも次のようにできます:

lines.map { |line| do_some_action if check_some_property  }.reject(&:nil?)

またはさらに良い:

lines.inject([]) { |all, line| all << line if check_some_property; all }

14
reject(&:nil?)基本的にと同じcompactです。
イェルクWミッターク

ええ、インジェクト方式はさらに優れています。
Daniel O'Hara

2

アクションが接続されていることを明確に保ちながら、フィルター条件とマップされた値を分割するため、この方法はより読みやすいと思います。

results = @lines.select { |line|
  line.should_include?
}.map do |line|
  line.value_to_map
end

そして、あなたの特定のケースでは、result変数を一緒に削除します:

def example
  @lines.select { |line|
    line.should_include?
  }.map { |line|
    line.value_to_map
  }.uniq.sort
end

1
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

Ruby 1.9および1.8.7では、ブロックを渡さないだけでイテレーターをチェーンおよびラップすることもできます。

enum.select.map {|bla| ... }

しかし、この場合、ブロックのタイプが値を返し、selectmap一致しないため、これは実際には不可能です。これは、次のような場合により意味があります。

enum.inject.with_index {|(acc, el), idx| ... }

AFAICS、あなたができる最善は最初の例です。

ここに小さな例があります:

%w[a b 1 2 c d].map.select {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["a", "b", "c", "d"]

%w[a b 1 2 c d].select.map {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["A", "B", false, false, "C", "D"]

しかし、あなたが本当に欲しいのは["A", "B", "C", "D"]です。


昨夜、「Rubyでのメソッドチェーン」について非常に短いWeb検索を行ったところ、十分にサポートされていなかったようです。Tho、たぶんやってみるべきだったのに…また、なぜブロック引数の型が合わないのか?私の例では、両方のブロックが配列からテキストの行を取得していますよね?
Seth Petry-Johnson

@Seth Petry-Johnson:ええ、すみません、私は戻り値を意味しました。select要素を保持するかどうかを決定するブールのような値をmap返し、変換された値を返します。変換された値自体はおそらく真実になるため、すべての要素が選択されます。
イェルクWミッターク

1

私がメソッドを追加した私のライブラリー、Rearmed Rubyを使用してみてくださいEnumerable#select_map。以下に例を示します。

items = [{version: "1.1"}, {version: nil}, {version: false}]

items.select_map{|x| x[:version]} #=> [{version: "1.1"}]
# or without enumerable monkey patch
Rearmed.select_map(items){|x| x[:version]}

select_mapこのライブラリでは、select { |i| ... }.map { |i| ... }上記の多くの回答から同じ戦略を実装しています。
ジョーダンシトキン

1

2つの異なる配列を作成したくない場合は、使用できますcompact!が、注意してください。

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}
new_array.compact!

興味深いことに、compact!nilのインプレース削除を行います。の戻り値compact!は、変更があった場合は同じ配列ですが、nilがない場合はnilです。

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}.tap { |array| array.compact! }

ワンライナーになります。


0

あなたのバージョン:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

私のバージョン:

def example
  results = {}
  @lines.each{ |line| results[line] = true if ... }
  return results.keys.sort
end

これは1回の反復を行い(ソートを除く)、一意性を維持するという追加のボーナスがあります(uniqを気にしない場合は、結果を配列にして、 results.push(line) if ...


-1

ここに例があります。それはあなたの問題と同じではありませんが、あなたが望むものかもしれません、またはあなたの解決策への手がかりを与えることができます:

def example
  lines.each do |x|
    new_value = do_transform(x)
    if new_value == some_thing
      return new_value    # here jump out example method directly.
    else
      next                # continue next iterate.
    end
  end
end
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.