Введение метода 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.

Изначально опубликовано в Альфредо Мотта.