// A walk-though of **Timezone**, a database friendly, timezone aware replacement // for the `Date` object that implements timezone conversions, timezone aware // date math, timezone and locale aware date formatting, for any date, anywhere // in the world, since the dawn of standardized time. // // **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: // // ``` // node synopsis.js // ``` // // You can find a copy where **Timezone** is installed or [download a // copy](https://raw.github.com/bigeasy/timezone/master/src/synopsis.js) 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 `tz`. // var ok = require("assert") , eq = require("assert").equal , tz = require("timezone"); // ### POSIX Time // // **Timezone** replaces the JavaScript `Date` object with [POSIX // time](http://en.wikipedia.org/wiki/Unix_time) — milliseconds since the // epoch in UTC — for a cross-platform, internationalized, and durable // representation of a point in time. // // 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 with little or no support for time // stamps. // // Because POSIX time is an integer, it 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 `Date.UTC` function also returns an integer representing POSIX // time. We will use it check our work in our synopsis. // *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 represent an 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](http://www.ietf.org/rfc/rfc3339.txt) 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 `T` is silly. It's part of ISO 8601, but RFC 3999 lets us // replace it with a space so it's easier to read. We're not going to use it // again. // *Parse an RFC 3999 date with a time in seconds, using the optional space to // replace that silly `T`.* 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' Clock // // When 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 `tz` function that knows about most of the US timezones. // *Load timezones for all of the Americas.* var us = tz(require("timezone/America")); // Our new **Timezone** function `us` knows the rules for a lot of timezones. // Not only timezones in the United States, but also in Canada, Mexico and all // of South America. It won't include Hawaii, however, that's // `Pacific/Honolulu`. Still, we have pleany of rules to work with to obtain // some wall-clock times. // // Our `tz` function is left unchanged. It doesn't have any timezone rules // loaded. // If we don't specify a zone name, our new `us` function will behave just as // old `tz` function did. // *Time of the moon walk in UTC.* var moonwalk = us("1969-07-21 02:56"); // *Does* `Date.UTC` *agree?* 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 `"America/Detroit"` timezone rule // set to parse 10:39 PM wall-clock time the day before the moon walk. // *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 `require` and select using the timezone string // `"Europe/Amsterdam"`. // *Load Amsterdam's rules for just this conversion.* eq( us("1969-07-21 03:56", require("timezone/Europe/Amsterdam"), "Europe/Amsterdam"), moonwalk ); // ### UNIX Date Formats // // When 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 `date` utility. **Timezone** supports the full compliment of [GNU // date](http://en.wikipedia.org/wiki/Date_%28Unix%29) format specifiers. // // This is the same format language used by the UNIX function `strftime`. You'll // find a version of `strftime` baked right into C, Ruby, Python, Perl and Lua. // With **Timezone** you can also find a version of `strftime` in your // JavaScript program. // *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 `date` extensions, including some date // calculations you won't find in JavaScript's `Date`. // *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](http://en.wikipedia.org/wiki/ISO_8601#Week_dates) 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 * `UTC` * by default.* 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 `%z`. If you put colons between the `%` and `z` colons appear in // the time zone offset. // *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 `strftime`, inspired // by the GNU `date` extensions for `%z`, to support formatting RFC 3999 date // strings. The format specifier `%^z` flexibly formats the time zone offset. // *Format UTC as* `Z` *instead of* `+00:00` *.* 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* `UTC` *.* eq( tz(moonwalk, "%F %T%^z"), "1969-07-21 02:56:00Z" ); // *RFC 3999 string not at* `UTC` *.* 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" ); // #### Padding // // **Timezone** implements the GNU padding extensions to `strftime`. // For zero padding we use `0`, but most formats are already zero padded. // *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 Time // // To 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 `eu` **Timezone** function to convert to the wall-clock // time of European localities. // *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 Timezones // // To 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. // ### Locales // // Timezone 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 `tz` // function by passing a locale definition into the `tz` function. // *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 Math // // When **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 `0` through `6` of the date array are year, month, date, hour, // minute, second and milliseconds. If you leave an element `undefined`, it will // be interpreted as zero. The date array must at contain at least a year and a // month, a year alone will interpreted as POSIX time. // // Unlike the JavaScript `Date`, the **Timezone** function does not use a // zero-based month index in an array representation of a date. It uses instead // the humane month number that you'd find if you formatted the date. // 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 `7` is `1` or `-1`, that is treated as the time zone // offset direction, `-1` for a negative time zone offset, `1` for a positive // time zone offset. If present, then the elements `8` through `10` of the date // array the time zone offset hours, minutes and seconds. // *A date array with a time zone offset of `-05:00`. eq( tz([ 1969, 7, 20, 21, 56, 0, 0, -1, 5 ]), moonwalk ); // ### Creating Partials // // If you call `tz` without a date parameter, `tz` will create a new function // that is a [partial // application](http://ejohn.org/blog/partial-functions-in-javascript/) of the // `tz` function. This is how we load timezones and locales. // // We can also use this to create custom functions that are specialized with // `tz` parameters. This can help use reduce noise in our code, if we are // invoking `tz` with the same set of parameters over and over again. // *We've been using partial 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 `en_US`, or a time zone string in the form of // `America/Detroit`. // // 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 `Asia/Tashkent`, defaults to UTC.* 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 `Asia/Tashkent` is available to the our `asia` function.* 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 `en_US`.* 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 Numbers // // You 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 Fields // // The original `Date` object is missing a lot of functionality, but includes a // lot of getters and setters. I think they're silly and that's why **Timezone** // is a function and not an object. Because time is not object-oriented. // // 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 Parsers // // Create a function that returns our date array format. // #### Timezones in Date Strings // GNU `date` has a nice feature where you can specify the timezone of a date // string using `TZ` like so `'TZ="America/Detroit" 1999-12-01 20:00'`. This // allows you to store a string with a timezone. // *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" );