14

The Rails I18n library transforms a YAML file into a data structure that is accessible via a dotted path call using the t() function.

t('one.two.three.four')

Does anyone know how to do this with a Ruby Hash? Or is it only possible directly via a YAML object?

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
matsko
  • 21,895
  • 21
  • 102
  • 144

7 Answers7

25

Just split on a dot in the path and iterate over this to find the right hash?

path.split(".").inject(hash) { |hash, key| hash[key] }

Alternatively you can build a new hash by iterating recursively over the whole structure:

def convert_hash(hash, path = "")
  hash.each_with_object({}) do |(k, v), ret|
    key = path + k

    if v.is_a? Hash
      ret.merge! convert_hash(v, key + ".")
    else
      ret[key] = v
    end
  end
end
Mon ouïe
  • 948
  • 5
  • 6
13

Ruby 2.3 introduces the dig method that looks into nested arrays/hashes, it returns nil when no data is found.

For example:

test_data = {a: {b: {c: {d: 1}, e: 2}}}
path = 'a.b.c.d'.split('.').map(&:to_sym)
# path => [:a, :b, :c, :d]
test_data.dig(*path)

Of course if your nested use string keys, the to_sym step is not needed.

David Costa
  • 1,738
  • 18
  • 33
11

Yeah, I don't think that's built-in, anywhere else. But I use something like this in one of my projects:

class Hash
  def dig(dotted_path)
    parts = dotted_path.split '.', 2
    match = self[parts[0]]
    if !parts[1] or match.nil?
      return match
    else
      return match.dig(parts[1])
    end
  end
end

And then call it like

my_hash = {'a' => {'b' => 'a-b', 'c' => 'a-c', 'd' => {'e' => 'a-d-e'}}, 'f' => 'f'}
my_hash.dig('a.d.e') # outputs 'a-d-e' (by calling my_hash['a']['d']['e'])
chbrown
  • 11,865
  • 2
  • 52
  • 60
  • 2
    a similar method is now available in Ruby 2.3 and it's exactly called [dig](http://ruby-doc.org/core-2.3.0/Hash.html#method-i-dig) – David Costa Oct 12 '16 at 15:48
2

I would suggest taking a look at this gist:
https://gist.github.com/potatosalad/760726

It adds implode and explode methods to Hash object that transforms nested keys to single-level dotted path keys, and vice versa.

Rolands Bondars
  • 3,087
  • 1
  • 19
  • 8
2

There is a Gem too keypath-ruby

gem 'key_path', :git => 'https://github.com/nickcharlton/keypath-ruby.git'

Looking at the code (and guessing a little about what t is), it looks like you can do this:

t.value_at_keypath('one.two.three.four')
Nate
  • 12,963
  • 4
  • 59
  • 80
2

This code not only allows dot notation to traverse a Hash but also square brackets to traverse Arrays with indices. It also avoids recursion for efficiency.

class Hash

  def key_path(dotted_path)
    result = self
    dotted_path.split('.').each do |dot_part|
      dot_part.split('[').each do |part|
        if part.include?(']')
          index = part.to_i
          result = result[index] rescue nil
        else
          result = result[part] rescue nil
        end
      end
    end

    result
  end

end

Example:

a = {"b" => {"c" => [0, [1, 42]]}}
a.key_path("b.c[-1][1]") # => 42
Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
0

There is also HashDot.

HashDot allows dot notation syntax use on hashes. It is faster, and more traversable than an object created with OpenStruct.

a = {b: {c: {d: 1}}}
a.b.c.d => 1
steel
  • 11,883
  • 7
  • 72
  • 109