Ruby on Rails 3 Check In/Check Out ranges by hour - ruby-on-rails

I'm using Ruby on Rails 3 and I have a "visit" model which stores a check_in and check_out datetime and I need to search through visits in a general date range and count the number of "visitors present" grouped by all hours of the day.
...i.e. I need something like:
8:00am - 8:59am : 12 visitors
9:00am - 9:59am : 5 visitors
10:00am - 10:59am : 4 visitors
...given a table of visits with a check in and check out time stored.
The idea is to take check-in and check-out times for "visits" and then determine how many visitors (assuming each visit logs one visitor, which it does by policy) were visiting during any given hour of the day in order to find out peak visiting times.
I've tried setting up queries like:
eight_am_visits = Visit.where("EXTRACT(HOUR_MINUTE FROM check_in) <= 859").where("EXTRACT(HOUR_MINUTE FROM check_out) >= 800")
...and haven't quite hit on it because Rails stores dates in such an odd fashion (in UTC, which it will convert on database query) and it doesn't seem to be doing that conversion when I use something like EXTRACT in SQL...
...any idea how I can do this?

Looks like you're not actually interested in the Visit objects at all. If you just want a simple summary then push AR out of the way and let the database do the work:
# In visit.rb
def self.check_in_summary(date)
connection.select_rows(%Q{
select extract(hour from check_in), count(*)
from visits
where cast(check_in as date) = '#{date.iso8601}'
group by extract(hour from check_in)
}).inject([ ]) do |a, r|
a << { :hour => r[0].to_i, :n => r[1].to_i }
end
end
Then a = Visit.check_in_summary(Date.today - 1) will give you the summary for yesterday without doing any extra work. That demo implementation will, of course, have holes in the array for hours without any checkins but that is easy to resolve (if desired):
def self.check_in_summary(date)
connection.select_rows(%Q{
select extract(hour from check_in), count(*)
from visits
where cast(check_in as date) = '#{date.iso8601}'
group by extract(hour from check_in)
}).each_with_object([0]*24) do |r, a| # Don't forget the arg order change!
a[r[0].to_i] = r[1].to_i
end
end
That version returns an array with 24 elements (one for each zero-based hour) whose values are the number of checkins within that hour.
Don't be afraid to drop down to SQL when it is convenient, AREL is just one tool and you should have more than one tool in your toolbox. Also, don't be afraid to add extra data mangling and summarizing methods to your models, your models should have an interface that allows you to clearly express your intent in the rest of your code.

Maybe something like that?!
t = Time.now
eight_am_visits = Visit.all(:conditions => ['check_in > ? and check_in < ?', Time.utc(t.year, t.month, t.day, 8), Time.utc(t.year, t.month, t.day, 8, 59)])
EDIT:
Or you can grab all visits by day and filter it in Rails:
t = Time.now
visits = Visit.all(:conditions => ['created_at > ? and created_at < ?', Time.utc(t.year, t.month, t.day - 1), Time.utc(t.year, t.month, t.day + 1)])
visits_by_hour = []
(0..23).each do |h|
visits_by_hour << visits.map {|e| e if e.created_at > Time.utc(t.year, t.month, t.day, h) && e.created_at < Time.utc(t.year, t.month, t.day, h, 59)}.count
end
And in view:
<% visits_by_hour.each_with_index do |h, v| %>
<%= "#{h}:00 - #{h}:59: #{v} visitors" %>
<% end %>

Thanks for your help Olexandr and mu, I managed to figure something out with the insight you gave me here.
I came up with this, and it seems to work:
#grab the data here, this is nice because
#I can get other stats out of it (which I don't show here)
#visits = Visit.where(:check_in => #start_date..#end_date, :check_out => #start_date..#end_date).where("check_out IS NOT NULL");
#Here we go
#visitors_present_by_hour = {}
(0..23).each do |h|
# o.o Ooooooh.... o_o Hee-hee! ^_^
#visitors_present_by_hour[h] = #visits.collect{|v| v.id if v.check_in.hour <= h and v.check_out.hour >= h}.compact.count
end
Then I can just dump out that hash in my view.
It seems the solution was a bit simpler than I thought, and doing it this way actually makes rails do the time conversions from UTC.
So, I could just collect all the visits which have hours in the hour range, then compact out the nils and count what's left. I was surprised once I hit on it. I didn't need any custom SQL at all as I thought I would (unless this is completely wrong, but it seems to be working with some test data).
Thanks guys!

Related

How to get weekly records on Rails?

I have Payment model with data and amount attributes. Now I need to get all the records for the current week on the page. I want to use some kind of pagination, e.g.: On the first page I get all records for the current week, on the second page - records from the previous week etc.
And on every page I need to get total amount for this week and average amount per day.
So, I have two questions:
How to get all the records for particular week on the page?
How to count amount for this week?
Now everything I've done was array with weeks and amounts
def self.count_by_week
raw_result = group_by_week_and_state.count
# {['2014-12-01-2014-12-07', 'foo'] => 100, ['2014-12-01-2014-12-07', 'bar'] => 100, '...' => '...'}
raw_result.each_with_object({}) do |(k, v), result|
result[k[0]] ||= {}
result[k[0]][k[1]] = v
end
end
def self.group_by_week_and_state
group("#{weekday_query(0)} || \'-\' || #{weekday_query(6)}").group('amount')
end
# build sql part for day offset of week (0 => mon, 6 => sun)
def self.weekday_query(offset)
"to_char(cast(date_trunc(\'week\', created_at) as date) + #{offset}, \'YYYY-MM-DD\')"
end
You could use the groupdate gem to accomplish this.
https://github.com/ankane/groupdate
Once you have successfully grouped your records, it's just too simple to get the sum for each groups.

Ruby on Rails with sqlite, trying to query and return results from the last 7 days?

Noob here, I'm trying to query my SQLite database for entries that have been made in the last 7 days and then return them.
This is the current attempt
user.rb
def featuredfeed
#x = []
#s = []
Recipe.all.each do |y|
#x << "SELECT id FROM recipes WHERE id = #{y.id} AND created_at > datetime('now','-7 days')"
end
Recipe.all.each do |d|
#t = "SELECT id FROM recipes where id = #{d.id}"
#x.each do |p|
if #t = p
#s << d
end
end
end
#s
end
This code returns each recipe 6(total number of objects in the DB) times regardless of how old it is.
#x should only be 3 id's
#x = [13,15,16]
if i run
SELECT id FROM recipes WHERE id = 13 AND created_at > datetime('now','-7 days')
1 Rows returned with id 13 is returned
but if look for an id that is more than 7 days old such as 12
SELECT id FROM recipes WHERE id = 12 AND created_at > datetime('now','-7 days')
0 Rows returned
I'm probably over complicating this but I've spent way too long on it at this point.
the return type has to be Recipe.
To return objects created within last 7 days just use where clause:
Recipe.where('created_at >= ?', 1.week.ago)
Check out docs for more info on querying db.
Edit according to comments:
Since you are using acts_as_votable gem, add the votes caching, so that filtering by votes score is straightforward:
Recipe.where('cached_votes_total >= ?', 10)
Ruby is expressive. I would take the opportunity to use a scope. With Active Record Scopes, this query can be represented in a meaningful way within your code, using syntactic sugar.
scope :from_recent_week, -> { where('created_at >= ?', Time.zone.now - 1.week) }
This allows you to chain your scoped query and enhance readability:
Recipe.from_recent_week.each do
something_more_meaningful_than_a_SQL_query
end
It looks to me that your problem is database abstraction, something Rails does for you. If you are looking for a function that returns the three ids you indicate, I think you would want to do this:
#x = Recipe.from_recent_week.map(&:id)
No need for any of the other fluff, no declarations necessary. I also would encourage you to use a different variable name instead of #x. Please use something more like:
#ids_from_recent_week = Recipe.from_recent_week.map(&:id)

Rails Date Query for setting Expiration

I'm creating a marketplace app where sellers can list items to sell. I want to set an expiry date so listings over 30 days old do not show on the site.
I found some similar examples online but can't get this query to work.
#listings = Listing.where('created_at <= ?', Date.created_at + 30.day)
You want to query items whose created_at time is >= the current date (Date.current) - 30 days (30.day). So the query should simply be:
#listings = Listing.where('created_at >= ?', Date.current - 30.day)
You can also replace Date.current with Time.now or DateTime.now.
UPDATE: as user3334690 mentioned in a comment, it's recommended that you make this a model method since it's something that should be in the Model layer:
# app/models/listing.rb
def self.not_expired
where('created_at >= ?', Date.current - 30.day)
end
# now in controllers you can do something like
#listings = Listing.not_expired

Rails query: Compare calculation of two attribute values

I have a model, Product, which has both a :created_at timestamp and an :expiration_in_days attribute. Products are considered expired a certain number of days after their creation. How do I write a query that only returns products that have not expired?
Rails 4, Ruby 2.1, PG 0.17
I have been trying queries like this, with no success:
#product.rb
def self.not_expired
where('created_at + expiration_in_days * 86400 >= ?', Time.now)
end
I have a pure Ruby version that works fine, it's just slower:
#product.rb
def self.not_expired
select{ |p| (p.created_at.to_i + p.expiration_in_days * 86400) >= Time.now.to_i}
end
note, the 86400 calculation converts the the :expiration_in_days integer into seconds
Any pointers on more advanced Rails queries than the documentation (http://guides.rubyonrails.org/active_record_querying.html) would be very welcome as well.
Thanks in advance.
Try this:
def self.not_expired
where("created_at + (expiration_in_days * 86400)::text::interval >= ?", Time.now)
end
UPDATE: Add references
You can learn more about date and time function here.
Given your case has a special requirement, which is the value of expiration_in_days is a column in the table, we cannot use created_at + interval expiration_in_days day. Instead, we need to type cast its value to interval. But you can't type cast straight to an integer, that's why we cast it to text first.
A + B > C is true if A <= C - B
So, instead of trying to add the expiration time to created_at, subtract the expiration time from Time.now.
def expiration_threshold
Time.now - amount_of_time # I'm not sure what type to use
end
def self.not_expired
where( "created_at > ? ", expiration_threshold )
end
I've always been a little stumped about what type Rails/Ruby will want when dealing with various dates/times, you may have to play around.
I'm not sure if this would work but try something like this
def self.not_expired
where("TIMESTAMP created_at + INTERVAL '1 day' * expiration_in_days >= NOW()")
end

Rails named scope for left joined model

I have a table named acts, and I'll like to run a query that rolls up act values for a whole week. I'd like to make sure the query always returns one row for each day of the week, even if there are no records for that day. Right now I'm doing it like this:
def self.this_week_totals
sunday = Time.now.beginning_of_week(:sunday).strftime("%Y-%m-%d")
connection.select_values(<<-EOQ)
SELECT COALESCE(SUM(end_time - start_time), '0:00:00') AS total_time
FROM generate_series(0, 6) AS s(t)
LEFT JOIN acts
ON acts.day = '#{sunday}'::date + s.t
WHERE deleted_at IS NULL
GROUP BY s.t
ORDER BY s.t
EOQ
end
Is there any way I could make this a named scope on the Act class so it can be combined with other conditions, for example to filter the Acts by a client_id? Since acts isn't in my FROM, but is part of the LEFT JOIN, I'm guessing not, but perhaps someone out there knows a way.
Edit: Just to clarify, the goal is for this method to always return exactly 7 Act objects, regardless of what's in the database.
if you want your query object to be chainable it must be an ActiveRelation object
where, select, order and the other Arel objects return ActiveRelation objects that are chainable, so if the below works you can chain off of the returned query object
note in rails 3 and up having a class method that returns an ActiveRelation is basically the same as a scope, they are both chainable query objects
class Act
def self.this_week_totals
sunday = Time.now.beginning_of_week(:sunday).strftime("%Y-%m-%d")
select("COALESCE(SUM(end_time - start_time), '0:00:00') AS total_time")
.from("generate_series(0, 6) AS s(t)")
.joins("LEFT JOIN acts ON acts.day = '#{sunday}'::date + s.t")
.where("deleted_at IS NULL")
.group("s.t")
.order("s.t")
end
# ...
end
client_id = params[:client_id]
Act.this_week_totals.where("client_id = ?", client_id)
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-from
Although I really thought I could use the solution from #house9, I don't see any way to avoid compromising on at least one of these goals:
Always yield 7 Act objects.
Return an ActiveRelation so I can compose this method with other scopes.
Permit joining to the clients table.
Here is the part-SQL/part-Ruby solution I'm actually using, which sadly gives up on point #2 above and also returns tuples rather than Acts:
def self.this_week(wk=0)
sunday = Time.now.beginning_of_week(:sunday)
sunday += wk.weeks
not_deleted.where("day BETWEEN ? AND ?", sunday, sunday + 7.days)
end
scope :select_sum_total_hours,
select("EXTRACT(EPOCH FROM COALESCE(SUM(end_time - start_time), '0:00:00'))/3600 AS total_hours")
scope :select_sum_total_fees,
joins(:client).
select("SUM(COALESCE(clients.rate, 0) * EXTRACT(EPOCH FROM COALESCE(end_time - start_time, '0:00:00'))/3600) AS total_fees")
def self.this_week_totals_by_day(wk=0)
totals = Hash[
this_week(wk)
.select("EXTRACT(DAY FROM day) AS just_day")
.select_sum_total_hours
.select_sum_total_fees
.group("day")
.order("day")
.map{|act| [act.just_day, [act.total_hours.to_f, act.total_fees.to_money]]}
]
sunday = Time.now.beginning_of_week(:sunday)
sunday += wk.weeks
(0..6).map do |x|
totals[(sunday + x.days).strftime("%d")] || [0, 0.to_money]
end
end
That could be DRYed up a bit, and it would produce errors if there were ever a month with fewer than 7 days, but hopefully it shows what I'm doing. The scopes for this_week, select_sum_total_hours, and select_sum_total_fees are used elsewhere, so I want to pull them out into scopes rather than repeating them in several big raw SQL strings.

Resources