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


361

У мене є a, 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

Але це не здається ідіоматичним. Чи є приємний спосіб відобразити функцію за списком, видаляючи / виключаючи нулі під час руху?


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]

У вашому випадку, як блок оцінює фальсифікацію, просто:

items.filter_map { |x| process_x url }

" Ruby 2.7 додає численні # 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

Так, це приємна річ про мови, які мають основні команди, які слухають.
Олов'яна людина

Приємний жест рекомендувати змінити вибрані відповіді, але це трапляється рідко. ТАК не надає галочки, щоб нагадувати людям, і люди зазвичай не переглядають старі запитання, якщо вони не заявляють, що там було активне. В якості бічної панелі я рекомендую подивитися на Fruity для орієнтирів, оскільки це набагато менш хитро і полегшує проведення розумних тестів.
Олов'яний чоловік

930

Ви можете використовувати compact:

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

Я хотів би нагадати людям, що якщо ви отримуєте масив, що містить нулі як вихід 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прибирання безладу як зусилля останнього канаву, щоб позбутися речей, з якими ми не впоралися правильно, як правило, тому, що ми не знали, що нас чекає. Ми завжди повинні знати, які дані перекидаються в нашій програмі; Несподівані / невідомі дані погані. Щоразу, коли я бачу нулі в масиві, над яким я працюю, я переконуюсь, чому вони існують, і побачу, чи можу я вдосконалити код, що генерує масив, а не дозволяти Рубі витрачати час і пам’ять, генеруючи нулі, а потім просіювати масив для видалення їх пізніше.

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

29
Тепер це ruby-esque!
Крістоф Маруа

4
Навіщо це робити? В ОП потрібно знімати nilзаписи, а не порожні рядки. BTW - nilце не те саме, що порожня рядок.
Олов'яний чоловік

9
Обидва рішення повторюються двічі над колекцією ... чому б не використовувати reduceабо inject?
Ziggy

4
Це не здається, що ви читаєте питання ОП або відповідь. Питання в тому, як видалити нулі з масиву. compactнайшвидший, але насправді правильно написання коду на початку знімає потребу повністю розібратися з нулями.
Олов'яна людина

3
Я не погоджуюсь! Питання - "Зіставити та вилучити нульові значення". Ну а відображення та видалення нульових значень - це зменшення. У їх прикладі карта ОП відображає, а потім вибирає нулі. Виклик карти, а потім компактний або вибір, а потім картографування, означає однакову помилку: як ви вказуєте у своїй відповіді, це запах коду.
Ziggy

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набору. У будь-якому випадку ви повторюєте двічі протягом усього набору, коли потрібно reduceлише один раз пройти.

Також англійською мовою ви намагаєтеся "зменшити набір цілих чисел до набору парних цілих чисел".


4
Бідний Зіггі, любові до вашої пропозиції. Лол. плюс один, хтось ще має сотні оновлень!
DDDD

2
Я вірю, що одного дня з вашою допомогою ця відповідь перевершить прийняту. ^ o ^ //
Ziggy

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

1
повторення численних структур даних двічі, якщо потрібен лише пропуск, як у прийнятій відповіді, здається марним. Таким чином зменшіть кількість проходів за допомогою зменшення! Дякуємо @Ziggy
sebisnow

Це правда! Але робити два проходи над колекцією з n елементів все одно O (n). Якщо ваша колекція не настільки велика, що не вміщується у вашому кеш-пам’яті, робити два проходи, мабуть, добре (я просто думаю, що це більш елегантно, виразніше і менше шансів призвести до помилок у майбутньому, коли, скажімо, петлі потрапляють не синхронізовано). Якщо вам також подобається робити речі за один прохід, вам може бути цікаво дізнатися про перетворювачі! github.com/cognitect-labs/transducers-ruby
Ziggy

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

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

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і визначивши, який з xs дійсно обробляється в дійсних ненульових результатах - то, можливо 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.