I am using the following gems:
timezone
tzinfo
I am trying to have a newsletter sent at user
's time zone, so that they get the newsletter at their 9:00:00; not 09:00:00 PST where my server is.
I am storing the time zones by name, like "Pacific Time (US & Canada)". Each user has a column user.time_zone
where their specific time zone is stored.
I have built a job
that checks every hour for user
s who could receive the newsletter (i.e. user
s whose local time is 9:00 AM).
class NewsletterTimezoneJob < ActiveJob::Base
def self.perform
users = User.all.gets_newsletter
users.each do |user|
d = Date.current
t = "09:00:00 -0700"
t = t.to_time
newsletter_sendtime = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec, t.zone)
newsletter_sendtime = newsletter_sendtime.to_s
if user.is_in_time_around(newsletter_sendtime)
NewsletterMailer.send_mailer(user, newsletter_sendtime).deliver_later
end
end
end
end
I have built a method on user
that tries to see if the user's local time matches newsletter_sendtime
.
I added a +/- 5 minute window to account for slow servers.
User.rb
:
def is_in_time_around(timeofday)
timeofday = timeofday.to_time
user_zoned = timeofday.in_time_zone(self.time_zone)
user_zoned = user_zoned.strftime("%r").to_time
timeofday = timeofday.strftime("%r").to_time
if user_zoned >= (timeofday - 5.minutes) && user_zoned <= (timeofday + 5.minutes)
true
end
end
This job
and method
should run every hour and send to different users in different time zones each hour; and yet it currently only sends to users in "Pacific Time (US & Canada)," each hour and never sends it to any other `users.
I see a few problems with your approach: the code seems overly complex and you are most probably (as Jon noticed in his comment above) comparing only against 9AM Pacific time. Also, with your approach you need to traverse all users and compare the timezone of each of them - this will lead to bad performance when the users table grows.
I suggest a completely opposite approach, in plain words it's this: Find all timezone names in which the current local time is around 9AM now and then select users in those timezones.
You are using Rails, everything is possible with built-in methods then. With the help of this answer, you can get a given time (e.g. 9AM) in the given timezone (e.g. Pacific time) like this:
ActiveSupport::TimeZone["Pacific Time (US & Canada)"].parse("09:00:00")
# => Tue, 29 Mar 2016 09:00:00 PDT -07:00
The TimeZone
module also provides the all
method to get all defined timezones. We need to compare the local time (rounded to hours) with the desired time (9AM) in all timezones to select those zones in which the hour is currently 9AM. First let's get the local time rounded to hours:
Time.zone.now
# => Tue, 29 Mar 2016 13:46:25 CEST +02:00
Time.zone.parse("#{Time.zone.now.hour}:00:00")
# => Tue, 29 Mar 2016 13:00:00 CEST +02:00
Next, compare this rounded local time to 9AM in all timezones and get the matching timezone names:
local_time = Time.zone.parse("#{Time.zone.now.hour}:00:00")
ActiveSupport::TimeZone.all.select { |tz| tz.parse("09:00:00") == local_time }.map(&:name)
# => ["Greenland", "Mid-Atlantic"]
This particular result shows that while it is 13 hours (1PM) here in Prague, it is 9 hours (9AM) in the Greenland zone, for example.
Having that, the rest is easy:
class NewsletterTimezoneJob < ActiveJob::Base
def self.perform
User.all.gets_newsletter.with_time_zone(zones_with_local_time("09:00")).each do |user|
NewsletterMailer.send_mailer(user).deliver_later
# for simplicity, I removed the `newsletter_sendtime` parameter from the mailer call
end
end
end
Where the zones_with_local_time
is a helper function defined anywhere which returns the timezones in which the local time is the given time:
# if defined in the NewsletterTimezoneJob, it should be a class method
def self.zones_with_local_time(hhmm)
local_time = Time.zone.parse("#{Time.zone.now.hour}:00:00")
ActiveSupport::TimeZone.all.select { |tz| tz.parse("#{hhmm}:00") == local_time }.map(&:name)
end
And with_time_zone
is just a scope acting upon the timezone condition in User
model:
# app/models/user.rb
scope :with_time_zone,
->(zones) { where("time_zone in (?)", zones) }
Now all that you have to do is set up a cron job to run every hour e.g. the 1st minute of the hour.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With