Converting Between Timezones

So Twitter stores and returns timezones in a relatively non-standard format. Well, non-standard from what PHP (and most languages) can understand. IANA is a fairly universal format and includes 548 zones, covering most, if not all, regional and political handling of UTC offsets and daylight saving times. And PHP datetime functions and methods are hard-coded to handle IANA formats. Twitter handles timezones with what appears to be a Rails gem that both limits zones to a meaningful subset of 146 zones and displays them in a friendlier name. Which is incredibly frustrating from a non-Ruby programmer perspective.

In order to use timezones from Twitter I had to map them to a more standard format. My first thought was to wrap the core \DateTimeZone class and allow non-IANA formats to be passed in. This process was inspired by how Aura.Sql wraps the core \PDO object in order to give it lazy loading and additional functionality without changing any of the default behavior. I would enforce things using interfaces and could bolt on additional functionality in the future, even going as far as outputting into Twitter's Rail format.

  1. class LooseDateTimeZone

  2. extends DateTimeZone

  3. implements DateTimeZoneInterface

  4. {

  5. protected $timezone_string;

  6. public function __construct($timezone_string)

  7. {

  8. $this->timezone_string = $timezone_string;

  9. }

  10. protected function getTimezoneObject()

  11. {

  12. $iana_timezone = $this->doFancyConvert($this->timezone_string);

  13. return new DateTimeZone($iana_timezone);

  14. }

  15. public function getLocation()

  16. {

  17. return $this->getTimezoneObject()->getLocation();

  18. }

  19. public function getName()

  20. {

  21. return $this->getTimezoneObject()->getName();

  22. }

  23. // etc

  24. }

This seemed like a great way to go. After all, the \DateTime object allows loose strings for instantiation, parsing the string into a date after it gets passed, this would just let \DateTimeZone do the same thing. Via an internal call to '$this->doFancyConvert()', that is. There was one big problem with this approach. The \DateTime class allows passing in of \DateTimeZone objects for construct and offsets. Not children of \DateTimeZone or anything implementing a standard timezone interface. So \LooseDateTimeZone would be useless as anything but a standalone object.

What I needed was a quick convert. Something that could take 'Eastern Time (US & Canada)' and return a \DateTimeZone of 'America/New_York'. But why stop at Rails formats? IANA and the Rails gem were only two types of timezone formats. I dug around a little bit and came up with three more: UTC offsets, abbreviations, and military. Why not create a generic converter class that could take any of those formats and return something that PHP could handle?

After a lot of research into timezones and far more manual mapping that I wanted to do I created an invokable class that did just that. You instantiate it with a format to bias the parsing, or leave it empty to hit all possible formats, and then call it to return valid instances of \DateTimeZone. It was relatively lightweight and would work great in a larger project.

  1. $converter = new Converter(Converter::RAILS_FORMAT);

  2. // returns \DateTimeZone w/ America/New_York

  3. $timezone = $converter('Eastern Time (US & Canada)');

  4. // returns \DateTimeZone w/ America/Phoenix

  5. $timezone = $converter('Arizona');

There are plenty of catches with timezones. A timezone is not merely an offset from UTC… Inside each IANA timezone key is information about daylight savings time, historical data, and even geographical boundaries. So converting between timezones loses some of this background. And trying to convert from a UTC offset, something I opted to include, really glosses over a ton of details.

I did try to be intelligent about the conversions. For instance, military timezones are hard offsets from UTC. A military timezone does not care about daylight savings time. So instead of mapping those to some of the more conventional zones, like 'America/New_York', I used the esoteric 'Etc\GMT+5' key. Also, for the abbreviations, I did my best to map to the best guess options, which was difficult given the variety of zones 'EST' could designate.

But anyways, you can find the timezone converter on my Github. And it'll work for my Twitter bot just fine, which was my initial trigger for this project. I may add more functionality to it eventually, though I'm worried about travelling too far into timezones, as that way leads to madness.