Метод select

Иногда надо выбрать из массива элементы по определнному условию.

Например, все положительные числа:

a = [-2, -1, 0, 1, 2]

a.select { |n| n > 0 }
# => [1, 2]

У массивов в руби есть метод select (от англ. выбирать / отбирать), он возвращает новый массив, в который попадут только нужные элементы.

Работает это так: select берет каждый элемент в массиве и передает его в блок. Если блок возвращает true, то этот элемент остается, если же блок возвращает false — нет.

Напомним, что в Ruby все, что не является false или nil вычисляется как true (истинноподобное значение).

Еще раз select — как шлагбаум на дороге. Он пропускает только те элементы для которых его блок дает «добро».

В верхнем примере сравнение с помощью оператора > можно заменить на вызов метода positive? у числа, тогда код можно будет сильно упростить:

a.select { |n| n.positive? }
# => [1, 2]

a.select(&:positive?)
# => [1, 2]

В последнем случае блок не задан явно, а сгенерирован с помощью шортката. Про создание блока через шорткат у нас есть две статьи: попроще и посложнее.

Вот более практический пример. Найдем футбольные команды чьи названия забыли капитализировать (написать с заглавной буквы):

teams = ['ЦСКА', 'зенит', 'Динамо', 'Спартак']

teams.select { |team| team.start_with?(*'а'..'я') }
# => ["зенит"]

Метод reject

Как это часто бывает в руби, у метода select есть антагонист, который делает ровно обратное — метод reject (англ. отклонять / забраковывать).

Метод reject наоборот пропускает в конечный массив только те значения для которых его блок возвращает false или nil.

a = [-2, -1, 0, 1, 2]

a.reject { |n| n < 0 }
# => [0, 1, 2]

a.reject { |n| n.negative? }
# => [0, 1, 2]

a.reject(&:negative?)
# => [0, 1, 2]

И еще один практический пример. Мы прочитали данные из файла с пользователями и у кого-то есть отчество, а у кого-то нет. Мы хотим получить список всех отчеств (без nil):

patronimic_names =
  [
    'Александрович',
    'Андреевич',
    nil,
    'Михайлович',
    nil,
    'Сергеевич'
  ]

patronimic_names.reject { |p_name| p_name.nil? }
patronimic_names.reject(&:nil?)
# => ["Александрович", "Андреевич", "Михайлович", "Сергеевич"]

Подобный случай такой распространенный, что для него создали отдельный метод у массива:

patronimic_names.compact
# => ["Александрович", "Андреевич", "Михайлович", "Сергеевич"]

select / reject для диапазонов

Фильтровать можно не только массивы, но и другие коллекции, например, диапазоны (Range), но в ответ select и reject все равно вернут массив:

# Четные числа от 1 до 10
(1..10).select { |n| n.even? }
# => [2, 4, 6, 8, 10]

# Нечетные числа от 1 до 10
(1..10).reject { |n| n.even? }
# => [1, 3, 5, 7, 9]

select / reject для хэшей

Хэши (Hash) можно фильтровать как по ключу (key), так и по значению (value). В ответ возвращаются новые хэши.

Фильтруем маршруты автобусов (ключ — номер автобуса, значение — длина маршрута).

routes_with_lengths =
  {
    '5' => 3.25,
    '8' => 1.2,
    '10к' => 2.6,
    '10'  => 3.8,
    '25'  => 3.6,
    '25д' => 5.9
  }

# Все десятки
routes_with_lengths.select { |route, length| route.match(/10/) }
# => {"10к"=>2.6, "10"=>3.8}

# Маршруты короче 3.5км
routes_with_lengths.reject { |route, length| length > 3.5 }
# => {"5"=>3.25, "8"=>1.2, "10к"=>2.6}

# Только ключи маршрутов короче 3.5км
routes_with_lengths.reject { |route, length| length > 3.5 }.keys
# => ["5", "8", "10к"]

Это все возможно, т.к. классы Hash и Range включают модуль Enumerable в котором и определен метод select. Очевидно, любой другой класс, который включает в себя этот модуль, также будет обладать методами select и reject.

Выбор на основе индекса элемента

Иногда необходимо отобрать элементы не по их значению, а по их индексу в коллекции. Для этого сразу после вызова метода select (еще до передачи блока) можно по цепочке вызвать метод with_index и передать ему блок уже с двумя параметрами — для элемента и для индекса.

Давайте получим каждую пятую букву алфавита:

alphabet = ('A'..'Z')

alphabet.select.with_index do |letter, index|
  (index + 1) % 5 == 0
end

# => ["E", "J", "O", "T", "Y"]

Как видите блок принимает решение не на основе значения элемента, а на основе его индекса в коллекции.

В блоке индекс пришлось увеличить на 1, т.к. в программировании все индексы начинаются с 0, а в общечеловеческом понимании — с единицы. Но в Ruby об этом позаботились и в метод with_index в качестве параметра (до блока) можно передать число, с которого надо начинать считать индексы:

('A'..'Z').select.with_index(1) { |letter, index| index % 5 == 0 }
# => ["E", "J", "O", "T", "Y"]

В нашем случае первым параметром в блоке идет элемент массива, который мы кладем в переменную letter. Но мы эту переменную в блоке никак не используем, однако передать ее надо из-за порядка следования параметров.

В этом случае ее принято называть просто символом подчеркивания:

('A'..'Z').select.with_index(1) { |_, index| index % 5 == 0 }
# => ["E", "J", "O", "T", "Y"]

Методы filter, find_all

Ruby — язык программирования для человека. Поэтому нельзя просто взять и придумать всего одно название для метода. Кому-то для выбора нужных элементов коллекции может показаться более логичным названия filter (типа «отфильтровать» элементы массива) или find_all (типа «найти все» элементы массива по критерию).

Поэтому в Ruby у select есть не только антагонист, но и два брата-близнеца: filter и find_all, которые делают ровно то же самое:

a = [-2, -1, 0, 1, 2]

a.filter { |n| n.positive? }
# => [1, 2]

a.find_all { |n| n.positive? }
# => [1, 2]

Теперь вы умеете выбирать нужные элементы из массивов, диапазонов и хэшей как настоящие рубисты.

Изучаешь руби?

Заходи в дружелюбный чат, задавай вопросы, делись опытом.

Присоединиться