diff --git a/tests/unit/datepicker/core.js b/tests/unit/datepicker/core.js index a8e394696f..485ce375ba 100644 --- a/tests/unit/datepicker/core.js +++ b/tests/unit/datepicker/core.js @@ -455,7 +455,7 @@ QUnit.test( "keystrokes", function( assert ) { } ); QUnit.test( "mouse", function( assert ) { - assert.expect( 15 ); + assert.expect( 16 ); var inl, inp = testHelper.init( "#inp" ), dp = $( "#ui-datepicker-div" ), @@ -481,6 +481,13 @@ QUnit.test( "mouse", function( assert ) { $( "button.ui-datepicker-close", dp ).simulate( "click", {} ); testHelper.equalsDate( assert, inp.datepicker( "getDate" ), new Date( 2008, 2 - 1, 4 ), "Mouse click - abandoned" ); + inp.datepicker( "hide" ); + + inp.val( "02/24/0999" ).datepicker( "show" ); + $( ".ui-datepicker-calendar tbody a:contains(15)", dp ).simulate( "click", {} ); + inp.datepicker( "show" ); + testHelper.equalsDate( assert, inp.datepicker( "getDate" ), new Date( 999, 2 - 1, 15 ), "Mouse click inline - preset year 999" ); + inp.datepicker( "hide" ); // Current/previous/next inp.val( "02/04/2008" ).datepicker( "option", { showButtonPanel: true } ).datepicker( "show" ); diff --git a/tests/unit/datepicker/helper.js b/tests/unit/datepicker/helper.js index 652609d542..fa5b516ecb 100644 --- a/tests/unit/datepicker/helper.js +++ b/tests/unit/datepicker/helper.js @@ -8,7 +8,7 @@ define( [ return $.extend( helper, { addMonths: function( date, offset ) { - var maxDay = 32 - new Date( date.getFullYear(), date.getMonth() + offset, 32 ).getDate(); + var maxDay = 32 - $.datepicker._createDate( date.getFullYear(), date.getMonth() + offset, 32 ).getDate(); date.setDate( Math.min( date.getDate(), maxDay ) ); date.setMonth( date.getMonth() + offset ); return date; @@ -19,8 +19,8 @@ return $.extend( helper, { assert.ok( false, message + " - missing date" ); return; } - d1 = new Date( d1.getFullYear(), d1.getMonth(), d1.getDate() ); - d2 = new Date( d2.getFullYear(), d2.getMonth(), d2.getDate() ); + d1 = $.datepicker._createDate( d1.getFullYear(), d1.getMonth(), d1.getDate() ); + d2 = $.datepicker._createDate( d2.getFullYear(), d2.getMonth(), d2.getDate() ); assert.equal( d1.toString(), d2.toString(), message ); }, diff --git a/tests/unit/datepicker/options.js b/tests/unit/datepicker/options.js index 3055bc1997..67bd592c05 100644 --- a/tests/unit/datepicker/options.js +++ b/tests/unit/datepicker/options.js @@ -1124,7 +1124,7 @@ QUnit.test( "Ticket #7244: date parser does not fail when too many numbers are p } ); QUnit.test( "formatDate", function( assert ) { - assert.expect( 16 ); + assert.expect( 24 ); testHelper.init( "#inp" ); var gmtDate, fr, settings; assert.equal( $.datepicker.formatDate( "d m y", new Date( 2001, 2 - 1, 3 ) ), @@ -1164,6 +1164,17 @@ QUnit.test( "formatDate", function( assert ) { assert.equal( $.datepicker.formatDate( "'jour' d 'de' MM (''DD''), yy", new Date( 2001, 4 - 1, 9 ), settings ), "jour 9 de avril ('lundi'), 2001", "Format date 'jour' d 'de' MM (''DD''), yy with settings" ); + + assert.equal( $.datepicker.formatDate( "yy-mm-dd", $.datepicker._createDate( 0, 0, 1 ) ), "0000-01-01" ); + assert.equal( $.datepicker.formatDate( "yy-mm-dd", $.datepicker._createDate( 12, 0, 1 ) ), "0012-01-01" ); + assert.equal( $.datepicker.formatDate( "yy-mm-dd", $.datepicker._createDate( 99, 0, 1 ) ), "0099-01-01" ); + assert.equal( $.datepicker.formatDate( "yy-mm-dd", new Date( 100, 0, 1 ) ), "0100-01-01" ); + assert.equal( $.datepicker.formatDate( "yy-mm-dd", new Date( 999, 0, 1 ) ), "0999-01-01" ); + assert.equal( $.datepicker.formatDate( "yy-mm-dd", new Date( 1000, 0, 1 ) ), "1000-01-01" ); + + // -1 and 100000 will not be parsed correctly, but can be selected + assert.equal( $.datepicker.formatDate( "yy-mm-dd", new Date( -1, 0, 1 ) ), "-1-01-01" ); + assert.equal( $.datepicker.formatDate( "yy-mm-dd", new Date( 10000, 0, 1 ) ), "10000-01-01" ); } ); // TODO: Fix this test so it isn't mysteriously flaky in Browserstack on certain OS/Browser combos @@ -1260,4 +1271,52 @@ QUnit.test( "Ticket #15284: escaping text parameters", function( assert ) { } ); } ); +QUnit.test( "Ticket #7098: Broken handling of four digit years before year 100", function( assert ) { + assert.expect( 123 ); + + var year = null, + someDate = null, + parsedHDate = null, + inp = testHelper.init( "#inp", { + changeMonth: true, + changeYear: true, + yearRange: "0:2100" + } ); + + someDate = $.datepicker._createDate( 0, 1, 29 ); + assert.equal( someDate.getFullYear(), 0, "creates Date object for 0000-02-29" ); + assert.equal( someDate.getMonth(), 1, "creates Date object for 0000-02-29" ); + assert.equal( someDate.getDate(), 29, "creates Date object for 0000-02-29" ); + + for ( year = 0; year < 100; year++ ) { // loop 100 + parsedHDate = $.datepicker.parseDate( "yy-m-d", ( year < 10 ? "000" : "00" ) + year + "-10-23", { shortYearCutoff: 0 } ); + someDate = new Date( year, 2 - 1, 3 ); + someDate.setFullYear( year ); + assert.equal( + someDate.getFullYear(), parsedHDate.getFullYear(), + "parseDate returns date object with the correct year" + ); + } + + for ( year = 1; year < 100; year += 10 ) { // loop 10 + someDate = new Date( year, 2, 3 ); + someDate.setFullYear( year ); + inp.datepicker( "option", "defaultDate", someDate ); + inp.datepicker( "show" ); + $( "td[data-handler='selectDay'] a" ).first().click(); + inp.datepicker( "show" ); + assert.equal( + $( ".ui-datepicker-year option:selected" ).val(), + someDate.getFullYear(), + "Selected year stays the same as the default" + ); + inp.datepicker( "hide" ); + assert.ok( + inp.val().indexOf( someDate.getFullYear() ) !== -1, + "inp val has the default/selected year" + ); + inp.datepicker( "setDate", null ); + } +} ); + } ); diff --git a/ui/widgets/datepicker.js b/ui/widgets/datepicker.js index da70d5e00c..c977c1a03b 100644 --- a/ui/widgets/datepicker.js +++ b/ui/widgets/datepicker.js @@ -1220,6 +1220,8 @@ $.extend( Datepicker.prototype, { day = -1, doy = -1, literal = false, + length = -1, + isFromFullYear = false, date, // Check whether a format character is doubled @@ -1242,7 +1244,8 @@ $.extend( Datepicker.prototype, { if ( !num ) { throw "Missing number at position " + iValue; } - iValue += num[ 0 ].length; + length = num[ 0 ].length; + iValue += length; return parseInt( num[ 0 ], 10 ); }, @@ -1304,18 +1307,21 @@ $.extend( Datepicker.prototype, { break; case "y": year = getNumber( "y" ); + isFromFullYear = length === 4; break; case "@": date = new Date( getNumber( "@" ) ); year = date.getFullYear(); month = date.getMonth() + 1; day = date.getDate(); + isFromFullYear = true; break; case "!": date = new Date( ( getNumber( "!" ) - this._ticksTo1970 ) / 10000 ); year = date.getFullYear(); month = date.getMonth() + 1; day = date.getDate(); + isFromFullYear = true; break; case "'": if ( lookAhead( "'" ) ) { @@ -1339,7 +1345,7 @@ $.extend( Datepicker.prototype, { if ( year === -1 ) { year = new Date().getFullYear(); - } else if ( year < 100 ) { + } else if ( year < 100 && !isFromFullYear ) { year += new Date().getFullYear() - new Date().getFullYear() % 100 + ( year <= shortYearCutoff ? 0 : -100 ); } @@ -1357,7 +1363,7 @@ $.extend( Datepicker.prototype, { } while ( true ); } - date = this._daylightSavingAdjust( new Date( year, month - 1, day ) ); + date = this._daylightSavingAdjust( this._createDate( year, month - 1, day ) ); if ( date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day ) { throw "Invalid date"; // E.g. 31/02/00 } @@ -1419,6 +1425,7 @@ $.extend( Datepicker.prototype, { dayNames = ( settings ? settings.dayNames : null ) || this._defaults.dayNames, monthNamesShort = ( settings ? settings.monthNamesShort : null ) || this._defaults.monthNamesShort, monthNames = ( settings ? settings.monthNames : null ) || this._defaults.monthNames, + year, // Check whether a format character is doubled lookAhead = function( match ) { @@ -1465,7 +1472,7 @@ $.extend( Datepicker.prototype, { break; case "o": output += formatNumber( "o", - Math.round( ( new Date( date.getFullYear(), date.getMonth(), date.getDate() ).getTime() - new Date( date.getFullYear(), 0, 0 ).getTime() ) / 86400000 ), 3 ); + Math.round( ( this._createDate( date.getFullYear(), date.getMonth(), date.getDate() ).getTime() - this._createDate( date.getFullYear(), 0, 0 ).getTime() ) / 86400000 ), 3 ); break; case "m": output += formatNumber( "m", date.getMonth() + 1, 2 ); @@ -1474,8 +1481,10 @@ $.extend( Datepicker.prototype, { output += formatName( "M", date.getMonth(), monthNamesShort, monthNames ); break; case "y": - output += ( lookAhead( "y" ) ? date.getFullYear() : - ( date.getFullYear() % 100 < 10 ? "0" : "" ) + date.getFullYear() % 100 ); + year = date.getFullYear(); + output += ( lookAhead( "y" ) ? + ( year >= 0 && year < 1000 ? ( year < 10 ? "000" : year < 100 ? "00" : "0" ) : "" ) + year : + ( year % 100 < 10 ? "0" : "" ) + year % 100 ); break; case "@": output += date.getTime(); @@ -1622,7 +1631,7 @@ $.extend( Datepicker.prototype, { } matches = pattern.exec( offset ); } - return new Date( year, month, day ); + return $.datepicker._createDate( year, month, day ); }, newDate = ( date == null || date === "" ? defaultDate : ( typeof date === "string" ? offsetString( date ) : ( typeof date === "number" ? ( isNaN( date ) ? defaultDate : offsetNumeric( date ) ) : new Date( date.getTime() ) ) ) ); @@ -1674,9 +1683,9 @@ $.extend( Datepicker.prototype, { /* Retrieve the date(s) directly. */ _getDate: function( inst ) { var startDate = ( !inst.currentYear || ( inst.input && inst.input.val() === "" ) ? null : - this._daylightSavingAdjust( new Date( - inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); - return startDate; + this._daylightSavingAdjust( this._createDate( + inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); + return startDate; }, /* Attach the onxxx handlers. These are declared statically so @@ -1726,7 +1735,7 @@ $.extend( Datepicker.prototype, { printDate, dRow, tbody, daySettings, otherMonth, unselectable, tempDate = new Date(), today = this._daylightSavingAdjust( - new Date( tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate() ) ), // clear time + this._createDate( tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate() ) ), // clear time isRTL = this._get( inst, "isRTL" ), showButtonPanel = this._get( inst, "showButtonPanel" ), hideIfNoPrevNext = this._get( inst, "hideIfNoPrevNext" ), @@ -1736,7 +1745,7 @@ $.extend( Datepicker.prototype, { stepMonths = this._get( inst, "stepMonths" ), isMultiMonth = ( numMonths[ 0 ] !== 1 || numMonths[ 1 ] !== 1 ), currentDate = this._daylightSavingAdjust( ( !inst.currentDay ? new Date( 9999, 9, 9 ) : - new Date( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ), + this._createDate( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ), minDate = this._getMinMaxDate( inst, "min" ), maxDate = this._getMinMaxDate( inst, "max" ), drawMonth = inst.drawMonth - showCurrentAtPos, @@ -1747,10 +1756,12 @@ $.extend( Datepicker.prototype, { drawYear--; } if ( maxDate ) { - maxDraw = this._daylightSavingAdjust( new Date( maxDate.getFullYear(), - maxDate.getMonth() - ( numMonths[ 0 ] * numMonths[ 1 ] ) + 1, maxDate.getDate() ) ); + maxDraw = this._daylightSavingAdjust( this._createDate( + maxDate.getFullYear(), + maxDate.getMonth() - ( numMonths[ 0 ] * numMonths[ 1 ] ) + 1, + maxDate.getDate() ) ); maxDraw = ( minDate && maxDraw < minDate ? minDate : maxDraw ); - while ( this._daylightSavingAdjust( new Date( drawYear, drawMonth, 1 ) ) > maxDraw ) { + while ( this._daylightSavingAdjust( this._createDate( drawYear, drawMonth, 1 ) ) > maxDraw ) { drawMonth--; if ( drawMonth < 0 ) { drawMonth = 11; @@ -1763,7 +1774,7 @@ $.extend( Datepicker.prototype, { prevText = this._get( inst, "prevText" ); prevText = ( !navigationAsDateFormat ? prevText : this.formatDate( prevText, - this._daylightSavingAdjust( new Date( drawYear, drawMonth - stepMonths, 1 ) ), + this._daylightSavingAdjust( this._createDate( drawYear, drawMonth - stepMonths, 1 ) ), this._getFormatConfig( inst ) ) ); if ( this._canAdjustMonth( inst, -1, drawYear, drawMonth ) ) { @@ -1798,7 +1809,7 @@ $.extend( Datepicker.prototype, { nextText = this._get( inst, "nextText" ); nextText = ( !navigationAsDateFormat ? nextText : this.formatDate( nextText, - this._daylightSavingAdjust( new Date( drawYear, drawMonth + stepMonths, 1 ) ), + this._daylightSavingAdjust( this._createDate( drawYear, drawMonth + stepMonths, 1 ) ), this._getFormatConfig( inst ) ) ); if ( this._canAdjustMonth( inst, +1, drawYear, drawMonth ) ) { @@ -1883,7 +1894,7 @@ $.extend( Datepicker.prototype, { group = ""; this.maxRows = 4; for ( col = 0; col < numMonths[ 1 ]; col++ ) { - selectedDate = this._daylightSavingAdjust( new Date( drawYear, drawMonth, inst.selectedDay ) ); + selectedDate = this._daylightSavingAdjust( this._createDate( drawYear, drawMonth, inst.selectedDay ) ); cornerClass = " ui-corner-all"; calender = ""; if ( isMultiMonth ) { @@ -1921,7 +1932,7 @@ $.extend( Datepicker.prototype, { curRows = Math.ceil( ( leadDays + daysInMonth ) / 7 ); // calculate the number of rows to generate numRows = ( isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows ); //If multiple months, use the higher number of rows (see #7043) this.maxRows = numRows; - printDate = this._daylightSavingAdjust( new Date( drawYear, drawMonth, 1 - leadDays ) ); + printDate = this._daylightSavingAdjust( this._createDate( drawYear, drawMonth, 1 - leadDays ) ); for ( dRow = 0; dRow < numRows; dRow++ ) { // create date picker rows calender += ""; tbody = ( !showWeek ? "" : "" + @@ -2055,7 +2066,7 @@ $.extend( Datepicker.prototype, { var year = inst.selectedYear + ( period === "Y" ? offset : 0 ), month = inst.selectedMonth + ( period === "M" ? offset : 0 ), day = Math.min( inst.selectedDay, this._getDaysInMonth( year, month ) ) + ( period === "D" ? offset : 0 ), - date = this._restrictMinMax( inst, this._daylightSavingAdjust( new Date( year, month, day ) ) ); + date = this._restrictMinMax( inst, this._daylightSavingAdjust( this._createDate( year, month, day ) ) ); inst.selectedDay = date.getDate(); inst.drawMonth = inst.selectedMonth = date.getMonth(); @@ -2095,19 +2106,21 @@ $.extend( Datepicker.prototype, { /* Find the number of days in a given month. */ _getDaysInMonth: function( year, month ) { - return 32 - this._daylightSavingAdjust( new Date( year, month, 32 ) ).getDate(); + return 32 - this._daylightSavingAdjust( this._createDate( year, month, 32 ) ).getDate(); }, /* Find the day of the week of the first of a month. */ _getFirstDayOfMonth: function( year, month ) { - return new Date( year, month, 1 ).getDay(); + return this._createDate( year, month, 1 ).getDay(); }, /* Determines if we should allow a "next/prev" month display change. */ _canAdjustMonth: function( inst, offset, curYear, curMonth ) { var numMonths = this._getNumberOfMonths( inst ), - date = this._daylightSavingAdjust( new Date( curYear, - curMonth + ( offset < 0 ? offset : numMonths[ 0 ] * numMonths[ 1 ] ), 1 ) ); + date = this._daylightSavingAdjust( this._createDate( + curYear, + curMonth + ( offset < 0 ? offset : numMonths[ 0 ] * numMonths[ 1 ] ), + 1 ) ); if ( offset < 0 ) { date.setDate( this._getDaysInMonth( date.getFullYear(), date.getMonth() ) ); @@ -2160,9 +2173,21 @@ $.extend( Datepicker.prototype, { inst.currentYear = inst.selectedYear; } var date = ( day ? ( typeof day === "object" ? day : - this._daylightSavingAdjust( new Date( year, month, day ) ) ) : - this._daylightSavingAdjust( new Date( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); + this._daylightSavingAdjust( this._createDate( year, month, day ) ) ) : + this._daylightSavingAdjust( this._createDate( inst.currentYear, inst.currentMonth, inst.currentDay ) ) ); return this.formatDate( this._get( inst, "dateFormat" ), date, this._getFormatConfig( inst ) ); + }, + + /** Create a date object with the correct year for years 0 through 99 */ + _createDate: function( year, month, day ) { + var dateObject; + if ( year >= 0 && year < 100 ) { + dateObject = new Date( 2000, month, day ); + dateObject.setFullYear( year ); + } else { + dateObject = new Date( year, month, day ); + } + return dateObject; } } );