TimeZone classes confusion

Grzegorz Daniluk 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