Question

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?

Was it helpful?

Solution 3

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!

OTHER TIPS

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 %>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top