Rubyでnil値をマップおよび削除する方法


361

map値を変更するか、nilに設定するを持っています。次に、リストからnilエントリを削除します。リストを保持する必要はありません。

これは私が現在持っているものです:

# A simple example function, which returns a value or nil
def transform(n)
  rand > 0.5 ? n * 10 : nil }
end

items.map! { |x| transform(x) } # [1, 2, 3, 4, 5] => [10, nil, 30, 40, nil]
items.reject! { |x| x.nil? } # [10, nil, 30, 40, nil] => [10, 30, 40]

私はループを実行して、次のような別の配列に条件付きで収集できることを知っています:

new_items = []
items.each do |x|
    x = transform(x)
    new_items.append(x) unless x.nil?
end
items = new_items

しかし、それはそれほど慣用的ではないようです。リストに関数をマップして、nilを削除/除外する良い方法はありますか?


3
Ruby 2.7ではが導入されていますがfilter_map、これはこれに最適なようです。配列を再処理する必要をなくし、最初に必要に応じて配列を取得します。詳細はこちら。
SRack

回答:


21

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]

あなたの場合、ブロックが偽と評価されるので、単に:

items.filter_map { |x| process_x url }

Ruby 2.7がEnumerable#filter_mapを追加する」は、この問題に関する以前のアプローチのいくつかに対するいくつかのパフォーマンスベンチマークとともに、この件に関する優れた資料です。

N = 1_00_000
enum = 1.upto(1_000)
Benchmark.bmbm do |x|
  x.report("select + map")  { N.times { enum.select { |i| i.even? }.map{|i| i + 1} } }
  x.report("map + compact") { N.times { enum.map { |i| i + 1 if i.even? }.compact } }
  x.report("filter_map")    { N.times { enum.filter_map { |i| i + 1 if i.even? } } }
end

# Rehearsal -------------------------------------------------
# select + map    8.569651   0.051319   8.620970 (  8.632449)
# map + compact   7.392666   0.133964   7.526630 (  7.538013)
# filter_map      6.923772   0.022314   6.946086 (  6.956135)
# --------------------------------------- total: 23.093686sec
# 
#                     user     system      total        real
# select + map    8.550637   0.033190   8.583827 (  8.597627)
# map + compact   7.263667   0.131180   7.394847 (  7.405570)
# filter_map      6.761388   0.018223   6.779611 (  6.790559)

1
いいね!アップデートをありがとう:) Ruby 2.7.0がリリースされたら、受け入れられた回答をこの回答に切り替えることはおそらく意味があると思います。ここでエチケットが何であるかはわかりませんが、一般的に、既存の承認済みの応答に更新の機会を与えるかどうかはどうですか これは2.7の新しいアプローチを参照する最初の回答なので、受け入れられるものになるはずです。@ the-tin-manこのテイクに同意しますか?
ピートハミルトン

@PeterHamiltonに感謝-フィードバックに感謝し、多くの人々に役立つことを願っています。私はあなたの決定に満足していますが、明らかにあなたがした議論が好きです:)
SRack

はい、それは、コアチームがリッスンする言語の良いところです。
ティンマン

選択した回答の変更を推奨するのは良いジェスチャーですが、ほとんど起こりません。SOは、人々に思い出させるためのくすぐりを提供していません。SOが活動があったと言わない限り、人々は通常、彼らが尋ねた古い質問を再訪しません。サイドバーとして、私はベンチマークのためにFruityを調べることをお勧めします。なぜなら、それは手間がはるかに少なく、賢明なテストを簡単に作成できるからです。
Tin Man、

930

あなたは使うことができますcompact

[1, nil, 3, nil, nil].compact
=> [1, 3] 

mapブロックの出力としてnilsを含む配列を取得していて、そのブロックが条件付きで値を返そうとする場合、コードのにおいがしてロジックを再考する必要があることを人々に思い出させてください。

たとえば、これを行う何かをしている場合:

[1,2,3].map{ |i|
  if i % 2 == 0
    i
  end
}
# => [nil, 2, nil]

その後、しないでください。代わりに、前にmaprejectあなたはしたくないまたはスタッフselectあなたが望む何をすべきか:

[1,2,3].select{ |i| i % 2 == 0 }.map{ |i|
  i
}
# => [2]

私がcompact正しく処理していないものを取り除くための最後の努力として、混乱を片付けるためにを使用することを検討します。プログラムではどのような種類のデータが発生しているのかを常に知る必要があります。予期しない/不明なデータは不良です。私が取り組んでいる配列にnilが表示されたときはいつでも、それらが存在する理由を調べ、Rubyが時間を浪費してメモリを生成するのではなく、配列をふるいにかけて削除するのではなく、配列を生成するコードを改善できるかどうかを確認しますそれらは後で。

'Just my $%0.2f.' % [2.to_f/100]

29
これがルビー風です!
Christophe Marois 2013年

4
なぜそれが必要ですか?OPはnil、空の文字列ではなく、エントリを削除する必要があります。ところで、nil空の文字列と同じではありません。
ティンマン

9
どちらのソリューションは、コレクションを二回繰り返す...なぜ使用しませんreduceinject
Ziggy

4
OPの質問や回答を読んでいるようには聞こえません。問題は、配列からnilを削除する方法です。compact最速ですが、実際にコードを最初に正しく記述することで、nilを完全に処理する必要がなくなります。
Tin Man

3
同意しません!問題は、「nil値のマッピングと削除」です。まあ、nil値をマップして削除することは減らすことです。彼らの例では、OPがマップし、nilを選択します。マップを呼び出してから圧縮する、または選択してからマップするということは、同じ間違いを犯すことになります。答えで指摘したように、それはコードのにおいです。
ジギー

96

reduceまたはを使用してみてくださいinject

[1, 2, 3].reduce([]) { |memo, i|
  if i % 2 == 0
    memo << i
  end

  memo
}

私はしてはいけないmap、というのは受け入れられた答えに同意しますがcompact、同じ理由ではありません。

私はそれを心の奥底に感じmap、その後compactに相当しselect、その後にmap。考慮:map1対1の関数です。いくつかの値のセットからマッピングしている場合、入力セットの各値に対して、出力セットに1つの値がmap必要ですselect事前に確認する必要がある場合は、おそらくmapセットにaは必要ありません。select後で(またはcompact)しなければならない場合は、おそらくmapセットにaは必要ありません。どちらの場合も、reduce1回だけ実行する必要があるときに、セット全体で2回繰り返します。

また、英語では、「整数のセットを偶数の整数のセットに減らす」ことを試みています。


4
貧しいジギー、あなたの提案への愛はありません。笑。加えて、他の誰かが何百もの賛成票を持っています!
DDDD 2015年

2
いつの日か、あなたの助けがあれば、この答えは受け入れられたものを超えると思います。^ o ^ //
Ziggy

2
+1現在受け入れられている回答では、選択フェーズで実行した操作の結果を使用できません
chees

1
受け入れられた回答のように、オンパスのみが必要な場合は、列挙可能なデータ構造を2回繰り返すことは無駄に思えます。したがって、reduceを使用してパスの数を減らします!ありがとう@Ziggy
sebisnow

それは本当だ!しかし、n個の要素のコレクションに対して2つのパスを実行することは、依然としてO(n)です。コレクションが大きすぎてキャッシュに収まらない場合を除いて、2つのパスを実行することはおそらく問題ありません(これはよりエレガントで表現力があり、ループが落ちたときに将来バグが発生する可能性が低いと思います)非同期)。ワンパスで物事を行うのも好きなら、トランスデューサについて学ぶことに興味があるかもしれません!github.com/cognitect-labs/transducers-ruby
Ziggy

33

あなたの例では:

items.map! { |x| process_x url } # [1, 2, 3, 4, 5] => [1, nil, 3, nil, nil]

置き換えられる以外に値が変更されたようには見えません nil。その場合は、次のようになります。

items.select{|x| process_x url}

十分であろう。


27

空の文字列とnilを拒否するなど、拒否の基準を緩くしたい場合は、次のように使用できます。

[1, nil, 3, 0, ''].reject(&:blank?)
 => [1, 3, 0] 

さらに進んでゼロ値を拒否する(またはプロセスにさらに複雑なロジックを適用する)場合は、ブロックを渡して拒否することができます。

[1, nil, 3, 0, ''].reject do |value| value.blank? || value==0 end
 => [1, 3]

[1, nil, 3, 0, '', 1000].reject do |value| value.blank? || value==0 || value>10 end
 => [1, 3]

5
。ブランク?レールでのみ使用できます。
ewalk 2014

将来の参考のために、blank?はレールでのみ使用できるため、レールにitems.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]結合されていないを使用することができます。(ただし、空の文字列や0は除外されません)
Fotis

27

間違いなくcompact、このタスクを解決するための最良のアプローチです。ただし、単純な減算だけで同じ結果を得ることができます。

[1, nil, 3, nil, nil] - [nil]
 => [1, 3]

4
はい、セット減算は機能しますが、オーバーヘッドがあるため、約半分の速度です。
Tin Man

4

each_with_object おそらくここに行くための最もきれいな方法です:

new_items = items.each_with_object([]) do |x, memo|
    ret = process_x(x)
    memo << ret unless ret.nil?
end

私の意見でeach_with_objectは、ブロックの戻り値を気にする必要がないため、条件付きの場合はinject/ よりも優れreduceています。


0

これを実現するもう1つの方法は、以下に示すとおりです。ここではEnumerable#each_with_object、値を収集するために使用し、メソッドの結果のチェックにObject#tap必要な一時変数を取り除くために使用します。nilprocess_x

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

説明のための完全な例:

items = [1,2,3,4,5]
def process x
    rand(10) > 5 ? nil : x
end

items.each_with_object([]) {|x, obj| (process x).tap {|r| obj << r unless r.nil?}}

代替アプローチ:

呼び出しているメソッドを見ても、そのメソッドへprocess_x urlの入力の目的が何であるかは明確ではありませんxxを渡しての値を処理し、実際にurlどのxnが有効なnon-nilの結果に処理されるかを判断するとしEnumerabble.group_byますEnumerable#map

h = items.group_by {|x| (process x).nil? ? "Bad" : "Good"}
#=> {"Bad"=>[1, 2], "Good"=>[3, 4, 5]}

h["Good"]
#=> [3,4,5]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.