Map/Collect first true result - ruby-on-rails

Consider the following
users.collect do |user|
user.favorite_song.presence
end.compact.first
In this case, I want the first favorite song I encounter among my users.
Can this be written in a nicer way?
I tried
users.find do |user|
user.favorite_song.presence
end
But it returns the first user with a favorite song, rather than the favorite song itself.

If the users array isn't too big, your first solution is fine, and can be rewritten like this:
users.map(&:favorite_song).compact.first
You can also modify your second approach as follows:
users.find { |user| user.favorite_song.present? }.favorite_song
Both of these solutions assume that there exists a favorite_song in some user and will raise an exception if there isn't. You can elegantly avoid this with try (Rails only):
users.find { |user| user.favorite_song.present? }.try(:favorite_song)

What about:
users.each do |user|
break user.favorite_song if user.favorite_song.present?
end
Will return user.favorite_song if condition is true otherwise will return users

favorite_song = nil
users.map do |user|
favorite_song = user.favorite_song
break if favorite_song
end

Please have a try with
users.map{|user| user.favorite_song}.compact.first

As of Ruby 2.0 you can use Enumerator::Lazy#lazy to solve this more cleanly and efficiently:
users.lazy.map(&:favorite_song).find(&:present?)
This is more efficient because it will only call favorite_song on users until it finds one that's present. It's cleaner because it's more concise and lazy is self documenting of the intention.

Related

Group by ruby on rails

I need to group the users based on created_at column by year and month,
User.all.group_by{ |q| [q.created_at.year, q.created_at.month]},
where I am getting hash with key [year, month], Is there any way to group the records which results like
{
year1 =>{ month1 =>[array of records], month2=>[array]},
year2 =>{ month1 =>[array of records], month2=>[array]}
}
Try to the following:
User.all
.group_by { |user| user.created_at.year }
.transform_values { |users| users.group_by { |user| user.created_at.month } }
you can get by
result = {}
User.all.each do |user|
result[user.created_at.year] = {} if !result[user.created_at.year].present?
result[user.created_at.year][user.create_at.month] = [] if !result[user.created_at.year][user.create_at.month].present?
result[user.created_at.year][user.create_at.month].push(user.attributes)
end
There are many ways to do this. Although the suggested duplicate does propose a solution, there are other ways to achieve it. One of them is using the code below (it's a refactor of one of the first answers).
{}.tap do |hash|
User.all.find_each do |user|
year = user.created_at.year
month = user.created_at.month
hash[year] ||= {}
hash[year][month] ||= []
hash[year][month] << user
end
end
What's good about this code is that you are not loading all user records into memory (bad if you have, let's say, 1M user records) because it uses find_each which, by default, fetches users by 1000.
It also only goes through each item once, unlike the accepted answer in the duplicate suggested above.
Overall, there's a lot of ways to tackle your problem in ruby. It's up to you to discover what you think is clean code. But make sure that what you decide to use is efficient.

Run code in block only when certain condition

Here I want to show you a demo code:
if ENV["PRODUCTION"]
user.apply_discount!
product.update!
else
VCR.use_cassette(vcr_cassette) do
user.apply_discount!
product.update!
end
end
So basically two times I have there the same code:
user.apply_discount!
product.update!
How can I prevent this duplication of code? How would you do it?
I was thinking of putting the code inside a Block and then either call it directly or in the block. Here's an example:
actions = Proc.new do
user.apply_discount!
product.update!
end
if ENV["PRODUCTION"]
actions.call
else
VCR.use_cassette(vcr_cassette) do
actions.call
end
end
Do you have another idea? Better solution? Thanks
Your version is explicit and readable.
The only thing I'd do is moving it to a general method:
def do_env_specific_stuff(stuff)
ENV('PRODUCTION') ? stuff.call : VCR.use_cassette(vcr_cassette) { stuff.call }
end
Then:
stuff = proc do
user.apply_discount!
product.update!
end
do_env_specific_stuff(stuff)
Andrey's answer is excellent and should be accepted.
But just want to point out that you can convert a proc to a block instead of calling a proc in a block...
VCR.use_cassette(vcr_cassette, &actions)
I think the explicit call is better, but just wanted to point out an alternative technique.

Rails Eager loading has_many associations for an existing object

I am fairly new to rails & I am having this performance issue that I would appreciate any help with.
I have a User model & each user has_many UserScores associated. I am preparing a dashboard showing different user stats including counts of user_scores based on certain conditions. Here is a snippet of the code:
def dashboard
#users = Array.new
users = User.order('created_at ASC')
users.each do |u|
user = {}
user[:id] = u.id
user[:name] = u.nickname
user[:email] = u.email
user[:matches] = u.user_scores.count
user[:jokers_used] = u.user_scores.where(:joker => true).length
user[:jokers] = u.joker
user[:bonus] = u.user_scores.where(:bonus => 1).length
user[:joined] = u.created_at.strftime("%y/%m/%d")
if user[:matches] > 0
user[:last_activity] = u.user_scores.order('updated_at DESC').first.updated_at.strftime("%y/%m/%d")
else
user[:last_activity] = u.updated_at.strftime("%y/%m/%d")
end
#users << user
end
#user_count = #users.count
end
The issue I am seeing is repeated UserScore db queries for each user to get the different counts.
Is there a way to avoid those multiple queries??
N.B. I'm not sure if my approach for preparing data for the view is the optimal way, so any advice or tips regarding that will be greatly appreciated as well.
Thanks
You need to eager load users_scores to reduce multiple queries. #Slava.K provided good explanation on how to eliminate that.
Add includes(:user_scores) for querying users, and use ruby's methods to work with collections once data is fetched from DB through query.
See code below to understand that:
users = User.includes(:user_scores).order('created_at ASC')
users.each do |u|
....
user[:matches] = u.user_scores.length
user[:jokers_used] = u.user_scopes.select{ |score| score.joker == true }.length
user[:jokers] = u.joker
user[:bonus] = u.user_scores.select{ |score| score.bonus == 1 }.length
....
end
Also, The way you are preparing response is not clean and flexible. Instead you should override as_json method to prepare json which can consumed by views properly. as_json method is defined for models by default. You can read more about it from official documentation http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html or visit article on preparing clean json response where I explained about overriding as_json properly in-depth.
Use includes method for eager loading your has many associations. You can understand this concept here: https://www.youtube.com/watch?v=s2EPVMqOsTQ
Firstly, reference user_scores association in your query:
users = User.includes(:user_scores).order('created_at ASC')
Follow rails documentation associations eager loading: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Also note that where makes new query to the database even if your association is already preloaded. Therefore, instead of
u.user_scores.where(:joker => true).length
u.user_scores.where(:bonus => 1).length
try:
u.user_scores.count { |us| us.joker }
u.user_scores.count { |us| us.bonus == 1 }
You will probably have to rewrite .user_scores.order('updated_at DESC').first.updated_at.strftime("%y/%m/%d") somehow as well

Can this ruby method be refactored?

A user has multiple libraries, and each library has multiple books. I want to know if a user has a book in one of his libraries. I'm calling this method with: current_user.has_book?(book):
def has_book?(book)
retval = false
libraries.each do |l|
retval = true if l.books.include?(book)
end
return retval
end
Can my method be refactored?
def has_book?(book)
libraries.any?{|lib| lib.books.include?book}
end
Pretty much the simplest i can imagine.
Not Nil safe though.
def has_book?(book)
libraries.map { |l| l.books.include?(book) }.any?
end
map turns collection of library objects into collection of bools, depending do they include the book, and any? returns true if any of the elements in the array is true.
this has fewer lines, but your original solution could be more efficient if you return true as soon as you find a library that contains the book.
def has_book?(book)
libraries.each do |l|
return true if l.books.include?(book)
end
return false
end
btw. both php and ruby appeared about the same time.
This looks to be the most minified version, I can't see any way to minify it more:
def has_book?(a)c=false;libraries.each{|b|c=true if b.books.include?a};c end
I don't know why #Зелёный removed his answer but this was a good one IMO so I paste it here:
def has_book?(book)
libraries.includes(:books)
.where(books: {id: book.id})
.any?
end

Rails find_or_create_by where block runs in the find case?

The ActiveRecord find_or_create_by dynamic finder method allows me to specify a block. The documentation isn't clear on this, but it seems that the block only runs in the create case, and not in the find case. In other words, if the record is found, the block doesn't run. I tested it with this console code:
User.find_or_create_by_name("An Existing Name") do |u|
puts "I'M IN THE BLOCK"
end
(nothing was printed). Is there any way to have the block run in both cases?
As far as I understand block will be executed if nothing found. Usecase of it looks like this:
User.find_or_create_by_name("Pedro") do |u|
u.money = 0
u.country = "Mexico"
puts "User is created"
end
If user is not found the it will initialized new User with name "Pedro" and all this stuff inside block and will return new created user. If user exists it will just return this user without executing the block.
Also you can use "block style" other methods like:
User.create do |u|
u.name = "Pedro"
u.money = 1000
end
It will do the same as User.create( :name => "Pedro", :money => 1000 ) but looks little nicer
and
User.find(19) do |u|
..
end
etc
It doesn't seem to me that this question is actually answered so I will. This is the simplest way, I think, you can achieve that:
User.find_or_create_by_name("An Existing Name or Non Existing Name").tap do |u|
puts "I'M IN THE BLOCK REGARDLESS OF THE NAME'S EXISTENCE"
end
Cheers!

Resources