В Ruby существует метод Array, который объединяет "select" и "map"?

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

  • Найти все элементы, которые соответствуют предикату
  • Запуск соответствующих элементов с помощью преобразования
  • Возвращает результаты в виде массива

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

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

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

Ответ 1

Обычно я использую 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] 

Ответ 2

Вы можете использовать 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

Ответ 3

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

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


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

Ответ 4

Если у вас есть 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"]

Если нам нужна более сложная логика, мы можем создать lambdas:

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

Ответ 5

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

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

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

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

Ответ 6

Нет, но вы можете сделать это следующим образом:

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

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

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

Ответ 7

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

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

Ответ 8

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

В Ruby 1.9 и 1.8.7 вы также можете цепочки и обертывать итераторы, просто не передавая им блок:

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

Но в этом случае это действительно невозможно, так как типы возвращаемых значений блока select и map не совпадают. Это имеет смысл для чего-то вроде этого:

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

Ответ 9

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

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

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

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

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

Ответ 10

Вам следует попробовать использовать мою библиотеку 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]}

Ответ 11

Если вы не хотите создавать два разных массива, вы можете использовать compact!, но будьте осторожны.

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

Интересно, что compact! делает удаление nil. Возвращаемое значение compact! - это тот же массив, если были изменения, но nil, если не было нил.

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

Будет один лайнер.

Ответ 12

Ваша версия:

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...

Ответ 13

Рубин 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]

Здесь хорошо читать на эту тему.

Надеюсь, что кому-то пригодится!

Ответ 14

Вот пример. Это не то же самое, что и ваша проблема, но может быть то, что вы хотите, или можете дать ключ к вашему решению:

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