Як відобразити і видалити нульові значення в 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.