I'm Ashley Foster, a React and Rails developer in Boston, MA
March 2017
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:

  1. 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.
  2. 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 Postres'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 return 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.