Ruby 1.9.2 non-existent hash element - ruby-on-rails

I'm on Rails 3.0.x, Ruby 1.9.2 and needs a way to test for params that may or may not exists, e.g.,
params[:user] #exists
params[:user][:login] #may not exist
What's the proper Ruby syntax for the 2nd check so it doesn't barf?

Try following:
params.has_key? :user #=> true because exists
params[:user].has_key? :login #=> true if exist otherwise false

#WarHog has it right, pretty much. It's very unusual for an item in params to sometimes return a string but other times return a Hash, but regardless you can handle that easily enough:
if params.has_key?(:user) && params[:user].respond_to?(:has_key?)
do_something_with params[:user][:login]
end
Instead of respond_to? :has_key? you could also do respond_to? :[] or just is_a? Hash. Mostly a matter of preference.

You would just get nil in the second case.. that shouldn't be a problem, no?
e.g. params[:user][:login] just returns nil, which evaluates to false if the :user entry exists in the first Hash.
However if the nesting would be one or more levels deeper, and the missing hash entry was somewhere in the middle, you would have problems. e.g.:
params[:user][:missing_key][:something]
in that case Ruby would try to evaluate nil[:something] and raise an exception
you could do something like this:
begin
x = params[:user][:missing_key][:something]
rescue
x = nil
end
... which you could further abstract...

Related

Do something if value is present

I frequently find myself writing Ruby code where I check for the presence of a value and subsequently do something with that value if it is present. E.g.
if some_object.some_attribute.present?
call_something(some_object.some_attribute)
end
I think it would be cool, if it could be written as
some_object.some_attribute.presence { |val| call_something(val) }
=> the return value of call_something
Anyone know if there's such a feature in Ruby or though activesupport?
I opened a pull request for this feature.
You can use a combination of presence and try:
If try is called without arguments it yields the receiver to a given block unless it is nil:
'foo'.presence.try(&:upcase)
#=> "FOO"
' '.presence.try(&:upcase)
#=> nil
nil.presence.try(&:upcase)
#=> nil
You could try
do_thing(object.attribute) if object.attribute
This is usually fine, unless the attribute is a boolean. In which case it will not call if the value is false.
If your attribute can be false, use .nil? instead.
do_thing(object.attribute) unless object.attribute.nil?
Though there is no such functionality out of the box, one could do:
some_object.some_attribute.tap do |attr|
attr.present? && call_smth(attr)
end
On the other hand, Rails provides so many monkeypatches, that one could append one to this circus:
class Object
def presense_with_rails
raise 'Block required' unless block_given?
yield self if self.present? # requires rails
end
def presense_without_rails
raise 'Block required' unless block_given?
skip = case self
when NilClass, FalseClass then true
when String, Array then empty?
else false
end
yield self unless skip
end
end

ERROR: Nil Can't be Coerced into a Fixnum

I have the following function to sum all the records of an :amount field in my Pack model for that given user:
user.rb
def total_money_spent_cents
amount = self.packs.map(&:amount).sum
return amount
end
However, when I use this function I receive the following error:
nil can't be coerced into Fixnum
Any suggestions?
EDIT
I am still having issues in regards to Fixnum in my tests, and have another question open here.
This suggests that one of your packs has an amount field which has not yet been set, so is nil. When you try and add it to something else, it undergoes Type coercion, to see if Ruby can massage its type into one that can be added to numbers, but it can't, and so you have this error.
One solution is this:
def total_amount_spent_cents
packs.map(&:amount).compact.sum
end
Array#compact removes the nil elements.
This may be fixing the symptom and not the actual problem though. It could be the case that you shouldn't have nil's in there at all, in which case you should check the initialisation of your Pack model (or perhaps its validations, to ensure that amount is mandatory).
I added some extra methods into Array and Hash for this sort of thing: they're like compact but they remove all values returning true for blank? rather than just nil: so will remove empty strings, empty arrays, hashes etc.
class Hash
def compact_blank!
self.each{|k,v| self.delete(k) if v.blank? }
self
end
def compact_blank
self.dup.compact_blank!
end
end
class Array
def compact_blank!
self.delete_if(&:blank?)
end
def compact_blank
self.dup.compact_blank!
end
end
use like
["1", "abc", "", nil, []].compact_blank
=> ["1", "abc"]
it's useful with params especially, where you might get a lot of empty strings through.

What is the best practice for handling nil objects and properties?

Say I have a User object, which has an email property, and I need the upper cased last letter of their email:
u = User.find(1)
letter = u.email.upcase.last
If u or email is nil in this chain, then I get a NoMethodError: undefined method 'blah' for nil:Nilclass. I should be able to work around it in most cases, but sometimes, a nil gets where it shouldn't or its hard to contain. One way would be verbose:
u = User.find(1)
letter = nil
if u && u.email
letter = u.email.upcase.last
end
But this gets annoying and hazardous in a view, or in a long chain of a.bunch.of.properties.down.a.hierarchy. I read about try method in Rails:
u = User.find(1)
letter = u.try(:email).try(:upcase).try(:last)
This is less verbose, but I feel icky writing all those tries. And once I put try in the chain, I have to use them all the way down. Is there a better way?
I like to use the Null Object Pattern. Avdi has a great post explaining this, but the basic idea is you have a little class that can stand in for an object and respond reasonably to the messages you might pass the original object. I've found these are useful not only for avoiding NoMethodErrors but also for setting default values/nice messages.
For instance, you could do:
class NilUser
def email
"(no email address)"
end
end
u = User.find(1) || NilUser.new
u.email.upcase.last # => No errors!
I just wanted to update this thread with one more option: Ruby now (as of 2.3) gives us a safe navigation operator, the &. syntax.
So:
u.email.upcase
Would become:
u.email&.upcase
Similarly to Rail's try method, the whole chain will return nil if it encounters NoMethodError on a nil.
User.find(1)
Will raise exception if user with id 1 not exist so you don't need to worry about nil here
u.email
If you have in your model
validates :email, presence: true
You don't need to worry about nil because User without email cant be in database
But I think you are asking about general way of handling nils in ruby code. Lately I'm using Null Object pattern
http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/
http://robots.thoughtbot.com/post/20907555103/rails-refactoring-example-introduce-null-object
Rails: replacing try with the Null Object Pattern
https://github.com/martinciu/nullobject
You could also map the result of find
[User.find(1)].map{|u| (u != nil ? u.mail : "no mail")}[0]

Ruby === not acting as it when left-hand argument is a class

I'm using Ruby 1.8.7 with Rails 3.0.1 and am having a problem whose root cause appears to be the "Array === object" operation. I saw the same behavior before in a class of my own creation, and programmed around it by not using the "===" operator (I assumed that there was some flaw in my knowledge of Ruby, which is still rather limited). But now that it is happening inside ActionPack, I need to do something about it.
This surfaced when the FormHelper "fields_for" was not acting the way it should. The following view code snippet ("<% %>" removed to improve readability):
form_for #coupon do |f|
...
f.fields_for #coupon.participants do |cp|
...
end
end
gave the error "ActionView::Template::Error (undefined method `model_name' for Array:Class):"
inside the form_for helper method. I determined that it was executing the wrong branch of a "case" command, set a breakpoint and started testing. Here are the results:
/Library/Ruby/Gems/1.8/gems/actionpack-3.0.1/lib/action_view/helpers/form_helper.rb:1152
case record_or_name_or_array
(rdb:1) pp record_or_name_or_array.instance_of? Array
true
(rdb:1) pp Array === record_or_name_or_array
false
(rdb:1) pp Array.object_id
2148267660
(rdb:1) pp record_or_name_or_array.class.object_id
2148267660
This shows pretty definitively that, while "record_or_name_or_array" is definitely an array, "Array === record_or_name_or_array" is returning false.
BTW, in case you're suspecting that "#f.fields_for" is the wrong syntax, I tried it both with and without the "#f." and got the same result. I have also restarted RoR and my machine and the results remain unchanged.
Try this:
#coupon = Coupon.last
Array === #coupon.participants #=> false
Array === #coupon.participants.find(:all) #=> true
Association #coupon.participants is not an array, it is a proxy. The reason why #coupon.participants.class == Array is true is described in activerecord-3.0.9/lib/active_record/associations/association_proxy.rb:25
Added: Another interesting experiment would be #coupon.participants.superclass.
From the console (rails c) try running:
#coupon = Coupon.last
Array == #coupon.participants
If that call returns false, it is most likely that your associations are incorrectly setup (i.e. has_many :participants and belongs_to :coupon).
#coupon.is_a? Array should return true, #coupon === Array would mean #coupon was equal to the singleton instance of Array

Equivalent of .try() for a hash to avoid "undefined method" errors on nil? [duplicate]

This question already has answers here:
How to avoid NoMethodError for nil elements when accessing nested hashes? [duplicate]
(4 answers)
Closed 7 years ago.
In Rails we can do the following in case a value doesn't exist to avoid an error:
#myvar = #comment.try(:body)
What is the equivalent when I'm digging deep into a hash and don't want to get an error?
#myvar = session[:comments][#comment.id]["temp_value"]
# [:comments] may or may not exist here
In the above case, session[:comments]try[#comment.id] doesn't work. What would?
You forgot to put a . before the try:
#myvar = session[:comments].try(:[], #comment.id)
since [] is the name of the method when you do [#comment.id].
The announcement of Ruby 2.3.0-preview1 includes an introduction of Safe navigation operator.
A safe navigation operator, which already exists in C#, Groovy, and
Swift, is introduced to ease nil handling as obj&.foo. Array#dig and
Hash#dig are also added.
This means as of 2.3 below code
account.try(:owner).try(:address)
can be rewritten to
account&.owner&.address
However, one should be careful that & is not a drop in replacement of #try. Take a look at this example:
> params = nil
nil
> params&.country
nil
> params = OpenStruct.new(country: "Australia")
#<OpenStruct country="Australia">
> params&.country
"Australia"
> params&.country&.name
NoMethodError: undefined method `name' for "Australia":String
from (pry):38:in `<main>'
> params.try(:country).try(:name)
nil
It is also including a similar sort of way: Array#dig and Hash#dig. So now this
city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)
can be rewritten to
city = params.dig(:country, :state, :city)
Again, #dig is not replicating #try's behaviour. So be careful with returning values. If params[:country] returns, for example, an Integer, TypeError: Integer does not have #dig method will be raised.
The most beautiful solution is an old answer by Mladen Jablanović, as it lets you to dig in the hash deeper than you could with using direct .try() calls, if you want the code still look nice:
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc}
end
end
You should be careful with various objects (especially params), because Strings and Arrays also respond to :[], but the returned value may not be what you want, and Array raises exception for Strings or Symbols used as indexes.
That is the reason why in the suggested form of this method (below) the (usually ugly) test for .is_a?(Hash) is used instead of (usually better) .respond_to?(:[]):
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}
end
end
a_hash = {:one => {:two => {:three => "asd"}, :arr => [1,2,3]}}
puts a_hash.get_deep(:one, :two ).inspect # => {:three=>"asd"}
puts a_hash.get_deep(:one, :two, :three ).inspect # => "asd"
puts a_hash.get_deep(:one, :two, :three, :four).inspect # => nil
puts a_hash.get_deep(:one, :arr ).inspect # => [1,2,3]
puts a_hash.get_deep(:one, :arr, :too_deep ).inspect # => nil
The last example would raise an exception: "Symbol as array index (TypeError)" if it was not guarded by this ugly "is_a?(Hash)".
The proper use of try with a hash is #sesion.try(:[], :comments).
#session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')
Update: As of Ruby 2.3 use #dig
Most objects that respond to [] expect an Integer argument, with Hash being an exception that will accept any object (such as strings or symbols).
The following is a slightly more robust version of Arsen7's answer that supports nested Array, Hash, as well as any other objects that expect an Integer passed to [].
It's not fool proof, as someone may have created an object that implements [] and does not accept an Integer argument. However, this solution works great in the common case e.g. pulling nested values from JSON (which has both Hash and Array):
class Hash
def get_deep(*fields)
fields.inject(self) { |acc, e| acc[e] if acc.is_a?(Hash) || (e.is_a?(Integer) && acc.respond_to?(:[])) }
end
end
It can be used the same as Arsen7's solution but also supports arrays e.g.
json = { 'users' => [ { 'name' => { 'first_name' => 'Frank'} }, { 'name' => { 'first_name' => 'Bob' } } ] }
json.get_deep 'users', 1, 'name', 'first_name' # Pulls out 'Bob'
say you want to find params[:user][:email] but it's not sure whether user is there in params or not. Then-
you can try:
params[:user].try(:[], :email)
It will return either nil(if user is not there or email is not there in user) or otherwise the value of email in user.
As of Ruby 2.3 this gets a little easier. Instead of having to nest try statements or define your own method you can now use Hash#dig (documentation).
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
Or in the example above:
session.dig(:comments, #comment.id, "temp_value")
This has the added benefit of being more like try than some of the examples above. If any of the arguments lead to the hash returning nil then it will respond nil.
#myvar = session.fetch(:comments, {}).fetch(#comment.id, {})["temp_value"]
From Ruby 2.0, you can do:
#myvar = session[:comments].to_h[#comment.id].to_h["temp_value"]
From Ruby 2.3, you can do:
#myvar = session.dig(:comments, #comment.id, "temp_value")
Another approach:
#myvar = session[:comments][#comment.id]["temp_value"] rescue nil
This might also be consider a bit dangerous because it can hide too much, personally I like it.
If you want more control, you may consider something like:
def handle # just an example name, use what speaks to you
raise $! unless $!.kind_of? NoMethodError # Do whatever checks or
# reporting you want
end
# then you may use
#myvar = session[:comments][#comment.id]["temp_value"] rescue handle
When you do this:
myhash[:one][:two][:three]
You're just chaining a bunch of calls to a "[]" method, an the error occurs if myhash[:one] returns nil, because nil doesn't have a [] method. So, one simple and rather hacky way is to add a [] method to Niclass, which returns nil: i would set this up in a rails app as follows:
Add the method:
#in lib/ruby_extensions.rb
class NilClass
def [](*args)
nil
end
end
Require the file:
#in config/initializers/app_environment.rb
require 'ruby_extensions'
Now you can call nested hashes without fear: i'm demonstrating in the console here:
>> hash = {:foo => "bar"}
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:doo]
=> nil
>> hash[:doo][:too]
=> nil
Andrew's answer didn't work for me when I tried this again recently. Maybe something has changed?
#myvar = session[:comments].try('[]', #comment.id)
The '[]' is in quotes instead of a symbol :[]
Try to use
#myvar = session[:comments][#comment.id]["temp_value"] if session[:comments]

Resources