Nested hash defined?() [duplicate]
Asked Answered
C

6

3

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)

Courser answered 28/7, 2010 at 18:10 Comment(0)
K
6

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
Klansman answered 29/7, 2010 at 4:36 Comment(0)
W
4

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]
Weider answered 29/7, 2010 at 4:52 Comment(2)
x = {a:{b:{c:3}}} defined?(x[:z]) # => "method"Headmistress
@hash = {:key1 => true} @hash && @hash[:key1] && @hash[:key1][:key2] # => NoMethodError (undefined method []' for true:TrueClass)`Headmistress
L
2

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)
Long answered 28/7, 2010 at 18:57 Comment(0)
I
0
@hash[:key1].has_key? :key2
Inadequate answered 28/7, 2010 at 18:13 Comment(3)
This will raise NoMethodError: undefined method has_key?' for nil:NilClass` if @hash doesn't have a value for :key1Long
@Wayne yep, fetch is one of the approaches I suggested in my answer. See below (or above depending on SO's randomizer!)Long
@mikej, You're right. Sorry for being redundant. I've nuked my comment, since it was not too useful.Arsenate
D
0

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)
Drusy answered 28/7, 2010 at 23:54 Comment(1)
Abusing exceptions for normal program flow is a code smell, afaik.Klansman
S
0

Another option, one that I just discovered, is to extend Hash with a seek method. Technique comes from Corey O'Daniel.

Stick this in an initializer:

class Hash
  def seek(*_keys_)
    last_level    = self
    sought_value  = nil

    _keys_.each_with_index do |_key_, _idx_|
      if last_level.is_a?(Hash) && last_level.has_key?(_key_)
        if _idx_ + 1 == _keys_.length
          sought_value = last_level[_key_]
        else                   
          last_level = last_level[_key_]
        end
      else 
        break
      end
    end

    sought_value
  end 
end

Then just call:

@key_i_need = @hash.seek :one, :two, :three

You'll get the value, or nil if it doesn't exist.

Sympathy answered 29/11, 2012 at 2:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.