From 3fc8c9a8d5e5667b13dbbea3f7758ab88c57823e Mon Sep 17 00:00:00 2001 From: madd Date: Sun, 8 May 2016 18:31:58 +0200 Subject: [PATCH] jquery.tablesorter: Improve detection and handling of isoDate * Detect years 0...99 correct. * Short forms possible: JJJJ, JJJJ-MM, JJJJMM, JJJJMMTT QUnit Test sorts now with parser 'isoDate' (because of 2009 former test sorts with parser 'text') * Prefix and postfix allowed. * Between date and time a 'T' or 'any white space' is allowed (Bug: T126886) Bug: T126886 Change-Id: I664b4cc9d5fb472ea0bc0e36a3c209f04048e769 --- resources/src/jquery/jquery.tablesorter.js | 59 +++++++++++++------ .../jquery/jquery.tablesorter.parsers.test.js | 51 ++++++++-------- .../jquery/jquery.tablesorter.test.js | 2 +- 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/resources/src/jquery/jquery.tablesorter.js b/resources/src/jquery/jquery.tablesorter.js index ec917730db..922da319cf 100644 --- a/resources/src/jquery/jquery.tablesorter.js +++ b/resources/src/jquery/jquery.tablesorter.js @@ -90,6 +90,7 @@ config = $( table ).data( 'tablesorter' ).config, cellIndex, nodeValue, + nextRow = false, // Start with 1 because 0 is the fallback parser i = 1, lastRowIndex = -1, @@ -113,24 +114,34 @@ if ( nodeValue !== '' ) { if ( parsers[ i ].is( nodeValue, table ) ) { concurrent++; - rowIndex++; + nextRow = true; if ( concurrent >= needed ) { // Confirmed the parser for multiple cells, let's return it return parsers[ i ]; } + } else if ( parsers[ i ].id.match( /isoDate/ ) && /^\D*(\d{1,4}) ?(\[.+\])?$/.test( nodeValue ) ) { + // For 1-4 digits and maybe reference(s) parser "isoDate" or "number" is possible, check next row + empty++; + nextRow = true; } else { // Check next parser, reset rows i++; rowIndex = 0; concurrent = 0; empty = 0; + nextRow = false; } } else { // Empty cell empty++; + nextRow = true; + } + + if ( nextRow ) { + nextRow = false; rowIndex++; if ( rowIndex >= rows.length ) { - if ( concurrent >= rows.length - empty ) { + if ( concurrent > 0 && concurrent >= rows.length - empty ) { // Confirmed the parser for all filled cells return parsers[ i ]; } @@ -736,8 +747,8 @@ new RegExp( /(https?|ftp|file):\/\// ) ], isoDate: [ - new RegExp( /^([-+]?\d{1,4})-([01]\d)-([0-3]\d)([T\s]((([01]\d|2[0-3])(:?[0-5]\d)?|24:?00)?(:?([0-5]\d|60))?([.,]\d+)?)([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?/ ), - new RegExp( /^([-+]?\d{1,4})-([01]\d)-([0-3]\d)/ ) + new RegExp( /^[^-\d]*(-?\d{1,4})-(0\d|1[0-2])(-([0-3]\d))?([T\s]([01]\d|2[0-4]):?(([0-5]\d):?(([0-5]\d|60)([.,]\d{1,3})?)?)?([zZ]|([-+])([01]\d|2[0-3]):?([0-5]\d)?)?)?/ ), + new RegExp( /^[^-\d]*(-?\d{1,4})-?(\d\d)?(-?(\d\d))?([T\s](\d\d):?((\d\d)?:?((\d\d)?([.,]\d{1,3})?)?)?([zZ]|([-+])(\d\d):?(\d\d)?)?)?/ ) ], usLongDate: [ new RegExp( /^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/ ) @@ -1145,22 +1156,36 @@ return ts.rgx.isoDate[ 0 ].test( s ); }, format: function ( s ) { - var isodate, matches; - if ( !Date.prototype.toISOString ) { - // Old browsers don't understand iso, Fallback to US date parsing and ignore the time part. - matches = $.trim( s ).match( ts.rgx.isoDate[ 1 ] ); - if ( !matches ) { - return $.tablesorter.formatFloat( 0 ); + var match, i, isodate, ms, hOffset, mOffset; + match = s.match( ts.rgx.isoDate[ 0 ] ); + if ( match === null ) { + // Otherwise a signed number with 1-4 digit is parsed as isoDate + match = s.match( ts.rgx.isoDate[ 1 ] ); + } + if ( !match ) { + return 0; + } + // Month and day + for ( i = 2; i <= 4; i += 2 ) { + if ( !match[ i ] || match[ i ].length === 0 ) { + match[ i ] = 1; } - isodate = new Date( matches[ 2 ] + '/' + matches[ 3 ] + '/' + matches[ 1 ] ); - } else { - matches = s.match( ts.rgx.isoDate[ 0 ] ); - if ( !matches ) { - return $.tablesorter.formatFloat( 0 ); + } + // Time + for ( i = 6; i <= 15; i++ ) { + if ( !match[ i ] || match[ i ].length === 0 ) { + match[ i ] = '0'; } - isodate = new Date( $.trim( matches[ 0 ] ) ); } - return $.tablesorter.formatFloat( ( isodate !== undefined ) ? isodate.getTime() : 0 ); + ms = parseFloat( match[ 11 ].replace( /,/, '.' ) ) * 1000; + hOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 14 ] ); + mOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 15 ] ); + + isodate = new Date( 0 ); + // Because Date constructor changes year 0-99 to 1900-1999, use setUTCFullYear() + isodate.setUTCFullYear( match[ 1 ], match[ 2 ] - 1, match[ 4 ] ); + isodate.setUTCHours( match[ 6 ] - hOffset, match[ 8 ] - mOffset, match[ 10 ], ms ); + return isodate.getTime(); }, type: 'numeric' } ); diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js index 200395e20e..257699af3a 100644 --- a/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js @@ -174,40 +174,45 @@ parserTest( 'Y Dates', 'date', YDates ); ISODates = [ - [ '2000', false, 0, 'Plain 4-digit year' ], - [ '2000-01', false, 0, 'Year with month' ], - [ '2000-01-01', true, 946684800000, 'Year with month and day' ], - [ '2000-13-01', true, -Infinity, 'Non existant month' ], - [ '2000-01-32', true, -Infinity, 'Non existant day' ], + [ '2000', false, 946684800000, 'Plain 4-digit year' ], + [ '2000-01', true, 946684800000, 'Year with month' ], + [ '2000-01-01', true, 946684800000, 'Year with month and day' ], + [ '2000-13-01', false, 978307200000, 'Non existant month' ], + [ '2000-01-32', true, 949363200000, 'Non existant day' ], + [ '2000-01-01T12:30:30', true, 946729830000, 'Date with a time' ], [ '2000-01-01T12:30:30Z', true, 946729830000, 'Date with a UTC+0 time' ], - [ '2000-01-01T24:30:30Z', true, -Infinity, 'Date with invalid hours' ], - [ '2000-01-01T12:60:30Z', true, -Infinity, 'Date with invalid minutes' ], + [ '2000-01-01T24:30:30Z', true, 946773030000, 'Date with invalid hours' ], + [ '2000-01-01T12:60:30Z', true, 946728000000, 'Date with invalid minutes' ], + [ '2000-01-01T12:30:61Z', true, 946729800000, 'Date with invalid amount of seconds, drops seconds' ], [ '2000-01-01T23:59:59Z', true, 946771199000, 'Edges of time' ], [ '2000-01-01T12:30:30.111Z', true, 946729830111, 'Date with milliseconds' ], [ '2000-01-01T12:30:30.11111Z', true, 946729830111, 'Date with too high precision' ], - [ '2000-01-01T12:30:30,111Z', true, -Infinity, 'Date with milliseconds and , separator' ], + [ '2000-01-01T12:30:30,111Z', true, 946729830111, 'Date with milliseconds and , separator' ], [ '2000-01-01T12:30:30+01:00', true, 946726230000, 'Date time in UTC+1' ], [ '2000-01-01T12:30:30+01:30', true, 946724430000, 'Date time in UTC+1:30' ], [ '2000-01-01T12:30:30-01:00', true, 946733430000, 'Date time in UTC-1' ], [ '2000-01-01T12:30:30-01:30', true, 946735230000, 'Date time in UTC-1:30' ], [ '2000-01-01T12:30:30.111+01:00', true, 946726230111, 'Date time and milliseconds in UTC+1' ], [ '2000-01-01Postfix', true, 946684800000, 'Date with appended postfix' ], - [ '2000-01-01 Postfix', true, 946684800000, 'Date with separate postfix' ] - /* Disable testcases, because behavior is browser dependant */ - /* - [ '2000-11-31', true, 0, '31 days in 30 day month' ], - [ '50-01-01', false, -60589296000000, 'Year with just two digits' ], - [ '-1000-01-01', true, -93724128000000, 'Year BC' ], - [ '+1000-01-01', true, -30610224000000, 'Date with +sign' ], - [ '2000-01-01 12:30:30Z', true, 0, 'Date and time with no T marker' ], + [ '2000-01-01 Postfix', true, 946684800000, 'Date with separate postfix' ], + [ '2 Postfix', false, -62104060800000, 'One digit with separate postfix' ], + [ 'ca. 2', false, -62104060800000, 'Three digit with separate prefix' ], + [ '~200', false, -55855785600000, 'Three digit with appended prefix' ], + [ 'ca. 200[1]', false, -55855785600000, 'Three digit with separate prefix and postfix' ], + [ '2000-11-31', true, 975628800000, '31 days in 30 day month' ], + [ '50-01-01', true, -60589296000000, 'Year with just two digits' ], + [ '2', false, -62104060800000, 'Year with one digit' ], + [ '02-01', true, -62104060800000, 'Year with one digit and leading zero' ], + [ ' 2-01', true, -62104060800000, 'Year with one digit and leading space' ], + [ '-2-10', true, -62206704000000, 'Year BC with month' ], + [ '-9999', false, -377705116800000, 'max. Year BC' ], + [ '+9999-12', true, 253399622400000, 'max. Date with +sign' ], + [ '2000-01-01 12:30:30Z', true, 946729830000, 'Date and time with no T marker' ], [ '2000-01-01T12:30:60Z', true, 946729860000, 'Date with leap second' ], - [ '2000-01-01T12:30:30-24:00', true, 946816230000, 'Date time in UTC-24' ], - [ '2000-01-01T12:30:30+24:00', true, 946643430000, 'Date time in UTC+24' ], - [ '2000-01-01T12:30:30+0100', true, 946726230000, 'Time without separator in timezone offset' ] - // No "Z", uses local timezone: - [ '2000-01-01T12:30:30', true, 946729830000, 'Date with a time' ], - [ '2000-01-01T12:30:61Z', true, 946729800000, 'Date with invalid amount of seconds, drops seconds' ], - */ + [ '2000-01-01T12:30:30-23:59', true, 946816170000, 'Date time in UTC-23:59' ], + [ '2000-01-01T12:30:30+23:59', true, 946643490000, 'Date time in UTC+23:59' ], + [ '2000-01-01T123030+0100', true, 946726230000, 'Time without separators' ], + [ '20000101T123030+0100', false, 946726230000, 'All without separators' ] ]; parserTest( 'ISO Dates', 'isoDate', ISODates ); diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js index c88941ee5d..27d7e8daf5 100644 --- a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -184,8 +184,8 @@ ], isoDateSortingSorted = [ [ '2009' ], - [ '2009-12-25T12:30:45' ], [ '2009-12-25T12:30:45+01:00' ], + [ '2009-12-25T12:30:45' ], [ '2009-12-25T12:30:45.001Z' ], [ '2009-12-25T12:30:45.111' ], [ '2010-01-31' ], -- 2.20.1