Sunday, November 25, 2012

Timezones in Node

I won't get into the details of my current little project but it involves porting over some functionality from Python. This has generally been going well, but I ran into a problem when I was trying to port over this bit of code to get a formatted time in a particular timezone:
import pytz, datetime

timeformat = '%Y-%m-%dT%H:%M:%S.%f'
timezone = pytz.timezone('US/Pacific')
timenow = datetime.datetime.now(timezone)
formattedtime = timenow.strftime(timeformat)

I actually only realized that I needed this after deploying my app to Azure. My local time is in the right timezone so everything was working locally, but apparently Azure servers run in UTC and, after a bit of poking around, I discovered that there was no good way of changing this.

I figured this would be a fairly simple fix, since this is surely a common task, so I started searching around. I found two packages that seemed to do what I wanted, node-time and timezone-js. node-time looked ideal, but I was unable to pull it in to my dependencies without running into compile errors. I'm sure I could have worked it out eventually, but I wasn't too confident in being able to fix it on the Azure side so I gave up on that. TimezoneJS also looked promising, but seems like it was designed for client-side code rather than backend Node.js. A few hours later I've worked out how to set it up properly for node, so I figured I should write a bit about it.

I'm using TypeScript, so I made a definition file and slowly filled it in with the required definitions as I wrote the code. I will probably clean it up and submit it to this lovely repo eventually, but I've just put it up on a gist for now.

Since TimezoneJS is designed with client-side in mind, its default setup is to only pull in timezone information as it is required and to do that by using web requests. It took a bit of poking around the code, but it turns out that it is possible to set it up how you would expect server-side code to work, by getting it to pull in all timezone information from the filesystem on startup. The first step is to download the timezone files as it says on the setup instructions and put them in a folder in the project root. You then have to tell the library where that folder is, get it to load everything straight away, and change the transport method so that it loads from file rather than requesting a URL (this loader actually comes from the test-utils.js file in the timezone-js repo). You then initialize the timezones, I chose to do this synchronously since I'm doing it on startup so making this a blocking call isn't really an issue.
import tzjs = module("timezone-js")
import fs = module('fs');

export function init() {
    tzjs.timezone.zoneFileBasePath = './tz';
    tzjs.timezone.loadingScheme = tzjs.timezone.loadingSchemes.PRELOAD_ALL;
    tzjs.timezone.transport = function (opts) {
        if (opts.async) {
            if (typeof opts.success !== 'function') return;
            opts.error = opts.error || console.error;
            return fs.readFile(opts.url, 'utf8', function (err, data) {
                return err ? opts.error(err) : opts.success(data);
            });
        }
        return fs.readFileSync(opts.url, 'utf8');
    };

    tzjs.timezone.init({ async: false });
}

Now we can create dates with a specified timezone (new tzjs.Date(timezoneName)) and set a date's timezone (date.setTimezone(timezoneName)), but it is still a bit of a hassle to format the date. This is where Moment.js comes in. Moment doesn't work too well with timezones, but we can use it to translate UTC times to the timezone we want by subtracting the timezone offset from our date objects. To make things as simple as possible, I made a little helper function that would give me a moment object, offset by a specified timezone:
import moment = module("moment")
import tzjs = module("timezone-js")

export function momentInTimezone(timezoneName: string): moment.Moment {
    var timezone = new tzjs.Date(timezoneName);
    return moment.utc().subtract('minutes', timezone.getTimezoneOffset());
}

And there you have it, after running init, I can now format the local time in any timezone in one line:
import dateutils = module("./dateutils")

dateutils.init();

console.log("Time in Seattle: " + dateutils.momentInTimezone('America/Los_Angeles').format('MMMM Do YYYY, h:mm:ss a'));
console.log("Time in Brisbane: " + dateutils.momentInTimezone('Australia/Brisbane').format('MMMM Do YYYY, h:mm:ss a'));
console.log("Time in UTC: " + dateutils.momentInTimezone('UTC').format('MMMM Do YYYY, h:mm:ss a'));

Output:
Time in Seattle: November 25th 2012, 2:58:06 pm
Time in Brisbane: November 26th 2012, 8:58:06 am
Time in UTC: November 25th 2012, 10:58:06 pm

I've put the full dateutils file on a gist, but I'm tempted to turn it into my first public npm package.

No comments:

Post a Comment