views:

71

answers:

5

What's the most concise way to determine if @hash[:key1][:key2] is defined, that does not throw an error if @hash or @hash[:key1] are nil?

defined?(@hash[:key1][:key2]) returns True if @hash[:key1] exists (it does not determine whether :key2 is defined)

A: 
@hash[:key1].has_key? :key2
Adrian
This will raise `NoMethodError: undefined method `has_key?' for nil:NilClass` if `@hash` doesn't have a value for `:key1`
mikej
@Wayne yep, `fetch` is one of the approaches I suggested in my answer. See below (or above depending on SO's randomizer!)
mikej
@mikej, You're right. Sorry for being redundant. I've nuked my comment, since it was not too useful.
Wayne Conrad
+1  A: 

Using Hash#fetch

You can use the Hash#fetch method with a default of {} so that it is safe to call has_key? even if the first level key doesn't exist. e.g.

!hash.nil? && hash.fetch(key1, {}).has_key?(key2)

Alternative

Alternatively you can use the conditional operator e.g.

!hash.nil? && (hash.has_key?(key1) ? hash[key1].has_key?(key2) : false)

i.e. if hash doesn't have key key1 then just return false without looking for the second level key. If it does have key1 then return the result of checking key1's value for key2.

Also, if you want to check that hash[key1]'s value has a has_key? method before calling it:

!hash.nil? && (hash.has_key?(key1) ? hash[key1].respond_to?(:has_key?) &&
   hash[key1].has_key?(key2) : false)
mikej
A: 

If you don't care about distinguishing nonexistent @hash[:key1][:key2] (at any of 3 levels) from @hash[:key1][:key2] == nil, this is quite clean and works for any depth:

[:key1,:key2].inject(hash){|h,k| h && h[k]}

If you want nil to be treated as existing, use this instead:

(hash[:key1].has_key?(:key2) rescue false)
taw
Abusing exceptions for normal program flow is a code smell, afaik.
Konstantin Haase
+1  A: 

When using ActiveSupport (Rails) or Backports, you can use try:

@hash[:key1].try(:fetch, :key2)

You could even handle @hash being nil:

@hash.try(:fetch, :key1).try(:fetch, :key2)

If you want @hash to always return a hash for a missing key:

@hash = Hash.new { |h,k| h[k] = {} }
@hash[:foo] # => {}

You could also define this recursive:

def recursive_hash
  Hash.new { |h,k| h[k] = recursive_hash }
end

@hash = recursive_hash
@hash[:foo][:bar][:blah] = 10
@hash # => {:foo => {:bar => {:blah => 10}}}

But to answer your question:

module HasNestedKey
  Hash.send(:include, self)
  def has_nested_key?(*args)
    return false unless sub = self[args.shift]
    return true if args.empty?
    sub.respond_to?(:has_nested_key?) and sub.has_nested_key?(*args)
  end
end

@hash.has_nested_key? :key1, :key2
Konstantin Haase
+1  A: 

Perhaps I am missing something, but if all you care about is concise...why not:

@hash && @hash[:key1] && @hash[:key1][:key2]

or if you want to save a few characters

@hash && (h = @hash[:key1]) && h[:key2]

if any part of this fails, it returns nil otherwise it returns the value associated with :key2 or true.

The reason the defined? returns true even if :key2 is not there is because it just checks whether the object you are referencing exists, which in that case is the method [] which is an alias for the method fetch which does exist on the hash @hash[:key1] but if that were to return nil, there is no fetch method on nil and it would return nil. That being said, if you had to go n deep into an embedded hash, at some point it would become more efficient to call:

defined?(@hash[:key1][:key2][:key3]) && @hash[:key1][:key2][:key3]
Geoff Lanotte