Iterate over object(s) if present, single or plural - ruby-on-rails

Let's say that i have object my_obj
and i get this object from database, with some more calculation, it doesn't matter...
and then i need to iterate via such object, like:
my_obj.each do |m|
some_method(m.id)
end
this code is not good, becouse if my_obj is nil, i will get error like:
undefined method `each' for nil:NilClass
so i decide to write:
if my_obj.present?
my_obj.each do |m|
some_method(m.id)
end
end
but i think that there is one more way of doing this, without writting everywhere if construction.
so how could i iterate via object, only if it is not null?

I found the code a bit anti-pattern for normal OOP principle "Tell, Don't ask". I tried the question in console and found your worry unnecessary.
No matter what the result is, blank Array or blank ActiveRecord::Relation object, each all works and return a blank array [].
Article.count
# => 0
articles = Article.all # Return array in Rails 3
articles.each { |a| puts a.title }
# => []
articles = Article.scoped # Return ActiveRecord::Relation object in Rails 3
articles.each { |a| puts a.title }
# => []
I would suggest you to review the method and returned result of your query. If your query returns unusual things, make sure it returns at least a blank Array. Then you don't need to consider too much.

They easiest way to handle this is to surround your object with the 'Array' conversion function which will coerce your possibly nil input into an array while leaving an existing array untouched, e.g.
>> Array(nil)
=> []
and
>> Array([1])
=> [1]
So in your case:
Array(my_obj).each do |m|
some_method(m.id)
end

The collection query should always return iterable object, but there are several ways. The problem of nil checks led to a pattern named NullObjects which is often the best solution. Apart from that you can do:
my_object.to_a.each do |m|
some_method(m.id)
end
or
my_object.try(:each) do |m|
some_method(m.id)
end
or
(my_object || []).each do |m|
some_method(m.id)
end

In Rails, you can do Object#try method:
my_object.try(:each) do |m|
some_method(m.id)
end
It will call each (returning its result) with attached block if my_object is other than nil. Otherwise, it won't call each method and will return nil.

You can initialize like this;
def fields_each(fields)
if fields.present? && fields.keys.present?
fields.each do |key, value|
yield(key, value) if block_given?
end
end
end
and usage;
fields_each({a: 1, b: 2, c: 3}) do |key, value|
puts "Key: #{key} value: #{value}"
end

Everyone forgot the awesome [my_objects].flatten.compact!

in the newer ruby version, tested with 2.4.1, this can be written as
my_obj&.each do |m|
some_method(m.id)
end

when you initialize the my_obj do it like this
my_obj = Model.all || [] # Empty array will not iterate or throw error
Ex:
[].each do |x|
p "I will not execute"
end

you can try this above the loop:
if my_obj == nil then
redirect_to "something_else"
end

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

Iterating through Hash to output an unordered list

I have a Hash where the majority of it is filled with a key with two values associated with the key. There is also another hash within this Hash which is where I've been stuck.
Lets say the hash looks like:
{'sports'=>['football', 'basketball'], 'season'=>['summer','fall'], 'data'=>[{'holiday'=>'Christmas', 'genre' => 'Comedy'}, {'holiday'=>'Thanksgiving', 'genre' => 'Action'}]}
The output should look like:
Sports
- football
- basketball
Season
- summer
- fall
Holiday
- Christmas
- Thanksgiving
Genre
- Comedy
- Action
So far I have a helper that gives me everything except the data section.
def output_list_from(hash)
return if hash.empty?
content_tag(:ul) do
hash.map do |key, values|
content_tag(:li, key.to_s.humanize) +
content_tag(:ul) do
# if values.is_a?(Hash)...
content_tag(:li, values.first) +
content_tag(:li, values.last)
end
end.join.html_safe
end.html_safe
end
This returns the output:
Sports
- football
- basketball
Season
- summer
- fall
Data
- {'holiday'=>'Christmas', 'genre' => 'Comedy'}
- {'holiday'=>'Thanksgiving', 'genre' => 'Action'}
Which of course makes sense...so I've tried to check in the loop if the value is a Hash, but the way it's set up has tricked me. I think it's be easier if I knew what the hash would look like everytime, but it would be a new hash each time. One time there could be a holiday within data and the other time there could be both holiday and genre.
Any advice would be appreciated.
You will need to create a hash with the correct format. Something like this:
hash = {'sports'=>['football', 'basketball'], 'season'=>['summer','fall'], 'data'=>[{'holiday'=>'Christmas', 'genre' => 'Comedy'}, {'holiday'=>'Thanksgiving', 'genre' => 'Action'}]}
formatted_data = hash.dup
data = formatted_data.delete('data')
if data
data.each do |item|
item.each do |k, v|
formatted_data[k] ||= []
formatted_data[k] << v
end
end
end
puts formatted_data
# => {"sports"=>["football", "basketball"], "season"=>["summer", "fall"],
# => "holiday"=>["Christmas", "Thanksgiving"], "genre"=>["Comedy", "Action"]}
content_tag(:ul) do
formatted_data.map do |key, values|
#... your code here...
end.join.html_safe
end.html_safe
Suppose your hash looked like this:
hash = { 'sports'=>['football', 'basketball'],
'season'=>['summer', 'fall'],
'data1' =>[{ 'holiday'=>'Christmas', 'genre'=>'Comedy'},
{ 'holiday'=>'Thanksgiving', 'genre'=>'Action' }],
'data2' =>[{ 'data3'=>[{ 'sports'=>'darts', 'genre'=>'Occult' }] }]
}
and you wanted a general solution that would work for any number of levels and does not depend on the names of the keys that will not be in the resulting hash (here 'data1', 'data2' and 'data3'). Here's one way you could do that, using recursion.
Code
def extract(h, new_hash = {})
h.each do |k,v|
[*v].each do |e|
case e
when Hash then extract(e, new_hash)
else new_hash.update({ k=>[e] }) { |_,ov,nv| ov << nv.first }
end
end
end
new_hash
end
Example
extract(hash)
#=> {"sports"=>["football", "basketball", "darts"],
# "season"=>["summer", "fall"],
# "holiday"=>["Christmas", "Thanksgiving"],
# "genre"=>["Comedy", "Action", "Occult"]}
Explanation
There are, I think, mainly two things in the code that may require clarification.
#1
The first is the rather lonely and odd-looking expression:
[*v]
If v is an array, this returns v. If v is a literal, the splat operator has no effect, so it returns [v]. In other words, it leaves arrays alone and converts literals to an array containing one element, itself. Ergo:
[*['football', 'basketball']] #=> ["football", "basketball"]
[*'Thanksgiving'] #=> ["Thanksgiving"]
This saves us the trouble of having three, rather than two, possibilities in the case statement. We simply convert literals to arrays of one element, allowing us to deal with just hashes and arrays.
#2
The second snippet that may be unfamiliar to some is this:
new_hash.update({ k=>[e] }) { |_,ov,nv| ov << nv.first }
This uses the form of the method Hash#update (a.k.a. merge!) that uses a block to resolve the values of keys that are present in both hashes being merged. As an example, at some stage of the calculations, new_hash will have a key-value pair:
'sports'=>['football', 'basketball']
and is to be updated with the hash1:
{ 'sports'=>['darts'] }
Since both of these hashes have the key 'sport', the block is called upon as arbiter:
{ |k,ov,nv| ov << nv.first }
#=> { |'sport', ['football', 'basketball'], ['darts']| ov << nv.first }
#=> { |'sport', ['football', 'basketball'], ['darts']|
['football', 'basketball'] << 'darts' }
#=> ['football', 'basketball'] << 'darts'
As I'm not using the key 'sport' in the block, I've replaced that block variable with a placeholder (_) to reduce opportunities for error and also to inform the reader that the key is not being used.
1 I sometimes use darts as example of a sport because it is one of the few in which one can be successful without being extremely physically fit.

Pass a Rails variable into a case statement

I'm using Rails and have an array that I'm looking to control the output of.
In short, if a project with an ID exists in #projects, then I don't want to output the corresponding part of the array, if that exists. So if a project with ID of 1 exists, then I don't want to output #array[1], even if that's present.
The following hardcoded case statement works successfully, to avoid outputting #array[1], #array[2] and #array[3]:
#array.each do |key, value|
case key when '1','2','3' then
next # Skip this array key
end
# Otherwise, do something else
end
But instead of hardcoding '1','2','3', I actually want these to be passed in dynamically, via ActiveRecord. i tried to build a string:
#projects_string = #projects.map {|element|
"'#{element.id}'"
}.join(',')
This outputs successfully as '1','2','3'. But I can't figure out a way to pass this into the case statement. This fails to match:
#array.each do |key, value|
case key when "#{#projects_string}" then
next # Skip this array key
end
# Otherwise, do something else
end
Is there a way to achieve this using this method, or maybe a better way to achieve this using some of Rails' capabilities?
I did look through this answer, which seemed most relevant, but couldn't see how to implement it.
You can do it in this way as well.
#project_arr = #projects.map{|p| p.id.to_s}
#array.each do |key, value|
unless #project_arr.include?(key.to_s)
#YOUR CODE GOES HERE
else
# SKIP IT
end
end
You should not use string but use array and do like
#array = Array.new
#array = #projects.map{|arr| arr.id}
#array.each do |key, value|
if #array.include?(key)
#code to skip if project is included
else
#your code
end
end
Array can be used easily for comparisons than string
I feel like this depends on the type of collection you are dealing with: Array or ActiveRelation.
If they are just arrays and you want elements of #array that aren't in #parent, you can just do:
#array - #parent
If they are both ActiveRecord objects, why not just filter out the #array when it's created?
#array = Project.where.not(id: #projects.pluck(:id))
It seems inefficient (n^2) to have to scan a lookup array (#projects) for each element in the collection you are iterating over.
You can try the following:
#array = [1,2,3,4]
#projects = [2,3]
#array.each do |key|
case
when true === #projects.include?(key)
next
end
puts "Doing something else for #{key}"
end
Outputs:
Doing something else for 1
Doing something else for 4

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]

Uniq by object attribute in Ruby

What's the most elegant way to select out objects in an array that are unique with respect to one or more attributes?
These objects are stored in ActiveRecord so using AR's methods would be fine too.
Use Array#uniq with a block:
#photos = #photos.uniq { |p| p.album_id }
Add the uniq_by method to Array in your project. It works by analogy with sort_by. So uniq_by is to uniq as sort_by is to sort. Usage:
uniq_array = my_array.uniq_by {|obj| obj.id}
The implementation:
class Array
def uniq_by(&blk)
transforms = []
self.select do |el|
should_keep = !transforms.include?(t=blk[el])
transforms << t
should_keep
end
end
end
Note that it returns a new array rather than modifying your current one in place. We haven't written a uniq_by! method but it should be easy enough if you wanted to.
EDIT: Tribalvibes points out that that implementation is O(n^2). Better would be something like (untested)...
class Array
def uniq_by(&blk)
transforms = {}
select do |el|
t = blk[el]
should_keep = !transforms[t]
transforms[t] = true
should_keep
end
end
end
Do it on the database level:
YourModel.find(:all, :group => "status")
You can use this trick to select unique by several attributes elements from array:
#photos = #photos.uniq { |p| [p.album_id, p.author_id] }
I had originally suggested using the select method on Array. To wit:
[1, 2, 3, 4, 5, 6, 7].select{|e| e%2 == 0}
gives us [2,4,6] back.
But if you want the first such object, use detect.
[1, 2, 3, 4, 5, 6, 7].detect{|e| e>3} gives us 4.
I'm not sure what you're going for here, though.
I like jmah's use of a Hash to enforce uniqueness. Here's a couple more ways to skin that cat:
objs.inject({}) {|h,e| h[e.attr]=e; h}.values
That's a nice 1-liner, but I suspect this might be a little faster:
h = {}
objs.each {|e| h[e.attr]=e}
h.values
Use Array#uniq with a block:
objects.uniq {|obj| obj.attribute}
Or a more concise approach:
objects.uniq(&:attribute)
The most elegant way I have found is a spin-off using Array#uniq with a block
enumerable_collection.uniq(&:property)
…it reads better too!
If I understand your question correctly, I've tackled this problem using the quasi-hacky approach of comparing the Marshaled objects to determine if any attributes vary. The inject at the end of the following code would be an example:
class Foo
attr_accessor :foo, :bar, :baz
def initialize(foo,bar,baz)
#foo = foo
#bar = bar
#baz = baz
end
end
objs = [Foo.new(1,2,3),Foo.new(1,2,3),Foo.new(2,3,4)]
# find objects that are uniq with respect to attributes
objs.inject([]) do |uniqs,obj|
if uniqs.all? { |e| Marshal.dump(e) != Marshal.dump(obj) }
uniqs << obj
end
uniqs
end
You can use a hash, which contains only one value for each key:
Hash[*recs.map{|ar| [ar[attr],ar]}.flatten].values
Rails also has a #uniq_by method.
Reference: Parameterized Array#uniq (i.e., uniq_by)
I like jmah and Head's answers. But do they preserve array order? They might in later versions of ruby since there have been some hash insertion-order-preserving requirements written into the language specification, but here's a similar solution that I like to use that preserves order regardless.
h = Set.new
objs.select{|el| h.add?(el.attr)}
ActiveSupport implementation:
def uniq_by
hash, array = {}, []
each { |i| hash[yield(i)] ||= (array << i) }
array
end
Now if you can sort on the attribute values this can be done:
class A
attr_accessor :val
def initialize(v); self.val = v; end
end
objs = [1,2,6,3,7,7,8,2,8].map{|i| A.new(i)}
objs.sort_by{|a| a.val}.inject([]) do |uniqs, a|
uniqs << a if uniqs.empty? || a.val != uniqs.last.val
uniqs
end
That's for a 1-attribute unique, but the same thing can be done w/ lexicographical sort ...
If you are not married with arrays, we can also try eliminating duplicates through sets
set = Set.new
set << obj1
set << obj2
set.inspect
Note that in case of custom objects, we need to override eql? and hash methods

Resources