Find records created in last 3 months in rails - ruby-on-rails

Below is the query to get the numbers of agents created in each month in last three months.
agents_per_month = Agents.where("created_at > ? AND created_at < ?", Date.today.at_beginning_of_month - 2.months, Date.today).group("date_trunc('month', created_at)").count
The result output is as follows:
{2020-07-01 00:00:00 UTC=>75, 2020-08-01 00:00:00 UTC=>31}
The issue with the above result is that since the last third month had no agents created so it doesn't show any value. But I want it to show result in the order as below:
{2020-06-01 00:00:00 UTC=>0, 2020-07-01 00:00:00 UTC=>75, 2020-08-01 00:00:00 UTC=>31}
So if a month doesn't have any value it should show 0 rather than no value shows up for the particular month.
Please help me resolve this issue.

You can join to a generate_series table using PostgreSQL:
class Agent < ApplicationRecord
def self.totals_in_last_three_months
joins("
RIGHT JOIN generate_series(
date_trunc('month', statement_timestamp() - interval'2 months'),
date_trunc('month', statement_timestamp()),
interval'1 month'
) as months(month)
ON date_trunc('month', agents.created_at) = months.month")
.group('months.month')
.count('agents.*')
end
end

Related

How to get users that was created on specific day of week on ruby?

i'm trying to create some kpi's on ruby on rails.
How can i count all users that was created on sunday or on wednesday for example?
I now that ruby can get day of week with the follow code for example:
u = User.last
u.created_at.wday #==> wday get the day of week (0: sunday, 1: monday..)
But the following doesn't work:
User.where(created_at.wday: 0).count
And I don't want to loop each user, check if it was created on sunday and put it in an array, because it seems to be costly.
Any ideas?
If you're using MYSQL you can use dayofweek
User.where('DAYOFWEEK(created_at) = ?', 0).count
For Postgres this should do the trick:
User.where("extract(dow from created_at) = ?", 0)
EDIT: For SQLITE the following works:
User.where("strftime('%w', created_at) = ?", 0)

Rails - How to group dates by time when they're in different timezones?

I have a series of Appointments where the date and time is stored under start_date as DateTime. I'd like to categorize them as starting in the Morning, Daytime, or Evening. I created an array of hashes with labels and ranges I used for a SQL statement where I convert the start_date records into seconds using CAST(EXTRACT (EPOCH FROM start_time) AS INT)
TIME_RANGES = [
{label: "Morning", min: 0, max: 32399},
{label: "Daytime", min: 32400, max: 61199},
{label: "Evening", min: 61200, max: 86399}
]
cases = TIME_RANGES.map do |r|
"when CAST (EXTRACT (EPOCH FROM start_date) AS INT) % 86400 between '#{r[:min]}' and '#{r[:max]}' then '#{r[:label]}'"
end
time_ranges = Appointment.select("count(*) as n, case #{time_cases.join(' ')} end as time_range").group('time_range')
This takes the number of seconds in a day (86400) and labels the appointments based on the modulo of the start_date with 86400. However, a number of appointments take place in different timezones, but they're all stored as UTC. So an appointment at 08:00 AM EST is equivalent to one at 07:00 AM CST, but both are stored internally as 12:00 PM UTC. This would cause the appointments to be incorrectly labelled as "Daytime" when they're intended to be "Morning" (from the perspective of the User booking it).
Ideally, I would like some way to convert the start_date based on the User's timezone to make it look like it occurred in UTC. So I would want a 12:00 PM EST appointment to be labelled as if it were a 12:00 PM UTC appointment instead of 04:00 PM UTC. More specifically, I would like to subtract 14400 seconds from the converted start_date before performing the modulo.
I can join Appoinments to Users, which contains the User's timezone. How can I incorporate this information into my query above, so that a modified start_date is used for each record, depending on the User's timezone in that same record?
I know I could accomplish this with a loop of each timezone and adding/substracting a specific amount of seconds in each loop, then combining the results of all the loops, but I was wondering if there was a way to do it in one query.
Per my comment, I am assuming we have three tables: appointments, users, and preferences. In appointments we have start_date and user_id. In users we have preference_id. In preferences we have some column that names the time zone, so I'll call that tz_name.
Note: Postgres timezone functions are messy. I would highly recommend you read up on them. This excellent article is a good place to start.
It is possible to use pure SQL to generate the time ranges and return a grouped result. A pure SQL solution would be best if you need to label and group many records (thousands or more) at a time.
Assuming you are working with 1000 records or fewer at a time, you'll probably want to use Rails scopes, as this will give you an ActiveRecord result. Then you'll do your grouping using Ruby's Array methods.
That solution would look something like this:
# app/user.rb
class User < ApplicationRecord
belongs_to :preference
has_many :appointments
end
# app/appointment.rb
class Appointment < ApplicationRecord
belongs_to :user
scope :with_seconds, lambda {
joins(user: :preference)
.select('appointments.*, extract(epoch from timezone(tz_name, start_date::timestamptz)::time)::int as seconds')
}
# This method is optional. If it is excluded, calling #seconds
# on an appointment instance will raise an error if the
# .with_seconds scope has not been applied.
def seconds
attributes['seconds']
end
def time_range
return nil unless seconds.present?
if seconds.between?(0, 32_399)
'Morning'
elsif seconds.between?(32_400, 61_199)
'Daytime'
else
'Evening'
end
end
end
The select portion of the scope probably deserves some explanation.
start_date::timestamptz: Take the start_date, which is stored as a Postgres timestamp, and convert it into a Postgres timestamp with time zone in the time zone of the Postgres server (presumably UTC).
timezone(tz_name, start_date::timestamptz): Convert the timestamp with time zone back into a timestamp type in the local time of the tz_name time zone.
timezone(tz_name, start_date::timestamptz)::time: Drop the date and keep the time component.
Then we extract epoch from that time component, which converts it into seconds.
Finally we convert the result to an integer to avoid anything falling through the cracks when we determine the time range.
Now you can do:
Appointment.all.with_seconds.group_by(&:time_range)
or
user = User.first
user.appointments.with_seconds.group_by(&:time_range)
For a pure SQL solution that will return ids grouped under the three time ranges, add this method to your Appointment model:
def self.grouped_by_time_range
current_scope = with_seconds.to_sql
query = <<~SQL
with converted_seconds as (#{current_scope})
select array_agg(id) as ids, case when seconds < 32400 then 'Morning'
when seconds < 61200 then 'Daytime'
else 'Evening' end as time_range
from converted_seconds
group by time_range
SQL
result = ActiveRecord::Base.connection.execute(query.squish)
result.to_a
end
If you don't need strictly SQL based solution you might use Ruby's select method to extract this appointments as in this example:
(I am assuming there is some kind of tz_name field in appointment model which holds timezone name)
morning_appointments = Appointment.all.select do |a|
a.start_date.in_time_zone(a.tz_name).hour > 6 && a.start_date.in_time_zone(a.tz_name).hour < 20
end
Edit:
Thanks #moveson for pointing out my mistake, I changed solution a bit.

ruby how to group by given date

I want to find signup count daily, for the date range say this month. so
starts_at = DateTime.now.beginning_of_month
ends_at = DateTime.now.end_of_month
dates = ((starts_at.to_date)..(ends_at.to_date)).to_a
dates.each_with_index do |date,i|
User.where("created_at >= ? and created_at <= ?", date, date.tomorrow)
end
So nearly 30 queries running, how to avoid running 30 query and do it in single query?
I need something like
group_by(:created_at)
But in group by if there is no data present for particular date it's showing nothing, but I need date and count as 0
I followed this:
How do I group by day instead of date?
def group_by_criteria
created_at.to_date.to_s(:db)
end
User.all.group_by(&:group_by_criteria).map {|k,v| [k, v.length]}.sort
Output
[["2016-02-05", 5], ["2016-02-06", 12], ["2016-02-08", 6]]
There is no data for 2016-02-05 so it should be included with count 0
I can't test it at the moment, but it should be possible to filter your date range and group it with a little help of your dbms like this:
User.select('DATE(created_at)').where("created_at >= ? and created_at <= ?", DateTime.now.beginning_of_month, DateTime.now.end_of_month).group('DATE(created_at)').count
Would this do?
starts_at = DateTime.now.beginning_of_month
ends_at = DateTime.now.end_of_month
User.where(created_at: starts_at..ends_at).group("date(created_at)").count
# => {Tue, 09 Feb 2016=>151, Mon, 08 Feb 2016=>130}
Note that you won't get any results for dates when there has been zero creations, so you might want to do something like this:
Hash[*(starts_at..ends_at).to_a.flat_map{|d| [d, 0]}].merge(
User.where(created_at: starts_at..ends_at).group("date(created_at)").count
)
Not pretty, but what happens there is you first create a hash with all dates in the range having zero values and merging the results from database into that hash.

Count users created in last 7 days in rails ? [need help in syntax of Count]

I saw this Count records created within the last 7 days but still need some help in getting the syntax of COUNT right, right now I have
#count_users = User.count('comment')
which gives me Count of all comments but I need to know count of all comments made only in last 7 days or in last 1 month, but I am not able to figure out the correct syntax for it
This count:
#count_users = User.count('comment')
Generates the following SQL:
SELECT COUNT(comment) FROM "users"
So unless you have a comment column in your users table, it is not counting the right objects.
If you have a Comment model, you can use the following to count all comments created in the past 7 days:
Comment.where('created_at >= ?', Time.zone.now - 7.days).count # count for a week
Comment.where('created_at >= ?', Time.zone.now - 1.months).count # count for a month
If you want to count the Comments of a specific User, you can use this (assuming a Comment belongs_to a User and a User has_many Comments):
Comment.where(user_id: user.i).where('created_at >= ?', Time.zone.now - 7.days).count

Rails Date After

I need to do a query on Rails model time stamps (created_at and updated_at) finding all records after a specified date. When executing the query:
Item.where("created_at > :date OR updated_at > :date", date: "2011-05-29")
> SELECT "items".* FROM "items" WHERE (created_at > '2011-05-29' OR updated_at > '2011-05-29')
It includes records like:
attributes:
created_at: 2011-05-29 16:07:10.664227
updated_at: 2011-05-29 16:07:10.664229
However, this date isn't greater than the one specified. Any simple way to fix this query? Thanks.
Edit:
I've also tried doing:
Item.where("created_at > :date OR updated_at > :date", date: "2011-05-29 16:07:10")
With the same result, however adding the fraction of seconds stamp as well gives the correct result.
Say you want records starting 30th May:
Then search for records that are >= 2011-05-30 00:00:00:
some_date = Time.now
Item.where("created_at >= :date OR updated_at >= :date", date: some_date.tomorrow.beginning_of_day)
I think the problem is that the created_at and updated_at are datetime, so if you only pass a date it defaults the time to 00:00:00 and anything past that is obviously greater. So the records you are seeing are past that date since the hour is greater.
Take a look at your precision, you can either pass 1 more day, and I'll show anything above that day, even greater for a second, or simply pass the hour as 23:59:59.
You can use my gem by_star to do this.
Post.after(date)
Unfortunately SQL disagrees with you:
mysql> SELECT '2011-05-30 12:00:00' > '2011-05-30';
+--------------------------------------+
| '2011-05-30 12:00:00' > '2011-05-30' |
+--------------------------------------+
| 1 |
+--------------------------------------+
And for comparing dates with datetimes, MySQL has this to say:
A DATE value is coerced to the DATETIME type by adding the time portion as '00:00:00'.

Resources