On the current project I am working on I came across an issue with validation of date strings using jQuery.Validation and Safari on both Mac and PC.

It would appear our friends at Apple don’t like anything other than the annoying US date format of ‘mm/dd/yyyy’. For the life of me I could never and will never understand why that format is used. I have Googled and Binged for the history of this but alas have not found an answer. If anyone knows please drop me a line.

Anyway, I found that when I used the default ‘date’ validation method provided by jQuery.Validation it worked great on every browser except Safari on both Mac and Windows.

Safari just insists that the following date is an ‘Invalid Date’. Maybe in the United States and a few other countries but not across the whole world guys.

new Date("16/01/2011")

There really is no excuse for this in my opinion as their is no ambiguity no matter which way you write it “16/01/2011” or “01/16/2011” and all other browsers implementation of javascript handle this just fine.

Anyway, to solve the problem I have had to write my own method to specifically cater for Safari’s quirks.

The first thing I needed to do was determine what browser is being used so I can route the code to the appropriate function.

Our friends over at Quirksmode provide a nice little script which is shown below:

var BrowserDetect = { init: function () { this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; this.version = this.searchVersion(navigator.userAgent) || this.searchVersion(navigator.appVersion) || "an unknown version"; this.OS = this.searchString(this.dataOS) || "an unknown OS"; }, searchString: function (data) { for (var i = 0; i < data.length; i++) { var dataString = data[i].string; var dataProp = data[i].prop; this.versionSearchString = data[i].versionSearch || data[i].identity; if (dataString) { if (dataString.indexOf(data[i].subString) != -1) return data[i].identity; } elseif (dataProp) return data[i].identity; } }, searchVersion: function (dataString) { var index = dataString.indexOf(this.versionSearchString); if (index -1) return; return parseFloat(dataString.substring(index + this.versionSearchString.length + 1)); }, dataBrowser: [ { string: navigator.userAgent, subString: "Chrome", identity: "Chrome" }, { string: navigator.userAgent, subString: "OmniWeb", versionSearch: "OmniWeb/", identity: "OmniWeb" }, { string: navigator.vendor, subString: "Apple", identity: "Safari", versionSearch: "Version" }, { prop: window.opera, identity: "Opera" }, { string: navigator.vendor, subString: "iCab", identity: "iCab" }, { string: navigator.vendor, subString: "KDE", identity: "Konqueror" }, { string: navigator.userAgent, subString: "Firefox", identity: "Firefox" }, { string: navigator.vendor, subString: "Camino", identity: "Camino" }, { // for newer Netscapes (6+)string: navigator.userAgent, subString: "Netscape", identity: "Netscape" }, { string: navigator.userAgent, subString: "MSIE", identity: "Explorer", versionSearch: "MSIE" }, { string: navigator.userAgent, subString: "Gecko", identity: "Mozilla", versionSearch: "rv" }, { // for older Netscapes (4-)string: navigator.userAgent, subString: "Mozilla", identity: "Netscape", versionSearch: "Mozilla" } ], dataOS: [ { string: navigator.platform, subString: "Win", identity: "Windows" }, { string: navigator.platform, subString: "Mac", identity: "Mac" }, { string: navigator.userAgent, subString: "iPhone", identity: "iPhone/iPod" }, { string: navigator.platform, subString: "Linux", identity: "Linux" } ] }; BrowserDetect.init();

With that in hand we can now write code like the following to route to the appropriate function:

function ValidateAUDateAllBrowsers(value, element) { if (BrowserDetect.browser "Safari") { return ValidateAUDateOnSafari(value); } else { return ValidateAUDateOnOtherBrowsers(value); } }

The above code is part of the custom function that I will use on my mark-up to trigger date validation that will cater for Safari. If you are not familiar with how jQuery.Validation works it is as follows: You decorate the class name on your element with the name(s) of functions that you want to call to validate the content. There are plenty provided out of the box but you can also write your own as shown below:

jQuery.validator.addMethod("dateAU", function (value,element) { returnthis.optional(element) || ValidateAUDateAllBrowsers(value); }, "The date supplied is invalid. Required format is dd/mm/yyyy." );

So with the code above in place I can now decorate my element as shown below:

<inputid="attended-date"name="attendeddate"type="text"class="dateAU"alt="The date you attended the Australian Open (dd/mm/yyyy)"/><br/>

Note the class=”dateAU” above. The validation process will look at that class on the element and trigger the dateAU function which in turn will call the function ValidateAUDateAllBrowsers. If you are wondering what that alt tag is doing that is used by qTip, another excellent jQuery plugin.

Ok so now comes the fun part and that is the validation for Safari and all other browsers.

First let’s look at the other browsers code:

function ValidateAUDateOnOtherBrowsers(value) { var dateParts = value.split("/"); if (dateParts.length != 3) { returnfalse; } var day = dateParts[0]; var dayDigit1 = Math.floor(day / 10); var dayDigit2 = day % 10; var month = dateParts[1]; var monthDigit1 = Math.floor(month / 10); var monthDigit2 = month % 10; var isoDate = dateParts[2] + '/' + monthDigit1 + '' + monthDigit2 + '/' + dayDigit1 + '' + dayDigit2; var newDate = new Date(isoDate); return (dateParts[1] >= 1 && dateParts[1] <= 12) && (dateParts[0] >= 1 && dateParts[0] <= 31) && /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(isoDate) && !/Invalid|NaN/.test(newDate); }

In my case I want to force dates in the format ‘dd/mm/yyyy’ so the first thing I do is split out the date string that has been entered. If there are not 3 elements in the string array then straight away we know we do not have a valid date.

The next two parts cater for the case where the user may have entered d/m/yyyy as this is perfectly valid. Essentially I am adding on the 0 at the start if the user has left it off.

We then format the date parts as ‘yyyy/mm/dd‘ and create a new date object. If this is an invalid date such as 31/02/2011 then the date will not parse and this will be caught on the next line of code with does all the testing.

So the final line checks that days are between 1 and 31, months are between 1 and 12, the isoDate is formatted as ‘yyyy/mm/dd’ (although ‘yyyy/m/d’ would also work) and finally that the newDate we created is in fact a valid date.

Ok so that is the normal way of doing things. Now lets see what Safari throws in to the mix:

function ValidateAUDateOnSafari(value) { var dateParts = value.split("/"); if (dateParts.length != 3) { returnfalse; } var day = dateParts[0]; var dayDigit1 = Math.floor(day / 10); var dayDigit2 = day % 10; var month = dateParts[1]; var monthDigit1 = Math.floor(month / 10); var monthDigit2 = month % 10; var safariDate = monthDigit1 + '' + monthDigit2 + '/' + dayDigit1 + '' + dayDigit2 + '/' + dateParts[2]; var newDate = new Date(safariDate); var safariDays = new Number(dayDigit1 + '' + dayDigit2); // getMonths() return 0 - 11 so need to adjust herevar safariMonths = new Number(monthDigit1 + '' + monthDigit2) - 1; return (dateParts[1] >= 1 && dateParts[1] <= 12) && (dateParts[0] >= 1 && dateParts[0] <= 31) && /^\d{1,2}[\/-]\d{1,2}[\/-]\d{4}$/.test(safariDate) && !/Invalid|NaN/.test(newDate) && (newDate.getDate() safariDays && newDate.getMonth() safariMonths); }

At first we start off just the same up until we get to formatting the date to create a new Date object and test the parsing.

Safari will not parse a date unless it is in the US date format ‘mm/dd/yyyy’. Check out the debug session below from Safari:

image

That’s just crazy talk folks. Safari won’t even parse a date in the standard ISO Date format ‘yyyy-mm-dd’. WTF? We will get to that weirdness circled in red next.

Ok so now that we (read I) have accepted that Safari sucks when it comes to parsing dates we can move on. Oh but wait, we can’t just yet. That bit circled in Red, notice I have put in an invalid date of the 31st of Feb 2011. Safari for some bizarre reason interprets that as the 3rd of March 2011. I will say it again, Safari sucks at parsing dates.

So the final check that we have to do (because Safari sucks at dates) is check that the Date Safari is giving us is actually the date (be it valid or not) that we gave it, since it sees fit to not tell us an invalid date is invalid…unlike other browsers.

So we work out safariMonths as the month entered minus 1, as getMonth() returns months between 0 and 11, and check that the day and month entered matches what Safari thinks we have entered:

(newDate.getDate() safariDays && newDate.getMonth() safariMonths);

And that’s it. So in conclusion we have had to write reams of code to cater for Safari’s inept way of parsing dates.

Please, if someone out there looks at my code and the points I have made and feels like saying “Actually you are way off base here and you should be doing it this way instead” I would love to hear from you.

For now this has fixed the problem for my current project.

BondiGeek.