synopsis.js | |
---|---|
A walk-though of Timezone, a database friendly, timezone aware replacement
for the Timezone is a JavaScript library with no dependencies. It runs in the browser and in Node.js. Timezone is a micro JavaScript library weighing only 2.7k. It is is feature complete. Timezone* is unlikely to get any larger as time goes by. This walk-through is written for Node.js. You can run this JavaScript program at the command line like so:
You can find a copy where Timezone is installed or download a copy from GitHub. | |
Functional API | |
Timezone is a function. When you import Timezone, you probably want
to assign it to a terse variable name. We recommend | |
var ok = require("assert")
, eq = require("assert").equal
, tz = require("timezone"); | |
POSIX TimeTimezone replaces the JavaScript POSIX time is absolute. It always represents a time in UTC. It doesn't spring forward or fall back. It's not affected by the decisions of local governments or administrators. It is a millisecond in the grand time line. POSIX time is simple. It is always an integer, making it easy to store in databases and data stores, even ones little or no support for time stamps. Because POSIX is an integer, it time is easy to sort and easy to compare. Sorting and searching POSIX time is fast. | |
Timezone returns number representing POSIX time by default. | var y2k = tz("2000-01-01"); |
Unless you provide a format specifier, the return value of a call to the Timezone function will be POSIX time. | |
The POSIX time number is always an integer, usually quite large. | eq( y2k, 946684800000 ); |
The JavaScript | |
Did Timezone give us the correct POSIX time for 2000? | eq( y2k, Date.UTC(2000, 0, 1) ); |
POSIX time is milliseconds since the epoch in UTC. The epoch is New Year's 1970. POSIX time for dates before 1970 are negative. POSIX time for dates after 1970 are positive. | |
The epoch is January 1st, 1970 UTC. | eq( tz("1970-01-01"), 0 ); |
Apollo 11 was before the epoch. | ok( tz("1969-07-21 02:56") < 0 ); |
The first Apollo-Soyuz docking was after the epoch. | ok( tz("1975-07-17 16:19:09") > 0 ); |
POSIX time is durable and portable. Any other language you might use will have date facilities that convert POSIX time into that language's date representation. We use POSIX time to represents and unambiguous point in time, free of timezone offsets, daylight savings time; all the whimsical manipulations of local governments. POSIX time is simply an integer, an efficient data type that easily sorts and compares. | |
Date Strings | |
Timezone uses RFC 3999 for date strings. RFC 3999 is a well-resonsed subset of the meandering ISO 8601 standard. RFC 3999 is the string date format for use in new Internet protocols going forward. It superceeds the RFC 2822 date format you're familiar with from HTTP headers. You've seen us parsing RFC 3999 date strings above. Let's look at a few more variations. | |
Parse an RFC 3999 date with a time in seconds. | eq( tz("2000-01-01T00:00:00"), y2k ); |
My goodness, that | |
Parse an RFC 3999 date with a time in seconds, using the optional space to
replace that silly | eq( tz("2000-01-01 00:00:00"), y2k ); |
Parse an RFC 3999 date with just the date, no time. | eq( tz("2000-01-01"), y2k ); |
Parse an RFC 3999 date with the date and a time in minutes. | eq( tz("2000-01-01 00:00"), y2k ); |
Parse an RFC 3999 date with the date and a time in seconds. | eq( tz("2000-01-01 00:00:00"), y2k ); |
Parse an RFC 3999 date with a time zone offset. | eq( tz("1999-12-31 20:00-04:00"), y2k ); |
We've gone and extended RFC 3999 for two special cases. | |
We've added milliseconds. | |
Parse an RFC 3999 looking date with the date and a time in milliseconds. | eq( tz("2000-01-01 00:00:00.0"), y2k ); |
Back in the day, not recently, there were some localities that specified their timzeone offset down to the second. Our timezone database goes back to the 19th century, when these exacting rules were in effect, so we allow timezone offsets to include seconds. | |
Parse an RFC 3999 date with a time zone offset with seconds. | eq( tz("1999-12-31 20:00-04:00:00"), y2k ); |
We use RFC 3999 date strings for an easy to type, easy to read, unambiguous date literal. When we want to type out a date in our code, or store a string representation in a message header or log file, we use RFC 3999. | |
Timezones — Time O' ClockWhen timezones are in play, we're no longer dealing with POSIX time. We're dealing with time that has been localized so that it matches the time according to the clock on the user's wall. That's why we call it wall-clock time. Wall-clock time is determined according to the laws of a government or the rules of an administrative body. Wall-clock time is determined by applying the timezone offset for the locality, plus any daylight savings offsets according to these rules. We don't venture a guess as to what these offsets might be. No. We use the IANA Timezone Database to convert POSIX time to obtain the best guess available. Yes, it's still a guess, because the IANA Database is a product of a lot of research; leafing through newspapers and government records for talk of clock changes. However, the IANA Database guess will be right for most cases, and your guess will be wrong far more often than you'd imagine. | |
Converting from Wall-Clock Time | |
We first need to load a timezone rule set from the IANA timezone database.
Let's create a | |
Load timezones for all of the Americas. | var us = tz(require("timezone/America")); |
Our new Timezone function Our | |
If we don't specify a zone name, our new | |
Time of the moon walk in UTC. | var moonwalk = us("1969-07-21 02:56"); |
Does | eq( us("1969-07-21 02:56"), Date.UTC(1969, 6, 21, 2, 56) ); |
However, if we name a zone rule set, we will parse the RFC 3999 date as
wall-clock time, not UTC. Here we use the | |
One small step for [a] man... | eq( us("1969-07-20 21:56", "America/Detroit"), moonwalk ); |
We can parse 7:39 PM in California. | |
...one giant leap for mankind. | eq( us("1969-07-20 19:56", "America/Los_Angeles"), moonwalk ); |
Amsterdam was an hour ahead of UTC at the time of the moon walk. We can't convert Amsterdam, however, because we didn't load its zone rule set. | |
Won't work, didn't load Amsterdam. | ok( us("1969-07-21 03:56", "Europe/Amsterdam") != moonwalk ); |
Instead of applying Amsterdam's rules, it falls back to UTC. | eq( us("1969-07-21 02:56", "Europe/Amsterdam"), moonwalk ); |
We can load Amsterdam's rules for just this conversion. Here we both include
the rules for Amsterdam with | |
Load Amsterdam's rules for just this conversion. | eq( us("1969-07-21 03:56", require("timezone/Europe/Amsterdam"), "Europe/Amsterdam"), moonwalk ); |
UNIX Date FormatsWhen you provide a format string, the Timezone function returns a formatted date string, instead of POSIX time. Timezone implements same date format pattern langauge as GNU's version of
the UNIX This is the same format language used by the UNIX function | |
Format POSIX time using a GNU date format string. | eq( tz(y2k, "%m/%d/%Y"), "01/01/2000" ); |
You can adjust the padding with padding flags. | eq( tz(y2k, "%-m/%-d/%Y"), "1/1/2000" ); |
Two digit year? Yeah, that's right! I don't learn lessons. | eq( tz(y2k, "%-m/%-d/%y"), "1/1/00" ); |
Format date and time. | eq( tz(moonwalk, "%m/%d/%Y %H:%M:%S"), "07/21/1969 02:56:00" ); |
12 hour clock formats. | eq( tz(moonwalk, "%A, %B %-d, %Y %-I:%M:%S %p"), "Monday, July 21, 1969 2:56:00 AM" ); |
Timezone supports all of the GNU | |
Day of the year. | eq( tz(moonwalk, "%j") , "202" ); |
Day of the week zero-based index starting Sunday. | eq( tz(moonwalk, "%w"), "1" ); |
Day of the week one-based index starting Monday. | eq( tz(moonwalk, "%u"), "1" ); |
Week of the year index week starting Monday. | eq( tz(moonwalk, "%W"), "29" ); |
ISO 8601 week date format. | eq( tz(moonwalk, "%G-%V-%wT%T"), "1969-30-1T02:56:00" ); |
Timezone is timezone aware so it can print the time zone offset or time zone abbreviation. | |
Get the time zone abbreviation which is * | eq( tz(moonwalk, "%Z"), "UTC" ); |
Get the time zone offset RFC 822 style. | eq( tz(moonwalk, "%z"), "+0000" ); |
When you format a date string and name a zone rule set, the zone format specifiers show the effect of zone rule set. | |
Get the timezone offset abbreviation for Detroit. | eq( us(moonwalk, "America/Detroit", "%Z"), "EST" ); |
Timezone offset RFC 822 style. | eq( us(moonwalk, "America/Detroit", "%z"), "-0500" ); |
Timezone supports the GNU extensions to the time zone offset format
specifier | |
Timezone offset colon separated. | eq( us(moonwalk, "America/Detroit", "%:z"), "-05:00" ); |
Some time zone rules specify the time zone offset down to the second. None of the contemporary rules are that precise, but in history of standardized time, there where some time zone offset sticklers, like the Dutch Railways. | |
The time at which the end of the First World War came into effect. | var armistice = tz("1911-11-11 11:00"); |
Timezone offset colon separated, down to the second. | eq( tz("1969-07-21 03:56", "Europe/Amsterdam", require("timezone/Europe/Amsterdam"), "%::z")
, "+01:00:00" ); |
The Timezone function itself offers one extension to | |
Format UTC as | eq( tz(moonwalk, "%^z"), "Z" ); |
Timezone offset colon separated, down to the minute. | eq( us(moonwalk, "America/Detroit", "%^z"), "-05:00" ); |
Timezone offset colon separated, down to the second, only if needed. | eq( tz(armistice, "Europe/Amsterdam", require("timezone/Europe/Amsterdam"), "%^z")
, "+00:19:32" ); |
RFC 3999 string for | eq( tz(moonwalk, "%F %T%^z"), "1969-07-21 02:56:00Z" ); |
RFC 3999 string not at | eq( us(moonwalk, "America/Detroit", "%F %T%^z"), "1969-07-20 21:56:00-05:00" ); |
Not part of the RFC 3999 standard, but Timezone will parse a time zone offset specified in seconds. | eq( tz(armistice, "Europe/Amsterdam", require("timezone/Europe/Amsterdam"), "%T %F%^z")
, "11:19:32 1911-11-11+00:19:32" ); |
PaddingTimezone implements the GNU padding extensions to | |
For zero padding we use | |
Zero padded day of month, but it is already zero padded. | eq( tz(y2k, "%B %0d %Y"), "January 01 2000" ); |
Same as above./ | eq( tz(y2k, "%B %d %Y"), "January 01 2000" ); |
To remove padding, add a hyphen after the percent sign. | |
With padding. | eq( tz(y2k, "%m/%d/%Y"), "01/01/2000" ); |
Padding stripped. | eq( tz(y2k, "%-m/%-d/%Y"), "1/1/2000" ); |
To pad with spaces put an underscore after the percent sign. | |
Space padded day of month. | eq( tz(y2k, "%B %_d %Y"), "January 1 2000" ); |
Nanoseconds, silly because we only have millisecond prevision. | eq( tz(1, "%F %T.%N"), "1970-01-01 00:00:00.001000000" ); |
Milliseconds using a with padding width specifier. | eq( tz(1, "%F %T.%3N"), "1970-01-01 00:00:00.001" ); |
Converting to Wall-Clock TimeTo convert to from POSIX time to wall-clock time, we format a date string specifying the name of a time zone rule set. The Timezone function formats a date string with the time zone rules applied. | |
Before you can use a time zone rule set, to create create a Timezone function that contains the rule set. | |
Create a Timezone function that contains European time zone rules. | var eu = tz(require("timezone/Europe")); |
Now we can use the | |
Convert to wall-clock time in and around Amsterdam. TK Use armistice. | eq( eu(moonwalk, "%F %T", "Europe/Amsterdam")
, "1969-07-21 03:56:00" ); |
Convert to wall-clock time in and around Instanbul. | eq( eu(moonwalk, "%F %T", "Europe/Istanbul")
, "1969-07-21 04:56:00" ); |
Note that wall-clock time is represented as a string. We do not represent wall-clock time as an integer. Integers are only used to represent POSIX time. This allows the Timezone to interpret an integer date value unambiguously as POSIX time, seconds since the epoch in UTC. We use a string to represent wall-clock time because the time zone offset is really a display property, because wall-clock time is a display of time. If feel that you really do need to record wall-clock time, include the effective time zone offset in the format. That way you will record wall-clock time and the offset necessary to get to POSIX time. This is the best format for log files, where string are appropriate, and wall-clock times are a sometimes nice to have. | |
Notice how we're traveling forward in POSIX time but backward in wall-clock time. | eq( eu(tz("2012-10-28 00:59:59"), "%F %T", "Europe/Amsterdam")
, "2012-10-28 02:59:59" );
eq( eu(tz("2012-10-28 01:00:00"), "%F %T", "Europe/Amsterdam")
, "2012-10-28 02:00:00" ); |
With the time zone offset, we can see why the wall-clock time went backward. | eq( eu(tz("2012-10-28 00:59:59"), "%F %T%^z", "Europe/Amsterdam")
, "2012-10-28 02:59:59+02:00" );
eq( eu(tz("2012-10-28 01:00:00"), "%F %T%^z", "Europe/Amsterdam")
, "2012-10-28 02:00:00+01:00" ); |
TK Move. Recording the time zone offset rule set name is not very meaningful. If you need to store location, store a proper address or the latitude and longitude of the event. | |
Converting Between TimezonesTo convert wall-clock time from one time zone to another, we first convert the wall-clock time of the source time zone to POSIX time. We then convert from POSIX time to the wall-clock time of the destination time zone. To do this, we call the Timezone function twice. | |
var posix = us("1969-07-20 21:56", "America/Detroit");
eq( posix, moonwalk );
var wallclock = eu(posix, "%F %T", "Europe/Amsterdam")
eq( wallclock , "1969-07-21 03:56:00" ); | |
All at once. | |
eq( eu( us("1969-07-20 21:56", "America/Detroit"), "Europe/Amsterdam", "%F %T" )
, "1969-07-21 03:56:00" ); | |
Whenever we parse a date, we are parsing that date in the context of a timezone. If no timezone is specified, we use the default UTC timezone. | |
Thus, specify your starting timezone when you parse. Then specify your target timezone when you format. | |
It's noon in Detroit. What time is it in Warsaw? | eq( eu(us("2012-04-01 12:00", "America/Detroit" ), "Europe/Warsaw", "%H:%M" ), "18:00" ); |
Remember that we can only represent wall-clock time using date strings. POSIX time is an absolute point in time and has no concept of timezone. | |
LocalesTimezone supports Locales for formatting dates using the GNU Date format specifiers. You apply a locale the same way you apply a timezone. You create a | |
Add a Polish locale. | us = us(require("timezone/pl_PL")); |
Time of moonwalk in the default Polish date format. | eq( us( moonwalk, "pl_PL", "%c", "America/Detroit" )
, "nie, 20 lip 1969, 21:56:00" ); |
Add a UK, French and German locales. | var eu = tz( require("timezone/en_GB")
, require("timezone/fr_FR")
, require("timezone/de_DE")
, require("timezone/Europe") ); |
Time of moon walk in three European cities. | eq( eu( moonwalk, "en_GB", "%c", "Europe/London" )
, "Mon 21 Jul 1969 03:56:00 BST" );
eq( eu( moonwalk, "fr_FR", "%c", "Europe/Paris" )
, "lun. 21 juil. 1969 03:56:00 CET" );
eq( eu( moonwalk, "de_DE", "%c", "Europe/Berlin" )
, "Mo 21 Jul 1969 03:56:00 CET" ); |
Date MathWhen Timezone performs date math, Timezone does so fully aware of timezone rules. Timezone accounts for daylight savings time, leap years and the occasional changes to timezone offsets. | |
With no timezone specified, Timezone uses UTC. | |
Here are some examples of date math using UTC. | |
Add a millisecond to the epoch. | eq( tz( 0, "+1 millisecond" ), 1 ); |
Travel back in time to the moon walk. | eq( tz(y2k, "-30 years", "-5 months", "-10 days", "-21 hours", "-4 minutes", "%c"), tz(moonwalk, "%c") ); |
Jump to the first Saturday after y2k. | eq( tz(y2k, "+1 saturday", "%A %d"), "Saturday 08" ); |
Jump to the first Saturday after y2k, including y2k. | eq( tz(y2k, "-1 day", "+1 saturday", "%A %d"), "Saturday 01" ); |
When a timezone is specified, Timezone with adjust the clock for daylight savings time when moving by hour, minute, second or millisecond. | |
When moving by day, month, or year with a timezone specified Timezone will instead adjust the time so that it lands at the same time of day. This if for whe the user reschedules a six o'clock dinner appointment, from the day before daylight savings to the day after. They didn't adjust the appointment by 24 hours, making it a seven o'clock appointment on the first day of daylight savings. They still want to have dinner at six o'clock; at six according to the clock on the wall. Moving across daylight savings time by hour, minute, second or millisecond will adjust your wall-clock time. | |
Moving across daylight savings time by day lands at the same time. | |
Moving across daylight savings time by day, month or year will put you at the same time, you won't spring forward. | |
Moving across daylight savings time by day lands at the same time. | |
If you move across daylight savings by hour, you'll see the adjustment for daylight savings time. | |
When you land on a time that doesn't exist because of daylight savings time, timezone scoot past the missing hour. | eq( us("2010-03-13 02:30", "America/Detroit", "+1 day", "%c"), "Sun 14 Mar 2010 01:30:00 AM EST" ) |
Date Arrays | |
The Timezone function will also accept an array of integers as a date input. It will treat this value as wall-clock time and convert it according a specified time zone rule set. The date array is useful when working with GUI controls like a series of drop downs. It is also a good candidate for the output of a date parsing function. The date array is an easy data structure to populate programatically while parsing a date string. The elements Unlike the JavaScript | |
var picker = [ 1969, 7, 20, 21, 56 ];
eq( us(picker, "America/Detroit"), moonwalk ); | |
The date array format also allows you to specify a time zone offset. If the element at index | |
*A date array with a time zone offset of | eq( tz([ 1969, 7, 20, 21, 56, 0, 0, -1, 5 ]), moonwalk ); |
Creating PartialsIf you call We can also use this to create custom functions that are specialized with
| |
We've been using parital functions to load time zones and locales. | us = us( require("timezone/pl_PL") ); |
Format a week of days after Y2K. | eq( us(y2k, "+1 day", "America/Detroit", "pl_PL", "%A"), "sobota" );
eq( us(y2k, "+2 days", "America/Detroit", "pl_PL", "%A"), "niedziela" ); |
Reduce the noise by creating a partial with the timezone. | detroit = us("America/Detroit");
eq( detroit(y2k, "+3 days", "pl_PL", "%A"), "poniedziałek" );
eq( detroit(y2k, "+4 days", "pl_PL", "%A"), "wtorek" ); |
Let's get rid of more chatter by creating a partial with the locale. | hamtramck = detroit("pl_PL");
eq( hamtramck(y2k, "+5 days", "%A"), "środa" );
eq( hamtramck(y2k, "+6 days", "%A"), "czwartek" );
eq( hamtramck(y2k, "+7 days", "%A"), "piątek" ); |
Initialization | |
Locales and time zones are defined by rules, locale rules and time zone
rules. Locales and time zones are specified by a name, either a locale string
in the form of In order to apply either a locale or a time zone rule set, you must provide the Timezone function with both the rule data and the rule name. Rather than providing both arguments each time, you'll generally want to load a number of rule sets into a partial application funcition. We've done this a number of times already in our walk-though. | |
Doesn't know anything about | eq( tz(moonwalk, "Asia/Tashkent", "%F %T%^z"), "1969-07-21 02:56:00Z" ); |
Load all of the timezone data for Asia. | var asia = tz(require("timezone/Asia")); |
Now | eq( asia(moonwalk, "Asia/Tashkent", "%F %T%^z"), "1969-07-21 08:56:00+06:00" ); |
If you later need more timezone data, you can add it using your existing partial function. | |
Add the Pacific Islands to Asia. | asia = asia(require("timezone/Pacific")); |
Now we have Hawaii. | eq( asia(moonwalk, "Pacific/Honolulu", "%F %T%^z"), "1969-07-20 16:56:00-10:00" ); |
Note that you can provide the rule data and the rule name at the same time. | |
Load Asia and select Tashkent in one call. | eq( tz(moonwalk, require("timezone/Asia"), "Asia/Tashkent", "%F %T%^z"), "1969-07-21 08:56:00+06:00" ); |
It is generally preferable to create a partial function that loads the data you need, however. | |
Locales are loaded in the same fashion. | |
Knows nothing of Polish, defaults to | eq( tz(moonwalk, "pl_PL", "%A"), "Monday"); |
Create a Polish aware partial function. | var pl = tz(require("timezone/pl_PL"));
eq( pl(moonwalk, "pl_PL", "%A"), "poniedziałek"); |
Functional Composition | |
Timezone implements a set of standards and de facto standards. Timezone is not extensible. Quite the opposite. Timezone is sealed. Timezone focuses on getting wall-clock time right. It supports a robust, timezone aware formatting language, and it it parses an Internet standard date string. With all the unit tests in place, there is little reason for Timezone to add new features, so you can count on its size to be small, under 3k, for the foreseeable future. Most importantly, Timezone avoids the mistake of treating a date formatting and date parsing as two sides of the same coin. Much in the same way that generating web pages from a database, like a blog, is not as simple as generating a database from web pages, like a search engine. That's not to say that date parsing is as complicated as a search engine, just that it is generally application specific, requires a lot of context, and it is not proportionate in complexity to date formatting, date math or time zone offset lookup. We might be able to hide a lot of the bulk in data files that accompany our library, but we would so Rather than opening up Timezone to extend it, we build on top of it, through functional composition. Timezone is a function in a functional language. It is configurable and easy to pass around. | |
Ordinal NumbersYou want to print the date as a ordinal number. Create a function that convert a number to an ordinal number, then write a regular expression to match numbers in your format string that you want to ordinalize. | |
function ordinal (date) {
var nth = parseInt(date, 10) % 100;
if (nth > 3 && nth < 21) return date + "th";
return date + ([ "st", "nd", "rd" ][(nth % 10) - 1] || "th");
}
ok( tz(y2k, "%B %-d, %Y").replace(/\d+/, ordinal), "January 1st, 2000" ); | |
Plucking Date FieldsThe original However, you do find that you need to get properties as integers, just use date format and make an easy conversion to integer. | |
Get the year as integer. | ok( +(tz(y2k, "%Y")) === new Date(y2k).getUTCFullYear() ); |
Careful to strip leading zeros so it doesn't become octal. | ok( +(tz(y2k, "%-d")) === new Date(y2k).getUTCDate() ); |
January is one./ | ok( +(tz(y2k, "%-m")) === new Date(y2k).getUTCMonth() + 1 ); |
Here's your date of week. | ok( parseInt(tz(y2k, "%-w")) === new Date(y2k).getUTCDay() ); |
Plus there are a few properties you can get that are not available to date. | |
Here's your date of week starting Monday. | ok( +(tz(moonwalk, "%-V")) === 30 ); |
Day of the year. | ok( +(tz(moonwalk, "%-j")) === 202 ); |
Arrays of Date Fields | |
What if you want the integer value of a number of different fields? | |
Split a string into words and convert the words to integers. | function array (date) {
return date.split(/\s+/).map(function (e) { return parseInt(e, 10) });
}
var date = array(tz(moonwalk, "%Y %m %d %H %M %S"));
eq( date[0], 1969 );
eq( date[1], 7 );
eq( date[2], 21 );
eq( date[3], 2 );
eq( date[4], 56 ); |
Additional Date ParsersCreate a function that returns our date array format. | |
Timezones in Date Strings | |
GNU | |
Extract a specified timezone from a date string. | function tzdate (date) {
var match;
if (match = /^TZ="(\S+)"\s+(.*)$/.exec(date)) {
var a = match.slice(1, 3).reverse();
return a;
}
return date;
} |
Parse a date with a date string. First one wins. | eq( eu(eu(tzdate('TZ="Europe/Istanbul" 2012-02-29 04:00')), "Europe/Amsterdam", "%F %T"), "2012-02-29 03:00:00" ); |
Parse a date without a date string, defaults to UTC. | eq( eu(eu("2012-02-29 04:00"), "Europe/Amsterdam", "%F %T"), "2012-02-29 05:00:00" );
|