Ruby'de, 'select' ve 'map'i birleştiren bir Array yöntemi var mı?


96

Bazı dize değerleri içeren bir Ruby dizim var. Yapmam gerek:

  1. Bazı yüklemlerle eşleşen tüm öğeleri bulun
  2. Eşleşen öğeleri bir dönüşüm yoluyla çalıştırın
  3. Sonuçları bir dizi olarak döndür

Şu anda benim çözümüm şuna benziyor:

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

Select ve map'i tek bir mantıksal deyimde birleştiren bir Dizi veya Numaralandırılabilir yöntem var mı?


5
Şu anda bir yöntem değil, Ruby'ye
stefankolb

Enumerable#grepYöntem istendi ve üzeri on yıldır Ruby olmuştur tam olarak ne yapar. Bir yüklem argümanı ve bir dönüşüm bloğu alır. @hirolau bu soruya tek doğru cevabı veriyor.
inopinatus

2
Ruby 2.7 filter_maptam da bu amaç için geliyor. Daha fazla bilgi burada .
SRack

Yanıtlar:


115

Seçim kriterlerimle birlikte genellikle mapve compactbirlikte bir sonek olarak kullanıyorum if. compactsıfırlardan kurtulur.

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
Ah-ha, harita bloğumdan dönen sıfırları nasıl yoksayacağımı anlamaya çalışıyordum. Teşekkürler!
Seth Petry-Johnson

Sorun değil, kompakt seviyorum. göze batmadan orada oturur ve işini yapar. Ayrıca, bu yöntemi basit seçim kriterleri için numaralandırılabilir işlevleri zincirlemeye tercih ediyorum çünkü çok açıklayıcı.
Jed Schneider

4
map+ compactGerçekten daha iyi performans gösterip göstermeyeceğinden emin değildim injectve karşılaştırma sonuçlarımı ilgili bir başlıkta yayınladım: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton

3
bu, hem orijinal sıfırları hem de kriterlerinize uymayan tüm sıfırları kaldıracaktır. Öyleyse dikkat
user1143669

1
O değil tamamen zincirleme ortadan kaldırmak mapve selectsadece bu kadar, compactözel bir durumdur rejectyani nedeniyle C. doğrudan uygulanmakta olan için biraz daha iyi nils ve performansı gösterirler eserleri
Joe Atzberger

54

reduceSadece bir geçiş gerektiren bunun için kullanabilirsiniz :

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

Başka bir deyişle, durumu istediğiniz gibi başlatın (bizim durumumuzda doldurulacak boş bir liste :) [], ardından bu değeri her zaman orijinal listedeki her öğe için değişikliklerle döndürdüğünüzden emin olun (bizim durumumuzda, değiştirilmiş öğe listeye itildi).

Listede yalnızca bir geçişle ( map+ selectveya compactiki geçiş gerektirir) döngü yaptığı için bu en verimlidir .

Senin durumunda:

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

20
Does not each_with_objectbiraz daha mantıklı? Bloğun her yinelemesinin sonunda diziyi döndürmeniz gerekmez. Yapabilirsin my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha

@henrebotha Belki de öyle. İşlevsel bir geçmişten geliyorum, bu yüzden önce buldum reduce😊
Adam Lindberg

39

Ruby 2.7+

Şimdi var!

Ruby 2.7 filter_maptam da bu amaç için geliyor. Deyimsel ve performansa sahip ve çok yakında norm haline gelmesini bekliyorum.

Örneğin:

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

İşte konuyla ilgili güzel bir okuma .

Umarım bu birisi için yararlıdır!


1
Ne kadar sıklıkla yükseltme yapsam da, bir sonraki sürümde her zaman harika bir özellik var.
mlt

Güzel. Bir problem olduğunu çünkü olabilir filter, selectve find_alltıpkı eşanlamlıdır mapve collectvardır, yöntemin adını hatırlamak zor olabilir. Öyle mi filter_map, select_collect, find_all_mapveya filter_collect?
Eric Duminil

19

Buna yaklaşmanın başka bir farklı yolu da yeniyi kullanmaktır (bu soruya göre) Enumerator::Lazy:

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

.lazyYöntem, bir yavaş numaralandırıcıyı döndürür. Tembel bir numaralandırıcıyı çağırmak .selectveya .mapondan başka bir tembel numaralandırıcı döndürülür. Yalnızca bir kez çağırdığınızda .uniq, numaralandırıcıyı zorlar ve bir dizi döndürür. Öyleyse etkili olan şey, aramalarınız .selectve .maparamalarınız birde birleştirilir - @linesikisini birden yapmak için yalnızca bir kez yinelersiniz .selectve .map.

İçgüdülerim, Adam'ın reduceyönteminin biraz daha hızlı olacağıdır, ancak bence bu çok daha okunabilir.


Bunun birincil sonucu, sonraki her yöntem çağrısı için ara dizi nesnelerinin yaratılmamasıdır. Normal bir @lines.select.mapdurumda, selectdaha sonra değiştirilen ve mapyine bir dizi döndüren bir dizi döndürür. Buna karşılık, tembel değerlendirme yalnızca bir kez bir dizi oluşturur. Bu, ilk koleksiyon nesneniz büyük olduğunda kullanışlıdır. Aynı zamanda sonsuz numaralandırıcılarla çalışmanıza olanak tanır - örn random_number_generator.lazy.select(&:odd?).take(10).


4
Her birine kendi. Kendi çözümümle yöntem adlarına göz atabilirim ve giriş verilerinin bir alt kümesini dönüştüreceğimi, onu benzersiz yapacağımı ve sıralayacağımı hemen anlayabiliyorum. reduce"her şeyi yap" dönüşümü bana her zaman oldukça dağınık geliyor.
henrebotha

2
@henrebotha: Ne demek istediğini yanlış anlaşılmış ettik, ama bu çok önemli bir noktadır beni affedin: bu kadar doğru değil "sadece yinelerler üzerinde olduğunu söylemek @lineskez hem yapmak .selectve .map". Kullanımı .lazy, tembel bir numaralandırıcıda zincirleme işlemlerin tek bir yinelemede "daraltılacağı" anlamına gelmez. Bu, bir koleksiyon üzerinden zincirleme işlemlerinin tembel değerlendirmesine ilişkin yaygın bir yanlış anlaşılmadır. ( İlk örnekteki ve bloklarının putsbaşına bir ifade ekleyerek bunu test edebilirsiniz . Aynı sayıda satır yazdırdıklarını göreceksiniz)selectmap
pje

1
@henrebotha: ve kaldırırsanız .lazyaynı sayıda yazdırır. Demek istediğim bu - mapbloğunuz ve selectbloğunuz tembel ve istekli sürümlerde aynı sayıda çalıştırılıyor. Tembel versiyonu "senin birleştirmek gelmez .selectve .mapçağrıları"
pje

1
@pje: Aslında lazy bunları birleştirir çünkü selectkoşulda başarısız olan bir öğe map. Diğer bir deyişle: prepending lazydeğiştirilmesi eşdeğerdir selectve maptek olan reduce([])ve "akıllı" yapma select'in blok dahil için bir ön reducebireyin sonucu.
henrebotha

1
@henrebotha: Bence bu genel olarak tembel değerlendirme için yanıltıcı bir benzetme çünkü tembellik bu algoritmanın zaman karmaşıklığını değiştirmiyor. Demek istediğim şu: her durumda, tembel bir seçim-sonra-haritası her zaman istekli sürümüyle aynı sayıda hesaplama yapacaktır. Hiçbir şeyi hızlandırmaz, sadece her yinelemenin yürütme sırasını değiştirir - zincirdeki son işlev, değerleri önceki işlevlerden gerektiği gibi ters sırada "çeker".
pje

13

( ) Operatörünü selectkullanabilen bir hesabınız varsa, iyi bir alternatiftir: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"]

Daha karmaşık bir mantığa ihtiyacımız varsa, lambdalar oluşturabiliriz:

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***"]

Bu bir alternatif değil - sorunun tek doğru cevabı bu.
inopinatus

@inopinatus: Artık değil . Yine de bu hala iyi bir cevap. Grep'i başka türlü bir blokla gördüğümü hatırlamıyorum.
Eric Duminil

9

Bir tane olduğundan emin değilim. Enumerable modül ekler selectve mapbir tane göstermez.

select_and_transformYönteme iki blok halinde geçmeniz gerekecektir ki bu biraz sezgisel bir IMHO olacaktır.

Açıkçası, onları birbirine bağlayabilirsiniz, bu daha okunaklı:

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

3

Basit Cevap:

Eğer n kaydınız varsa selectve mapkoşula bağlı olarak istiyorsanız

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

Burada, nitelik kayıttan istediğinizi ve koşulu herhangi bir kontrol koyabilirsiniz.

kompakt, eğer koşuldan ortaya çıkan gereksiz sıfırları temizlemektir.


1
Sen de koşulsuz olarak aynı şeyi kullanabilirsin. Arkadaşımın sorduğu gibi.
Sk. Irfan

2

Hayır, ama bunu şu şekilde yapabilirsiniz:

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

Ya da daha iyisi:

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

14
reject(&:nil?)temelde aynıdır compact.
Jörg W Mittag

Evet, bu yüzden enjeksiyon yöntemi daha da iyi.
Daniel O'Hara

2

Bunun daha okunabilir olduğunu düşünüyorum, çünkü eylemlerin bağlantılı olduğu konusunda net kalırken filtre koşullarını ve eşlenen değeri böler:

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

Ve özel durumunuzda, resultdeğişkeni hep birlikte ortadan kaldırın :

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 ve 1.8.7'de, yineleyicileri onlara bir blok iletmeyerek de zincirleyebilir ve sarmalayabilirsiniz:

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

Ama blok dönüş değerlerinin türleri beri, bu durumda gerçekten mümkün değildir selectve mapyukarı uyuşmuyor. Böyle bir şey için daha mantıklı:

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

AFAICS, yapabileceğiniz en iyi ilk örnek.

İşte küçük bir örnek:

%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"]

Ama gerçekten istediğin şey ["A", "B", "C", "D"].


Dün gece "Ruby'de yöntem zincirleme" için çok kısa bir web araması yaptım ve pek desteklenmemiş gibi görünüyordu. Tho, muhtemelen denemeliydim ... ayrıca, neden blok argümanlarının türlerinin uyuşmadığını söylüyorsunuz? Örneğimde her iki blok da dizimden bir satır metin alıyor, değil mi?
Seth Petry-Johnson

@Seth Petry-Johnson: Evet, pardon, dönüş değerlerini kastetmiştim. selectÖğeyi tutup tutmayacağına karar veren bir Boolean-ish değeri mapdöndürür, dönüştürülen değeri döndürür. Dönüştürülen değerin kendisi muhtemelen doğru olacaktır, bu nedenle tüm öğeler seçilir.
Jörg W Mittag

1

Yöntemi eklediğim kitaplığım Rearmed Ruby'yi kullanmayı denemelisiniz Enumerable#select_map. İşte bir örnek:

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_mapbu kütüphanede select { |i| ... }.map { |i| ... }yukarıdaki birçok cevaptan aynı stratejiyi uygular .
Jordan Sitkin

1

İki farklı dizi oluşturmak istemiyorsanız kullanabilirsiniz compact!ancak buna dikkat edin.

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

İlginç bir şekilde, compact!sıfırın yerinde kaldırılması. Dönüş değeri, compact!değişiklik varsa aynı dizidir, sıfır yoksa sıfırdır.

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

Tek astar olur.


0

Senin versiyonun:

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

Benim versiyonum:

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

Bu, 1 yineleme yapacak (sıralama hariç) ve benzersizliği koruma ek avantajına sahip olacak (eğer uniq'i önemsemiyorsanız, sonuçları bir dizi yapın ve results.push(line) if ...


-1

İşte bir örnek. Sorununuzla aynı değil, ancak istediğiniz şey olabilir veya çözümünüz için bir ipucu verebilir:

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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.