Rails 4 where,order,group,count include zero's - postgresql - ruby-on-rails

Here is my query:
User.where("created_at >= ? AND created_at <=?", date1,date2).order('DATE(created_at) DESC').group("DATE(created_at)").count
and I get output as:
{Thu, 15 May 2014=>1}
But I want to get output as 0 for the rest of the days. For ex
{Thu, 15 May 2014=>1,Fri, 15 May 2014=>0}
What I want to get is Users created in a date range, ordered and grouped by created_at and number of such Users for each day. When no users are there for a particular day it should return 0, which the current query doesn't return.

I agree with Himesh
Try this:
User.where("created_at >= ? AND created_at <=?", date1,date2).order('DATE(created_at) DESC').group("DATE(created_at)").count
Which will give:
{Thu, 15 May 2014=>1}
Get the date range as hash initialized to 0:
hash1 = Hash[(date1..date2).collect { |v| [v, 0] }]
Now merge the two hashes:
hash = hash1.merge(hash)
Do remember to merge hash with hash1 and not the vice-versa, because we want to overwrite the value of keys from hash to hash1

Although it doesn’t seem to be well known, Ruby hashes have a “default value” feature that will help you do what you want in a better way. Another way of expressing what you’re trying to do is this:
Create a hash that gives 0 for any key within my data range unless I have stored some other value under that key.
You can accomplish exactly this using a block to establish the default value of the hash:
#data = Hash.new do |h, key|
if (date1..date2).include?(key)
0
else
nil
end
end
Or, more succinctly:
#data = Hash.new {|h, key| (date1..date2).include?(key) ? 0 : nil }
Then load your data from the database and merge it into this hash:
#data.merge! User.where(…).group(…).count
Note: Your order() is irrelevant because you are grouping. It will probably get optimized away, but you can simply leave it off and get the same result.
Now if you do #data[foo]:
if foo is not comparable to a date you will get nil
if foo is outside the date range you will get nil
if foo is within the date range but no data was returned by your query you will get 0
if foo is a date with data in the database you will get the correct count
This method is preferable to pre-loading the hash because it will work efficiently (in time and space) even for very large date ranges. And arguably it is more straightforward too.

I created a gem exactly for this - check out Groupdate.

Perhaps, this is a bit difficult as if any user is not created on 16th May then there would be no record of 16th May as created_at in the DB and group by query result wont contain any value for 16th May.
Probably you will have to handle this in ruby. Or what you can do is check if a particular date is present as key in the hash_result, and if it is not present then user count is 0 by default.
Hope this helps :)

Related

How to get records created in each month in rails

Looking for a way to get all the records that are created in each month for a table
For example i need to know how to get a result like:
January: 6,
Feb: 9,
March: 10
Ideally i'm looking at using the created_at field in the database to compare against.
You can use GROUP BY and COUNT from within SQL to efficiently retrieve the data. Rails offers various options here to build an SQL query which performs aggregations and calculations with ActiveRecord::Calculations.
Assuming you have a model named Record for your records and you use MySQL / MariaDB for your database, this can be used to get the number of records per month:
records_per_month = Record.group('EXTRACT(YEAR_MONTH FROM created_at)').count
This will return a hash of Integers (corresponding to the year and month of the group so that e.g. records in May 2022 will groups under the key 202205) and the number of records within this month as values.
From your example, this would be
{
202201 => 6,
202202 => 9,
202203 => 10
}
If desired, you can then further "format" the keys, e.g.
records_per_month.transform_keys! do |year_month|
Date.strptime(year_month.to_s, '%Y%m').strftime('%B %Y')
end
Here, we parse year-month integer as a date with Date.strptime and format the date with Date#strftime to show the month name and year, e.g. "February 2022".
Imagine you have a Users table (my Rails application has one), like this:
id
name
.
.
.
created_at
updated_at
You could use this code, which would return a hash of months with the count:
users = User.all
users.group_by {|u| u.created_at.strftime("%B")}.transform_values {|v| v.count}
Returns something like:
{"September"=>33,
"August"=>1,
"October"=>1,
"February"=>55,
"January"=>185,
"May"=>4,
"December"=>145,
"June"=>8,
"November"=>19,
"March"=>51,
"April"=>27,
"July"=>5}
Explanation
created_at.strftime("%B")
This converts the date to a Month, using strftime
users.group_by {|u| u.created_at.strftime("%B")}
Creates a hash that groups the user records by the Month name, using group_by
.transform_values {|v| v.count}
Instead of a collection of records, we just want the count. We leave the key alone in the hash, and use transform_values to count the values.

How to iterate over an ActiveRecord resultset in one line with nil check in Ruby

I have an array of Active Record result and I want to iterate over each record to get a specific attribute and add all of them in one line with a nil check. Here is what I got so far
def total_cost(cost_rec)
total= 0.0
unless cost_rec.nil?
cost_rec.each { |c| total += c.cost }
end
total
end
Is there an elegant way to do the same thing in one line?
You could combine safe-navigation (to "hide" the nil check), summation inside the database (to avoid pulling a bunch of data out of the database that you don't need), and a #to_f call to hide the final nil check:
cost_rec&.sum(:cost).to_f
If the cost is an integer, then:
cost_rec&.sum(:cost).to_i
and if cost is a numeric inside the database and you don't want to worry about precision issues:
cost_rec&.sum(:cost).to_d
If cost_rec is an array rather than a relation (i.e. you've already pulled all the data out of the database), then one of:
cost_rec&.sum(&:cost).to_f
cost_rec&.sum(&:cost).to_i
cost_rec&.sum(&:cost).to_d
depending on what type cost is.
You could also use Kernel#Array to ignore nils (since Array(nil) is []) and ignore the difference between arrays and ActiveRecord relations (since #Array calls #to_ary and relations respond to that) and say:
Array(cost_rec).sum(&:cost)
that'll even allow cost_rec to be a single model instance. This also bypasses the need for the final #to_X call since [].sum is 0. The downside of this approach is that you can't push the summation into the database when cost_rec is a relation.
anything like these?
def total_cost(cost_rec)
(cost_rec || []).inject(0) { |memo, c| memo + c.cost }
end
or
def total_cost(cost_rec)
(cost_rec || []).sum(&:cost)
end
Either one of these should work
total = cost_rec.map(&:cost).compact.sum
total = cost_rec.map{|c| c.cost }.compact.sum
total = cost_rec.pluck(:cost).compact.sum
Edit: if cost_rec is nil
total = (cost_rec || []).map{|c| c.cost }.compact.sum
When cost_rec is an ActiveRecord::Relatation then this should work out of the box:
cost_rec.sum(:cost)
See ActiveRecord::Calculations#sum.

Passing params to sql query

So, in my rails app I developed a search filter where I am using sliders. For example, I want to show orders where the price is between min value and max value which comes from the slider in params. I have column in my db called "price" and params[:priceMin], params[:priceMax]. So I can't write something kinda MyModel.where(params).... You may say, that I should do something like MyModel.where('price >= ? AND price <= ?', params[:priceMin], params[:priceMax]) but there is a problem: the number of search criteria depends on user desire, so I don't know the size of params hash that passes to query. Are there any ways to solve this problem?
UPDATE
I've already done it this way
def query_senders
query = ""
if params.has_key?(:place_from)
query += query_and(query) + "place_from='#{params[:place_from]}'"
end
if params.has_key?(:expected_price_min) and params.has_key?(:expected_price_max)
query += query_and(query) + "price >= '#{params[:expected_price_min]}' AND price <= '#{params[:expected_price_max]}'"
end...
but according to ruby guides (http://guides.rubyonrails.org/active_record_querying.html) this approach is bad because of SQL injection danger.
You can get the size of params hash by doing params.count. By the way you described it, it still seems that you will know what parameters can be passed by the user. So just check whether they're present, and split the query accordingly.
Edited:
def query_string
return = {}
if params[:whatever].present?
return.merge({whatever: #{params[:whatever]}}"
elsif ...
end
The above would form a hash for all of the exact values you're searching for, avoiding SQL injection. Then for such filters as prices you can just check whether the values are in correct format (numbers only) and only perform if so.

Save the most recent months of a hash

I have an array of hashes. Something like this...
transactions = [{"date"=>"2014-07-21", "amount"=>200},
{"date"=>"2012-06-21", "amount"=>400},
{"date"=>"2014-08-21", "amount"=>100},
{"date"=>"2014-08-12", "amount"=>150},
{"date"=>"2014-06-15", "amount"=>230}
{"date"=>"2013-05-21", "amount"=>900},]
I want to be able to save each months total amounts and then show the most recent 3 months to todays date and their total amount. Something like this...
Totals: 06-14 $230
07-14 $200
08-14 $250
I have this method but i am not sure how to get only the last 3 months to put in my database field and how to print it out.
def income_by_month
#payroll_transactions = current_user.transactions
#recent_payroll = #payroll_transactions.find_all {90.days.ago.to_date..Date.today}.map #finds transactions within 90 days
#amount_by_month = #recent_payroll.group_by { |t| t.date.to_date.month }.map do |month, transactions|
[month, transactions.sum(:amount)] #Groups transactions by month and adds month total
end.to_h
-EDIT-
I figured out a method to only get the transactions from the last 30 days I updated my method to show it. Now my question is how do I save the answer (Do i save it in one field as an Array?) and then how to show the answer in my view. Like I show it here. How do I print each key and value line by line in an order?
Totals: 06-14 $230
07-14 $200
08-14 $250
-EDIT-
Sorry my database is a mongoid db. And I want to save the most recent 3 months to todays date regardless of if an amount is available.
Let me start with a quick note on your code snippet:
group_by { |t| t.date.to_date.month }
Note that grouping objects by a single month does not take a year in count, so it would end summing up amounts for transactions of both 2012 and 2014 years in a one container. So what you really want is to group based on both month and year values.
Thinking of reducing the amount of redundant iterations through the input array (and using unnecessary aggregations), I've came to the following suggestion:
last_months = transactions.map{|i| Date.parse(i["date"]).strftime("%m-%Y")}.uniq.sort.last(3)
result = last_months.inject({}){|result, input| result[input] = 0; result}
transactions.inject(result) do |result, object|
# NOTE: we're already doing dates parsing and strftime two times here.
# In case you operate on Date objects themselves in your code, this is not the case.
# But the real perfomance measurement between summing all values up
# and strftiming more than once should be done additionally.
month = Date.parse(object["date"]).strftime("%m-%Y")
result[month] += object["amount"] if result[month]
result
end
# result now equals to {"06-2014"=>230, "07-2014"=>200, "08-2014"=>250}
First, we obtain those last three months (and years).
Next we create a hash to contain aggregated values with only those last months keys. At the end we sum up amount for only those transactions which seem to be one of the latter 3 months.
So, as long as ruby hashes (ruby v.1.9+) preserve the keys order, you can simply iterate over them to print out:
result.each{|k,v| puts "#{k}: #{v}"}
# 06-2014: 230
# 07-2014: 200
# 08-2014: 250
One last thing to note here is that doing this kind of aggregation inside of your server code is not quite efficient. Much more tempting option would be to move this calculations to your database layer.
ActiveSupport has some pretty slick date methods like Date#beginning_of_month:
require "date"
require "active_support/core_ext"
def process_transaction_group(month, transactions)
{
month: month.strftime("%Y/%m"),
total: transactions.map {|t| t["amount"] }.reduce(:+)
}
end
def process_transactions(transactions)
transactions
.group_by {|t| Date.parse(t["date"]).beginning_of_month }
.select {|month, _trxs| month < 3.months.ago }
.map {|month, trxs| process_transaction_group(month, trxs) }
end
###############
transactions = [{"date"=>"2014-07-21", "amount"=>200},
{"date"=>"2012-06-21", "amount"=>400},
{"date"=>"2014-08-21", "amount"=>100},
{"date"=>"2014-08-12", "amount"=>150},
{"date"=>"2014-06-15", "amount"=>230},
{"date"=>"2013-05-21", "amount"=>900}]
process_transactions(transactions)
#=> [{:month=>"2014/07", :total=>200}, {:month=>"2012/06", :total=>400}, {:month=>"2014/08", :total=>250}, {:month=>"2014/06", :total=>230}, {:month=>"2013/05", :total=>900}]

Rails: correct way of turning activerecord relation to array?

I was trying to select objects uniq by one attribute, using #videos.uniq{|p| p.author}
time = Time.new(2014, 12)
start_time = time.beginning_of_month
end_time = time.end_of_month
videos = Video.where("created_at > ? AND created_at < ?", start_time, end_time).where("likes > ?", 15)
selected_videos = videos.uniq{|p| p.author}
puts videos.count, videos.class, selected_videos.count
#=> 23, Video::ActiveRecord_Relation, 23
videos_first = videos.first(23)
selected_videos = videos_first.uniq{|p| p.author}
puts videos_first.count, videos_first.class, selected_videos.count
#=> 23, array, 10
.uniq is not for ActiveRecord_Relation. And the problem is that the query returns a Video::ActiveRecord_Relation, but I need array.
Certainly, this could be achieved by using to_a, but is this elegant?
What's the correct way of handling this ?
Is it possible to use .uniq for activerecord:relation?
If you need to access to the query result, just use #to_a on ActiveRecord::Relation instance.
At rails guides you can find on notable changes at Rails 4.0: "Model.all now returns an ActiveRecord::Relation, rather than an array of records. Use Relation#to_a if you really want an array. In some specific cases, this may cause breakage when upgrading." That is valid for other relation methods like :where.
selected_videos = videos.to_a.uniq{|p| p.author}
.uniq does not make much sense when it is applied across the full active-record record.
Given that at least one or more of the three attributes - id, created_at, and updated_at - are different for every row, applying videos.uniq{|p| p.author} where videos is a ActiveRecord::Relation including all fields, will return all the rows in the ActiveRecord::Relation.
When the ActiveRecord::Relation object has a subset of values, uniq will be able to figure out the distinct values from them.
Eg: videos.select(:author).uniq.count will give 10 in your example.
The difference between ActiveRecord::Relation#uniq and Array#uniq is that the Array version accepts a block and uses the return value of a block for comparison. The ActiveRecord::Relation version of uniq simply ignores the block.
If you need the records, you can use ActiveRecord::Relation#load
Causes the records to be loaded from the database if they have not been loaded already. You can use this if for some reason you need to explicitly load some records before actually using them. The return value is the relation itself, not the records.
https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-load

Resources