mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
449 lines
21 KiB
TypeScript
449 lines
21 KiB
TypeScript
|
/* global describe, it */
|
||
|
import {guessDateFormat, guessDateFormats, parseDate, parseDateStrict, parseDateTime} from 'app/common/parseDate';
|
||
|
import {assert} from 'chai';
|
||
|
import * as moment from 'moment-timezone';
|
||
|
|
||
|
const today = new Date();
|
||
|
const year = today.getUTCFullYear();
|
||
|
const month = String(today.getUTCMonth() + 1).padStart(2, '0');
|
||
|
|
||
|
/**
|
||
|
* Assert that parseDate and parseDateStrict parse `input` correctly,
|
||
|
* returning a date that looks like expectedDateStr in ISO format.
|
||
|
* parseDate should always produce a parsed date from `input`.
|
||
|
* parseDateStrict should return at most one date, i.e. the formats it tries shouldn't allow ambiguity.
|
||
|
*
|
||
|
* fallback=true indicates the date cannot be parsed strictly with the given format
|
||
|
* so parseDate has to fallback to another format and parseDateStrict gives no results.
|
||
|
*
|
||
|
* Otherwise, parseDateStrict should return a result
|
||
|
* unless no dateFormat is given in which case it may or may not.
|
||
|
*/
|
||
|
function testParse(dateFormat: string|null, input: string, expectedDateStr: string, fallback: boolean = false) {
|
||
|
assertDateEqual(parseDate(input, dateFormat ? {dateFormat} : {}), expectedDateStr);
|
||
|
|
||
|
const strict = new Set<number>();
|
||
|
parseDateStrict(input, dateFormat, strict);
|
||
|
assert.include([0, 1], strict.size);
|
||
|
|
||
|
// fallback=true indicates the date cannot be parsed strictly with the given format
|
||
|
// so it has to fallback to another format.
|
||
|
if (fallback) {
|
||
|
assert.isEmpty(strict);
|
||
|
} else if (dateFormat) {
|
||
|
assert.equal(strict.size, 1);
|
||
|
}
|
||
|
|
||
|
if (strict.size) {
|
||
|
const strictParsed = [...strict][0];
|
||
|
assertDateEqual(strictParsed, expectedDateStr);
|
||
|
assertDateEqual(parseDateTime(input, dateFormat ? {dateFormat} : {})!, expectedDateStr);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function assertDateEqual(parsed: number|null, expectedDateStr: string) {
|
||
|
const formatted = parsed === null ? null : new Date(parsed * 1000).toISOString().slice(0, 10);
|
||
|
assert.equal(formatted, expectedDateStr);
|
||
|
}
|
||
|
|
||
|
function testTimeParse(input: string, expectedUTCTimeStr: string | null, timezone?: string) {
|
||
|
const parsed1 = parseDateTime('1993-04-02T' + input,
|
||
|
{timeFormat: 'Z', timezone, dateFormat: 'YYYY-MM-DD'}) || null;
|
||
|
const parsed2 = parseDate('1993-04-02', {time: input, timeFormat: 'UNUSED', timezone});
|
||
|
for (const parsed of [parsed1, parsed2]) {
|
||
|
if (expectedUTCTimeStr === null) {
|
||
|
assert.isNull(parsed);
|
||
|
return;
|
||
|
}
|
||
|
const output = new Date(parsed! * 1000).toISOString().slice(11, 19);
|
||
|
assert.equal(output, expectedUTCTimeStr, `testTimeParse(${input}, ${timezone})`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function testDateTimeParse(
|
||
|
date: string, time: string, expectedUTCTimeStr: string | null, timezone: string, dateFormat?: string
|
||
|
) {
|
||
|
const parsed1 = parseDateTime(date + ' ' + time,
|
||
|
{timeFormat: 'Z', timezone, dateFormat: dateFormat || 'YYYY-MM-DD'}) || null;
|
||
|
|
||
|
// This is for testing the combination of date and time which is important when daylight savings is involved
|
||
|
const parsed2 = parseDate(date, {time, timeFormat: 'UNUSED', timezone, dateFormat});
|
||
|
|
||
|
for (const parsed of [parsed1, parsed2]) {
|
||
|
if (expectedUTCTimeStr === null) {
|
||
|
assert.isNull(parsed);
|
||
|
return;
|
||
|
}
|
||
|
const output = new Date(parsed! * 1000).toISOString().slice(0, 19).replace("T", " ");
|
||
|
assert.equal(output, expectedUTCTimeStr);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function testDateTimeStringParse(
|
||
|
dateTime: string, expectedUTCTimeStr: string | null, dateFormat: string, timezone?: string,
|
||
|
) {
|
||
|
const parsed = parseDateTime(dateTime, {timezone, dateFormat});
|
||
|
|
||
|
if (expectedUTCTimeStr === null) {
|
||
|
assert.isUndefined(parsed);
|
||
|
return;
|
||
|
}
|
||
|
const output = new Date(parsed! * 1000).toISOString().slice(0, 19).replace("T", " ");
|
||
|
assert.equal(output, expectedUTCTimeStr);
|
||
|
}
|
||
|
|
||
|
describe('parseDate', function() {
|
||
|
this.timeout(5000);
|
||
|
|
||
|
it('should allow parsing common date formats', function() {
|
||
|
testParse(null, 'November 18th, 1994', '1994-11-18');
|
||
|
testParse(null, 'nov 18 1994', '1994-11-18');
|
||
|
testParse(null, '11-18-94', '1994-11-18');
|
||
|
testParse(null, '11-18-1994', '1994-11-18');
|
||
|
testParse(null, '1994-11-18', '1994-11-18');
|
||
|
testParse(null, 'November 18, 1994', '1994-11-18');
|
||
|
testParse('DD/MM/YY', '18/11/94', '1994-11-18');
|
||
|
// fallback format is used because 18 is not a valid month
|
||
|
testParse('MM/DD/YY', '18/11/94', '1994-11-18', true);
|
||
|
|
||
|
testParse(null, '18/11/94', '1994-11-18');
|
||
|
testParse(null, '12/11/94', '1994-12-11');
|
||
|
testParse('DD/MM/YY', '12/11/94', '1994-11-12');
|
||
|
testParse('MM/DD/YY', '11/12/94', '1994-11-12');
|
||
|
|
||
|
testParse(null, '25', `${year}-${month}-25`);
|
||
|
testParse(null, '10', `${year}-${month}-10`);
|
||
|
testParse('DD/MM/YY', '10', `${year}-${month}-10`);
|
||
|
testParse('DD/MM/YY', '3/4', `${year}-04-03`);
|
||
|
// Separators in the format should not affect the parsing (for better or worse).
|
||
|
testParse('YY-DD/MM', '3/4', `${year}-04-03`);
|
||
|
testParse('YY/DD-MM', '3/4', `${year}-04-03`);
|
||
|
testParse('MM/DD/YY', '3/4', `${year}-03-04`);
|
||
|
testParse('YY/MM/DD', '3/4', `${year}-03-04`);
|
||
|
testParse(null, '3/4', `${year}-03-04`);
|
||
|
|
||
|
// Single number gets parse according to the most specific item in the format string.
|
||
|
testParse('DD', '10', `${year}-${month}-10`);
|
||
|
testParse('DD/MM', '10', `${year}-${month}-10`);
|
||
|
testParse('MM', '10', `${year}-10-01`);
|
||
|
testParse('MM/YY', '10', `${year}-10-01`);
|
||
|
testParse('MMM', '10', `${year}-10-01`);
|
||
|
testParse('YY', '10', `2010-01-01`);
|
||
|
testParse('YYYY', '10', `2010-01-01`);
|
||
|
|
||
|
testParse('YY', '05', `2005-01-01`);
|
||
|
testParse('YY', '5', `${year}-05-01`, true); // Not a valid year, so falls back to "M" format
|
||
|
testParse('YYYY', '1910', `1910-01-01`);
|
||
|
testParse('YY', '3/4', `${year}-03-04`, true); // Falls back to another format
|
||
|
testParse('DD/MM', '3/4', `${year}-04-03`);
|
||
|
testParse('MM/YY', '3/04', `2004-03-01`);
|
||
|
testParse('MM/YY', '3/4', `${year}-03-04`, true); // Not a valid year, so falls back to "M/D" format
|
||
|
|
||
|
testParse(null, '4/2/93', '1993-04-02');
|
||
|
testParse(null, '04-02-1993', '1993-04-02');
|
||
|
testParse(null, '4-02-93', '1993-04-02');
|
||
|
testParse(null, 'April 2nd, 1993', '1993-04-02');
|
||
|
|
||
|
testParse('DD MMM YY', '15-Jan 99', '1999-01-15');
|
||
|
testParse('DD MMM YYYY', '15-Jan 1999', '1999-01-15');
|
||
|
testParse('DD MMM', '15-Jan 1999', '1999-01-15');
|
||
|
|
||
|
testParse('MMMM Do, YYYY', 'April 2nd, 1993', '1993-04-02');
|
||
|
testParse('MMM Do YYYY', 'Apr 2nd 1993', `1993-04-02`);
|
||
|
testParse('Do MMMM YYYY', '2nd April 1993', `1993-04-02`);
|
||
|
testParse('Do MMM YYYY', '2nd Apr 1993', `1993-04-02`);
|
||
|
testParse('MMMM D, YYYY', 'April 2, 1993', '1993-04-02');
|
||
|
testParse('MMM D YYYY', 'Apr 2 1993', `1993-04-02`);
|
||
|
testParse('D MMMM YYYY', '2 April 1993', `1993-04-02`);
|
||
|
testParse('D MMM YYYY', '2 Apr 1993', `1993-04-02`);
|
||
|
testParse('MMMM Do, ', 'April 2nd, 1993', '1993-04-02');
|
||
|
testParse('MMM Do ', 'Apr 2nd 1993', `1993-04-02`);
|
||
|
testParse('Do MMMM ', '2nd April 1993', `1993-04-02`);
|
||
|
testParse('Do MMM ', '2nd Apr 1993', `1993-04-02`);
|
||
|
testParse('MMMM D, ', 'April 2, 1993', '1993-04-02');
|
||
|
testParse('MMM D ', 'Apr 2 1993', `1993-04-02`);
|
||
|
testParse('D MMMM ', '2 April 1993', `1993-04-02`);
|
||
|
testParse('D MMM ', '2 Apr 1993', `1993-04-02`);
|
||
|
testParse('MMMM Do, ', 'April 2nd', `${year}-04-02`);
|
||
|
testParse('MMM Do ', 'Apr 2nd', `${year}-04-02`);
|
||
|
testParse('Do MMMM ', '2nd April', `${year}-04-02`);
|
||
|
testParse('Do MMM ', '2nd Apr', `${year}-04-02`);
|
||
|
testParse('MMMM D, ', 'April 2', `${year}-04-02`);
|
||
|
testParse('MMM D ', 'Apr 2', `${year}-04-02`);
|
||
|
testParse('D MMMM ', '2 April', `${year}-04-02`);
|
||
|
testParse('D MMM ', '2 Apr', `${year}-04-02`);
|
||
|
|
||
|
// Test the combination of Do and YY, which was buggy at one point.
|
||
|
testParse('MMMM Do, YY', 'April 2nd, 93', '1993-04-02');
|
||
|
testParse('MMM Do, YY', 'Apr 2nd, 93', '1993-04-02');
|
||
|
testParse('Do MMMM YY', '2nd April 93', `1993-04-02`);
|
||
|
testParse('Do MMM YY', '2nd Apr 93', `1993-04-02`);
|
||
|
|
||
|
testParse(' D MMM ', ' 2 Apr ', `${year}-04-02`);
|
||
|
testParse('D MMM', ' 2 Apr ', `${year}-04-02`);
|
||
|
testParse(' D MMM ', '2 Apr', `${year}-04-02`);
|
||
|
|
||
|
testParse(null, ' 11-18-94 ', '1994-11-18');
|
||
|
testParse(' DD MM YY', '18/11/94', '1994-11-18');
|
||
|
});
|
||
|
|
||
|
it('should allow parsing common date-time formats', function() {
|
||
|
// These are the test cases from before.
|
||
|
testTimeParse('22:18:04', '22:18:04');
|
||
|
testTimeParse('8pm', '20:00:00');
|
||
|
testTimeParse('22:18:04', '22:18:04', 'UTC');
|
||
|
testTimeParse('22:18:04', '03:18:04', 'America/New_York');
|
||
|
testTimeParse('22:18:04', '06:18:04', 'America/Los_Angeles');
|
||
|
testTimeParse('22:18:04', '13:18:04', 'Japan');
|
||
|
|
||
|
// Weird time formats are no longer parsed
|
||
|
// testTimeParse('HH-mm', '1-15', '01:15:00');
|
||
|
// testTimeParse('ss mm HH', '4 23 3', '03:23:04');
|
||
|
|
||
|
// The current behavior parses any standard-like format (with HH:MM:SS components in the usual
|
||
|
// order) regardless of the format requested.
|
||
|
|
||
|
// Test a few variations of spelling AM/PM.
|
||
|
for (const [am, pm] of [['A', ' p'], [' am', 'pM'], ['AM', ' PM']]) {
|
||
|
testTimeParse('1', '01:00:00');
|
||
|
testTimeParse('1' + am, '01:00:00');
|
||
|
testTimeParse('1' + pm, '13:00:00');
|
||
|
testTimeParse('22', '22:00:00');
|
||
|
testTimeParse('22' + am, '22:00:00'); // Best guess for 22am/22pm is 22:00.
|
||
|
testTimeParse('22' + pm, '22:00:00');
|
||
|
testTimeParse('0', '00:00:00');
|
||
|
testTimeParse('0' + am, '00:00:00');
|
||
|
testTimeParse('0' + pm, '00:00:00');
|
||
|
testTimeParse('12', '12:00:00'); // 12:00 is more likely 12pm than 12am
|
||
|
testTimeParse('12' + am, '00:00:00');
|
||
|
testTimeParse('12' + pm, '12:00:00');
|
||
|
testTimeParse('9:8', '09:08:00');
|
||
|
testTimeParse('9:8' + am, '09:08:00');
|
||
|
testTimeParse('9:8' + pm, '21:08:00');
|
||
|
testTimeParse('09:08', '09:08:00');
|
||
|
testTimeParse('09:08' + am, '09:08:00');
|
||
|
testTimeParse('09:08' + pm, '21:08:00');
|
||
|
testTimeParse('21:59', '21:59:00');
|
||
|
testTimeParse('21:59' + am, '21:59:00');
|
||
|
testTimeParse('21:59' + pm, '21:59:00');
|
||
|
testTimeParse('10:18:04', '10:18:04');
|
||
|
testTimeParse('10:18:04' + am, '10:18:04');
|
||
|
testTimeParse('10:18:04' + pm, '22:18:04');
|
||
|
testTimeParse('22:18:04', '22:18:04');
|
||
|
testTimeParse('22:18:04' + am, '22:18:04');
|
||
|
testTimeParse('22:18:04' + pm, '22:18:04');
|
||
|
testTimeParse('12:18:04', '12:18:04');
|
||
|
testTimeParse('12:18:04' + am, '00:18:04');
|
||
|
testTimeParse('12:18:04' + pm, '12:18:04');
|
||
|
testTimeParse('908', '09:08:00');
|
||
|
testTimeParse('0910', '09:10:00');
|
||
|
testTimeParse('2112', '21:12:00');
|
||
|
}
|
||
|
|
||
|
// Tests with time zones.
|
||
|
testTimeParse('09:08', '09:08:00', 'UTC');
|
||
|
testTimeParse('09:08', '14:08:00', 'America/New_York');
|
||
|
testTimeParse('09:08', '00:08:00', 'Japan');
|
||
|
testTimeParse('09:08 Z', '09:08:00');
|
||
|
testTimeParse('09:08z', '09:08:00');
|
||
|
testTimeParse('09:08 UT', '09:08:00');
|
||
|
testTimeParse('09:08 UTC', '09:08:00');
|
||
|
testTimeParse('09:08-05', '14:08:00');
|
||
|
testTimeParse('09:08-5', '14:08:00');
|
||
|
testTimeParse('09:08-0500', '14:08:00');
|
||
|
testTimeParse('09:08-05:00', '14:08:00');
|
||
|
testTimeParse('09:08-500', '14:08:00');
|
||
|
testTimeParse('09:08-5:00', '14:08:00');
|
||
|
testTimeParse('09:08+05', '04:08:00');
|
||
|
testTimeParse('09:08+5', '04:08:00');
|
||
|
testTimeParse('09:08+0500', '04:08:00');
|
||
|
testTimeParse('09:08+5:00', '04:08:00');
|
||
|
testTimeParse('09:08+05:00', '04:08:00');
|
||
|
});
|
||
|
|
||
|
it('should handle timezone abbreviations', function() {
|
||
|
// New York can be abbreviated as EDT or EST depending on the time of year for daylight savings.
|
||
|
// We ignore the abbreviation so it's parsed the same whichever is used.
|
||
|
// However the parsed UTC time depends on the date.
|
||
|
testDateTimeParse('2020-02-02', '09:45 edt', '2020-02-02 14:45:00', 'America/New_York');
|
||
|
testDateTimeParse('2020-10-10', '09:45 edt', '2020-10-10 13:45:00', 'America/New_York');
|
||
|
testDateTimeParse('2020-02-02', '09:45 est', '2020-02-02 14:45:00', 'America/New_York');
|
||
|
testDateTimeParse('2020-10-10', '09:45 est', '2020-10-10 13:45:00', 'America/New_York');
|
||
|
// Spaces and case shouldn't matter.
|
||
|
testDateTimeParse('2020-10-10', '09:45 EST', '2020-10-10 13:45:00', 'America/New_York');
|
||
|
testDateTimeParse('2020-10-10', '09:45EST', '2020-10-10 13:45:00', 'America/New_York');
|
||
|
testDateTimeParse('2020-10-10', '09:45EDT', '2020-10-10 13:45:00', 'America/New_York');
|
||
|
|
||
|
// Testing that AEDT is rejected in the New York timezone even though it ends with EDT which is valid.
|
||
|
testTimeParse('09:45:00 aedt', null, 'America/New_York');
|
||
|
testTimeParse('09:45:00AEDT', null, 'America/New_York');
|
||
|
testTimeParse('09:45:00 aedt', '23:45:00', 'Australia/ACT');
|
||
|
testTimeParse('09:45:00AEDT', '23:45:00', 'Australia/ACT');
|
||
|
|
||
|
// Testing multiple abbreviations of US/Pacific
|
||
|
testDateTimeParse('2020-02-02', '09:45 PST', null, 'America/New_York');
|
||
|
testDateTimeParse('2020-02-02', '09:45 PST', '2020-02-02 17:45:00', 'US/Pacific');
|
||
|
testDateTimeParse('2020-10-10', '09:45 PST', '2020-10-10 16:45:00', 'US/Pacific');
|
||
|
testDateTimeParse('2020-02-02', '09:45 PDT', '2020-02-02 17:45:00', 'US/Pacific');
|
||
|
testDateTimeParse('2020-10-10', '09:45 PDT', '2020-10-10 16:45:00', 'US/Pacific');
|
||
|
// PWT and PPT are some obscure abbreviations apparently used at some time and thus supported by moment
|
||
|
testDateTimeParse('2020-10-10', '09:45 PWT', '2020-10-10 16:45:00', 'US/Pacific');
|
||
|
testDateTimeParse('2020-10-10', '09:45 PPT', '2020-10-10 16:45:00', 'US/Pacific');
|
||
|
// POT is not valid
|
||
|
testDateTimeParse('2020-10-10', '09:45 POT', null, 'US/Pacific');
|
||
|
|
||
|
// Both these timezones have CST and CDT, but not COT.
|
||
|
// The timezones are far apart so the parsed UTC times are too.
|
||
|
testTimeParse('09:45 CST', '01:45:00', 'Asia/Shanghai');
|
||
|
testTimeParse('09:45 CDT', '01:45:00', 'Asia/Shanghai');
|
||
|
testTimeParse('09:45 CST', '15:45:00', 'Canada/Central');
|
||
|
testTimeParse('09:45 CDT', '15:45:00', 'Canada/Central');
|
||
|
testTimeParse('09:45 COT', null, 'Asia/Shanghai');
|
||
|
testTimeParse('09:45 COT', null, 'Canada/Central');
|
||
|
});
|
||
|
|
||
|
it('should parse datetime strings', function() {
|
||
|
for (const separator of [' ', 'T']) {
|
||
|
for (let tz of ['Z', 'UTC', '+00:00', '-00', '']) {
|
||
|
for (const tzSeparator of ['', ' ']) {
|
||
|
tz = tzSeparator + tz;
|
||
|
|
||
|
let expected = '2020-03-04 12:34:56';
|
||
|
testDateTimeStringParse(
|
||
|
` 2020-03-04${separator}12:34:56${tz} `, expected, 'YYYY-MM-DD'
|
||
|
);
|
||
|
testDateTimeStringParse(
|
||
|
` 03-04-2020${separator}12:34:56${tz} `, expected, 'MM/DD/YYYY'
|
||
|
);
|
||
|
testDateTimeStringParse(
|
||
|
` 04-03-20${separator}12:34:56${tz} `, expected, 'DD-MM-YY'
|
||
|
);
|
||
|
testDateTimeStringParse(
|
||
|
` 2020-03-04${separator}12:34:56${tz} `, expected, '',
|
||
|
);
|
||
|
expected = '2020-03-04 12:34:00';
|
||
|
testDateTimeStringParse(
|
||
|
` 04-03-20${separator}12:34${tz} `, expected, 'DD-MM-YY'
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
it('should handle datetimes as formatted by moment', function() {
|
||
|
for (const date of ['2020-02-03', '2020-06-07', '2020-10-11']) { // different months for daylight savings
|
||
|
const dateTime = date + ' 12:34:56';
|
||
|
const utcMoment = moment.tz(dateTime, 'UTC');
|
||
|
for (const dateFormat of ['DD/MM/YY', 'MM/DD/YY']) {
|
||
|
for (const tzFormat of ['z', 'Z']) { // abbreviation (z) vs +/-HH:MM (Z)
|
||
|
assert.isTrue(utcMoment.isValid());
|
||
|
for (const tzName of moment.tz.names()) {
|
||
|
const tzMoment = moment.tz(utcMoment, tzName);
|
||
|
const formattedTime = tzMoment.format('HH:mm:ss ' + tzFormat);
|
||
|
const formattedDate = tzMoment.format(dateFormat);
|
||
|
testDateTimeParse(formattedDate, formattedTime, dateTime, tzName, dateFormat);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
it('should be flexible in parsing the preferred format', function() {
|
||
|
for (const format of ['DD-MM-YYYY', 'DD-MM-YY', 'DD-MMM-YYYY', 'DD-MMM-YY']) {
|
||
|
testParse(format, '1/2/21', '2021-02-01');
|
||
|
testParse(format, '01/02/2021', '2021-02-01');
|
||
|
testParse(format, '1-02-21', '2021-02-01');
|
||
|
}
|
||
|
|
||
|
for (const format of ['MM-DD-YYYY', 'MM-DD-YY', 'MMM-DD-YYYY', 'MMM-DD-YY']) {
|
||
|
testParse(format, '1/2/21', '2021-01-02');
|
||
|
testParse(format, '01/02/2021', '2021-01-02');
|
||
|
testParse(format, '1-02-21', '2021-01-02');
|
||
|
}
|
||
|
|
||
|
for (const format of ['YY-MM-DD', 'YYYY-MM-DD', 'YY-MMM-DD', 'YYYY-MMM-DD']) {
|
||
|
testParse(format, '01/2/3', '2001-02-03');
|
||
|
testParse(format, '2001/02/03', '2001-02-03');
|
||
|
testParse(format, '01-02-03', '2001-02-03');
|
||
|
testParse(format, '10/11', `${year}-10-11`);
|
||
|
testParse(format, '2/3', `${year}-02-03`);
|
||
|
testParse(format, '12', `${year}-${month}-12`);
|
||
|
}
|
||
|
|
||
|
testParse('DD MMM YYYY', '1 FEB 2021', '2021-02-01');
|
||
|
testParse('DD MMM YYYY', '1-feb-21', '2021-02-01');
|
||
|
testParse('DD MMM YYYY', '1/2/21', '2021-02-01');
|
||
|
testParse('DD MMM YYYY', '01/02/2021', '2021-02-01');
|
||
|
testParse('DD MMM YYYY', '1-02-21', '2021-02-01');
|
||
|
testParse('DD MMM YYYY', '1 2', `${year}-02-01`);
|
||
|
testParse('DD MMM YYYY', '1 feb', `${year}-02-01`);
|
||
|
|
||
|
testParse('DD MMM', '1 FEB 2021', '2021-02-01');
|
||
|
testParse('DD MMM', '1-feb-2021', '2021-02-01');
|
||
|
testParse('DD MMM', '1/2/2021', '2021-02-01');
|
||
|
testParse('DD MMM', '01/02/2021', '2021-02-01');
|
||
|
testParse('DD MMM', '1-02-2021', '2021-02-01');
|
||
|
testParse('DD MMM', '1 2 2021', `2021-02-01`);
|
||
|
testParse('DD MMM', '1 feb 2021', `2021-02-01`);
|
||
|
});
|
||
|
|
||
|
it('should support underscores as separators', async function() {
|
||
|
testParse('DD_MM_YY', '3/4', `${year}-04-03`);
|
||
|
testParse('DD_MM_YY', '3_4', `${year}-04-03`);
|
||
|
testParse('DD_MM_YY', '3_4_98', `1998-04-03`);
|
||
|
testParse('DD/MM/YY', '3_4_98', `1998-04-03`);
|
||
|
});
|
||
|
|
||
|
it('should interpret two-digit years as bootstrap datepicker does', function() {
|
||
|
const yy = year % 100;
|
||
|
// These checks are expected to work as long as today's year is between 2021 and 2088.
|
||
|
testParse('MM-DD-YY', `1/2/${yy}`, `20${yy}-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/${yy + 9}`, `20${yy + 9}-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/${yy + 11}`, `19${yy + 11}-01-02`);
|
||
|
// These should work until 2045 (after that 55 would be interpreted as 2055).
|
||
|
testParse('MM-DD-YY', `1/2/00`, `2000-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/08`, `2008-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/20`, `2020-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/30`, `2030-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/55`, `1955-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/79`, `1979-01-02`);
|
||
|
testParse('MM-DD-YY', `1/2/98`, `1998-01-02`);
|
||
|
});
|
||
|
|
||
|
describe('guessDateFormat', function() {
|
||
|
it('should guess date formats', function() {
|
||
|
// guessDateFormats with an *s* shows all the equally likely guesses.
|
||
|
// It's only directly used in tests, just to reveal the inner workings.
|
||
|
// guessDateFormat picks one of those formats which is actually used in type conversion etc.
|
||
|
|
||
|
// ISO YYYY-MM-DD is king
|
||
|
assert.deepEqual(guessDateFormats(["2020-01-02"]), ["YYYY-MM-DD"]);
|
||
|
assert.deepEqual(guessDateFormat(["2020-01-02"]), "YYYY-MM-DD");
|
||
|
|
||
|
// Some ambiguous dates
|
||
|
assert.deepEqual(guessDateFormats(["01/01/2020"]), ["DD/MM/YYYY", "MM/DD/YYYY"]);
|
||
|
assert.deepEqual(guessDateFormats(["01/02/03"]), ['DD/MM/YY', 'MM/DD/YY', 'YY/MM/DD']);
|
||
|
assert.deepEqual(guessDateFormats(["01-01-2020"]), ["DD-MM-YYYY", "MM-DD-YYYY"]);
|
||
|
assert.deepEqual(guessDateFormats(["01-02-03"]), ['DD-MM-YY', 'MM-DD-YY', 'YY-MM-DD']);
|
||
|
assert.deepEqual(guessDateFormat(["01/01/2020"]), "MM/DD/YYYY");
|
||
|
assert.deepEqual(guessDateFormat(["01/02/03"]), 'YY/MM/DD');
|
||
|
assert.deepEqual(guessDateFormat(["01-01-2020"]), "MM-DD-YYYY");
|
||
|
assert.deepEqual(guessDateFormat(["01-02-03"]), 'YY-MM-DD');
|
||
|
|
||
|
// Ambiguous date with only two parts
|
||
|
assert.deepEqual(guessDateFormats(["01/02"]), ["DD/MM", "MM/DD", "YY/MM"]);
|
||
|
assert.deepEqual(guessDateFormat(["01/02"]), "YY/MM");
|
||
|
|
||
|
// First date is ambiguous, second date makes the guess unambiguous.
|
||
|
assert.deepEqual(guessDateFormats(["01/01/2020", "20/01/2020"]), ["DD/MM/YYYY"]);
|
||
|
assert.deepEqual(guessDateFormats(["01/01/2020", "01/20/2020"]), ["MM/DD/YYYY"]);
|
||
|
assert.deepEqual(guessDateFormat(["01/01/2020", "20/01/2020"]), "DD/MM/YYYY");
|
||
|
assert.deepEqual(guessDateFormat(["01/01/2020", "01/20/2020"]), "MM/DD/YYYY");
|
||
|
|
||
|
// Not a date at all, guess YYYY-MM-DD as the default.
|
||
|
assert.deepEqual(guessDateFormats(["foo bar"]), null);
|
||
|
assert.deepEqual(guessDateFormat(["foo bar"]), "YYYY-MM-DD");
|
||
|
});
|
||
|
});
|
||
|
});
|