From ca814a3c01d395fecd5b8ac5408f0d325dcdf848 Mon Sep 17 00:00:00 2001 From: alsundukov <74598771+alsundukov@users.noreply.github.com> Date: Sat, 10 Jun 2023 16:44:18 +0300 Subject: [PATCH] Align how strings are converted to timestamps (#1534) * Better date support for Safari * Config separators for now() output * Support timezone timestamps --- src/17alasql.js | 3 ++ src/61date.js | 72 ++++++++++++++++++++++++----- test/test845.js | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 11 deletions(-) diff --git a/src/17alasql.js b/src/17alasql.js index bfa144770..3fd59e1db 100755 --- a/src/17alasql.js +++ b/src/17alasql.js @@ -59,6 +59,9 @@ alasql.options = { /** Use valueof in orderfn */ valueof: true, + progress: false, // Callback for async queries progress + nowdateseparator: '-', // Date separator for Now() function + nowdatetimeseparator: ' ', //'T'; // date-time Separator for Now() function /** DROP database in any case */ dropifnotexists: false, diff --git a/src/61date.js b/src/61date.js index d07c08e49..1b8361643 100644 --- a/src/61date.js +++ b/src/61date.js @@ -20,7 +20,7 @@ stdfn.ASCII = function (a) { return a.charCodeAt(0); }; -/** +/** Return first non-null argument See https://msdn.microsoft.com/en-us/library/ms190349.aspx */ @@ -43,20 +43,24 @@ stdfn.OBJECT_ID = function (objid) { }; stdfn.DATE = function (d) { - if (/\d{8}/.test(d)) return new Date(+d.substr(0, 4), +d.substr(4, 2) - 1, +d.substr(6, 2)); + if (/^\d{8}$/.test(d.toString())) + return new Date(+d.substr(0, 4), +d.substr(4, 2) - 1, +d.substr(6, 2)); return newDate(d); }; -stdfn.NOW = function () { - var d = new Date(); +stdfn.NOW = function (param) { + var d; + if (param) d = stdfn.DATE(param); + else d = new Date(); + var separator = alasql.options.nowdateseparator; var s = d.getFullYear() + - '-' + + separator + ('0' + (d.getMonth() + 1)).substr(-2) + - '-' + + separator + ('0' + d.getDate()).substr(-2); s += - ' ' + + alasql.options.nowdatetimeseparator + ('0' + d.getHours()).substr(-2) + ':' + ('0' + d.getMinutes()).substr(-2) + @@ -166,12 +170,58 @@ alasql.stdfn.DATE_SUB = alasql.stdfn.SUBDATE = function (d, interval) { return new Date(nd); }; -var dateRegexp = /^\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}/; +var dateRegexp = + // /^(?\d{4})[-\.](?\d{1,2})[-\.](?\d{1,2})( (?\d{2}):(?\d{2})(:(?\d{2})(\.(?)\d{3})?)?)?/; + /^\W*(?\d{4})[-\/\.](?[0-1]?\d)[-\/\.](?[0-3]?\d)([T ](?[0-2][0-9]):(?[0-5][0-9])(:(?[0-5][0-9])(\.(?\d{3}))?\d*)?(?Z|(?[-+])(?\d\d):(?\d\d))?)?/i; function newDate(d) { + // Read https://stackoverflow.com/questions/2587345/why-does-date-parse-give-incorrect-results/20463521#20463521 to understand + // why we need to rely on our own parsing to ensure consistency across runtimes. + // Also: YYYY-MM-DD defaults to UTC is required by ECMA-262 but YYYY-MM-DD HH:ss is not. + // AlaSQL will treat _anything_ with no timezone as local time. + + var date; + + if (typeof d === 'interger') { + return new Date(d); + } + if (typeof d === 'string') { - if (dateRegexp.test(d)) { - d = d.replace('.', '-').replace('.', '-'); + const match = d.match(dateRegexp); + if (match) { + const {year, month, day, hour, min, sec, msec, tz, tz_dir, tz_HH, tz_mm} = match.groups; + + if (tz) { + var offset_HH = 0; + var offset_mm = 0; + + if ('Z' != tz) { + offset_HH = -1 * +(tz_dir + tz_HH); + offset_mm = -1 * +(tz_dir + tz_mm); + } + date = new Date( + Date.UTC(+year, +month - 1, +day, +hour + offset_HH, +min + offset_mm, +sec, +msec) + ); + } else { + const dateArrguments = [year, month - 1, day]; + if (hour) { + dateArrguments.push(hour, min, sec || 0, msec || 0); + } + date = new Date(...dateArrguments); + } + } else { + date = new Date(Date.parse(d)); } } - return new Date(d); + + if (isNaN(date)) { + date = new Date(d); + } + + // https://github.com/AlaSQL/alasql/pull/1534#discussion_r1051623032 + //if (isNaN(date)) { + // throw 'The value could not be converted to a date: ' + JSON.stringify(d); + // //console.warn(`d:${d}, date:${date}`) + //} + + return date; } diff --git a/test/test845.js b/test/test845.js index df7d3507c..68d595ab8 100644 --- a/test/test845.js +++ b/test/test845.js @@ -21,4 +21,124 @@ describe('Test ' + test + ' - use NOW() function', function () { //02/25/22 assert(/\d{2}\/\d{2}\/\d{2}/.test(res[0].conv)); }); + + it('3. NOW() with point', function () { + var currentSeparator = alasql.options.nowdateseparator; + alasql.options.nowdateseparator = '.'; + var res = alasql('SELECT NOW() AS now'); + //2022.02.25 19:21:27.839 + assert(/\d{4}.\d{2}.\d{2} \d{2}:\d{2}:\d{2}.\d{3}/.test(res[0].now)); + alasql.options.nowdateseparator = currentSeparator; + }); + + it('4. CONVERT with NOW() with point as an argument', function () { + var currentSeparator = alasql.options.nowdateseparator; + alasql.options.nowdateseparator = '.'; + var res = alasql('SELECT CONVERT(STRING,NOW(),1) AS conv'); + //02/25/22 + assert(/\d{2}\/\d{2}\/\d{2}/.test(res[0].conv)); + alasql.options.nowdateseparator = currentSeparator; + }); + + it('5. Reads multiple formats of date strings', function () { + assert.equal( + alasql('VALUE OF SELECT DATE("2023-06-10 16:27:36.224Z")').toISOString(), + '2023-06-10T16:27:36.224Z' + ); + + // Despite ECMA-262 where YYYY-MM-DD must be set in UTC and YYYY-MM-DD HH:ss must not AlaSQL will treat _anything_ with no timezone as local time. + + assert.equal( + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:00")').toISOString(), + alasql('VALUE OF SELECT DATE("2022-01-10")').toISOString() + ); + + assert.equal( + alasql('VALUE OF SELECT DATE("2022-01-20T12:01:02.123456789Z")').toISOString(), + '2022-01-20T12:01:02.123Z' + ); + + assert.equal( + alasql('VALUE OF SELECT DATE("2022-01.20 12:00:00.123456789-10:30")').toISOString(), + '2022-01-20T22:30:00.123Z' + ); + + assert.equal( + alasql('VALUE OF SELECT DATE("2022-01.20 12:00:00.123456789+10:30")').toISOString(), + '2022-01-20T01:30:00.123Z' + ); + + assert.equal( + alasql('VALUE OF SELECT DATE("2022-01.20 12:00+10:30")').toISOString(), + '2022-01-20T01:30:00.000Z' + ); + + assert.equal( + alasql('VALUE OF SELECT DATE("2022-01-20T12:00:00.123456789+03:30")').toISOString(), + '2022-01-20T08:30:00.123Z' + ); + + assert.equal( + alasql('VALUE OF SELECT DATE("2022-01-20T12:00:00.123456789+03:30")').toISOString(), + '2022-01-20T08:30:00.123Z' + ); + + /* + Hmmm. How best to test that this gets converted correclty to locala time (including DST)? + "2022.01.10 04:10", + "2022.01.10 04:10:11", + "2022.01.10 04:10:11.123", + "2022-01-10 04:10", + "2022-01-10 04:10:11", + "2022-01-10 04:10:11.123", + "2022.1.10", + "2022-1-1" + "2022/1/1" + + For now: Lets make sure that it converts to a valid date. + */ + + assert(!isNaN(alasql('VALUE OF SELECT DATE("2012.01.10 04:10")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022.01.10 04:10:11")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022.01.10 04:10:11.123")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022.01.10T04:10:11.123")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022-01-10 04:10")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022-01-10 04:10:11")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022-01-10 04:10:11.123")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022.1.10")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022-1-1")'))); + assert(!isNaN(alasql('VALUE OF SELECT DATE("2022/1/1")'))); + }); + + it('6. Will seek to trim value', function () { + assert.deepEqual( + alasql('VALUE OF SELECT DATE(" 2022-01-10T00:00:01")'), + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:01")') + ); + + assert.deepEqual( + alasql('VALUE OF SELECT DATE(" 2022-01-10T00:00:02")'), + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:02")') + ); + + assert.notDeepEqual( + alasql('VALUE OF SELECT DATE("xyz 2022-01-10T00:00:03")'), + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:03")') + ); + + assert.deepEqual( + alasql('VALUE OF SELECT DATE("€%&/()\'2022-01-10T00:00:04")'), + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:04")') + ); + + assert.deepEqual( + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:05 XXYYYZZZ5678&/(")'), + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:05")') + ); + + assert.deepEqual( + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:06 2000-02-20T20:20:20")'), + alasql('VALUE OF SELECT DATE("2022-01-10T00:00:06")') + ); + }); });