Javascript: ISO8601 parser and pretty dates

Compared to PHP, with its many helpful date functions, working with dates in Javascript is a pain in the neck.

The js Date object can parse some human-readable strings into a date, but with the rise of RSS etc (in my case it was working with Google gdata apis…) you’ll often be encountering ISO8601 date strings.ย  Apparently ECMAScript 5 will include built in methods to parse this standard format, but until that’s available it’s a bit of a headache.

I went looking around and soon discovered that none other than John Resig, of jQuery fame, had posted a script to not only parse the ISO8601 date (provided it’s in UTC time) but also output a nice string like ‘5 minutes ago’, ‘1 year ago’ etc.

http://ejohn.org/blog/javascript-pretty-date/

I tried it out but couldn’t get it to work. In the comments on his page there were other people posting adapted versions. Zach Leatherman’s one looked promising:

http://www.zachleat.com/web/2008/03/23/yet-another-pretty-date-javascript/

I couldn’t get that to work either.ย  The functions were just returning the original UTC date string back to me. Looking into the code it was clear they were returning that as a default, because they were unable to parse the date string.

The problem seems to be that they all do something like this:

 var time = ('' + date_str).replace(/-/g,"/").replace(/[TZ]/g," ");
 var date = new Date(time);

That means asking the js Date object to parse a string like ‘2008/11/23 08:00:00.000’ into a Date.

As far as I can tell it just doesn’t work. No one else seems to have pointed this out so I’m prepared to accept that I may have missed something – if you know what, let me know.

Perhaps it’s locale/platform dependent (I’m on Win XP, UK, FF3)… maybe on a Mac or in the U.S.A. these scripts work fine.

The examples given in most Javascript documentation for parsing date strings use a format like: ’23 Nov, 2008 08:00:00′ …but that’s hard to generate from a ISO string.

Well, recently I recalled having to write an ISO8601 parser in Actionscript 2, so I dug it up, removed all the type-hints etc and ended up with this:

function parseISO8601(str) {
 // we assume str is a UTC date ending in 'Z'

 var parts = str.split('T'),
 dateParts = parts[0].split('-'),
 timeParts = parts[1].split('Z'),
 timeSubParts = timeParts[0].split(':'),
 timeSecParts = timeSubParts[2].split('.'),
 timeHours = Number(timeSubParts[0]),
 _date = new Date;

 _date.setUTCFullYear(Number(dateParts[0]));
 _date.setUTCMonth(Number(dateParts[1])-1);
 _date.setUTCDate(Number(dateParts[2]));
 _date.setUTCHours(Number(timeHours));
 _date.setUTCMinutes(Number(timeSubParts[1]));
 _date.setUTCSeconds(Number(timeSecParts[0]));
 if (timeSecParts[1]) _date.setUTCMilliseconds(Number(timeSecParts[1]));

 // by using setUTC methods the date has already been converted to local time(?)
 return _date;
}

It’s nowhere near as elegant as John Resig’s one-liner, but it does seem to work ok for me.

A couple of things to note:

  • It won’t parse any old ISO8601 date, it requires a UTC date (ending in ‘Z’)
  • I discovered with fascination/horror that the setUTCMonth() method needs a month value in the range 0-11 !ย  Days start at 1 as you’d expect.
  • By using the setUTC methods the resulting Date object seems to convert the supplied date to local timezone, so it will no longer be UTC. See comments for further discussion.
  • The milliseconds part of the date string is optional but all the other parts are required. Should seconds be optional? Should the whole time be optional?

So with this parser function I was able to get Zach’s ‘humane_date’ script working, by changing the following lines:

var time = parseISO8601(date_str),
 dt = new Date,
 seconds = (dt.getTime() - time.getTime()) / 1000,
 token = ' Ago',
 i = 0,
 format;

I’d love to get your feedback, especially any bugs or improvements that could be made.

17 thoughts on “Javascript: ISO8601 parser and pretty dates”

  1. You/they missed the “new” keyword somehow.

    var time = ('' + date_str).replace(/-/g,"/").replace(/[TZ]/g," ");
    var date = new Date(time);

      1. No such thing as too much Python (or too much JavaScript). But there is such a thing as sitting too long in front of a computer sometimes. ๐Ÿ™‚

        Thanks for your post.

  2. I had better luck with this:

    new Date(s.replace(/-/g,”/”).replace(/T/g,” “).replace(/Z/, ‘ UTC’))

    Just stripping the “Z” messes up the time (unless you’re in GMT); replacing “Z” with “UTC” makes JS convert it correctly to local time.

    Works for me in Safari and Chrome in USA. YMMV ๐Ÿ™‚

    1. Thanks for the tip!

      I would at least want to test it on FF and IE and on various OS platforms though. The reason for going to all the trouble of using the setUTCMonth() etc methods instead of just letting the Date() constructor parse the string was that the behaviour was inconsistent, possibly platform dependent. AFAIK the date string formats it can parse are undocumented.

      From my reading of ISO 8601 a string ending in ‘UTC’ is not valid so it’s odd if that was parsed correctly, possibly the ‘UTC’ is not even understood but an unrecognised timezone just defaults to UTC+0 or something so it works by lucky chance.

      But it’s a good point about the code above only working in GMT.

      I guess therefore we should strip off the ‘Z’ as before and then manually add the current local timezone offset to the following line:

      _date.setUTCHours(Number(timeHours) + [your timezone offset]);

  3. ‘UTC’ is not valid in iso8601, but it is valid in whatever parsing format string JS is using. Chrome’s date parser (new Date(s)) parses iso8601 strings, including “T” and “Z”, correctly, but Safari’s (and I presume others) are expecting a different format, one with slashes instead of dashes, a space between the time and date parts, and a time zone indicator like “UTC”. Sadly, it *doesn’t* seem to correctly parse “+00:00” in place of “Z”, but I didn’t exhaustively test all the permutations once I got it working.

    I dunno about your timezone offset code. All I can say is, be very careful with time zones! It’s very easy to change the hours when you should have changed the zone, or vice versa, or both, or neither… The JS date does correctly convert UTC times in local time when asked, e.g. via date.toTimeString().

    Someday, someone will write the perfect Date/Time/DateTime/TimeZone/Locale library and port it to all major lanugages. And then nobody will use it. ๐Ÿ™‚

  4. I’ve just come back to doing some date stuff in JS and it’s worrying to find current Safari 5.1 still won’t parse iso8601 dates using Date.parse(str)…

    Chrome and FF are fine (of course)

  5. Just checked it out. Sugar.js looks very nice!

    If you’re concerned about size/dependency, there’s a “Note about using Dates as a standalone module” in the README at https://github.com/andrewplummer/Sugar . But I’m less worried about JS file size these days; it seems the major factor in page load time is the number of files, not the size of individual files.

  6. The above function has a major bug with respect to months!

    If you are parsing a date in February, but the current date happens to be, say, the 31st, the above code will fail because when you call setUTCMonth(1) (for February), since the current date is the 31st, it will change the date to March 2nd. Then, setting the date will make the actual date a month off.

    To correct this:

    function parseISO8601(str) {
    // we assume str is a UTC date ending in ‘Z’

    var parts = str.split(‘T’),
    dateParts = parts[0].split(‘-‘),
    timeParts = parts[1].split(‘Z’),
    timeSubParts = timeParts[0].split(‘:’),
    timeSecParts = timeSubParts[2].split(‘.’),
    timeHours = Number(timeSubParts[0]),
    _date = new Date;

    _date.setUTCFullYear(Number(dateParts[0]));
    _date.setUTCDate(1);
    _date.setUTCMonth(Number(dateParts[1])-1);
    _date.setUTCDate(Number(dateParts[2]));
    _date.setUTCHours(Number(timeHours));
    _date.setUTCMinutes(Number(timeSubParts[1]));
    _date.setUTCSeconds(Number(timeSecParts[0]));
    if (timeSecParts[1]) _date.setUTCMilliseconds(Number(timeSecParts[1]));

    // by using setUTC methods the date has already been converted to local time(?)
    return _date;
    }

    1. thanks for the correction, that’s something I never imagined. Would be better if there was a ‘setUTC’ method in js that took all the parts as args and did everything at the same time I guess!

Leave a reply to Mitsu Hadeishi Cancel reply