TimeZone classes confusion

In “Rails Recipes” book there is a recipe how to deal with time zones.
However standard RoR TimeZone class doesn’t handle Daylight Saving Time.
It is rather impossible to use a such solution in a commercial app. So
the recipe suggests to install TZInfo gem with TimeZone class correctly
handling DST (you need also tzinfo_timezone plugin).

The TZInfo gem is pure ruby code and is quite slow. In fact the gem
functionality is already implemented in libc (they are based on the same
Olson time zone database) and Ruby Time class uses libc functions.

Assuming that you keep date/time in DB as UTC and default_timezone is
set to :utc (most reasonable solution), you can implement time zone
conversions something like this:

to convert posted date/time to UTC and save to DB

def user2utc(t)
ENV[“TZ”] = current_user.time_zone_name
res = Time.local(t.year, t.month, t.day, t.hour, t.min, t.sec).utc
ENV[“TZ”] = “UTC”
res
end

to display date/time in a view

def utc2user(t)
ENV[“TZ”] = current_user.time_zone_name
res = t.getlocal
ENV[“TZ”] = “UTC”
res
end

Above seems to work on Linux/Ubuntu and should be faster than TZInfo gem
based solution. So why people try to implement their own time zone
classes? Did I missed something?

My tests show that TZInfo (version 0.3.1 on Ruby 1.8.5) is about 9 times
slower than libc converting from UTC to local and about 7 times slower
converting from local to UTC:

Good result taking into account Ruby interpreter speed.

The reason I started developing TZInfo was that I needed a solution that
would work on both Windows and Linux. Setting the TZ environment
variable doesn’t work on Windows

I see, thanks to your library time zone conversion are fully portable.

Thanks for extensive answer,
GD

Grzegorz D. wrote:

In “Rails Recipes” book there is a recipe how to deal with time zones.
However standard RoR TimeZone class doesn’t handle Daylight Saving Time.
It is rather impossible to use a such solution in a commercial app. So
the recipe suggests to install TZInfo gem with TimeZone class correctly
handling DST (you need also tzinfo_timezone plugin).

The TZInfo gem is pure ruby code and is quite slow. In fact the gem
functionality is already implemented in libc (they are based on the same
Olson time zone database) and Ruby Time class uses libc functions.

My tests show that TZInfo (version 0.3.1 on Ruby 1.8.5) is about 9 times
slower than libc converting from UTC to local and about 7 times slower
converting from local to UTC:

tz = ‘America/New_York’
t = Time.now.utc
n = 100000

Benchmark.bm do |bm|
bm.report(‘tzinfo u->l:’) {
n.times { TZInfo::Timezone.get(tz).utc_to_local(t) } }
bm.report(‘libc u->l:’) {
n.times { utc2user(tz, t) } }
bm.report(‘tzinfo l->u:’) {
n.times { TZInfo::Timezone.get(tz).local_to_utc(t) } }
bm.report(‘libc l->u:’) {
n.times { user2utc(tz, t) } }
end

                 user     system      total        real

tzinfo u->l: 7.880000 0.000000 7.880000 ( 7.888051)
libc u->l: 0.870000 0.000000 0.870000 ( 0.868656)
tzinfo l->u: 10.110000 0.000000 10.110000 ( 10.116807)
libc l->u: 1.500000 0.000000 1.500000 ( 1.504355)

Above seems to work on Linux/Ubuntu and should be faster than TZInfo gem
based solution. So why people try to implement their own time zone
classes? Did I missed something?

The reason I started developing TZInfo was that I needed a solution that
would work on both Windows and Linux. Setting the TZ environment
variable doesn’t work on Windows (see
http://article.gmane.org/gmane.comp.lang.ruby.rails/75790). Even if
ENV[‘TZ’] could be made to work, Windows only stores timezone transition
information for the current year and doesn’t use compatible zone
identifiers.

For my purposes (use within Rails), the performance of TZInfo is good
enough. I probably do at most ~100 UTC to local conversions per page
(and usually far less). I have though been able to make some significant
performance gains in the year since the first release and I hope to
continue to make improvements in this area.

Another area TZInfo improves upon using the TZ environment variable is
in its handling of invalid and ambiguous local times (i.e. during the
transitions to and from daylight savings). Time.local always returns a
time regardless of whether it was invalid or ambiguous. TZInfo reports
invalid times and allows the ambiguity to be resolved by specifying
whether to use the DST or non-DST time or running a block to do the
selection.

In the example below 01:30 on 26-Mar-2006 shouldn’t exist as it occurs
during the transition from GMT to BST. 01:30 on 29-Oct-2006 refers to
both 02:30 and 01:30 UTC as it occurs during the transition from BST to
GMT:

irb(main):001:0> require_gem ‘tzinfo’
irb(main):002:0> tz = TZInfo::Timezone.get(‘Europe/London’)
irb(main):003:0> ENV[‘TZ’] = ‘Europe/London’
irb(main):004:0> tz.current_period.local_start.to_s
=> “2006-03-26T02:00:00Z”

irb(main):005:0> tz.local_to_utc(Time.utc(2006,3,26,1,30,0))
TZInfo::PeriodNotFound: TZInfo::PeriodNotFound
from ./lib/tzinfo/timezone.rb:338:in `period_for_local’

irb(main):006:0> Time.local(2006,10,29,1,30,0).utc
=> Sun Oct 29 01:30:00 UTC 2006

irb(main):007:0> tz.local_to_utc(Time.utc(2006,10,29,1,30,0))
TZInfo::AmbiguousTime: Time: Sun Oct 29 01:30:00 UTC 2006 is an
ambiguous local time.
from ./lib/tzinfo/timezone.rb:363:in `period_for_local’

irb(main):008:0> Time.local(2006,10,29,1,30,0).utc
=> Sun Oct 29 01:30:00 UTC 2006


Philip R.
http://tzinfo.rubyforge.org/ – DST-aware timezone library for Ruby

OK. If someone uses only Linux and doesn’t need to handle exceptions for
these two special hours per year then following code might be useful for
him.

This is very simple extension, two methods, to Ruby Time class to
convert time objects between different time zones. Set your TZ evn
variable to “UTC” and RoR default_timezone to :utc. Then:

  1. Load new methods require(“gdc_tzconv”)
  2. timeob.utc_to_local(“Europe/Warsaw”) to convert a datetime object
    loaded as UTC from DB to a user’s time zone.
  3. timeob.local_to_utc(“Europe/Warsaw”) to convert a posted datetime
    (created as UTC time by RoR) to a real UTC time value with assumption
    that the original time is in a user’s local time.

file extconf.rb:
require ‘mkmf’
create_makefile(“gdc_tzconv”)

file gdc_tzconv.c:
#include <string.h>
#include <time.h>
#include “ruby.h”

extern VALUE rb_cTime;

struct time_object {
struct timeval tv;
struct tm tm;
int gmt;
int tm_got;
};

static void
time_modify(time)
VALUE time;
{
rb_check_frozen(time);
if (!OBJ_TAINTED(time) && rb_safe_level() >= 4)
rb_raise(rb_eSecurityError, “Insecure: can’t modify Time”);
}

static VALUE
gdc_tzconv_utc_to_local(VALUE self, VALUE dest_tz_name)
{
struct time_object *tobj;
struct tm *tm_tmp;
time_t t;

ruby_setenv("TZ", StringValuePtr(dest_tz_name));
tzset();
Data_Get_Struct(self, struct time_object, tobj);
time_modify(self);

t = tobj->tv.tv_sec;
tm_tmp = localtime(&t);
if (!tm_tmp) {
    rb_raise(rb_eArgError, "localtime error");
}
tobj->tm = *tm_tmp;
tobj->tm_got = 1;
tobj->gmt = 0;
ruby_setenv("TZ", "GMT");
return self;

}

static VALUE
gdc_tzconv_local_to_utc(VALUE self, VALUE dest_tz_name)
{
struct time_object *tobj;
struct tm *tm_tmp, tm;
time_t t;
int is_dst;

ruby_setenv("TZ", StringValuePtr(dest_tz_name));
tzset();
Data_Get_Struct(self, struct time_object, tobj);
time_modify(self);

gmtime_r(&tobj->tv.tv_sec, &tm);
/* make mktime figure out whether DST is in effect */
tm.tm_isdst = -1;
t = mktime(&tm);
tm_tmp = gmtime(&t);
if (!tm_tmp) {
    rb_raise(rb_eArgError, "gmtime error");
}
tobj->tm = *tm_tmp;
tobj->tm_got = 1;
tobj->gmt = 1;
ruby_setenv("TZ", "GMT");
return self;

}

void Init_gdc_tzconv()
{
rb_define_method(rb_cTime, “utc_to_local”, gdc_tzconv_utc_to_local,
1);
rb_define_method(rb_cTime, “local_to_utc”, gdc_tzconv_local_to_utc,
1);
}