If your project uses time zones, it’s likely that you will sometimes want to perform a task at a certain hour of the day in the timezone. It turns out that, with the help of ActiveSupport::Timezone it’s actually quite simple.

The problem

Let’s take a simple example. You have a User model where the timezone is stored. The user has tasks to perform and you want to send him a daily email at 7am with the task to do today.

class User < ActiveRecord::Base
  #first_name
  #last_name

  validates :timezone, presence: true

  has_many :tasks
end

Has we see, our User has a first name, last name, a timezone and many tasks.

In order to send a daily email at 7am, we will need to create a rake task and an associated cron job.

If you are hosting your application on Heroku, your options are the basic scheduler or the more advanced clock process.

For the purpose of this exercice say you have a basic scheduler task, bundle exec rake user_tasks:send_daily_due that will run hourly at xx:10.

How do we know the timezones where it’s 7am right now in order for our daily email to be sent?

ActiveSupport::Timezone to the rescue

ActiveSupport::Timezone lets you loop thought all the time zones and query their current time.

So if your task is being called every hour at 10, and you are looking for every time zone where it’s 7am you can achieve this with

zones = ActiveSupport::TimeZone.all.select{ |time| time.now.hour == 7 }.map(&:name)
# => ["Dublin", "Edinburgh", "Lisbon", "London", "West Central Africa"]

Now zones contains an array of zone names where it’s 7am right now, so you can select the users by doing

User.where(timezone: zones)

And we now have our users where time is 7am right now.

Create a lib for it

This system works fine where you only have to do it once or twice within the app, but if you find yourself having to duplicate this call multiple times, we’ve created a lib for it.

class TimeZoneSelector

  def initialize(options = [])
    reset_zones_caches
    # See here why we need to reset zone caches https://github.com/rails/rails/issues/7245

    all_zones
  end

  def where(options = [])
    options.each do |method|
      send(method)
    end
    self
  end

  def reset_zones_caches
    ActiveSupport::TimeZone.instance_variable_set("@zones", nil)
    ActiveSupport::TimeZone.instance_variable_set("@zones_map", nil)
  end

  def all_zones
    @zones ||= ActiveSupport::TimeZone.all
  end

  def where_hour_is(hour)
    # Hours range from 0,1,2,3 to 23
    all_zones.select!{ |time| time.now.hour == hour }
    self
  end

  def where_day_is(day)
    all_zones.select!{ |time| time.now.try(day) }
    self
  end

  def hour_7
    where_hour_is(7)
  end

  def hour_14
    where_hour_is(14)
  end

  def monday
    where_day_is(:monday?)
  end

  def wednesday
    where_day_is(:monday?)
  end

  def zones_names
    all_zones.map(&:name)
  end
end

Stick this in a lib/time_zone_selector.rb or app/services/time_zone_selector.rb depending on how trendy you are, and you can now query the lib at your own will:

# Assumes it's now Wednesday 14pm in the UK


TimeZoneSelector.new.where_hour_is(14).where_day_is(:wednesday?).zones_names
# => ["Dublin", "Edinburgh", "Lisbon", "London", "West Central Africa"]

TimeZoneSelector.new.where_hour_is(14).where_day_is(:monday?).zones_names
# => []

TimeZoneSelector.new.where([:hour_14, :wednesday]).zones_names
# => ["Dublin", "Edinburgh", "Lisbon", "London", "West Central Africa"]

TimeZoneSelector.new.where_hour_is(14).where_day_is(:wednesday?)
# => #<TimeZoneSelector:0x00000006ecec98 @zones=[#<ActiveSupport::TimeZone:0x00000005174760 @name="Dublin", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Europe/Dublin>, @current_period=#<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1427590800>,#<TZInfo::TimezoneOffset: 0,3600,IST>>,#<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1445734800>,#<TZInfo::TimezoneOffset: 0,0,GMT>>>>, #<ActiveSupport::TimeZone:0x000000050cc330 @name="Edinburgh", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Europe/London>, @current_period=#<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1427590800>,#<TZInfo::TimezoneOffset: 0,3600,BST>>,#<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1445734800>,#<TZInfo::TimezoneOffset: 0,0,GMT>>>>, #<ActiveSupport::TimeZone:0x00000004f2a4f0 @name="Lisbon", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Europe/Lisbon>, @current_period=#<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1427590800>,#<TZInfo::TimezoneOffset: 0,3600,WEST>>,#<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1445734800>,#<TZInfo::TimezoneOffset: 0,0,WET>>>>, #<ActiveSupport::TimeZone:0x00000004e76860 @name="London", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Europe/London>, @current_period=#<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1427590800>,#<TZInfo::TimezoneOffset: 0,3600,BST>>,#<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 1445734800>,#<TZInfo::TimezoneOffset: 0,0,GMT>>>>, #<ActiveSupport::TimeZone:0x00000002f1ee18 @name="West Central Africa", @utc_offset=nil, @tzinfo=#<TZInfo::TimezoneProxy: Africa/Algiers>, @current_period=#<TZInfo::TimezonePeriod: #<TZInfo::TimezoneTransitionDefinition: #<TZInfo::TimeOrDateTime: 357523200>,#<TZInfo::TimezoneOffset: 3600,0,CET>>,nil>>]>

I hope you find it useful, and if you do, don’t hesitate to share it!

Picture by Ieoplus on Flickr