Как отобразить и удалить нулевые значения в Ruby


361

У меня есть, mapкоторый либо меняет значение, либо устанавливает его на ноль. Затем я хочу удалить ноль записей из списка. Список не нужно хранить.

Вот что у меня сейчас есть:

# 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

Но это не кажется идиоматичным. Есть ли хороший способ отобразить функцию на список, удаляя / исключая nils по мере продвижения?


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]

В вашем случае, поскольку блок оценивается как Falsey, просто:

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 для тестов, потому что он намного менее сложен и облегчает проведение разумных тестов.
Железный человек

931

Вы можете использовать compact:

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

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

Например, если вы делаете что-то, что делает это:

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

Тогда не надо. Вместо этого, до того map, rejectчто вы не хотите или selectчто вы хотите:

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

Я считаю использование compactуборки беспорядком последней попыткой избавиться от вещей, с которыми мы не справились правильно, обычно потому, что мы не знали, что нас ждет. Мы всегда должны знать, какие данные используются в нашей программе; Неожиданные / неизвестные данные плохие. Каждый раз, когда я вижу nils в массиве, над которым я работаю, я копаюсь, почему они существуют, и вижу, могу ли я улучшить код, генерирующий массив, вместо того, чтобы позволить Ruby тратить время и память на генерацию nils, а затем просеивать через массив, чтобы удалить их позже.

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

29
Теперь это рубиновый эск!
Кристоф Маруа

4
Зачем это? ОП должен удалять nilзаписи, а не пустые строки. Кстати, nilне то же самое, что пустая строка.
Жестянщик

9
Оба решения повторяются дважды по коллекции ... почему бы не использовать reduceили inject?
Зигги

4
Не похоже, что вы читаете вопрос ОП или ответ. Вопрос в том, как удалить nils из массива. compactэто самый быстрый способ, но на самом деле правильное написание кода в начале избавляет от необходимости полностью разбираться с nils.
Жестянщик

3
Я не согласен! Вопрос «Карта и удалить нулевые значения». Ну, чтобы отобразить и удалить нулевые значения, это уменьшить. В их примере OP отображает и затем выбирает nils. Вызов map, а затем сжатие, или выбор, а затем map, означает совершение той же ошибки: как вы указали в своем ответе, это запах кода.
Зигги

96

Попробуйте использовать reduceили inject.

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

  memo
}

Я согласен с принятым ответ , что мы не должны mapи compact, но не по тем же причинам.

Я чувствую глубоко внутри, что mapтогда compactэквивалентно selectтогда map. Рассмотрим: mapэто функция «один к одному». Если вы отображаете из некоторого набора значений и вас map, то вы хотите одно значение в выходном наборе для каждого значения во входном наборе. Если вам нужно заранее select, то вы, вероятно, не хотите mapна съемочной площадке. Если вам придется selectпотом (или compact), то вы, вероятно, не хотите mapна съемочной площадке. В любом случае вы дважды выполняете итерацию по всему набору, когда a reduceнужно пройти только один раз.

Кроме того, на английском языке вы пытаетесь «сократить набор целых чисел в набор четных целых чисел».


4
Бедный Зигги, нет любви к твоему предложению. лол. плюс один, у кого-то есть сотни голосов!
DDDD

2
Я считаю, что однажды, с вашей помощью, этот ответ превзойдет принятый ответ. ^ o ^ //
Зигги

2
+1 текущий принятый ответ не позволяет вам использовать результаты операций, которые вы выполнили в течение выбранной фазы
chees

1
итерации по перечисляемым структурам данных дважды, если требуется только проход, как в принятом ответе, кажутся расточительными. Таким образом, уменьшите количество проходов с помощью метода уменьшить! Спасибо @Ziggy
Sebisnow

Это правда! Но сделать два прохода над набором из n элементов - все равно O (n). Если ваша коллекция не настолько велика, что она не помещается в ваш кеш, выполнение двух проходов, вероятно, будет хорошо (я просто думаю, что это более элегантно, выразительно и с меньшей вероятностью приведет к ошибкам в будущем, например, когда выпадет цикл не синхронизировано). Если вам нравится делать вещи за один проход, вам может быть интересно узнать о преобразователях! github.com/cognitect-labs/transducers-ruby
Зигги

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

Если вы хотите, чтобы более слабый критерий отклонения, например, отклонял как пустые строки, так и ноль, вы можете использовать:

[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
.blank? доступно только в рельсах.
ewalk

Для дальнейшего использования, поскольку blank?он доступен только в рельсах, мы можем использовать его, items.reject!(&:nil?) # [1, nil, 3, nil, nil] => [1, 3]который не связан с рельсами. (не исключая пустых строк или нулей)
Fotis

27

Определенно compactэто лучший подход для решения этой задачи. Тем не менее, мы можем достичь того же результата просто с помощью простого вычитания:

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

4
Да, установленное вычитание будет работать, но оно примерно вдвое быстрее из-за накладных расходов.
Жестянщик

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

Еще один способ сделать это будет, как показано ниже. Здесь мы используем Enumerable#each_with_objectдля сбора значений и используем, Object#tapчтобы избавиться от временной переменной, которая в противном случае необходима для nilпроверки результата process_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, не ясно, какова цель ввода xв этот метод. Если я предполагаю, что вы собираетесь обработать значение x, передав его несколько, urlи определить, какие из них xдействительно обрабатываются в допустимые отличные от нуля результаты - тогда, возможно, будет 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]
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.