Иногда надо выбрать из массива элементы по определнному условию.
Например, все положительные числа:
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?(*'а'..'я') }
# => ["зенит"]
Как это часто бывает в руби, у метода 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
# => ["Александрович", "Андреевич", "Михайлович", "Сергеевич"]
Фильтровать можно не только массивы, но и другие коллекции, например, диапазоны (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]
Хэши (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"]
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]
Теперь вы умеете выбирать нужные элементы из массивов, диапазонов и хэшей как настоящие рубисты.