@villedemontreal/general-utils
Version:
General utilities library
384 lines (345 loc) • 14.3 kB
text/typescript
import { assert } from 'chai';
import * as _ from 'lodash';
import { DateTime } from 'luxon';
import { getValueDescription, getValueDescriptionWithType, utils } from '.';
import { getCartesianProduct } from './collectionUtils';
import {
DateRangeDefinition,
DEFAULT_DATE_FORMAT,
endOfDay,
formatUtcDate,
getSafeDate,
getSafeDateRange,
isDateBetween,
isDateCompatible,
isDateRange,
ISO_DATE_PATTERN,
isValidIso8601Date,
parseDate,
startOfDay,
} from './dateUtils';
const VALID_DATE_UTC_REPRESENTATION = '2018-07-31T12:34:56.789Z';
const VALID_DATE = new Date(VALID_DATE_UTC_REPRESENTATION);
describe('Date Utility', () => {
describe('#isDateBetween', () => {
it('should support open ranges', () => {
assert.isTrue(isDateBetween('2018-01-01T12:34:56', ['2018-01-01T12:34:56', undefined]));
assert.isTrue(isDateBetween('2018-01-01T12:34:56', ['2018-01-01T12:34:56', null]));
assert.isFalse(isDateBetween('2018-01-01T12:34:56', ['2018-01-01T12:34:56.123', undefined]));
assert.isFalse(isDateBetween('2018-01-01T12:34:56', ['2018-01-01T12:34:56.123', null]));
assert.isTrue(isDateBetween('2018-01-01T12:34:56', [undefined, '2018-01-01T12:34:56']));
assert.isTrue(isDateBetween('2018-01-01T12:34:56', [null, '2018-01-01T12:34:56']));
assert.isFalse(isDateBetween('2018-01-01T12:34:56', [undefined, '2018-01-01T12:34:55.999']));
assert.isFalse(isDateBetween('2018-01-01T12:34:56', [null, '2018-01-01T12:34:55.999']));
assert.isTrue(isDateBetween('2018-01-01T12:34:56', [undefined, undefined]));
assert.isTrue(isDateBetween('2018-01-01T12:34:56', [null, null]));
});
});
describe('#isDateCompatible', () => {
const dateRepr = '2018-09-12T21:45:12.243Z';
const dateValues = [dateRepr, new Date(dateRepr), DateTime.fromISO(dateRepr)];
getCartesianProduct(dateValues, dateValues).forEach((dateParameters) => {
const parameter1 = dateParameters[0];
const parameter2 = dateParameters[1];
const parameter1TypeName = parameter1.constructor.name;
const parameter2TypeName = parameter2.constructor.name;
it(`should support \`${parameter1TypeName}\` & \`${parameter2TypeName}\` parameters`, () => {
assert.isTrue(isDateCompatible(parameter1, parameter2));
});
});
const date1Repr = '2018-09-12T12:34:56.789Z';
const date1Values = [date1Repr, new Date(date1Repr), DateTime.fromISO(date1Repr)];
const date2Repr = '2018-09-12T21:45:12.243Z';
const date2Values = [date2Repr, new Date(date2Repr), DateTime.fromISO(date2Repr)];
getCartesianProduct(date1Values, date2Values).forEach((dateRangeParameter) => {
const dateRangeLowBoundary = dateRangeParameter[0];
const dateRangeHighBoundary = dateRangeParameter[1];
const dateRangeLowBoundaryTypeName = dateRangeLowBoundary.constructor.name;
const dateRangeHighBoundaryTypeName = dateRangeHighBoundary.constructor.name;
dateValues.forEach((dateValue) => {
const dateValueParameterType = dateValue.constructor.name;
it(`should support \`${dateValueParameterType}\` & [\`${dateRangeLowBoundaryTypeName}\`:\`${dateRangeHighBoundaryTypeName}\`] parameters`, () => {
assert.isTrue(isDateCompatible(dateValue, dateRangeParameter));
});
});
});
});
const INVALID_DATE_VALUES = [
undefined,
null,
true,
false,
123,
NaN,
'pouet',
_.noop,
/^$/,
{},
[],
];
const VALID_DATE_VALUES = [
new Date(2018, 7, 31, 23, 59, 59, 999),
new Date(2019, 0, 1, 0, 0, 0, 0),
];
describe('#isDate', () => {
INVALID_DATE_VALUES.forEach((value) => {
const valueDescription = getValueDescriptionWithType(value);
it(`should return \`false\` for ${valueDescription}`, () => {
assert.isFalse(utils.isValidDate(value));
});
});
VALID_DATE_VALUES.forEach((value) => {
const valueDescription = getValueDescription(value);
it(`should return \`true\` for ${valueDescription}`, () => {
assert.isTrue(utils.isValidDate(value));
});
});
});
const INVALID_DATE_RANGE_VALUES = getCartesianProduct(INVALID_DATE_VALUES, INVALID_DATE_VALUES);
const VALID_DATE_RANGE_VALUES = getCartesianProduct(VALID_DATE_VALUES, VALID_DATE_VALUES);
// Append open ranges in valid date range values:
VALID_DATE_VALUES.forEach((date) => {
VALID_DATE_RANGE_VALUES.push([date, null]);
VALID_DATE_RANGE_VALUES.push([null, date]);
});
// Append 3-value items into the invalid date range values:
VALID_DATE_RANGE_VALUES.forEach((dateRange) =>
INVALID_DATE_RANGE_VALUES.push(dateRange.concat(null)),
);
describe('#isDateRange', () => {
INVALID_DATE_RANGE_VALUES.forEach((value: any[]) => {
const valueDescription = value.map((item) => getValueDescriptionWithType(item));
it(`should return \`false\` for [${valueDescription}]`, () => {
assert.isFalse(isDateRange(value));
});
});
VALID_DATE_RANGE_VALUES.forEach((value) => {
const valueDescription = getValueDescription(value);
it(`should return \`true\` for ${valueDescription}`, () => {
assert.isTrue(isDateRange(value));
});
});
});
describe('#getSafeDate', () => {
// Cf. http://momentjs.com/docs/#/parsing/string/
const expectations = {
'2018-12-07T12:34:56.789': new Date(Date.UTC(2018, 11, 7, 12, 34, 56, 789)),
'20181207T123456.789': new Date(Date.UTC(2018, 11, 7, 12, 34, 56, 789)),
'2018-12-07': new Date(Date.UTC(2018, 11, 7, 0, 0, 0, 0)),
};
_.forEach(expectations, (expectedResult, value) => {
const valueDescription = getValueDescription(value);
it(`should support ${valueDescription}`, () => {
assert.deepEqual(getSafeDate(value), expectedResult);
});
});
const expectedlyFailingValues = [null, undefined, 'true', 'false', '???'];
expectedlyFailingValues.forEach((value) => {
const valueDescription = getValueDescription(value);
it(`should fail with ${valueDescription}`, () => {
let failed = false;
try {
getSafeDate(value);
} catch {
failed = true;
}
assert.isTrue(failed);
});
});
});
describe('#getSafeDateRange', () => {
const expectations = new Map<DateRangeDefinition, [Date, Date]>();
expectations.set([VALID_DATE_UTC_REPRESENTATION, undefined], [VALID_DATE, undefined]);
expectations.set([null, VALID_DATE_UTC_REPRESENTATION], [null, VALID_DATE]);
expectations.set([undefined, null], [undefined, null]);
expectations.set([VALID_DATE_UTC_REPRESENTATION, VALID_DATE], [VALID_DATE, VALID_DATE]);
expectations.set([VALID_DATE, VALID_DATE], [VALID_DATE, VALID_DATE]);
for (const entry of expectations) {
const value = entry[0];
const expectedResult = entry[1];
const valueDescription = `[${getValueDescriptionWithType(
value[0],
)}, ${getValueDescriptionWithType(value[1])}]`;
it(`should support ${valueDescription}`, () => {
assert.deepEqual(getSafeDateRange(value), expectedResult);
});
}
});
describe('#parseDate', () => {
it('should parse date representations properly', () => {
assert.deepStrictEqual(parseDate(VALID_DATE_UTC_REPRESENTATION), VALID_DATE);
assert.deepStrictEqual(
parseDate(VALID_DATE_UTC_REPRESENTATION, DEFAULT_DATE_FORMAT),
VALID_DATE,
);
const FUNKY_FORMAT = 'YYYY/MM/DD@HH:mm:ss.SSS';
let result: Date = parseDate('2018/07/31@12:34:56.789', [FUNKY_FORMAT]);
assert.deepStrictEqual(result.getDate(), VALID_DATE.getDate());
result = parseDate('2018/07/31@12:34:56.789', [DEFAULT_DATE_FORMAT, FUNKY_FORMAT]);
assert.deepStrictEqual(result.getDate(), VALID_DATE.getDate());
});
});
describe('#formatDate', () => {
it('should format dates properly', () => {
assert.equal(formatUtcDate(VALID_DATE), VALID_DATE_UTC_REPRESENTATION);
});
});
describe('ISO_DATE_PATTERN', () => {
const expectations = {
'2018-07-31': ['2018-07-31', '-', undefined, undefined, undefined, undefined],
'20180731': ['20180731', '', undefined, undefined, undefined, undefined],
'2018-07-31 00:00:59': ['2018-07-31', '-', '00:00:59', ':', undefined, undefined],
'2018-07-31T00:59:59': ['2018-07-31', '-', '00:59:59', ':', undefined, undefined],
'2018-07-31 23:59:59': ['2018-07-31', '-', '23:59:59', ':', undefined, undefined],
'2018-07-31T12:34:56.789+06': ['2018-07-31', '-', '12:34:56', ':', '789', '+06'],
'2018-07-31T12:34:56.789+10:11': ['2018-07-31', '-', '12:34:56', ':', '789', '+10:11'],
'20180731T123456,789+1011': ['20180731', '', '123456', '', '789', '+1011'],
'20180731 123456,789-1011': ['20180731', '', '123456', '', '789', '-1011'],
'20180731T123456,789+10:11': ['20180731', '', '123456', '', '789', '+10:11'],
'20180731 12:34:56,789-1011': ['20180731', '', '12:34:56', ':', '789', '-1011'],
'2018-07-31 12:34:56.789Z': ['2018-07-31', '-', '12:34:56', ':', '789', 'Z'],
};
_.forEach(expectations, (expectation, value) => {
it(`should match « ${value} »`, () => {
assert.deepEqual(ISO_DATE_PATTERN.exec(value).slice(1), expectation);
assert.isTrue(isValidIso8601Date(value));
});
});
const invalidDateValues = [
'201807-31', // mismatching date separator
'2018/07/31', // bad date separator
'2018-07-31T1234:56', // mismatching time separator
'20180731123456', // missing date-time separator
'2018-13-31', // month overflow
'2018-07-32', // day overflow
'2018-07-31 24:00:00', // hour overflow
'2018-07-31 00:60:00', // minute overflow
'2018-07-31 00:00:60', // second overflow
'2018-07-31 12', // incomplete time representation
'2018-07-31 12:34', // incomplete time representation
'2018-07-31T12:34:56789', // bad second format
'20180731 123456 789', // bad split second separator
'2018-07-31 12:34:56.', // bad split second format
'2018-07-31 12:34:56.7', // bad split second format
'2018-07-31 12:34:56.78', // bad split second format
'2018-07-31 01:23:45.6789', // bad split second format
'20180731T123456.7891011', // missing time-offset separator
'2018-07-31 01:23:45.678+1', // bad offset format
'2018-07-31 12:34:56.789+1011Z', // bad offset format
'2018-07-31 01:23:45.678+2400', // offset overflow
];
invalidDateValues.forEach((value) => {
it(`should *NOT* match « ${value} »`, () => {
assert.isNull(ISO_DATE_PATTERN.exec(value));
assert.isFalse(isValidIso8601Date(value));
});
});
});
describe('#endOfDay', () => {
it('Nil', () => {
let result = endOfDay(null);
assert.deepEqual(result, null);
result = endOfDay(undefined);
assert.deepEqual(result, undefined);
});
it('Invalid', () => {
let error;
try {
endOfDay(``);
} catch (err) {
error = err;
}
if (!error) {
assert.fail();
}
try {
endOfDay(`nope`);
} catch (err) {
error = err;
}
if (!error) {
assert.fail();
}
});
it('As a date - UTC', () => {
const testDate = new Date(`2017-11-02T02:07:11.123Z`);
const result = endOfDay(testDate, 'UTC');
assert.deepEqual(result.toISOString(), `2017-11-02T23:59:59.999Z`);
});
// ==========================================
// This test shows how timezone sensitive is the
// `endOfDay()` function!
// The specified date is "2017-11-02T02:07:11" but
// *UTC*. If we are in a timezone with a current offset.
// like "-4" (Montreal is -4 or -5, depending of if
// we are Daylight Time or not), then the local
// date is actually "2017-11-01T22:07:11"...
//
// When calling `endOfDay()` we need to tell it in
// which timezone we want the computation to be made
// otherwise it will use the timezone local to the
// server. If the server runs on a "UTC" timezone,
// the end of the day will be "2017-11-02T23:59:59".
// But on a "UTC-4" timezone, the end of the day would
// actually be "2017-11-01T23:59:59"!
//
// Because of this, we can't have reliable tests
// using "America/Monteal" as the timezone, because
// this timezone can be either "UTC-4" or "UTC-5".
// Tests using such timezone may succeed one day
// and fail the other!
// ==========================================
it('As a date - UTC-4', () => {
const testDate = new Date(`2017-11-02T02:07:11.123Z`);
const result = endOfDay(testDate, 'UTC-4');
assert.deepEqual(result.toISOString(), `2017-11-02T03:59:59.999Z`);
});
it('As a string', () => {
const date = `2017-11-02T02:07:11.123Z`;
const result = endOfDay(date, 'UTC');
assert.deepEqual(result.toISOString(), `2017-11-02T23:59:59.999Z`);
});
});
describe('#startOfDay', () => {
it('Nil', () => {
let result = startOfDay(null);
assert.deepEqual(result, null);
result = startOfDay(undefined);
assert.deepEqual(result, undefined);
});
it('Invalid', () => {
let error;
try {
startOfDay(``);
} catch (err) {
error = err;
}
if (!error) {
assert.fail();
}
try {
startOfDay(`nope`);
} catch (err) {
error = err;
}
if (!error) {
assert.fail();
}
});
it('As a date - UTC', () => {
const date = new Date(`2017-11-02T02:07:11.123Z`);
const result = startOfDay(date, 'UTC');
assert.deepEqual(result.toISOString(), `2017-11-02T00:00:00.000Z`);
});
it('As a date - UTC-4', () => {
const testDate = new Date(`2017-11-02T02:07:11.123Z`);
const result = startOfDay(testDate, 'UTC-4');
assert.deepEqual(result.toISOString(), `2017-11-01T04:00:00.000Z`);
});
it('As a string', () => {
const date = `2017-11-02T02:07:11.123Z`;
const result = startOfDay(date, 'UTC');
assert.deepEqual(result.toISOString(), `2017-11-02T00:00:00.000Z`);
});
});
});