Saturday, July 04, 2009

ECMAScript ISO Date For Every Browser

One most than welcome new entry of ECMAScript 5 is the Date.prototype.toISOString function and ability for Date constructor to accept an ISO String and to generate a Date instance.
This feature will make JSON Date instances import/export extremely fast and simple, but why should we wait ES5 release when we can have both features now?

Full Specs ISO 8601 Parser Plus Unobtrusive toISOString Method


We all do not like that much to extend native JavaScript constructors, so here I am with a proposal that will not touch the Date prototype:

if(!Date.ISO)(function(){"use strict";
/** ES5 ISO Date Parser Plus toISOString Method
* @author Andrea Giammarchi
* @blog WebReflection
* @version 2009-07-04T11:36:25.123Z
* @compatibility Chrome, Firefox, IE 5+, Opera, Safari, WebKit, Others
*/
function ISO(s){
var m = /^(\d{4})(-(\d{2})(-(\d{2})(T(\d{2}):(\d{2})(:(\d{2})(\.(\d+))?)?(Z|((\+|-)(\d{2}):(\d{2}))))?)?)?$/.exec(s);
if(m === null)
throw new Error("Invalid ISO String");
var d = new Date;
d.setUTCFullYear(+m[1]);
d.setUTCMonth(m[3] ? (m[3] >> 0) - 1 : 0);
d.setUTCDate(m[5] >> 0);
d.setUTCHours(m[7] >> 0);
d.setUTCMinutes(m[8] >> 0);
d.setUTCSeconds(m[10] >> 0);
d.setUTCMilliseconds(m[12] >> 0);
if(m[13] && m[13] !== "Z"){
var h = m[16] >> 0,
i = m[17] >> 0,
s = m[15] === "+"
;
d.setUTCHours((m[7] >> 0) + s ? -h : h);
d.setUTCMinutes((m[8] >> 0) + s ? -i : i);
};
return toISOString(d);
};
var toISOString = Date.prototype.toISOString ?
function(d){return d}:
(function(){
function t(i){return i<10?"0"+i:i};
function h(i){return i.length<2?"00"+i:i.length<3?"0"+i:3<i.length?Math.round(i/Math.pow(10,i.length-3)):i};
function toISOString(){
return "".concat(
this.getUTCFullYear(), "-",
t(this.getUTCMonth() + 1), "-",
t(this.getUTCDate()), "T",
t(this.getUTCHours()), ":",
t(this.getUTCMinutes()), ":",
t(this.getUTCSeconds()), ".",
h("" + this.getUTCMilliseconds()), "Z"
);
};
return function(d){
d.toISOString = toISOString;
return d;
}
})()
;
Date.ISO = ISO;
})();

The public static ISO Date method accepts a valid ISO 8601 String compatible with W3 Draft:

Year:
YYYY (eg 1997)
Year and month:
YYYY-MM (eg 1997-07)
Complete date:
YYYY-MM-DD (eg 1997-07-16)
Complete date plus hours and minutes:
YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
Complete date plus hours, minutes and seconds:
YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
Complete date plus hours, minutes, seconds and a decimal fraction of a second
YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)

where:

YYYY = four-digit year
MM = two-digit month (01=January, etc.)
DD = two-digit day of month (01 through 31)
hh = two digits of hour (00 through 23) (am/pm NOT allowed)
mm = two digits of minute (00 through 59)
ss = two digits of second (00 through 59)
s = one or more digits representing a decimal fraction of a second
TZD = time zone designator (Z or +hh:mm or -hh:mm)

The only limit is about milliseconds and JavaScript Date itself, the latest parameter will be rounded if there are more than 3 digits (JS precision is up to a millisecond, no microsecond yet).
The toISOString method is created for every browser that does not support this prototype yet but it is simply assigned and compiled once to avoid Date prototype problems and to optimize memory and performances.
I interpreted ISO specs as Zulu default:

var normalDate = new Date(2009, 06, 04);
var ISODate = Date.ISO("2009-07-04");

normalDate.toISOString = ISODate.toISOString;

alert([ // being in UK ...

// 2009-07-03T23:00:00.000Z
normalDate.toISOString(),

// 2009-07-04T00:00:00.000Z
ISODate.toISOString()
].join("\n"));

Since solar time is +01 hour, if I create a Date with midnight time, the respective Zulu one (UTC) will be the day before at 23:00:00 while since ISO consider an unspecified Time Zone the Zulu one, +00:00, the created Date will be exactly the specified one but obviously if we try to get ISODate.getHours() it will be 01 and not 00, accordingly with my local settings.

This is a little function/proposal that has zero dependencies, is widely compatible, and could be a standard or a must for every library/framework. Waiting for your thoughts, have a nice we 8)

7 comments:

Anonymous said...

Nice, except there seems to be a problem with Time... when I try iso date "2010-08-18T16:30:00.000+10:00"

it returns 12:00AM when it should be 04:30PM...?

CoolAJ86 said...

I appreciate the code on the one hand, but on the other hand... are you trying to win the unreadable code contest?

If you are, I hear you get better rankings if you can make a picture out of it.

If I wanted unreadable code, I'd just run packer.js on it.

Andrea Giammarchi said...

uhm ... "d" for date, "m" for match, "his" for php like gmdate format ... which part exactly is not clear? the "t" for ten? Glad you appreciated

CoolAJ86 said...

What's the license? Can I post it as MIT?

Andrea Giammarchi said...

Mit, yes :)

CoolAJ86 said...

There's an error with these lines:

d.setUTCHours((m[7] >> 0) + s ? -h : h);
d.setUTCMinutes((m[8] >> 0) + s ? -i : i);


This evaluates to something like this

if ((m[7] >> 0) + s) {
-h;
} else {
h;
}

When you mean something like this

(m[7] >> 0) +
if (s) {
-h;
} else {
h;
}

Should be

d.setUTCHours((m[7] >> 0) + (s ? -h : h));
d.setUTCMinutes((m[8] >> 0) + (s ? -i : i));

Or better

if (s) {
h = -h;
i = -i;
}
d.setUTCHours((m[7] >> 0) + h);
d.setUTCMinutes((m[8] >> 0) + i);

CoolAJ86 said...

There was also a bug when changing the timezone meant crossing from one date to the next.

Here's a snippet from my version

// The bit-shifting is shorthand for `Number(m) || 0`
date.setUTCFullYear(match[1] >> 0);
date.setUTCMonth(match[3] ? (match[3] >> 0) - 1 : 0);
date.setUTCDate(match[5] >> 0);
date.setUTCHours(match[7] >> 0);
date.setUTCMinutes(match[8] >> 0);
date.setUTCSeconds(match[10] >> 0);
date.setUTCMilliseconds(match[12] >> 0);

// Adjust to UTC offset
if (match[13] && match[13] !== "Z") {
hour = match[16] >> 0;
minute = match[17] >> 0;
aheadOfUtc = (match[15] === "+");

hour = hour * 60 * 60 * 1000;
minute = minute * 60 * 1000;

if (aheadOfUtc) {
hour = -hour;
minute = -minute;
}

// easy dateline wrapping
date = new Date(date.valueOf() + hour + minute);
}
return date;