Accessing Nested Hashes Directly

Discussion in 'Ruby' started by atraver@gmail.com, Sep 26, 2006.

  1. Guest

    I've been having a problem for a while with Ruby where accessing a hash
    within a hash will fail if the first hash is nil. As an example, Rails
    accepts XML input to an action by passing it through REXML. On an XML
    document like this:

    <body>
    <user>
    <name>
    <given>Tony</given>
    <family>Robbins</family>
    </name>
    </user>
    </body>

    You will receive a hash that looks like this (within the params hash):

    "body" => { "user" => { "name" => { "given" => "Tony", "family" =>
    "Robbins" } } }

    Thus, you can access the elements like this:

    params["body"]["user"]["name"]["given"]

    However, sometimes the "name" element might not come over the wire
    (through no fault of Ruby's nor Rails' -- it's just a problem we have
    to deal with in our system). In this case, Ruby will throw a
    NoMethodError on nil.[] because we tried to access the value at "given"
    on an element that does not exist. Furthermore, sometimes the "user"
    element does not exist, either! This led me to code that looks like
    this:

    if params["body"]["user"]
    if params["body"]["user"]["name"]
    first_name = params["body"]["user"]["name"]["given"]
    end
    end

    That's not very elegant when you're doing it with many (15 - 30)
    fields. I also tried doing it with exceptions:

    begin
    first_name = params["body"]["user"]["name"]["given"]
    rescue NoMethodError
    end

    Also, not very elegant.

    Finally, I wrote a method to work around this problem:

    def get_value(object, *path)
    new_object = object

    for item in path
    item = item.to_s

    if new_object
    if new_object[item]
    new_object = new_object[item]
    else
    new_object = nil
    end
    else
    break
    end
    end

    if new_object.kind_of?(String)
    return new_object.strip
    else
    return new_object
    end
    end

    This method can be called like this:

    first_name = get_value(params, :body, :user, :name, :given)

    It will traverse the hash and kick back a nil at the first problem it
    finds instead of raising an exception, or will return the value if it
    actually exists.

    Here's my question, though: is this code efficient? Is there a better
    way? Am I missing something fundamental in Ruby that would solve this
    without the need for the new method?
     
    , Sep 26, 2006
    #1
    1. Advertising

  2. Ross Bamford Guest

    On Wed, 2006-09-27 at 02:55 +0900, wrote:
    > I've been having a problem for a while with Ruby where accessing a hash
    > within a hash will fail if the first hash is nil.
    > [...snipped...]
    > Finally, I wrote a method to work around this problem:
    >
    > def get_value(object, *path)
    > new_object = object
    >
    > for item in path
    > item = item.to_s
    >
    > if new_object
    > if new_object[item]
    > new_object = new_object[item]
    > else
    > new_object = nil
    > end
    > else
    > break
    > end
    > end
    >
    > if new_object.kind_of?(String)
    > return new_object.strip
    > else
    > return new_object
    > end
    > end
    >
    > This method can be called like this:
    >
    > first_name = get_value(params, :body, :user, :name, :given)
    >
    > It will traverse the hash and kick back a nil at the first problem it
    > finds instead of raising an exception, or will return the value if it
    > actually exists.
    >
    > Here's my question, though: is this code efficient? Is there a better
    > way? Am I missing something fundamental in Ruby that would solve this
    > without the need for the new method?


    I think you can rewrite that method like this:

    def get_value(hash, *path)
    path.inject(hash) { |obj, item| obj[item] || break }
    end

    If you're up for some core-modifying mayhem, You could put it on Hash,
    and extend [] with 'path' capabilities:

    class Hash
    alias :__fetch :[]

    def traverse(*path)
    path.inject(self) { |obj, item| obj.__fetch(item) || break }
    end

    def [](*args)
    (args.length > 1) ? traverse(*args) : __fetch(*args)
    end
    end

    This way works like this:

    h = {
    :name => {
    :first => 'Ross',
    },
    :contact => {
    :phone => {
    :eek:ffice => '345345'
    }
    }
    }

    p h.traverse:)name, :first)
    # => "Ross"

    p h.traverse:)name, :middle)
    # => nil

    p h[:contact]
    # => {:phone=>{:eek:ffice=>"345345"}}

    p h[:contact, :phone, :eek:ffice]
    # => "345345"

    p h[:contact, :phone, :fax]
    # => nil

    Of course, there may be better ways to solve the underlying problem...

    --
    Ross Bamford -
     
    Ross Bamford, Sep 26, 2006
    #2
    1. Advertising

  3. Verno Miller Guest

    > Posted by Adam Traver
    > on 26.09.2006 19:57


    > I've been having a problem for a while with Ruby where accessing a hash
    > within a hash will fail if the first hash is nil.
    >
    > ...
    >
    > You will receive a hash that looks like this (within the params hash):
    >
    > "body" => { "user" => { "name" => { "given" => "Tony", "family" => "Robbins" } } }
    >
    >Thus, you can access the elements like this:
    >
    > params["body"]["user"]["name"]["given"]
    >
    > ...
    >
    > Here's my question, though: is this code efficient? Is there a better
    > way? Am I missing something fundamental in Ruby that would solve this
    > without the need for the new method?


    Yet another (though indirect) approach could be to first extract all
    keys from the nested hash!

    http://bigbold.com/snippets/posts/show/1908





    --
    Posted via http://www.ruby-forum.com/.
     
    Verno Miller, Sep 27, 2006
    #3
  4. Guest

    Thanks for all the responses.

    Here is the method I ended up with after looking over everything, just
    in case this topic is searched for in the future:

    def get_value(object, *path)
    for item in path
    break unless object
    object = object ? object[item.to_s] : nil
    end

    return object.kind_of?(String) ? object.strip : object
    end

    This might be a little more verbose than some of the solutions (also,
    Ross, your "inject" solution didn't seem to work out although I'm sure
    it would with some tweaking). However, it ends up doing exactly what I
    need it to do: get the value or return nil, in a quick-and-dirty way.

    -Adam
     
    , Sep 27, 2006
    #4
  5. On 9/26/06, <> wrote:
    > I've been having a problem for a while with Ruby where accessing a hash
    > within a hash will fail if the first hash is nil. As an example, Rails
    > accepts XML input to an action by passing it through REXML. On an XML
    > document like this:
    >
    > <body>
    > <user>
    > <name>
    > <given>Tony</given>
    > <family>Robbins</family>
    > </name>
    > </user>
    > </body>
    >
    > You will receive a hash that looks like this (within the params hash):
    >
    > "body" => { "user" => { "name" => { "given" => "Tony", "family" =>
    > "Robbins" } } }
    >
    > Thus, you can access the elements like this:
    >
    > params["body"]["user"]["name"]["given"]
    >
    > However, sometimes the "name" element might not come over the wire
    > (through no fault of Ruby's nor Rails' -- it's just a problem we have
    > to deal with in our system). In this case, Ruby will throw a
    > NoMethodError on nil.[] because we tried to access the value at "given"
    > on an element that does not exist. Furthermore, sometimes the "user"
    > element does not exist, either! This led me to code that looks like
    > this:
    >
    > if params["body"]["user"]
    > if params["body"]["user"]["name"]
    > first_name = params["body"]["user"]["name"]["given"]
    > end
    > end
    >
    > That's not very elegant when you're doing it with many (15 - 30)
    > fields. I also tried doing it with exceptions:
    >
    > begin
    > first_name = params["body"]["user"]["name"]["given"]
    > rescue NoMethodError
    > end


    What about:
    first_name = params["body"]["user"]["name"]["given"] rescue nil

    Hadley
     
    hadley wickham, Sep 27, 2006
    #5
  6. Ross Bamford Guest

    On Thu, 2006-09-28 at 02:05 +0900, wrote:
    > Thanks for all the responses.
    >
    > Here is the method I ended up with after looking over everything, just
    > in case this topic is searched for in the future:
    >
    > def get_value(object, *path)
    > for item in path
    > break unless object
    > object = object ? object[item.to_s] : nil
    > end
    >
    > return object.kind_of?(String) ? object.strip : object
    > end
    >
    > This might be a little more verbose than some of the solutions (also,
    > Ross, your "inject" solution didn't seem to work out although I'm sure
    > it would with some tweaking). However, it ends up doing exactly what I
    > need it to do: get the value or return nil, in a quick-and-dirty way.


    Hmm, out of interest, what didn't work out about it? Maybe I
    misunderstand what you're doing, but (if you ignore the Hash-extending
    silliness) it seems to work the same.

    require 'test/unit'

    def get_value(object, *path)
    for item in path
    break unless object
    object = object ? object[item.to_s] : nil
    end

    return object.kind_of?(String) ? object.strip : object
    end

    def my_get_value(hsh, *path)
    path.inject(hsh) { |obj, item| obj[item.to_s] || break }
    end

    class TestGetValue < Test::Unit::TestCase
    def setup
    @h = {
    'name' => {
    'first' => 'Ross'
    },
    'contact' => {
    'phone' => {
    'office' => '345345'
    }
    }
    }
    end

    def test_it
    assert_equal get_value(@h, :name, :first),
    my_get_value(@h, :name, :first)
    assert_equal get_value(@h, :name, :middle),
    my_get_value(@h, :name, :middle)
    assert_equal get_value(@h, :contact),
    my_get_value(@h, :contact)
    assert_equal get_value(@h, :contact, :phone, :eek:ffice),
    my_get_value(@h, :contact, :phone, :eek:ffice)
    end
    end

    # -> 1 tests, 4 assertions, 0 failures, 0 errors


    The only thing I changed here is adding to_s on the item in my version,
    so I could use the same hash to test both while keeping symbol keys.

    --
    Ross Bamford -
     
    Ross Bamford, Sep 27, 2006
    #6
  7. John Turner Guest

    wrote:
    > This method can be called like this:
    >
    > first_name = get_value(params, :body, :user, :name, :given)
    >
    > It will traverse the hash and kick back a nil at the first problem it
    > finds instead of raising an exception, or will return the value if it
    > actually exists.
    >
    > Here's my question, though: is this code efficient? Is there a better
    > way? Am I missing something fundamental in Ruby that would solve this
    > without the need for the new method?
    >


    I didn't really like the way that method is called, doesn't seem very
    "ruby" exactly. Came up with the following which is really dirty at the
    moment but I prefer how it's called. The method_missing in NilClass is
    kind of unforgivably dirty. Would like to see improvements I guess, but
    not sure there's really a way to do this nicely:

    So, yeah, don't use this, but possibly be amused by it anyway:

    require 'ostruct'
    class NestStruct < OpenStruct
    def self.fromNestedHash(h)
    struct = NestStruct.new(h)
    struct.marshal_dump.keys.each do |key|
    if struct.send(key).is_a? Hash
    struct.send(key.to_s + '=', fromNestedHash(struct.send(key)))
    end
    end
    return struct
    end

    def method_missing(m, *args)
    super(m, args)
    rescue NoMethodError
    nil
    end
    end

    class NilClass
    def method_missing(m, *args)
    nil
    end
    end

    h = {
    'name' => {
    'first' => 'Ross'
    },
    'contact' => {
    'phone' => {
    'office' => '345345'
    }
    }
    }

    s = NestStruct.fromNestedHash(h)
    puts s.contact.phone # => #<NestStruct office="345345">
    puts s.contact.phone.office # => 345345
    puts s.contact.phone.home # => nil
    puts s.foo.bar # => nil
     
    John Turner, Sep 29, 2006
    #7
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Ben Holness

    Hashes of Hashes via subs

    Ben Holness, Oct 5, 2003, in forum: Perl
    Replies:
    8
    Views:
    582
    Ben Holness
    Oct 8, 2003
  2. Steven Arnold

    using hashes as keys in hashes

    Steven Arnold, Nov 23, 2005, in forum: Ruby
    Replies:
    3
    Views:
    179
    Mauricio Fernández
    Nov 23, 2005
  3. michael greenly

    turn nested hashes into nested ostructs

    michael greenly, Dec 31, 2007, in forum: Ruby
    Replies:
    0
    Views:
    170
    michael greenly
    Dec 31, 2007
  4. Tim O'Donovan

    Hash of hashes, of hashes, of arrays of hashes

    Tim O'Donovan, Oct 27, 2005, in forum: Perl Misc
    Replies:
    5
    Views:
    229
  5. Replies:
    3
    Views:
    223
Loading...

Share This Page