Question

I'm trying to display a count of impressions per day for the last 30 days in the specific users time zone. The trouble is that depending on the time zone, the counts are not always the same, and I'm having trouble reflecting that in a query.

For example, take two impressions that happen at 11:00pm in CDT (-5) on day one, and one impression that happens at 1:00am CDT. If you query using UTC (+0) you'll get all 3 impressions occurring on day two, instead of two the first day and one the second. Both CDT times land on the day two in UTC.

This is what I'm doing now, I know I must be missing something simple here:

start = 30.days.ago
finish = Time.now

# if the users time zone offset is less than 0 we need to make sure
# that we make it all the way to the newest data
if Time.now.in_time_zone(current_user.timezone) < 0
  start += 1.day
  finish += 1.day
end

(start.to_date...finish.to_date).map do |date|
  # get the start of the day in the user's timezone in utc so we can properly
  # query the database
  day = date.to_time.in_time_zone(current_user.timezone).beginning_of_day.utc
  [ (day.to_i * 1000), Impression.total_on(day) ]
end

Impressions model:

class Impression < ActiveRecord::Base
  def self.total_on(day)
    count(conditions: [ "created_at >= ? AND created_at < ?", day, day + 24.hours ])
  end
end

I've been looking at other posts and it seems like I can let the database handle a lot of the heavy lifting for me, but I wasn't successful with using anything like AT TIME ZONE or INTERVAL.

What I have no seems really dirty, I know I must missing something obvious. Any help is appreciated.

Was it helpful?

Solution

Ok, with a little help from this awesome article I think I've figured it out. My problem stemmed from not knowing the difference between the system Ruby time methods and the time zone aware Rails methods. Once I set the correct time zone for the user using an around_filter like this I was able to use the built in Rails methods to simplify the code quite a bit:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  around_filter :set_time_zone

  def set_time_zone
    if logged_in?
      Time.use_zone(current_user.time_zone) { yield }
    else
      yield
    end
  end
end

# app/controllers/charts_controller.rb

start = 30.days.ago
finish = Time.current

(start.to_date...finish.to_date).map do |date|
  # Rails method that uses Time.zone set in application_controller.rb
  # It's then converted to the proper time in utc
  time = date.beginning_of_day.utc
  [ (time.to_i * 1000), Impression.total_on(time) ]
end

# app/models/impression.rb

class Impression < ActiveRecord::Base
  def self.total_on(time)
    # time.tomorrow returns the time 24 hours after the instance time. so it stays UTC
    count(conditions: [ "created_at >= ? AND created_at < ?", time, time.tomorrow ])
  end
end

There might be some more that I can do, but I'm feeling much better about this now.

OTHER TIPS

Presuming the around_filter correctly works and sets the Time.zone in the block, you should be able to refactor your query into this:

class Impression < ActiveRecord::Base
  def self.days_ago(n, zone = Time.zone)
    Impression.where("created_at >= ?", n.days.ago.in_time_zone(zone))
  end
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top