
My latest personal project, DayScore, is a site where users give themselves a daily todo list and keep a score each day charting their success at sticking to their goals. I wanted to make DayScore as simple as possible for a first time user, so I didn’t want them thinking about usernames or timezones or any of that annoying layer of configuration that sucks away your precious time. To make time-zones “just work” I implemented the following: each time the user makes an action, they send their local time-zone offset to the server so the server knows the time-zone the user is in.
The following javascript gets the local time-zone offset:
(new Date()).getTimezoneOffset()
For me this gives
(new Date()).getTimezoneOffset()
> -480
Where -480 is the number of minutes to UTC time from the time-zone I’m in, ie subtract 8 hours since I’m in China. The implicit equation is:
local time + offset = UTC
Send this offset to your server each time you post data so the server can always be up to date on the user’s time.
Things get a bit confusing here since Ruby inverses the offset. In Ruby you can get the time-zone offset using:
Time.now.utc_offset
On my local machine (in China) this gives:
Time.now.utc_offset
> 28800
Which is the number of seconds from UTC time to my time. Notice the implicit equation is reversed and is therefore:
server time - offset = UTC
Time-zones can get pretty confusing pretty fast so an easy approach is to just always use UTC time on the server, which means we only have to consider the UTC offset for the user. In order to update the offset whenever the user makes an action, I have the following filter in my main controller:
class MainController < ApplicationController
before_filter :update_user_time_diff
def update_user_time_diff
if params[:timezone_offset_minutes]
@user.update_user_time_diff params[:timezone_offset_minutes].to_i
end
end
By making this a before filter called before every action, I can ensure that even if the user’s time changes significantly while they are using the app, data returned from the database will be correct and in the user’s time-zone. The user model has an integer field, time_diff which is the time offset which satisfies the following equation:
UTC time + user.time_diff = user time
The time difference is then calculated and updated in the user model like so:
def update_user_time_diff (user_timezone_offset_minutes)
self.time_diff = - user_timezone_offset_minutes * 60
end
Notice we don’t need self.save since this function is called by the controller before filter, so the save will happen in the called action anyway (in this app at least) and we can keep the database requests down.
Finally to bring this all together, the user model implements a function called user_today which gets the date in the user’s time-zone.
def user_today
if self.time_diff != nil
user_time = Time.now.utc + self.time_diff
else
user_time = Time.now.utc
end
# consider the day to change over at 3AM
if user_time.hour < 3
return user_time.yesterday.to_date
else
return user_time.to_date
end
end
Any database queries or other actions requiring today’s date are called with @user.user_today rather than Date.today in order to give results in the user’s time-zone. Furthermore whenever the time is needed on the server, Time.now.utc is called rather than Time.now.