В 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