Question

Say I have an array of hashes like so:

items = [
  {:user=>1, :amount=>484}, 
  {:user=>2, :amount=>0}, 
  {:user=>3, :amount=>8633}
]

To get the amount for a user, I'd so something like this:

items.select{|key| key[:user] == 1 }.first[:amount]

But if there isn't a user with a given ID, then that doesn't work:

> items.select{|key| key[:user] == 8 }.first[:amount]
# NoMethodError: undefined method `[]' for nil:NilClass

So how can I return amount if it finds the item, but return 0 if there's nothing?

Was it helpful?

Solution

First of all, you can use find instead of select since you only want the first match. If you want a one-liner, you can do something like this:

(items.find { |key| key[:user] == 8 } || { :amount => 0 })[:amount]

If you happen to have Rails or ActiveSupport kicking around then you could use try and to_i (while remembering that nil.to_i == 0) like this:

items.find { |k| key[:user] == 1 }.try(:fetch, :amount).to_i

try just calls a method (fetch in this case) but always returns nil if the receiver is nil so nil.try(:fetch, :amount) is nil but some_hash.try(:fetch, :amount) is some_hash.fetch(:amount), it is a handy tool for swallowing up nils without adding a bunch of extra conditionals. AFAIK, the andand gem does similar things without requiring all the ActiveSupport.

OTHER TIPS

You could catch the Exception NoMethodError

begin
#code you want to execute
rescue NoMethodError
puts "0"
end

Three ways:

#1

def my_method(user, items)
  items.each {|h|
    return h[:amount] if h.key?(:user) && h[:user] == user && h.key?(:amount)}
  0
end

my_method(1, items) #=> 484
my_method(5, items) #=>   0

#2

def my_method(user, items)
  items.reduce(0) { |v,h|
    (h.key?(:user) && h[:user] == user && h.key?(:amount)) ? h[:amount] : v }
end

#3

def my_method(user, items)
    hash = items.find { |h| h.key?(:user) && h[:user] == user) }
    (hash && hash.key?(:amount)) ? hash[:amount] : 0
end

put rescue 0 on the end

items.select{|key| key[:user] == 8 }.first[:amount] rescue 0

Here's a variant using some built-in Ruby fallback mechanisms

items.find(->{{}}){|e| e[:user] == 8}.fetch(:amount, 0)

First, starting from the end of the line, fetch, which returns the value at a provided hash key. One thing that makes fetch different from using regular hash square brackets is that, if the key isn't there, fetch throws as a key error instead of returning nil. That's a neat trick, but the other useful thing is that it can take an optional second argument, which is the default thing to return if that key isn't found:

test = { a: 1, b:2 }
test[:c] #=> nil
test.fetch(:c) #=> raise KeyError
test.fetch(:c, 0) #=> 0

Handy, but you can't call fetch on nil, which is the default return for find if it finds nothing.

Luckily, find also lets you assign a default return value instead of nil. You can pass an object that responds to call, and that object will specify how to handle a find with no matches.

Here's a short lambda that returns an empty hash: ->{{}} and here it is passed as a first argument to find, before the usual block

items.find(->{{}}){ |key| key[:user] == 8 } #=> {}

All together: items.find(->{{}}){|e| e[:user] == 8}.fetch(:amount, 0) -- now find will always return some sort of hash, which will respond to fetch, and fetch can return a default if a key is missing.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top