3

I have Hash like this, representing a data tree

hash = {
    'key1' => {
         'sub1' => 1,
         'sub2' => 2
    },
    'key2' => 3
}

I want to explore the tree with an array of keys representing a path. Some exemples :

with a simple path:

keys = ['key2']

I want to get 3

with this path:

keys = ['key1', 'sub1']

I want to get 1

with invalide path :

keys = ['key1', 'sub1', 'blabla']
keys = ['key1', 'blabla']

get nil

etc ... etc ... you get the idea

ndnenkov
  • 35,425
  • 9
  • 72
  • 104
ijani
  • 53
  • 1
  • 3
  • I know know why this was down voted... but it is a duplicate. –  Mar 04 '12 at 01:18
  • possible duplicate of [How do you access nested elements of a hash with a single string key?](http://stackoverflow.com/questions/6672007/how-do-you-access-nested-elements-of-a-hash-with-a-single-string-key) (note the `to_sym` stuff should not be used in the case presented above as the keys are strings, but it's the same idea) –  Mar 04 '12 at 01:20

5 Answers5

7
keys.inject(hash) {|acc, value| acc[value]}
Dominik Honnef
  • 17,937
  • 7
  • 41
  • 43
4

Doesn't do any error checking, but

h = {'k1' => {'s1' => 1, 's2' => 2}, 'k2' => 3}
ks = ['k1', 's1']
ks.inject(h){|hh, k| hh[k]}  # => 1
['k1', 's2'].inject(h){|hh, k| hh[k]} # => 2
['k2'].inject(h){|hh, k| hh[k]} # => 3
dantswain
  • 5,427
  • 1
  • 29
  • 37
2

Better keep object in mind so let's add a feature to Hash class

# in intialize or whatever
class Hash
  def find_path path #recursive
    key = path.first
    if _path.size == 1 # end of path => return value
      [key]
    elsif [key].kind_of?(Hash) # continue
      [key].find_path path[1..-1]
    else # have to continue but not a has => out
      nil
    end
  end

  def find_path path # not recursive
    _path = path.clone #copy to shift
    _tree = self #start with self
    while(_path.size > 1 && _tree) do #while not the end and _tree 
      _v = _tree[_path.shift]
      _tree = _v.kind_of?(Hash) ? _v : nil
    end
    _tree ? _tree[_path.first] : nil
  end
end

This way :

hash = {:v1 => {:v1.1 => "yes", :v1.2 => "false"}}
hash.find_path [:v1, :v1.1]
# => "yes"
hash.find_path [:v1]
# => {:v1.1 => "yes", :v1.2 => "false"}
hash.find_path [:v2]
# => nil
hash.find_path [:v1, :v1.3]
# => nil
ndnenkov
  • 35,425
  • 9
  • 72
  • 104
ProxyGear
  • 835
  • 1
  • 10
  • 26
  • The first one blows up for me (you access _path but never define it) The second one does not return nil if invoked without args on an array with a key of nil `{nil => 1}.find_path [] # => 1` – Joshua Cheek Mar 04 '12 at 04:44
  • 1
    Other than that, though, this is the right approach (though monkey patching hash is questionable). – Joshua Cheek Mar 04 '12 at 04:45
  • I didn't check the code because I juste gave an idea, not code to copy paste :D Joshua Cheek, instead of monkey patch what would you advise to do ? – ProxyGear Mar 04 '12 at 12:24
  • You can see in the solution I added. – Joshua Cheek Mar 05 '12 at 04:47
1

Here is my solution (it is similar to proxygear's, but it does not monkey patch Hash, and it behaves correctly when invoked with no args). I think it is better than the two inject based solutions because they will blow up if you try to access paths that DNE.

class TraversibleHash
  attr_accessor :hash

  def initialize(hash)
    self.hash = hash
  end

  def access(*keys)
    recursive_access keys, hash
  end

private

  def recursive_access(keys, hash)
    return nil if keys.empty?
    element = hash.fetch(keys.shift) { return nil }
    return element if keys.empty?
    return nil unless element.kind_of? Hash # <-- should maybe raise an error?
    recursive_access keys, element
  end
end

Here is the test suite:

describe TraversibleHash do
  let(:traversible) { TraversibleHash.new 'key1' => { 'sub1' => 1, 'sub2' => 2 }, 'key2' => 3, nil => 1 }

  it 'returns nil when invoked without args' do
    traversible.access.should be_nil
  end

  it 'returns nil when accessing nonexistent keys' do
    traversible.access('key3').should be_nil
    traversible.access('key2', 'key3').should be_nil
    traversible.access('key1', 'sub3').should be_nil
    traversible.access('key1', 'sub2', 'subsub1').should be_nil
    traversible.access('abc', 'def', 'ghi').should be_nil
  end

  it 'finds toplevel keys' do
    traversible.access('key2').should == 3
    traversible.access('key1').should == {'sub1' => 1, 'sub2' => 2}
  end

  it 'traverses nested hashes to find nested keys' do
    traversible.access('key1', 'sub1').should == 1
    traversible.access('key1', 'sub2').should == 2
    TraversibleHash.new(1=>{2=>{3=>4}}).access(1, 2, 3).should == 4
  end
end
Joshua Cheek
  • 30,436
  • 16
  • 74
  • 83
0

From Ruby 2.3.0 onward, you can use Hash#dig, which does exactly that:

hash.dig(*keys)

You can use the ruby_dig gem until you migrate.

ndnenkov
  • 35,425
  • 9
  • 72
  • 104