Есть ли в Ruby метод Array, который объединяет «select» и «map»? - PullRequest
85 голосов
/ 30 июля 2010

У меня есть массив Ruby, содержащий некоторые строковые значения. Мне нужно:

  1. Найти все элементы, которые соответствуют некоторому предикату
  2. Запустить соответствующие элементы через преобразование
  3. Вернуть результаты в виде массива

Сейчас мое решение выглядит так:

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

Существует ли метод Array или Enumerable, который объединяет select и map в один логический оператор?

Ответы [ 14 ]

100 голосов
/ 30 июля 2010

Я обычно использую map и compact вместе с моими критериями выбора в качестве постфикса if. compact избавляется от нуля.

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] 
51 голосов
/ 17 июля 2013

Вы можете использовать reduce для этого, который требует только одного прохода:

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

Другими словами, инициализируйте состояние так, как вам нужно (в нашем случае это пустой список для заполнения: []), затем всегда обязательно возвращайте это значение с изменениями для каждого элемента в исходном списке (в в нашем случае измененный элемент помещается в список).

Это наиболее эффективный метод, поскольку он проходит по списку только с одним проходом (map + select или compact требует двух проходов).

В вашем случае:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end
15 голосов
/ 17 июня 2016

Другим другим способом решения этой проблемы является использование нового (относительно этого вопроса) 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.

Мой инстинкт заключается в том, что АдамМетод 1016 * будет немного быстрее, но я думаю, что он гораздо более читабелен.


Основным следствием этого является то, что промежуточные объекты массива не создаются для каждого последующего вызова метода.В обычной ситуации @lines.select.map, select возвращает массив, который затем изменяется на map, снова возвращая массив.Для сравнения, ленивая оценка создает массив только один раз.Это полезно, когда ваш начальный объект коллекции большой.Это также дает вам возможность работать с бесконечными счетчиками - например, random_number_generator.lazy.select(&:odd?).take(10).

10 голосов
/ 13 февраля 2016

Если у вас есть 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***"]
8 голосов
/ 30 июля 2010

Я не уверен, что есть один.Модуль Enumerable , который добавляет select и map, не показывает ни одного.

Вы должны были бы перейти в два блока к методу select_and_transform, что было бы немного не интуитивно, ИМХО.

Очевидно, вы могли бы просто связать их вместе, что более читабельно:

transformed_list = lines.select{|line| ...}.map{|line| ... }
2 голосов
/ 26 июня 2014

Я думаю, что этот способ более читабелен, поскольку разделяет условия фильтра и отображаемое значение, оставляя при этом ясным, что действия связаны:

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
2 голосов
/ 30 июля 2010

Нет, но вы можете сделать это так:

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

Или даже лучше:

lines.inject([]) { |all, line| all << line if check_some_property; all }
1 голос
/ 12 июня 2019

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 голос
/ 31 августа 2017

Вы должны попробовать использовать мою библиотеку 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]}
1 голос
/ 12 июля 2017

Простой ответ:

Если у вас есть n записей, и вы хотите select и map в зависимости от условия, тогда

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

Здесь атрибут - это то, что вы хотите отзапись и условие, которое вы можете поставить любой чек.

compact - сбросить ненужные нули, которые возникли из этого, если условие

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...