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.

Client-side

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.

Server-side

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.

  1. from-the-mediterranean reblogged this from ukoki
  2. ukoki posted this