Введение метода Hash # dig 1 в Ruby 2.3 полностью изменило способ навигации по глубоко вложенным хешам. Например, с учетом этого хеша:
client = { details: { first_name: "Florentino", last_name: "Perez" }, addresses: { postcode: "SE1 9SG", street: London Bridge St, number: 32, city: "London", location: { latitude: 51.504382, longitude: -0.086279 } } }
чтобы получить широту и долготу, вам необходимо сделать следующее:
client[:addresses] && client[:addresses][:location] && client[:addresses][:location][:latitude]
что не только странно, но и многословно. Начиная с Ruby 2.3 вы, наконец, можете написать:
client.dig(:addresses, :location, :latitude)
что само собой разумеется потрясающе. Однако я начал задаваться вопросом, можно ли еще больше расширить идею Hash # dig, когда вам придется иметь дело с хэшами, в которых также есть массивы.
Когда Hash # dig недостаточно?
В предыдущем примере ключ адресов содержит хэш, но часто это мягкая гарантия. Это означает, что при наличии нескольких адресов вы можете вместо этого найти массив. Например:
client = { details: { first_name: "Florentino", last_name: "Perez" }, addresses: [ { type: "home", postcode: "SE1 9SG", street: "London Bridge St", number: 32, city: "London", location: { latitude: 51.504382, longitude: -0.086279 } }, { type: "office", postcode: "SW1A 1AA", street: "Buckingham Palace Road", number: nil, city: "London", location: { latitude: 51.5013673, longitude: -0.1440787 } } ] }
В чем проблема? Теперь не только адреса могут быть нулевыми или нет, но в зависимости от того, есть ли у пользователя один адрес или несколько адресов, значение ключа: addresses может быть хешем или массивом. Это часто является реальностью многих реальных хешей, и хотя мы можем утверждать, что структура данных неправильная, мы каким-то образом должны с этим справиться.
Простое решение
Предположим, мы хотим собрать все значения широты адресов наших клиентов. Хэш # dig не будет работать в этом случае просто потому, что не знает, что делать, как только массив найден:
puts client.dig(:addresses, :location, :latitude) # `dig': no implicit conversion of Symbol into Integer (TypeError)
Код должен будет получить ключ: address. Затем, если это хэш, используйте dig, чтобы получить: latitude из ключа местоположения. Если это массив, он должен перебирать все адреса и собирать: latitude из различных мест. Вот:
def get_offices_latitudes_for(client) addresses = client[:addresses] return [] if addresses.nil? if addresses.is_a? Hash [ addresses.dig(:location, :latitude) ] else addresses.map { |address| address.dig(:location, :latitude) } end end
И это три простых теста, которые доказывают, что это работает:
client_with_no_addresses = { details: { first_name: "Florentino", last_name: "Perez" }, addresses: nil } client_with_one_address = { details: { first_name: "Florentino", last_name: "Perez" }, addresses: { type: "home", postcode: "SE1 9SG", street: "London Bridge St", number: 32, city: "London", location: { latitude: 51.504382, longitude: -0.086279 } } } client_with_many_addresses = { details: { first_name: "Florentino", last_name: "Perez" }, addresses: [ { type: "home", postcode: "SE1 9SG", street: "London Bridge St", number: 32, city: "London", location: { latitude: 51.504382, longitude: -0.086279 } }, { type: "office", postcode: "SW1A 1AA", street: "Buckingham Palace Road", number: nil, city: "London", location: { latitude: 51.5013673, longitude: -0.1440787 } } ] } puts get_offices_latitudes_for(client_with_no_addresses).join(", ") # empty string puts get_offices_latitudes_for(client_with_one_address).join(", ") # 51.504382 puts get_offices_latitudes_for(client_with_many_addresses).join(", ") # 51.504382, 51.5013673
Представляем хеш # dig_and_collect
Не знаю, как вы, но я ненавижу предыдущий код. Как мы можем сделать это лучше? Вдохновленный Hash # dig, то, что нам здесь нужно, является хорошим вариантом по умолчанию для более глубокой навигации по нашему Hash, когда мы находим массив. Я твердо уверен, что хорошее решение - собрать все значения, точно так же, как мы это сделали в нашем наивном решении.
Возвращаясь к нашему примеру, когда у клиента нет адреса, код должен возвращать пустой массив, собирать нечего.
client_with_no_addresses.dig_and_collect(:addresses, :location, :latitude) # []
Сценарии, в которых у клиента есть только один адрес (ключ адресов - хэш) или несколько адресов (ключ адресов - массив), теперь должны быть простыми:
client_with_one_address.dig_and_collect(:addresses, :location, :latitude) # [51.504382] client_with_many_addresses.dig_and_collect(:addresses, :location, :latitude) # [51.504382, 51.5013673]
Реализация довольно проста и вы можете прочитать ее на Github. Мы обезьяны исправляем объект Hash, но я уверен, что это можно было бы сделать лучше. Предложения, связанные с усовершенствованиями Ruby, более чем приветствуются.
Решение рекурсивно проверяет, какой тип ключа вы пытаетесь получить, и в зависимости от типа рекурсивно вызывайте dig_and_collect либо непосредственно для хеша, либо для всех элементов в массиве, которые вы нашли на своем пути.
Остальной код со спецификациями доступен в этом репозитории Github, и мне бы очень хотелось услышать ваши отзывы.
Вывод
В этом блоге я представил Hash # dig_and_collect, простой служебный метод, который построен на основе Hash # dig, чтобы помочь вам перемещаться по сложным вложенным хешам. Я нашел это действительно удобным при работе с плохо спроектированными хэшами в дикой природе, но я чувствую, что это может быть полезно в других сценариях. Жду ваших мыслей.
Если вам понравился этот пост в блоге, вы также можете подписаться на меня в twitter.
Изначально опубликовано в Альфредо Мотта.