Fixing timezones in Drupal / Date

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
Boris Mann's picture

This is the infamous "off by one error" that we see from daylight savings time, etc. I sat down and discussed this with KarenS and also briefly mentioned it to Dries about a good candidate for Drupal 6. Unfortunately, didn't have enough time to get it in as a Google Summer of Code project.

I'm going to try and explain this -- KarenS, please correct me if I'm wrong.

Drupal currently stores offsets, not timezones, for users and other date information. This means we'll always have the off by one error -- we need to have timezones in core, and make use of the "Olson" database aka ZoneInfo: http://en.wikipedia.org/wiki/Zoneinfo

For the Date API module, a lot of the code comes from the current offset handling from the original event module. It also deals with offsets. It's possible that we could start making the transition to PHP5 attractive by doing "proper" TZ handling, and fallback to the current event code for PHP4.

PHP really should be supporting this and make it easy. It looks like as of PHP5 there are some good classes. An excellent write up is here: http://laughingmeme.org/2007/02/27/looking-at-php5s-datetime-and-datetim...

Action items:

  • explore timezone support in Date API using Olson database (PHP 5.2+ only, fallback to current event_api under PHP4)
  • look at what it would take to get timezone support in Drupal core (start here )
  • talk to Rasmus / PHP maintainers about recommended best approach for TZ support going forward, other than the mentioned DateTime etc. classes

Comments

Drupal definitely needs

sshvetsov's picture

Drupal definitely needs proper date and time zone handling in the core. My sites are in Canada and DST is not being adjusted properly (at all in fact). I wish it was possible to introduce it in Drupal 5 without having to rely on PHP5 classes.

For better or for worse,

mfb's picture

For better or for worse, using PHP 5's built-in timezone support seems like the "right" solution since a drupal timezone library would be slower.

Provide both

seanr's picture

You'd be surprised at how few hosts use PHP5 and for many sites, switching hosts is simply not an option. I just has to be made to work one way or another, and I think PHP4 users would rather it be slow than not accurate.

I'd add another action item

KarenS's picture

I'd add another action item -- adding a column to the user table to track the timezone name. The 'timezone' column there now is tracking the timezone offset. Removing or changing that would break lots of things in both core and contrib, so it might be best just to add a new column for this purpose. It needs to be fairly big varchar field, some of the timezone names are quite long. Once a column is available, it can optionally be filled in on systems with php5 support (or whatever else we use to make this work).

And we need a variable to track the site timezone name, since the current one, again, is only tracking an offset. Again, I worry about changing what we store in the existing variable because of its potential to break lots of things. It would be cleaner to just add a new one.

And the other reason to use new columns/variables for these values is that it will allow us to fall back to the current behavior on systems without php5 support -- using the offset.

Core patch for timezone changes

KarenS's picture

I just proposed a patch at http://drupal.org/node/11077 to add some timezone handling to core when php5 is available. If falls back to the current system when the native php timezone functions won't work.

A Game Plan??

KarenS's picture

I'm bring my proposed patch from http://drupal.org/node/11077 back here for more discussion. After digging into things more, here's what I think might work well:

1) Create wrapper functions for each of the new timezone functions that only work in php5, or at least the ones we want to support, i.e. drupal_timezone_identifiers_list() to wrap around timezone_identifiers_list().

2) In those functions test for php5 support and use the php5 functions if available, otherwise include a separate .inc file that has php4 alternatives that produce the same result. For instance, the .inc file needs to produce the same timezone list that timezone_identifiers_list() produces.

3) In the php4 file try using putenv('TZ=myzone') before calculating a timezone offset, then return it to the previous state using putenv(TZ'). This is one of the things to discuss, is this the best way to emulate timezone support on php4? Killes has created another system in the event module that will work whether or not putenv() works, but it is limited to only certain parts of the world and has no historical records like when daylight savings time changed in 1980. Everything I've read about timezones suggests that the most reliable method of calculating timezones will be using putenv() to use the built in servers libraries, which should be based on the Olson database. The biggest problem is that some servers may not have the libaries installed or updated and that servers using safe mode will throw errors if you try to use putenv().

4) Once we have a method for calculating timezones, change the user and system timezone settings pages to use the timezone name instead of the offset.

5) Update any code in core that was using the timezone offset and alter it to use the timezone name and calculate the offset.

PHP5 is still very rare

seanr's picture

There are very few hosts out there, especially those offering shared server plans, that have upgraded to PHP5. It seems they wait all the way until the version they've got is completely obsolete and no longer supported by anyone until they finally upgrade (note all of the hosts out there still using MySQL 4.0, too, BTW). I hope that whatever method is finally chosen will actually fix the problem in PHP4 rather than just falling back on the current broken code. This has remained broken for far too long. It seems to me it should be possible to store this info in the database just like we currently do with zipcode info in the location module.

Some thought on TZ Handling

nlindley's picture

Hey, guys. I've been thinking about the TZ problem quite a bit lately, and have had a few thoughts. Anyway, if nothing comes up in the next week or two, I'm gonna try to work on some code to help out. I haven't had a chance to dig into the core very deeply yet, but I hope to this next week. Here are a few thoughts I've had...

I noted in http://drupal.org/node/11077#comment-256171 that either I don't quite understand the patch right, or that it looks like the proposed patch is replacing one offset with another. That is each day cron takes the users zonename and calculates their proper offset. During DST, this would cause the incorrect display of times of posts during parts of the year in standard time. See the comment in the link for an example. This tells me that we should be calculating the display for each date based on when it was posted. Yes, it is a little more overhead, but it will be correct. Reading back over KarenS's last comment, it seems as though that's the way this is headed, so I apologize if I'm being redundant.

I also like her idea of emulating support for PHP4 until PHP5 can be required. I think in the long run this will be easier since we won't have to worry about supporting offsets and zonenames, then we can just remove the .inc in the future.

Inputting dates: so far I haven't seen anything that deals with the input of dates and times. For example, if a user is submitting an event. Is this expected to be handled by the individual modules? It seems like it would be nice to have that functionality centralized, too.

Since I know I've already typed quite a bit, this will be the last thing for tonight... Since it looks like there's an inclination to use the PHP functions to handle timezones, are we to assume that the users will keep their db's up to date? I would hope most package management systems (at least in unixy environments) would take care of this, but I don't know if they do. Also, does Windows/PHP deal with timezones properly?

Windows

Michelle's picture

"Also, does Windows/PHP deal with timezones properly?"

Windows (at least XP) handles it fine. It knows when DST is and changes the clock (including the offset for time zones) accordingly. Except this year when the government in their infinite wisdom decided to change the date.

I was just talking with killes about time zones in the event module the other day. The module totally re-writes your timezone dropdown to use place names instead of offsets and stores the number of your timezone in the database. This causes a problem for DST because you need to switch to a different place name instead of a different offset. For me here in Wisconsin, I'll need to switch it to America/NY come this Fall.

A better solution would be great. :)

DST is such a PITA...

Michelle

Re: Windows

nlindley's picture

Thanks for the update. I haven't messed with Windows much in a couple of years, so I just wanted to check.

The idea of the timezones

mfb's picture

The idea of the timezones used by operating systems and PHP 5 is that they (theoretically) represent every geographic timezone in the world, usually named after the largest city in the timezone. For Wisconsin you would use America/Chicago. See also http://en.wikipedia.org/wiki/List_of_tz_zones_by_country

Any interest in keeping offsets?

nlindley's picture

In working on this, I was wondering if there was any interest in keeping numeric offsets as well. If not, then I'll switch all of the stuff running on PHP versions with timezone support over to true timezones. If there is, then I'll probably check to see if the parameter is numeric, and if not try to determine a true timezone. Checking if the timezone is numeric might be a good way to transition for modules that still use offsets. It would also allow sites that upgrade to keep the users' current offset settings. Anyway, I'm essentially going to try to follow the game plan that KarenS mentioned above, but will initially add timezone support for PHP5 with PHP4 falling back on the old method. Then once we figure out how to best support PHP4, it should be easy to change in the wrapper functions.

I don't know of any modules

mfb's picture

I don't know of any modules that need the (useless ;) numeric offset. Primarily it is used by format_date (which is what needs to be enhanced to use the named timezones and PHP 5 functions). I think it needs to be preserved only in the case that a site is reverted from a PHP 5 environment to a PHP 4 environment.

Modules using offset

nlindley's picture

It seems like the event module did. Either that or I hacked it so that it worked right for our site. Anyway, if this is for a future version of drupal, then the modules should be updated, anyway.

Sorry I haven't been very productive lately; there's been a lot going on that doesn't involve computers lately. Anyway, I should have some time to actually submit patches this week. If anybody else has something prepared, I'm keeping up with http://drupal.org/node/11077 and will help with whatever code you submit there.

Oh, contrib modules, yes a

mfb's picture

Oh, contrib modules, yes a few do use the offset at $user->timezone. It wouldn't be hard to calculate the offset on-demand (based on the named timezone) and add it to the $user object, if we want to maintain compatibility.

That would be easy, but

nlindley's picture

That would be easy, but would still leave the 1-off problem for half the year in parts of the world using DST.

Yes, I'm all for removing

mfb's picture

Yes, I'm all for removing the $user->timezone offset completely. If we do preserve it at all, as KarenS had suggested, then (at least on PHP5 sites) I think it should be calculated dynamically rather than stored in the db.

Update on timezone efforts

KarenS's picture

I am looking at this again now to see if there is anything I can propose that still might be able to slip into 6.x before code freeze. Here is where things stand, as far as I can see:

1) Native php timezone handling won't work until php version 5.2, and still requires that php be installed with the new functions enabled, so even once we get to the point where Drupal requires php 5, we can't assume that the native php timezone handling will be available. That means we need a fallback solution for sure.

2) The fallback solution probably has to be the current system. Creating another system from scratch would be very timeconsuming and would have to be done by someone who understands all this date and time stuff, and people who understand that are the ones who are most likely to have systems that support the native php functions, so why would they want to put in all that work?

3) If we have a fallback system that requires the current numeric offset, do we create a new field for the zonename (my solution) or try to use the current field to store both numeric offsets and zone names? Using the same field for both seems odd to me, but I guess we could. We would at least have to increase the size of the field if we do that -- zone names can be long.

4) My solution to use cron to update the timezone offset was clunky, I agree with that. If we don't do that we will either have both fields or a timezone field that might contain either numeric or string info and a function you can use to test if native timezone handling is enabled (drupal_handle_timezones()), so contribs would have to update themselves to check that and proceed accordingly in a way that will work whether the timezone is an offset or a string name. [Edit] That was a dumb way of putting it. If we don't update the timezone offset values for people using php 5, we just need to be sure both core and contrib are smart enough to ignore the value in that column (which will be wrong) and use the timezone name and timezone functions instead. We don't need to update it at all.

Also, everyone is asking why I didn't do anything with format_date(), but I was assuming that no changes would be needed if date_default_timezone_set() was used -- that is supposed to make all date() and mktime() and time() functions use the default timezone. So one approach is to set that for everyone where drupal_handle_timezones() is true and then leave everything else alone. If you read through the comments at http://us3.php.net/manual/en/function.date-default-timezone-set.php you can see there may be questions about how that affects they system in other ways. If we use it, I don't think any changes to format_date() are needed.

So:

1) Do we want to store both numeric offsets and timezone names in the same user column and expect contribs to check to see what is there and proceed accordingly?

2) Do we want to turn on date_default_timezone_set()?

[Edited] Oh yeah, one more thing. If we use the current timezone column for both offsets and zone names, that may make it harder for a contrib module to try to supply that info using some other method (like the way the Event module calculates timezones now). If we have two columns, one could have the offset value that core will be expecting for people who don't have php 5 and another where a contrib module could insert a zone name using some alternate method.

I would lean towards adding

mfb's picture

I would lean towards adding a new db column, since it seems odd to have a column that could have completely different data formats.

I do think we should modify format_date to be able to use the php5 timezone functions, so it's easy to print a time formatted in any arbitrary timezone. For instance, events, which can be assigned a timezone. Without the ability to use a timezone, format_date() would become a lot less useful than the built-in date_format().

My only concern about adding

nlindley's picture

My only concern about adding a column is that in future releases when we move away from the timezone column to the zonename column, the more logically named timezone column will get dropped. The only advantage I see is for contrib modules that do special things with numeric offsets, but at the same time I feel that if it's for a new release of drupal, the contrib modules should be updated to reflect changes in core.

[Edit] Doing a contrib module for now might be the way to go, in which case another column would make more sense. Too bad we can't force all the web hosts to install php5...

timezone_name is not bad for

mfb's picture

timezone_name is not bad for a column name, as it does correspond to PHP functions like timezone_name_get()

In my ideal world, all the contrib modules that deal with timezones (event etc.) would require PHP5... :(

Columns and column names: I

KarenS's picture

Columns and column names:

I think we definitely need two columns -- one for a numeric offset, as is used now for anyone with less than php5.2 and one for the timezone name for proper timezone handling. The current name, timezone, should really be the place for the timezone name. The current value, an offset, should really go into another column. So one option is to move the contents of user->timezone into user->offset and open up user->timezone for the timezone name. We can fix all the references to user->timezone in core and contribs will have to pick up the change.

Timezones in format_date():

We have a similar problem in this function as with the user table. There is already a 'timezone' argument which expects a numeric offset. There is also a new argument in 6.x for format_date(), $langcode, so if we want to add another argument for a timezone name, we'd have to add yet another argument to format_date() for the zone name, and that argument would either be tacked on after the $langcode argument or we would need to patch anything using format_date() to move the $langcode argument over. Both of those options sound bad.

The only other solution I can see is to try to use the existing $timezone argument for either a numeric offset or a timezone name, have the function figure out which one it is, and then execute the appropriate code -- either the current method for an offset or use native timezone handling for the timezone name. Having an argument that could be two such very different things is a problem for developers who need to keep it all straight.

Thoughts? I really want to get at least a basic patch submitted before code freeze.

Ok this is untested sudo

mfb's picture

Ok this is untested sudo code. Since offset is being phased out, offset and timezone parameters could be swapped.

<?php
function format_date($timestamp, $type = 'medium', $format = '', $offset = NULL, $langcode = NULL, $timezone = NULL) {
  switch (
$type) {
    case
'small':
     
$format = variable_get('date_format_short', 'm/d/Y - H:i');
      break;
    case
'large':
     
$format = variable_get('date_format_long', 'l, F j, Y - H:i');
      break;
    case
'custom':
     
// No change to format
     
break;
    case
'medium':
    default:
     
$format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
  }
 
$max = strlen($format);
  if (
USINGPHP4OFFSETS) {
    if (!isset(
$offset)) {
      global
$user;
      if (
variable_get('configurable_timezones', 1) && $user->uid && strlen($user->offset)) {
       
$offset = $user->offset;
      }
      else {
       
$offset = variable_get('date_default_offset', 0);
      }
    }
   
$timestamp += $offset;
   
$date = '';
    for (
$i = 0; $i < $max; $i++) {
     
$c = $format[$i];
      if (
strpos('AaDFlM', $c) !== FALSE) {
       
$date .= t(gmdate($c, $timestamp), array(), $langcode);
      }
      else if (
strpos('BdgGhHiIjLmnsStTUwWYyz', $c) !== FALSE) {
       
$date .= gmdate($c, $timestamp);
      }
      else if (
$c == 'r') {
       
$date .= format_date($timestamp - $offset, 'custom', 'D, d M Y H:i:s O', $offset, $langcode);
      }
      else if (
$c == 'O') {
       
$date .= sprintf('%s%02d%02d', ($offset < 0 ? '-' : '+'), abs($offset / 3600), abs($offset % 3600) / 60);
      }
      else if (
$c == 'Z') {
      
$date .= $offset;
      }
      else if (
$c == '\') {
        $date .= $format[++$i];
      }
      else {
        $date .= $c;
      }
    }
  }
  else if (USINGPHP5TIMEZONES) {
       if (!isset($timezone)) {
      global $user;
      if (variable_get('
configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
        $timezone = $user->timezone;
      }
      else {
        $timezone = variable_get('
date_default_timezone', 'UTC');
      }
    }
    $datetimezone = timezone_open($timezone);
    $date = '';
    for ($i = 0; $i < $max; $i++) {
      $c = $format[$i];
      if (strpos('
AaDFlM', $c) !== FALSE) {
        $date .= t(date_format(date_create($timestamp, $datetimezone), $c), array(), $langcode);
      }
      else if (strpos('
BdgGhHiIjLmnsStTUwWYyzrOZ', $c) !== FALSE) {
        $date .= date_format(date_create($timestamp, $datetimezone), $c);
      }
      else if ($c == '
\') {
        $date .= $format[++$i];
      }
      else {
        $date .= $c;
      }
    }
  }
  return $date;
}
?>

OK, it's better to upload a

mfb's picture

OK, it's better to upload a patch after all :p
A couple backslashes are missing, also no reason to run date_create($timestamp, $datetimezone) over and over as you cycle thru the string.

After some more thinking

KarenS's picture

After some more thinking about this, here is a possible solution. Make this into two or three patches so no one patch is so large and complex and debatable that it can't get into core.

1) Patch core to rename user->timezone to user->offset and create a new user->timezone that will hold the timezone name; alter system and user timezone forms to accept a timezone name instead of an offset in systems using php 5.2+; add some hooks so other modules can jump in and do things with timezone info, but otherwise make no changes to timezone handling in core. In other words, create the necessary infrastructure to do more with timezones.

2) Patch common.inc to restructure format_date so instead of format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL), it becomes format_date($timestamp, $type = 'medium', $params = array()), where an array of params can include things like 'timezone', 'langcode', 'offset', 'format', etc. Most core calls to format_date() set only the timestamp and type, so this would make it easier to add in new arguments to format_date without making very many changes in core.

3) Once format_date has been restructured, patch it to properly handle either offsets or timezones.

OK we are coming down to the

mfb's picture

OK we are coming down to the wire ;) I could volunteer for one of these if needed.

I've nearly got the

KarenS's picture

I've nearly got the infrastructure patch finished and ready to post. I'm doing a bit more testing before I do.

Why don't you file an issue to rework the way that format_date() works so we can pass it an array of options, and maybe start on a patch to make whatever changes would be needed to that function so it would properly do php5 date handling?

In my patch I am renaming functions so they use the 'date' space, both to make it easier to find and use the date-related functions and as a precursor to moving them into their own include file, as in my proposed patch at http://drupal.org/node/154477.

So we have:

variable_get('date_default_offset'); -- the offset now stored in date_default_timezone
variable_get('date_default_timezone') -- the timezone name;
date_zone_names(); -- the timezone name array returned by DateTimeZone::listIdentifiers()
date_zone_offsets(); -- the array of offsets we now use
date_handle_timezones(); -- true or false, test whether php5 timezone handling is available on this system

we also will have user->offset for the offset value and user->timezone for the timezone name

[Edit] Note that user->timezone will be empty on systems earlier than php5.2. user->offset may or may not have a correct value in it if user->timezone is being used. I'm updating it when user->timezone gets updated, but not trying to keep it current.

DateTimeZone Object

nlindley's picture

Would it be better to load a DateTimeZone Object into the user object? That way we wouldn't have to create the object each time through format_date.

That may be a good idea,

mfb's picture

That may be a good idea, especially if you could benchmark it to see what effect it has.

I just uploaded a first draft of the format_date patch, http://drupal.org/node/155442

I still need to fix all the format_date() calls in core, but it would be good if folks could start testing it.

Fyi I did have to patch a

mfb's picture

Fyi I did have to patch a fair number of instances of $format being specified in core: http://drupal.org/node/155442

Infrastructure patch is posted

KarenS's picture

I posted the infrastructure patch at http://drupal.org/node/11077. Anyone interested in getting this into core can help by reviewing it.

Thanks!

Don't see what all the fuss is about...

phantomkiwi's picture

/----------------------- code as follows below comment tags --------------------------------
I have noticed that windows apache with PHP doesn't seem to notice (take
into account) the DST (seems to be always set to on), but it works fine on
any Linux apache PHP version.

You could also try something on the lines of:

    # if statement used for daylight savings (whether or not the date is in daylight saving time)
  if (date("I") == 1) {
        # the timezone is in daylight saving time (summer time), so add one hour.
      $PHP_NOW = date("Y-m-d H:i:s", mktime(date("H")+1, date("i"), date("s"), date("m"), date("d"), date("Y")));
  } else {
       # the timezone isn't in daylight saving time, leave as is.
        $PHP_NOW = date("Y-m-d H:i:s", mktime(date("H"), date("i"), date("s"), date("m"), date("d"), date("Y")));
    }

Use the code below to write a fix for Drupal. Not that I have even used Drupal... Because I have my own CMS.

-------------------------------------------------------------------------------------------
/

# define your timezone below
$your_defined_timezone='Europe/London';

# stores the previous server setup timezone
$server_timezone=getenv("TZ");

# changes the server default timezone to the above timezone
putenv("TZ=".$your_defined_timezone);

# grab the different timezone
$PHP_NOW = date('Y-m-d H:i:s');

# return the server timezone to the previous state
putenv("TZ=$server_timezone");

code needs review

mfb's picture

By the way this patch is still alive @ http://drupal.org/node/11077 and currently in the patch spotlight.