class TimeZoneScheduler

A Ruby library that assists in scheduling events whilst taking time zones into account. E.g. when to best deliver notifications such as push notifications or emails.

It relies on ActiveSupport’s time and time zone functionality and expects a current system time zone to be specified through `Time.zone`.

### Terminology

Consider a server sending notifications to a user:

Constants

VERSION

Attributes

destination_time_zone[R]

@return [ActiveSupport::TimeZone]

the destination time zone for the various calculations this class performs.

Public Class Methods

new(destination_time_zone) click to toggle source

@param [String, ActiveSupport::TimeZone] destination_time_zone

the destination time zone that calculations will be performed in.
# File lib/time_zone_scheduler.rb, line 30
def initialize(destination_time_zone)
  @destination_time_zone = Time.find_zone!(destination_time_zone)
end

Public Instance Methods

in_timeframe?(reference_time, timeframe) click to toggle source

This checks if the reference time falls in the given timeframe in the destination time zone.

For instance, you could use this to disable playing a sound for notifications that have to be scheduled in real time, but you don’t necessarily want to e.g. wake the user.

@example Return that 1PM in the Europe/Amsterdam time zone falls in the timeframe.

Time.zone      = "UTC" # Set the system time zone
scheduler      = TimeZoneScheduler.new("Europe/Amsterdam")
reference_time = Time.parse("2015-10-25 12:00 UTC")

p scheduler.in_timeframe?(reference_time, "08:00".."14:00") # => true

@example Return that 3PM in the Europe/Moscow time zone falls outside the timeframe.

Time.zone      = "UTC" # Set the system time zone
scheduler      = TimeZoneScheduler.new("Europe/Moscow")
reference_time = Time.parse("2015-10-25 12:00 UTC")

p scheduler.in_timeframe?(reference_time, "08:00".."14:00") # => true

@param [Time] reference_time

the reference time that’s to be checked if it falls in the timeframe in the destination time zone.

@param [Range<String..String>] timeframe

a range of times (of the day) in which the reference time should fall.

@return [Boolean]

whether or not the reference time falls in the specified timeframe in the destination time zone.
# File lib/time_zone_scheduler.rb, line 172
def in_timeframe?(reference_time, timeframe)
  TimeFrame.new(@destination_time_zone, reference_time, timeframe).reference_in_timeframe?
end
schedule_in_timeframe(reference_time, timeframe) click to toggle source

This calculation schedules the time to be at the same time as the reference time (real time), except when that time, in the destination time zone, falls outside of the specified timeframe. In that case it delays the time until the next minimum time of the timeframe is reached.

For instance, you could use this to schedule notifications about an event starting in either real-time, if that’s a good time for the user in their time zone, or otherwise delay it to the next good time.

@example Return the real time, as the reference time falls in the specified timeframe in the Europe/Amsterdam time zone.

Time.zone      = "UTC" # Set the system time zone
scheduler      = TimeZoneScheduler.new("Europe/Amsterdam")
reference_time = Time.parse("2015-10-25 12:00 UTC")
system_time    = scheduler.schedule_in_timeframe(reference_time, "10:00".."14:00")
local_time     = system_time.in_time_zone("Europe/Amsterdam")

p reference_time # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
p system_time    # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
p local_time     # => Sun, 25 Oct 2015 13:00:00 CET +01:00

@example Delay the reference time so that it’s not scheduled before 10AM in the Pacific/Kiritimati time zone.

Time.zone      = "UTC" # Set the system time zone
scheduler      = TimeZoneScheduler.new("Pacific/Kiritimati")
reference_time = Time.parse("2015-10-25 12:00 UTC")
system_time    = scheduler.schedule_in_timeframe(reference_time, "10:00".."14:00")
local_time     = system_time.in_time_zone("Pacific/Kiritimati")

p reference_time # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
p system_time    # => Sun, 25 Oct 2015 20:00:00 UTC +00:00
p local_time     # => Mon, 26 Oct 2015 10:00:00 LINT +14:00

@example Delay the reference time so that it’s not scheduled after 2PM in the Europe/Moscow time zone.

Time.zone      = "UTC" # Set the system time zone
scheduler      = TimeZoneScheduler.new("Europe/Moscow")
reference_time = Time.parse("2015-10-25 12:00 UTC")
system_time    = scheduler.schedule_in_timeframe(reference_time, "10:00".."14:00")
local_time     = system_time.in_time_zone("Europe/Moscow")

p reference_time # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
p system_time    # => Mon, 26 Oct 2015 07:00:00 UTC +00:00
p local_time     # => Mon, 26 Oct 2015 10:00:00 MSK +03:00

@param [Time] reference_time

the reference time that’s to be re-scheduled in the destination time zone if it falls outside the timeframe.

@param [Range<String..String>] timeframe

a range of times (of the day) in which the scheduled time should fall.

@return [Time]

either the original reference time, if it falls in the timeframe, or the delayed time.
# File lib/time_zone_scheduler.rb, line 131
def schedule_in_timeframe(reference_time, timeframe)
  timeframe = TimeFrame.new(@destination_time_zone, reference_time, timeframe)
  if timeframe.reference_before_timeframe?
    timeframe.min
  elsif timeframe.reference_after_timeframe?
    timeframe.min.tomorrow
  else
    reference_time
  end.in_time_zone(Time.zone)
end
schedule_on_date(reference_time, raise_if_time_has_passed = true) click to toggle source

This calculation takes the local date and time of day of the reference time and converts that to the exact same date and time of day in the destination time zone and returns it in the system time. In other words, you’d use this to calculate the system time at which a specific date and time of day occurs in the destination time zone.

For instance, you could use this to schedule notifications that should be sent to users on specific days of the week at times of the day that they are most likely to be good for the user. E.g. every Thursday at 10AM.

@example Calculate the system time that corresponds to Sunday 2015-10-25 at 10AM in the Pacific/Niue time zone.

Time.zone      = "Pacific/Kiritimati" # Set the system time zone
scheduler      = TimeZoneScheduler.new("Pacific/Niue")
reference_time = Time.parse("2015-10-25 10:00 UTC")
system_time    = scheduler.schedule_on_date(reference_time, false)

p reference_time # => Sun, 25 Oct 2015 10:00:00 UTC +00:00
p system_time    # => Mon, 26 Oct 2015 11:00:00 LINT +14:00

p system_time.sunday? # => false
p system_time.hour    # => 11

p local_time = system_time.in_time_zone("Pacific/Niue")
p local_time.sunday? # => true
p local_time.hour    # => 10

@param [Time] reference_time

the reference date and time of day that’s to be scheduled in the destination time zone.

@param [Boolean] raise_if_time_has_passed

whether or not to check if the time in the destination time zone has already passed.

@raise [ArgumentError]

in case the check is enabled, this is raised if the time in the destination time zone has already passed.

@return [Time]

the system time that corresponds to the time scheduled in the destination time zone.
# File lib/time_zone_scheduler.rb, line 70
def schedule_on_date(reference_time, raise_if_time_has_passed = true)
  destination_time = @destination_time_zone.parse(reference_time.strftime('%F %T'))
  system_time = destination_time.in_time_zone(Time.zone)
  if raise_if_time_has_passed && system_time < Time.zone.now
    raise ArgumentError, "The specified time has already passed in the #{@destination_time_zone.name} timezone."
  end
  system_time
end