PDA

View Full Version : Date.createParser Millisecond override



Green
2 Oct 2007, 7:48 AM
ExtJS: 1.1

Not a bug per se, but a shortcoming for certain, and a explicit suggestion for sure.

While developing a PHP application which must be able to use SQLite, MySQL and MSSQL as backend via PDO, it became clear that there is no way to use the same general SQL for all three with the ExtJS Date.parseDate. Problem is MSSQL always gives the date back as 'Y-m-d h:i: s.xxx', where xxx is the milliseconds. This obviously mess up all datetime columns and pickers in ExtJS as it can not convert the raw date data.

After using up a day to find a way to change this default format for MSSQL for all eternity, I gave up and decided ExtJS must be able to handle these raw date values from MSSQL as well. Following is the resulting minor changes to Date.createParser and Date.formatCodeToRegex which allows ExtJS to parse the millisecond format as well:


Date.createParser = function(format) {
var funcName = "parse" + Date.parseFunctions.count++;
var regexNum = Date.parseRegexes.length;
var currentGroup = 1;
Date.parseFunctions[format] = funcName;

var code = "Date." + funcName + " = function(input){\n"
+ "var y = -1, m = -1, d = -1, h = -1, i = -1, s = -1, ms = -1, o, z, v;\n"
+ "var d = new Date();\n"
+ "y = d.getFullYear();\n"
+ "m = d.getMonth();\n"
+ "d = d.getDate();\n"
+ "var results = input.match(Date.parseRegexes[" + regexNum + "]);\n"
+ "if (results && results.length > 0) {";
var regex = "";

var special = false;
var ch = '';
for (var i = 0; i < format.length; ++i) {
ch = format.charAt(i);
if (!special && ch == "\\") {
special = true;
}
else if (special) {
special = false;
regex += String.escape(ch);
}
else {
var obj = Date.formatCodeToRegex(ch, currentGroup);
currentGroup += obj.g;
regex += obj.s;
if (obj.g && obj.c) {
code += obj.c;
}
}
}

code += "if (y >= 0 && m >= 0 && d > 0 && h >= 0 && i >= 0 && s >= 0 && ms >= 0)\n"
+ "{v = new Date(y, m, d, h, i, s, ms);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0 && h >= 0 && i >= 0 && s >= 0)\n"
+ "{v = new Date(y, m, d, h, i, s);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0 && h >= 0 && i >= 0)\n"
+ "{v = new Date(y, m, d, h, i);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0 && h >= 0)\n"
+ "{v = new Date(y, m, d, h);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0)\n"
+ "{v = new Date(y, m, d);}\n"
+ "else if (y >= 0 && m >= 0)\n"
+ "{v = new Date(y, m);}\n"
+ "else if (y >= 0)\n"
+ "{v = new Date(y);}\n"
+ "}return (v && (z || o))?\n" // favour UTC offset over GMT offset
+ " ((z)? v.add(Date.SECOND, (v.getTimezoneOffset() * 60) + (z*1)) :\n" // reset to UTC, then add offset
+ " v.add(Date.HOUR, (v.getGMTOffset() / 100) + (o / -100))) : v\n" // reset to GMT, then add offset
+ ";}";

Date.parseRegexes[regexNum] = new RegExp("^" + regex + "$");
eval(code);
};

// private
Date.formatCodeToRegex = function(character, currentGroup) {
switch (character) {
case "D":
return {g:0,
c:null,
s:"(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)"};
case "j":
return {g:1,
c:"d = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{1,2})"}; // day of month without leading zeroes
case "d":
return {g:1,
c:"d = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"}; // day of month with leading zeroes
case "l":
return {g:0,
c:null,
s:"(?:" + Date.dayNames.join("|") + ")"};
case "S":
return {g:0,
c:null,
s:"(?:st|nd|rd|th)"};
case "w":
return {g:0,
c:null,
s:"\\d"};
case "z":
return {g:0,
c:null,
s:"(?:\\d{1,3})"};
case "W":
return {g:0,
c:null,
s:"(?:\\d{2})"};
case "F":
return {g:1,
c:"m = parseInt(Date.monthNumbers[results[" + currentGroup + "].substring(0, 3)], 10);\n",
s:"(" + Date.monthNames.join("|") + ")"};
case "M":
return {g:1,
c:"m = parseInt(Date.monthNumbers[results[" + currentGroup + "]], 10);\n",
s:"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"};
case "n":
return {g:1,
c:"m = parseInt(results[" + currentGroup + "], 10) - 1;\n",
s:"(\\d{1,2})"}; // Numeric representation of a month, without leading zeros
case "m":
return {g:1,
c:"m = parseInt(results[" + currentGroup + "], 10) - 1;\n",
s:"(\\d{2})"}; // Numeric representation of a month, with leading zeros
case "t":
return {g:0,
c:null,
s:"\\d{1,2}"};
case "L":
return {g:0,
c:null,
s:"(?:1|0)"};
case "Y":
return {g:1,
c:"y = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{4})"};
case "y":
return {g:1,
c:"var ty = parseInt(results[" + currentGroup + "], 10);\n"
+ "y = ty > Date.y2kYear ? 1900 + ty : 2000 + ty;\n",
s:"(\\d{1,2})"};
case "a":
return {g:1,
c:"if (results[" + currentGroup + "] == 'am') {\n"
+ "if (h == 12) { h = 0; }\n"
+ "} else { if (h < 12) { h += 12; }}",
s:"(am|pm)"};
case "A":
return {g:1,
c:"if (results[" + currentGroup + "] == 'AM') {\n"
+ "if (h == 12) { h = 0; }\n"
+ "} else { if (h < 12) { h += 12; }}",
s:"(AM|PM)"};
case "g":
case "G":
return {g:1,
c:"h = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{1,2})"}; // 12/24-hr format format of an hour without leading zeroes
case "h":
case "H":
return {g:1,
c:"h = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"}; // 12/24-hr format format of an hour with leading zeroes
case "i":
return {g:1,
c:"i = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"};
case "s":
return {g:1,
c:"s = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"};
case "x":
return {g:1,
c:"ms = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{1,3})"};
case "O":
return {g:1,
c:[
"o = results[", currentGroup, "];\n",
"var sn = o.substring(0,1);\n", // get + / - sign
"var hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60);\n", // get hours (performs minutes-to-hour conversion also)
"var mn = o.substring(3,5) % 60;\n", // get minutes
"o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))?\n", // -12hrs <= GMT offset <= 14hrs
" (sn + String.leftPad(hr, 2, 0) + String.leftPad(mn, 2, 0)) : null;\n"
].join(""),
s:"([+\-]\\d{4})"};
case "T":
return {g:0,
c:null,
s:"[A-Z]{1,4}"}; // timezone abbrev. may be between 1 - 4 chars
case "Z":
return {g:1,
c:"z = results[" + currentGroup + "];\n" // -43200 <= UTC offset <= 50400
+ "z = (-43200 <= z*1 && z*1 <= 50400)? z : null;\n",
s:"([+\-]?\\d{1,5})"}; // leading '+' sign is optional for UTC offset
default:
return {g:0,
c:null,
s:String.escape(character)};
}
};

For lack of an identifier as well as direction from PHP's date function manual for an identifier, I simply used x, i.e. 'Y-m-d h:i: s.x', as it is currently not used.

Now all works perfect :)

The change is so minor and the override so huge, that it would be a pity if I had to distribute my override code everywhere simply because ExtJS does not include these 3 lines of JS. Something which ExtJS should actually provide for as milliseconds is a standard parameter on JS 1.3 for a date object. Thus, this forum post.

mystix
2 Oct 2007, 7:58 AM
dude, didn't you read the guidelines?

suggestions are always welcome, but they should go in the appropriate forums.

Green
2 Oct 2007, 8:51 AM
Must admit :"> I don't visit often, and last I looked up any guidelines was probably a year ago. Apologies, seems the guideline is a bit more comprehensive these days.

However, a bug, as per guideline definition, is when ExtJS deviates from intended behaviour.

Intended behaviour would be such that if you pull records via PHP from ExtJS into a grid, one would expect correctly formatted datetime values to be displayed correctly and/or datetime pickers to be able to display said date. In the case of SQLite and MySQL, this is indeed the case. In the case of ODBC/MSSQL/Oracle/etc this is not the case because of the added milliseconds. This will always be the issue because ExtJS does not provide for the full ISO date standard which clearly has the milliseconds as an option.

As ExtJS provides no other way to format raw DB date data into JS date objects client-side, the code shown above is the only way to go. I might have spinned it as a suggestion, but it was more intended as a suggestion to fix a bug with the provided solution on hand.

And I quote from ExtJS Docs:

ISO8601Long:"Y-m-d H:i:s"

And I quote from ISO8601 Docs:

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)

So ExtJS' intention is clearly to support the ISO date format, yet, it has a bug, as it does not actually provide for an ISO date's data. The fact that this issue is not a ExtJS code bug, but a ExtJS standards bug does not make it less of a bug :)

mystix
2 Oct 2007, 9:43 AM
as per the 1.1.1 Date docs (emphasis added):


The date parsing and format syntax is a subset of PHP's date() function, and the formats that are supported will provide results equivalent to their PHP versions. Following is the list of all currently supported formats:

PHP's u millisecond format was added in PHP 5.2.2.

here's a link to a PDF copy of ISO8601:2000(E) (i.e. year 2000 revision final draft)
http://www.probabilityof.com/iso/8601v2000.pdf

Under Section 5.4.1 - Complete Representation - of the ISO8601:2000(E) document above, the format YYYY-MM-DDTHH:MM:SS (i.e. Y-m-d H:i:s in PHP / Ext) is listed as one of the accepted Extended Formats.

Section 5.4.2 of that document - Representations other than complete - alludes to your format, although it isn't explicitly mentioned throughout the entire document.

the date format you pasted




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)


was taken from the W3C Date and Time Formats (http://www.w3.org/TR/NOTE-datetime) page which was submitted to the W3C in 1997, and i quote


Status of this document

This document is a NOTE made available by the W3 Consortium for discussion only. This indicates no endorsement of its content, nor that the Consortium has, is, or will be allocating any resources to the issues addressed by the NOTE.


Although Ext's Date class does not currently support this millisecond format, this in no way constitutes a bug, though it would be good to have.

Green
2 Oct 2007, 1:49 PM
Although Ext's Date class does not currently support this millisecond format, this in no way constitutes a bug,...

Ok, agreed, it is kind of a gray area /:)


..., though it would be good to have.

All I was saying as well. Better to have it and have ExtJS conform (i.e. cover more domain) than not. Or put another way, sooner or later it will have to be done. I rather be it sooner, especially as it is so easy to add thanks to ExtJS!

Until then, an out-of-the-box ExtJS will exclude a lot of intranet corporate platforms unless extensive work is done server-side concerning datetime columns (and, lets face it, datetimes are always somewhere in all databases). Which is kind of ironic as this is exactly the typical environment for ExtJS, i.e. where bandwidth is cheap or free.

JeffHowden
2 Oct 2007, 4:12 PM
I agree that it should be part of the extensions to the date object. I personally have run up against this as well, but wasn't anywhere near as successful as you in figuring out a fix. Supporting it requires a minimal of changes to the core compared to the headaches that it's causing by not supporting it.

mystix
2 Oct 2007, 6:48 PM
i'll look into it ;)

Green
2 Oct 2007, 10:25 PM
Ok, the code has now been thoroughly tested and I can attest it works. As per mystix suggestion, I changed the format identifier to 'u', as in 'Y-m-d H:i: s.u'. I also it to Date.getFormatCode, which now allows converting the Date back to it's original raw format. All 3 method changes are:

Date.getFormatCode = function(character) {
switch (character) {
case "d":
return "String.leftPad(this.getDate(), 2, '0') + ";
case "D":
return "Date.dayNames[this.getDay()].substring(0, 3) + ";
case "j":
return "this.getDate() + ";
case "l":
return "Date.dayNames[this.getDay()] + ";
case "S":
return "this.getSuffix() + ";
case "w":
return "this.getDay() + ";
case "z":
return "this.getDayOfYear() + ";
case "W":
return "this.getWeekOfYear() + ";
case "F":
return "Date.monthNames[this.getMonth()] + ";
case "m":
return "String.leftPad(this.getMonth() + 1, 2, '0') + ";
case "M":
return "Date.monthNames[this.getMonth()].substring(0, 3) + ";
case "n":
return "(this.getMonth() + 1) + ";
case "t":
return "this.getDaysInMonth() + ";
case "L":
return "(this.isLeapYear() ? 1 : 0) + ";
case "Y":
return "this.getFullYear() + ";
case "y":
return "('' + this.getFullYear()).substring(2, 4) + ";
case "a":
return "(this.getHours() < 12 ? 'am' : 'pm') + ";
case "A":
return "(this.getHours() < 12 ? 'AM' : 'PM') + ";
case "g":
return "((this.getHours() % 12) ? this.getHours() % 12 : 12) + ";
case "G":
return "this.getHours() + ";
case "h":
return "String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0') + ";
case "H":
return "String.leftPad(this.getHours(), 2, '0') + ";
case "i":
return "String.leftPad(this.getMinutes(), 2, '0') + ";
case "s":
return "String.leftPad(this.getSeconds(), 2, '0') + ";
case "u":
return "String.leftPad(this.getMilliseconds(), 3, '0') + ";
case "O":
return "this.getGMTOffset() + ";
case "T":
return "this.getTimezone() + ";
case "Z":
return "(this.getTimezoneOffset() * -60) + ";
default:
return "'" + String.escape(character) + "' + ";
}
};

// private
Date.createParser = function(format) {
var funcName = "parse" + Date.parseFunctions.count++;
var regexNum = Date.parseRegexes.length;
var currentGroup = 1;
Date.parseFunctions[format] = funcName;

var code = "Date." + funcName + " = function(input){\n"
+ "var y = -1, m = -1, d = -1, h = -1, i = -1, s = -1, ms = -1, o, z, v;\n"
+ "var d = new Date();\n"
+ "y = d.getFullYear();\n"
+ "m = d.getMonth();\n"
+ "d = d.getDate();\n"
+ "var results = input.match(Date.parseRegexes[" + regexNum + "]);\n"
+ "if (results && results.length > 0) {";
var regex = "";

var special = false;
var ch = '';
for (var i = 0; i < format.length; ++i) {
ch = format.charAt(i);
if (!special && ch == "\\") {
special = true;
}
else if (special) {
special = false;
regex += String.escape(ch);
}
else {
var obj = Date.formatCodeToRegex(ch, currentGroup);
currentGroup += obj.g;
regex += obj.s;
if (obj.g && obj.c) {
code += obj.c;
}
}
}

code += "if (y >= 0 && m >= 0 && d > 0 && h >= 0 && i >= 0 && s >= 0 && ms >= 0)\n"
+ "{v = new Date(y, m, d, h, i, s, ms);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0 && h >= 0 && i >= 0 && s >= 0)\n"
+ "{v = new Date(y, m, d, h, i, s);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0 && h >= 0 && i >= 0)\n"
+ "{v = new Date(y, m, d, h, i);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0 && h >= 0)\n"
+ "{v = new Date(y, m, d, h);}\n"
+ "else if (y >= 0 && m >= 0 && d > 0)\n"
+ "{v = new Date(y, m, d);}\n"
+ "else if (y >= 0 && m >= 0)\n"
+ "{v = new Date(y, m);}\n"
+ "else if (y >= 0)\n"
+ "{v = new Date(y);}\n"
+ "}return (v && (z || o))?\n" // favour UTC offset over GMT offset
+ " ((z)? v.add(Date.SECOND, (v.getTimezoneOffset() * 60) + (z*1)) :\n" // reset to UTC, then add offset
+ " v.add(Date.HOUR, (v.getGMTOffset() / 100) + (o / -100))) : v\n" // reset to GMT, then add offset
+ ";}";

Date.parseRegexes[regexNum] = new RegExp("^" + regex + "$");
eval(code);
};

// private
Date.formatCodeToRegex = function(character, currentGroup) {
switch (character) {
case "D":
return {g:0,
c:null,
s:"(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)"};
case "j":
return {g:1,
c:"d = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{1,2})"}; // day of month without leading zeroes
case "d":
return {g:1,
c:"d = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"}; // day of month with leading zeroes
case "l":
return {g:0,
c:null,
s:"(?:" + Date.dayNames.join("|") + ")"};
case "S":
return {g:0,
c:null,
s:"(?:st|nd|rd|th)"};
case "w":
return {g:0,
c:null,
s:"\\d"};
case "z":
return {g:0,
c:null,
s:"(?:\\d{1,3})"};
case "W":
return {g:0,
c:null,
s:"(?:\\d{2})"};
case "F":
return {g:1,
c:"m = parseInt(Date.monthNumbers[results[" + currentGroup + "].substring(0, 3)], 10);\n",
s:"(" + Date.monthNames.join("|") + ")"};
case "M":
return {g:1,
c:"m = parseInt(Date.monthNumbers[results[" + currentGroup + "]], 10);\n",
s:"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"};
case "n":
return {g:1,
c:"m = parseInt(results[" + currentGroup + "], 10) - 1;\n",
s:"(\\d{1,2})"}; // Numeric representation of a month, without leading zeros
case "m":
return {g:1,
c:"m = parseInt(results[" + currentGroup + "], 10) - 1;\n",
s:"(\\d{2})"}; // Numeric representation of a month, with leading zeros
case "t":
return {g:0,
c:null,
s:"\\d{1,2}"};
case "L":
return {g:0,
c:null,
s:"(?:1|0)"};
case "Y":
return {g:1,
c:"y = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{4})"};
case "y":
return {g:1,
c:"var ty = parseInt(results[" + currentGroup + "], 10);\n"
+ "y = ty > Date.y2kYear ? 1900 + ty : 2000 + ty;\n",
s:"(\\d{1,2})"};
case "a":
return {g:1,
c:"if (results[" + currentGroup + "] == 'am') {\n"
+ "if (h == 12) { h = 0; }\n"
+ "} else { if (h < 12) { h += 12; }}",
s:"(am|pm)"};
case "A":
return {g:1,
c:"if (results[" + currentGroup + "] == 'AM') {\n"
+ "if (h == 12) { h = 0; }\n"
+ "} else { if (h < 12) { h += 12; }}",
s:"(AM|PM)"};
case "g":
case "G":
return {g:1,
c:"h = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{1,2})"}; // 12/24-hr format format of an hour without leading zeroes
case "h":
case "H":
return {g:1,
c:"h = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"}; // 12/24-hr format format of an hour with leading zeroes
case "i":
return {g:1,
c:"i = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"};
case "s":
return {g:1,
c:"s = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{2})"};
case "u":
return {g:1,
c:"ms = parseInt(results[" + currentGroup + "], 10);\n",
s:"(\\d{1,3})"};
case "O":
return {g:1,
c:[
"o = results[", currentGroup, "];\n",
"var sn = o.substring(0,1);\n", // get + / - sign
"var hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60);\n", // get hours (performs minutes-to-hour conversion also)
"var mn = o.substring(3,5) % 60;\n", // get minutes
"o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))?\n", // -12hrs <= GMT offset <= 14hrs
" (sn + String.leftPad(hr, 2, 0) + String.leftPad(mn, 2, 0)) : null;\n"
].join(""),
s:"([+\-]\\d{4})"};
case "T":
return {g:0,
c:null,
s:"[A-Z]{1,4}"}; // timezone abbrev. may be between 1 - 4 chars
case "Z":
return {g:1,
c:"z = results[" + currentGroup + "];\n" // -43200 <= UTC offset <= 50400
+ "z = (-43200 <= z*1 && z*1 <= 50400)? z : null;\n",
s:"([+\-]?\\d{1,5})"}; // leading '+' sign is optional for UTC offset
default:
return {g:0,
c:null,
s:String.escape(character)};
}
};

In Firebug, the tests are:



console.dir(Date.parseDate('2007-10-02 17:20:04.090','Y-m-d H:i:s.u')); //-> JS Date
console.dir(Date.parseDate('2007-10-02 09:37:27.483','Y-m-d H:i:s.u')); //-> JS Date



or for a double conversion back to string:



console.dir(Date.parseDate('2007-10-02 17:20:04.090','Y-m-d H:i:s.u').dateFormat('Y-m-d H:i:s.u')); //-> Date as string
console.dir(Date.parseDate('2007-10-02 09:37:27.483','Y-m-d H:i: s.u').dateFormat('Y-m-d H:i:s.u')); //-> Date as string

mystix
10 Oct 2007, 8:37 AM
alritey @Green / @Jeff. the "u" millisecond format option is in SVN.

to remain consistent with the lowercase "i" and "s" formats though, the "u" option will require leading zeros.

Green
10 Oct 2007, 12:31 PM
Much appreciated :)

Thanks for the great work and support guys.