0

In a pure ruby script I have this:

result = JSON.parse result.body_str
count = result && result["ke1"] && result["ke1"]["key2"] && result["ke1"]["key2"]["key3"] && result["ke1"]["key2"]["key3"]["key4"] ? 
        result["key1"]["key2"]["key3"]["key4"].to_i : 
        123

Is there any way to simplify this?

ಠ_ಠ
  • 3,060
  • 28
  • 43
Incerteza
  • 32,326
  • 47
  • 154
  • 261

4 Answers4

6
count = result["key1"]["key2"]["key3"]["key4"].to_i rescue 123

if you want to make a private method for better readability, you could do

def count(result)
  result["key1"]["key2"]["key3"]["key4"].to_i 
rescue NoMethodError
  123
end

I add the NoMethodError to limit the errors that the rescue can swallow. Despite arguments about using exceptions for flow control, I prefer this for readability. In a small function or one liner, it technically doesn't even change the flow, as it all remains contained in one location.

If it is used inside a tight loop with millions of records, you may want to compare with other solutions using a profiler, but you have to make that call based on the actual usage. If this is used on a bit of code that may run 5 times a day, stick with what's easier to read and maintain.

DGM
  • 26,629
  • 7
  • 58
  • 79
  • Your answer was needed! What are your views about including specific exceptions that would be rescued? – Cary Swoveland Aug 31 '14 at 20:58
  • 1
    Can you edit your post, so I can change my downvote? I did not understand the op's code well enough when I voted. I apologize. – 7stud Sep 01 '14 at 03:30
  • Cary: good idea, I added it to the method version to only rescue NoMethodError. I think with the right name, the method makes it clear what the value is, and the rescue neatly provides the default value with a minimum of fuss. – DGM Sep 01 '14 at 14:32
2

I would write it like this, and put it in a module to be included as required.

Code

def value_at_deep_key(hash, path)
  path.each_with_index.reduce(hash) do |current, (segment, i) |
    case c = current[segment]
    when Hash then c
    else (i==path.size-1) ? (current.key?(segment) ? c : :NO_MATCH) : {}
    end
  end      
end

Examples

value_at_deep_key({a: {b: {c: "cat"}}}, [:a, :b, :c]) #=> "cat"
value_at_deep_key({a: {b: {c: false}}}, [:a, :b, :c]) #=> false
value_at_deep_key({a: {b: {c: nil}}},   [:a, :b, :c]) #=> nil
value_at_deep_key({z: {b: {c: "cat"}}}, [:a, :b, :c]) #=> :NO_MATCH
value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c]) #=> :NO_MATCH
value_at_deep_key({a: {b: {z: "cat"}}}, [:a, :b, :c]) #=> :NO_MATCH
value_at_deep_key({a: {b: {c: "cat"}}}, [:a, :b])     #=> {:c=>"cat"}
value_at_deep_key({a: {b: {c: "cat"}}}, [:a])         #=> {:b=>{:c=>"cat"}}
value_at_deep_key({z: {b: {c: "cat"}}}, [])           #=> {:z=>{:b=>{:c=>"cat"}}}
value_at_deep_key({z: {b: {c: "cat"}}}, [:a, :b, :c]) #=> :NO_MATCH
value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c]) #=> :NO_MATCH
value_at_deep_key({a: {b: {z: "cat"}}}, [:a, :b, :c]) #=> :NO_MATCH
value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c, :d]) #=> :NO_MATCH

One could then write:

val = value_at_deep_key(hash, path)
(val = 123) if (val == :NO_MATCH)

If the value for the last key could not be nil,

else (i==path.size-1) ? (current.key?(segment) ? c : :NO_MATCH) : {}

could be replaced with:

else (i==path.size-1) ? c : {}

in which case nil would be returned when there is no match.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Your code recognizes a lot of fine distinctions, which is impressive, but it changes what the original code does. For instance, if the found value is false, then the original code will throw an error on the to_i() call. Same with any values that are further nested Hashes. And, neither of us has achieved the ultimate goal of the op: to simplify the original code! – 7stud Aug 31 '14 at 21:38
  • @7stud, you have a valid point, but I made the assumption that this is what the Alexander wanted (though he probably wasn't worried about `nil` values), despite his code. Also, I thought this would be the sort of thing to put in a module, to be used more than once. – Cary Swoveland Aug 31 '14 at 21:49
  • Yeah, I guess if the key_sequence contains 100 keys, then the op might get writer's cramp with their method. – 7stud Aug 31 '14 at 21:51
0

I occasionally define a method like this

def value_at_deep_key hash, path, default=nil
  path.inject(hash) {|current,segment| current && current[segment]} || default
end

This uses inject to grab each level of the hash in turn. You would use like this

value_at_deep_key(result, %w(key1 key2 key3 key4), 123)

Personally I don't like the use of rescue for this sort of thing - it can mask errors.

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • Right. Exceptions aren't for lazy flow control, they're for handling errors. Of course you have to be careful with your `||` in general, works fine if the values are numbers but not so well if `false` is a valid value. – mu is too short Aug 31 '14 at 18:16
  • Quite - gets a lot fiddler if you need to accommodate those cases – Frederick Cheung Aug 31 '14 at 18:20
  • shouldn't it be `default: nil` instead? – Incerteza Aug 31 '14 at 18:29
  • and shouldn't you simplify this `path.inject({}) {|.... }` or `path.inject([]) {|...` because `hash` isn't needed there, is it? – Incerteza Aug 31 '14 at 18:32
  • Depends if you want to use keyword arguments or not – Frederick Cheung Aug 31 '14 at 18:33
  • I think this is the same thing -> `path.inject(default) {|current,segment| current && current[segment]}` – Incerteza Aug 31 '14 at 18:34
  • 1
    I don't think that works - that would mean that you're calculating (first invocation of the block) default[segment] – Frederick Cheung Aug 31 '14 at 18:36
  • Frederick, @7stud has demonstrated flaws in your solution. You must either fix what you have or withdraw your answer. I rarely downvote, but I will if you leave it unchanged. – Cary Swoveland Aug 31 '14 at 20:40
  • It depends what you think should happen if the key path is not as expected - I'd rather it raise an exception so that I know that the data didn't match my expectations. – Frederick Cheung Aug 31 '14 at 20:42
  • If you've made assumptions about the data, you need to say so in your answer. – Cary Swoveland Aug 31 '14 at 20:52
  • Depending on the situation, a rescue can be far more pragmatic – DGM Aug 31 '14 at 20:54
  • This answer produces faulty results: 1)`h = { 'key1' => {'key2' => {'key3' => {'key4' => 3}}} } p value_at_deep_key(h, %w[key1 key2 key3 key4], 123) --output:-- 3` 2)`h = { 'key1' => 1, 'key2' => 2, 'key3' => 3, 'key4' => 4, } p value_at_deep_key(h, %w[key1 key2 key3 key4], 123) --output:-- 1.rb:16:in '[]': no implicit conversion of String into Integer` – 7stud Sep 01 '14 at 01:12
  • `(TypeError) from 1.rb:16:in 'block in value_at_deep_key' from 1.rb:16:in 'each' from 1.rb:16:in 'inject' from 1.rb:16:in 'value_at_deep_key' from 1.rb:19:in '
    '` 3)`h = { 'key1' => {'key2' => {'key3' => 4}} } p value_at_deep_key(h, %w[key1 key2 key3 key4], 123) --output:-- 1.rb:16:in '[]': no implicit conversion of String into Integer (TypeError)`
    – 7stud Sep 01 '14 at 01:13
  • One of the downvotes was mine, for the reason I gave. – Cary Swoveland Sep 01 '14 at 04:25
0

The accepted answer doesn't work very well:

Here is the code:

def value_at_deep_key(hash, path, default=nil)
  path.inject(hash) {|current,segment| current && current[segment]} || default
end

Here are some results:

1)--------------------

h = {
  'key1' => {'key2' => {'key3' => {'key4' => 3}}}
}

p value_at_deep_key(h, %w[key1 key2 key3 key4], 123)

--output:--
3

2)--------------------

h = {
  'key1' => 1,
  'key2' => 2,
  'key3' => 3,
  'key4' => 4,
}

p value_at_deep_key(h, %w[key1 key2 key3 key4], 123)

--output:--
1.rb:16:in `[]': no implicit conversion of String into Integer (TypeError)
    from 1.rb:16:in `block in value_at_deep_key'
    from 1.rb:16:in `each'
    from 1.rb:16:in `inject'
    from 1.rb:16:in `value_at_deep_key'
    from 1.rb:19:in `<main>'

3)---------------------

h = {
  'key1' => {'key2' => {'key3' => 4}}
}

p value_at_deep_key(h, %w[key1 key2 key3 key4], 123)

--output:--
1.rb:16:in `[]': no implicit conversion of String into Integer (TypeError)

The following answer seems to work better:

def value_at_deep_key(hash, key_sequence, default=nil)
  return "No keys to lookup!" if key_sequence.empty?

  value = hash

  key_sequence.each do |key|
    case value
    when Hash
      value = value[key]
    else
      value = nil
      break
    end
  end

  value.nil? ? default : Integer(value)   #A found value of nil produces the default, which is
                                          #also the case when one of the keys doesn't exist in the Hash.
                                          #Because to_i() will silently convert a found string with no leading numbers to 0, 
                                          #use Integer() instead, which will throw a descriptive error when trying to convert any String(or Hash or Array) to an int.

end

--output:--
p value_at_deep_key({a: {b: {c: "cat"}}}, [:a, :b, :c], 123)  #=> `Integer': invalid value for Integer(): "cat" (ArgumentError)
p value_at_deep_key({a: {b: {c: false}}}, [:a, :b, :c], 123)  #=> `Integer': can't convert false into Integer (TypeError)
p value_at_deep_key({a: {b: {c: nil}}},   [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({z: {b: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {z: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {c: "cat"}}}, [:a, :b], 123)      #=> `Integer': can't convert Hash into Integer (TypeError
p value_at_deep_key({a: {b: {c: "cat"}}}, [:a], 123)          #=> `Integer': can't convert Hash into Integer (TypeError)
p value_at_deep_key({z: {b: {c: "cat"}}}, [], 123)            #=> "No keys to lookup!"
p value_at_deep_key({z: {b: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {z: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c, :d], 123) #=> 123


p value_at_deep_key( 
  {'key1' => {'key2' => {'key3' => {'key4' => "4"}}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 4

p value_at_deep_key( 
  { 'key1' => {'key2' => {'key3' => "4"}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 123

p value_at_deep_key( 
   {
    'key1' => "1",
    'key2' => "2",
    'key3' => "3",
    'key4' => "4",
  },
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 123

p value_at_deep_key( 
  { 'key1' => {'key2' => {'key3' => {'key4' => nil}}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 123

p value_at_deep_key( 
  {'key1' => {'key2' => {'key3' => {'key4' => 'hello'}}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> `Integer': invalid value for Integer(): "hello" (ArgumentError)

But maybe the following answer will suit you better:

If you must have:

  1. A found String that looks like a number--converted to an int, or
  2. The default

...in other words no errors, you can do this:

def value_at_deep_key(hash, key_sequence, default=nil)
  value = hash

  key_sequence.each do |key|
    case value
    when Hash
      value = value[key]
    else
      value = hash.object_id  #Some unique value to signal that the Hash lookup failed.
      break
    end
  end

  begin
    value == hash.object_id ? default : Integer(value)
  rescue TypeError, ArgumentError #If the Hash lookup succeeded, but the value is: nil, true/false, a String that is not all numbers, Array, Hash, an object that neither responds to to_int() nor to_i()
    default
  end
end

p value_at_deep_key({a: {b: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {c: false}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {c: nil}}},   [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({z: {b: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {z: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {c: "cat"}}}, [:a, :b], 123)      #=> 123
p value_at_deep_key({a: {b: {c: "cat"}}}, [:a], 123)          #=> 123
p value_at_deep_key({z: {b: {c: "cat"}}}, [], 123)            #=> 123
p value_at_deep_key({z: {b: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {b: {z: "cat"}}}, [:a, :b, :c], 123)  #=> 123
p value_at_deep_key({a: {z: {c: "cat"}}}, [:a, :b, :c, :d], 123) #=> 123


p value_at_deep_key( 
  {'key1' => {'key2' => {'key3' => {'key4' => "4"}}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 4

p value_at_deep_key( 
  { 'key1' => {'key2' => {'key3' => "4"}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 123

p value_at_deep_key( 
   {
    'key1' => "1",
    'key2' => "2",
    'key3' => "3",
    'key4' => "4",
  },
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 123

p value_at_deep_key( 
  { 'key1' => {'key2' => {'key3' => {'key4' => nil}}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 123

p value_at_deep_key( 
  {'key1' => {'key2' => {'key3' => {'key4' => [1, 2, 3] }}}},
  %w[key1 key2 key3 key4],
  default=123,
)  #=> 123
7stud
  • 46,922
  • 14
  • 101
  • 127
  • Ah, I see. In that case, the op can just do: `result["key1"]["key2"]["key3"]["key4"].to_i` because that also works on #1! – 7stud Aug 31 '14 at 19:14
  • It is not clear why you claim "this doesn't work" and then show a *successful* example. It's confusing. – Sergio Tulentsev Aug 31 '14 at 19:14
  • Also this answer is not an answer (does not even attempt to answer the question) and should be removed. – Sergio Tulentsev Aug 31 '14 at 19:16
  • @SergioTulentsev, Which post do you think is more helpful to the op? Mine or the accepted answer? – 7stud Aug 31 '14 at 19:21
  • 2
    @Sergio, green checkmarks often leave me scratching my head. – Cary Swoveland Aug 31 '14 at 20:19
  • VII, you really should have pointed out the flaws in the selected answer in a comment or comments (which you could have done, despite formatting limitations), but that's a minor point. The main thing is that reported what you found, which you were obligated to do. Consider doing that now and editing your answer to include only your solution (which probably has been largely unnoticed). – Cary Swoveland Aug 31 '14 at 20:45