Time Zones, What a Mess
For my first big Rails project I decided I wanted to challenge myself and create an application that sends users reminder texts sent at a specified day and time. The user can select their time zone which will be used to send their reminder texts at the correct time.
def tasks
today = Date.today.strftime("%A").downcase
time = Time.current
beginning_of_day = Date.today.beginning_of_day
Task.
where("? = ANY(days_of_week)", today).
where("time <= ?", time).
where("last_sent_at IS NULL OR last_sent_at < ?", beginning_of_day)
end
end
The most difficult problem I had to solve was working with time and time zones. The code I started with was okay, but I found that I was running into two problems:
beggining_of_day
doesn’t take into account the user’s timezone eg: The beginning of today in UTC is actually the previous day in EST.time
had a similar problem, it’s the current time in UTC and we’re not taking the user’s timezone into account either.
After some of trial and error, I ended up with this:
def tasks
today = Date.today.strftime("%A").downcase
time = Time.current
Task.
joins(:user).
where("? = ANY(days_of_week)", today).
where("time <= ?", time).
where("last_sent_at IS NULL OR last_sent_at < current_date at time zone users.time_zone")
end
First I had to join users
onto tasks
so I could access the user’s time zones. Next I used the current_date
function from Postgres to convert the current date to the user’s time zone.
With the first issue out of the way, I was able to tackle the second issue:
def tasks
today = Date.today.strftime("%A").downcase
Task.
joins(:user).
where("? = ANY(days_of_week)", today).
where("time <= current_time at time zone users.time_zone").
where("last_sent_at IS NULL OR last_sent_at < current_date at time zone users.time_zone")
end
end
For the second issue, I simplified my code further. I removed the time
variable and instead used Postgres’s current_time
function with the user’s time zone. This ensures that the user is receiving their text at the correct time.
Bonus Round!
One more bug to go! The today
variable I had originally defined was calling Date.today
which was not using the user’s time zone. This resulted in the task being called on the wrong day. Eg. 1am Monday UTC would actually be 8pm Sunday EST.
def tasks
Task.
joins(:user).
where("trim(both from to_char(current_date at time zone users.time_zone, 'day')) = ANY(days_of_week)").
where("time <= current_time at time zone users.time_zone").
where("last_sent_at IS NULL OR last_sent_at < current_date at time zone users.time_zone")
end
end
To fix this I found Postgres’s to_char
function which when combined with the current_date function allowed me to find the name of the current day in the user’s time zone.
I ran into one gotcha when writing this. I had originally called to_char(current_date at time zone users.time_zone, 'day')
. This seemed like it would work, but what I wasn’t accounting for was that calling day
returns the day padded with spaces. Calling trim
to both ends of the day name fixed the problem by removing the extra spaces.
Wrapping Up
Needless to say so far this project has had many ups and downs but solving these issues has made it worthwhile. Working with dates, time, and time zones can be extremely difficult, which I now know from personal experience. I hope that this article can help you wade through the mess I like to call time zones.