Monday, March 24, 2014

client-time.js


This was another experiment of mine to solve a problem that has already been solved probably many times. My goal in doing this is to learn from coming up with my own solution and try to make it a little better than what's out there. I have decided that when I do this it warrants a blog post, at the very least, for those who are unfortunate enough to stumble upon my undocumented work of mystery.

What It Be

So now we have client-time.js. When I say my goal was to make it "better" realize that this term is subjective. In this case I was not trying to best the performance of the other solutions I found. I wanted to make a plugin that would translate dates and times from a server-rendered page to the timezone of the client rendering the page that had the following improvements:
  • Easier to read code (this is also subjective)
  • Adaptable format strings to match the server-side strings
The idea behind this is that the same string that is used to format a date will ideally be the same string required to parse a date. The process we are trying to support here is rendering a page with all dates and times in UTC, but having them seen by the end-user in his/her own timezone.

My Solution

Keep in mind that I wanted to make this easy to adapt to other format string schemas out there. So the first thing I did was try to break down the process of both parsing and formatting a date. What I came up with was 2 steps consisting of 2 parts each:
  • pattern & reader
  • parser & mapper
Writing this now in a blog makes me realize my naming conventions are off and should be fixed at some point...

The first step is to read a UTC time based on a formatter. For example if I am given a format string looking something like this: "yyyy MM dd HH:mm" I expect the time I am given to look something like this: "1999 12 20 15:32". 

To make this possible in the plugin I've paired the pattern-item for reading (yyyy) with a regex pattern for parsing (/\d{4}/) and now I am able to convert a format string into a regex statement for reading. What I end up with is something like this: /(\d{4}) (\d{2}) (\d{2}) (\d{2}):(\d{2})/. 

Once this is done all I have to do is take the information from the regex groups in the match and map it to the appropriate UTC setters on a javascript Date object. Unfortunately in javascript there are no named groups so prior to converting the format string to a regular expression I must make an index map letting me know what regex groups correspond with which pattern items. Anyways, to accomplish this I have grouped a function I am calling a "mapper" with each "pattern"/"parser" pair.

Finally all that is left to do is read the LOCAL values from the Date object and replace each part of the format string with the appropriate values. With this my formatter group grows to 4 items, the final being a function that takes a value from the Date object and returns it in the appropriate format.

What we get is something like this:

  1. var formatters = [
  2.     { pattern: 'yyyy', parser: '\\\\d{4}',              reader: function () { return leadZeros(this.year, 4); },              mapper: function (value) { this.setUTCFullYear(parseInt(value)); } },
  3.     { pattern: 'yy',   parser: '\\\\d{2}',              reader: function () { return leadZeros(this.year, 2); },              mapper: function (value) { this.setUTCFullYear(parseInt(getCentury() + value)); } },
  4.     { pattern: 'MMMM', parser: longMonths.join('|'),  reader: function () { return longMonths[this.month]; },               mapper: function (value) { this.setUTCMonth(indexOf(longMonths, value)); } },
  5.     { pattern: 'MMM',  parser: shortMonths.join('|'), reader: function () { return shortMonths[this.month]; },              mapper: function (value) { this.setUTCMonth(indexOf(shortMonths, value)); } },
  6.     { pattern: 'MM',   parser: '\\\\d{2}',              reader: function () { return leadZeros(this.month, 2); },             mapper: function (value) { this.setUTCMonth(parseInt(value) - 1); } },
  7.     { pattern: 'M',    parser: '\\\\d{1,2}',            reader: function () { return this.month; },                           mapper: function (value) { this.setUTCMonth(parseInt(value) - 1); } },
  8.     { pattern: 'dddd', parser: longDays.join('|'),    reader: function () { return longDays[this.dayOfWeek]; },             mapper: function (value) { } },
  9.     { pattern: 'ddd',  parser: shortDays.join('|'),   reader: function () { return shortDays[this.dayOfWeek]; },            mapper: function (value) { } },
  10.     { pattern: 'dd',   parser: '\\\\d{2}',              reader: function () { return leadZeros(this.day, 2); },               mapper: function (value) { this.setUTCDate(parseInt(value)); } },
  11.     { pattern: 'd',    parser: '\\\\d{1,2}',            reader: function () { return this.day; },                             mapper: function (value) { this.setUTCDate(parseInt(value)); } },
  12.     { pattern: 'HH',   parser: '\\\\d{2}',              reader: function () { return leadZeros(this.hour24, 2); },            mapper: function (value) { this.setUTCHours(parseInt(value)); } },
  13.     { pattern: 'H',    parser: '\\\\d{1,2}',            reader: function () { return this.hour24; },                          mapper: function (value) { this.setUTCHours(parseInt(value)); } },
  14.     { pattern: 'hh',   parser: '\\\\d{2}',              reader: function () { return leadZeros(this.hour12, 2); },            mapper: function (value) { this.setUTCHours(parseInt(value) % 12); } },
  15.     { pattern: 'h',    parser: '\\\\d{1,2}',            reader: function () { return this.hour12; },                          mapper: function (value) { this.setUTCHours(parseInt(value) % 12); } },
  16.     { pattern: 'mm',   parser: '\\\\d{2}',              reader: function () { return leadZeros(this.minute, 2); },            mapper: function (value) { this.setUTCMinutes(parseInt(value)); } },
  17.     { pattern: 'm',    parser: '\\\\d{1,2}',            reader: function () { return this.minute; },                          mapper: function (value) { this.setUTCMinutes(parseInt(value)); } },
  18.     { pattern: 'ss',   parser: '\\\\d{2}',              reader: function () { return leadZeros(this.second, 2); },            mapper: function (value) { this.setUTCSeconds(parseInt(value)); } },
  19.     { pattern: 's',    parser: '\\\\d{1,2}',            reader: function () { return this.second; },                          mapper: function (value) { this.setUTCSeconds(parseInt(value)); } },
  20.     { pattern: 'o',    parser: 'th|st|nd|rd',         reader: function () { return this.ordinal; },                         mapper: function (value) { } },
  21.     { pattern: 'TT',   parser: 'AM|PM',               reader: function () { return this.ampm.toUpperCase(); },              mapper: function (value) { pmAdjust(this, value); } },
  22.     { pattern: 'T',    parser: 'A|P',                 reader: function () { return this.ampm.toUpperCase().substr(0, 1); }, mapper: function (value) { pmAdjust(this, value); } },
  23.     { pattern: 'tt',   parser: 'am|pm',               reader: function () { return this.ampm.toLowerCase(); },              mapper: function (value) { pmAdjust(this, value); } },
  24.     { pattern: 't',    parser: 'a|p',                 reader: function () { return this.ampm.toLowerCase().substr(0, 1); }, mapper: function (value) { pmAdjust(this, value); } }
  25. ];
Instructions For Use

Here is how to use it:

First you will need to have both jQuery.js and client-time.js referenced by your page.

  1. <head>
  2.     <meta charset="utf-8" />
  3.     <title></title>
  4.     <script src="scripts/jquery.js"></script>
  5.     <script src="scripts/client-time.js"></script>
  6. </head>
Once that is done anywhere you want to display a date/time on your page make sure to wrap it in its own element. A span, or div would suffice.

  1.     <p>
  2.         A long time ago it was <span>1999 April 11, 12:00 pm</span>
  3.     </p>
Next we need to inform "client-time.js" what format string we are using. I have wired this up to use both a config object or data attributes. You will most likely only ever want the data attributes. So now the previous markup should look something like this:

  1.     <p>
  2.         A long time ago it was <span data-time-format="yyyy MMMM d, hh:mm tt">1999 April 11, 12:00 pm</span>
  3.     </p>
Finally we just need to let client-time.js know which elements need localizing

  1. $('[data-time-format]').clientTime();
Now when your page is shown in the browser client-time will read the date in your span as UTC, convert it to your machine's local time, and update the time appropriately using the same format.

My Disclaimers