UNPKG

@villedemontreal/general-utils

Version:
315 lines 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_DATE_FORMAT = exports.ISO_DATE_PATTERN = void 0; exports.isDateEqual = isDateEqual; exports.isDateBetween = isDateBetween; exports.isDateRange = isDateRange; exports.getSafeDate = getSafeDate; exports.getSafeDateRange = getSafeDateRange; exports.isDateCompatible = isDateCompatible; exports.getDateRangeAround = getDateRangeAround; exports.isValidIso8601Date = isValidIso8601Date; exports.parseDate = parseDate; exports.formatDate = formatDate; exports.formatUtcDate = formatUtcDate; exports.startOfDay = startOfDay; exports.endOfDay = endOfDay; const luxon_1 = require("luxon"); const _1 = require("."); const lodash_1 = require("lodash"); /** * Internal helper to convert a DateDefinition to a Luxon DateTime. */ function toDateTime(value) { if (value instanceof luxon_1.DateTime) { return value; } if ((0, lodash_1.isDate)(value)) { return luxon_1.DateTime.fromJSDate(value); } if (typeof value === 'string') { const dt = luxon_1.DateTime.fromISO(value); if (dt.isValid) { return dt; } // Fallback for some non-standard ISO formats that moment handled return luxon_1.DateTime.fromFormat(value, 'yyyy/MM/dd'); } return luxon_1.DateTime.invalid('Unsupported date definition'); } /** * Internal helper to convert a DateDefinition to a UTC Luxon DateTime. */ function toUtcDateTime(value, formats) { if (value instanceof luxon_1.DateTime) { return value.toUTC(); } if ((0, lodash_1.isDate)(value)) { return luxon_1.DateTime.fromJSDate(value, { zone: 'utc' }); } if (typeof value === 'string') { if (formats) { const formatList = Array.isArray(formats) ? formats : [formats]; for (const format of formatList) { // Map moment format to luxon format if needed, but here we assume luxon compatible formats are passed or we'll handle them in parseDate const dt = luxon_1.DateTime.fromFormat(value, format, { zone: 'utc' }); if (dt.isValid) { return dt; } } } const dt = luxon_1.DateTime.fromISO(value, { zone: 'utc' }); if (dt.isValid) { return dt; } } return luxon_1.DateTime.invalid('Unsupported date definition'); } function isDateEqual(value, expectedDate) { const dt = toDateTime(value); const expectedDt = toDateTime(expectedDate); return dt.toMillis() === expectedDt.toMillis(); } /** @see https://moment.github.io/luxon/#/ */ function isDateBetween(value, expectedDate, inclusivity = '[]') { const dt = toDateTime(value); const from = expectedDate[0]; const to = expectedDate[1]; const fromDt = from !== null && from !== undefined ? toDateTime(from) : null; const toDt = to !== null && to !== undefined ? toDateTime(to) : null; if (!fromDt) { if (!toDt) { return true; } if (inclusivity[1] === ')') { return dt.toMillis() < toDt.toMillis(); } return dt.toMillis() <= toDt.toMillis(); } if (!toDt) { if (inclusivity[0] === '(') { return dt.toMillis() > fromDt.toMillis(); } return dt.toMillis() >= fromDt.toMillis(); } const leftInclusive = inclusivity[0] === '['; const rightInclusive = inclusivity[1] === ']'; const afterFrom = leftInclusive ? dt.toMillis() >= fromDt.toMillis() : dt.toMillis() > fromDt.toMillis(); const beforeTo = rightInclusive ? dt.toMillis() <= toDt.toMillis() : dt.toMillis() < toDt.toMillis(); return afterFrom && beforeTo; } /** * Tells whether the provided value is a date range. * * Valid date ranges are either: * - `[null, null]`: Open date range. * - `[Date, null]`: Date range with low boundary, without high boundary. * - `[null, Date]`: Date range without low boundary, with high boundary. * - `[Date, Date]`: Date range with both low and high boundaries. */ function isDateRange(value) { let result = !!value && value.length === 2; if (result) { let dateItemCount = 0; let otherItemCount = 0; for (const item of value) { if (_1.utils.isValidDate(item)) { dateItemCount++; } else if (item !== undefined && item !== null) { otherItemCount++; } } result = dateItemCount > 0 && otherItemCount < 1; } return result; } /** * Returns a "safe" date from the given definition. * * - `String` values are not considered "safe" since they can contain anything, including invalid dates. * - `DateTime` values are not considered "safe" since they tolerate exceptions and advanced * features that `Date` doesn't support. */ function getSafeDate(dateDefinition) { let result; if (dateDefinition !== undefined && dateDefinition !== null) { result = toUtcDateTime(dateDefinition).toJSDate(); } if (!result || !_1.utils.isValidDate(result)) { throw new Error(`Unsupported date definition! ${(0, _1.getValueDescription)(dateDefinition)}`); } return result; } /** * Returns a "safe" date range from the given definition. * * @see `#getSafeDate` */ function getSafeDateRange(dateRangeDefinition) { const lowBoundary = dateRangeDefinition[0] ? getSafeDate(dateRangeDefinition[0]) : dateRangeDefinition[0]; const highBoundary = dateRangeDefinition[1] ? getSafeDate(dateRangeDefinition[1]) : dateRangeDefinition[1]; return [lowBoundary, highBoundary]; } /** * Tells whether the provided date is compatible with the specified date definition. * * Possible cases: * - `value`: `Date` & `expectedDate`: `Date` → whether `value` = `expectedDate`. * - `value`: `Date` & `expectedDate`: `DateRange` → whether `value` is within `expectedDate`. */ function isDateCompatible(value, expectedDate) { if (expectedDate instanceof Array) { return isDateBetween(value, expectedDate); } else { return isDateEqual(value, expectedDate); } } const TIME_UNIT_MAPPING = { ms: 'milliseconds', s: 'seconds', m: 'minutes', h: 'hours', d: 'days', w: 'weeks', }; function getDateRangeAround(value, marginValue, marginUnit) { const dt = toDateTime(value); const unit = TIME_UNIT_MAPPING[marginUnit]; return [dt.minus({ [unit]: marginValue }), dt.plus({ [unit]: marginValue })]; } /** * Pattern matching most ISO 8601 date representations (including time), and which can be used for any kind of validation. * @example `2018-07-31T12:34:56.789+10:11` */ exports.ISO_DATE_PATTERN = /^(\d{4}(-?)(?:0\d|1[0-2])\2?(?:[0-2]\d|3[0-1]))(?:[T ]([0-2][0-3](:?)[0-5]\d\4[0-5]\d)(?:[.,](\d{3}))?([+-](?:[01]\d(?::?[0-5]\d)?)|Z)?)?$/; /** * Tells whether the provided date representation is valid as per ISO 8601. * * @see `ISO_DATE_PATTERN` */ function isValidIso8601Date(representation) { let valid = false; if (representation !== undefined && representation !== null) { valid = exports.ISO_DATE_PATTERN.test(representation); } return valid; } /** * Format used to represent dates, and which is compatible with Luxon. * Note: It produces ISO-compatible dates, and which also works well with T-SQL. * @see `#parseDate` * @see `#formatDate` * @see https://moment.github.io/luxon/#/formatting?id=table-of-tokens */ exports.DEFAULT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; /** * Parses the given date representation using the provided format (or the default ISO format). * @see `#formatDate` */ function parseDate(representation, format = exports.DEFAULT_DATE_FORMAT) { const formats = format instanceof Array ? format : [format]; // Map moment tokens to luxon tokens if they look like moment formats const mappedFormats = formats.map((f) => f .replace(/Y/g, 'y') .replace(/D/g, 'd') .replace(/A/g, 'a') .replace(/(^|[^Z])Z($|[^Z])/, '$1ZZ$2')); return toUtcDateTime(representation, mappedFormats).toJSDate(); } /** * Formats the given date using the provided format (or the default ISO format). * * @see `#parseDate` */ function formatDate(date, format = exports.DEFAULT_DATE_FORMAT) { // Map moment tokens to luxon tokens if they look like moment formats const mappedFormat = format .replace(/Y/g, 'y') .replace(/D/g, 'd') .replace(/A/g, 'a') .replace(/(^|[^Z])Z($|[^Z])/, '$1ZZ$2'); return (toDateTime(date) .toFormat(mappedFormat) // Ensure that 'Z' is used instead of '+00:00' at the end of UTC dates: .replace(/[+-]00:?00$/, 'Z')); } /** * Formats the UTC version of the given date using the provided format (or the default ISO format). * * @see `#formatDate` */ function formatUtcDate(date, format = exports.DEFAULT_DATE_FORMAT) { const dt = toDateTime(date).toUTC(); return formatDate(dt, format); } /** * Return the specified date at its very beginning * "00:00:00.000". * * IMPORTANT: this function is very timezone sensitive! * If you want to start of day in another timezone than * 'America/Montreal', you *need* to specify it. * Here's why: * * Let say the specofoed ISO date is "2017-11-02T02:07:11.123Z". * If we are located in a UTC timezone, the end of day for * this date is "2017-11-02T23:59:59", it would be the same day * as the one displayed in the ISO string. * But if we are in Montreal, and the current timezone offset is "-4", * then the ISO date actually referes to the "2017-11-01" day and * start of day would need to be "2017-11-01T23:59:59", not * "2017-11-02T23:59:59"! * * By default, the handling of dates, by Node itself or by various * third-party, all use the timezone of the server to make calculations * such as "start of day". This is error-prone as the result depends * on how the server is configured. This is why a timezone must be * specified here. */ function startOfDay(isoDate, timezone = 'America/Montreal') { if ((0, lodash_1.isNil)(isoDate)) { return isoDate; } let luxonDate = luxon_1.DateTime.fromISO((0, lodash_1.isDate)(isoDate) ? isoDate.toISOString() : isoDate); if (!luxonDate.isValid) { throw new Error(`Invalid ISO date ${JSON.stringify(isoDate)} : ${luxonDate.invalidReason}`); } luxonDate = luxonDate.setZone(timezone); const date = luxonDate.startOf('day').toJSDate(); return date; } /** * Return the specified date at its last milliseconds: * "23:59:59.999". * * IMPORTANT: this function is very timezone sensitive! * If you want to start of day in another timezone than * 'America/Montreal', you *need* to specify it. * * Please read the comments of the `startOfDay` for more * information. * */ function endOfDay(isoDate, timezone = 'America/Montreal') { if ((0, lodash_1.isNil)(isoDate)) { return isoDate; } let luxonDate = luxon_1.DateTime.fromISO((0, lodash_1.isDate)(isoDate) ? isoDate.toISOString() : isoDate); if (!luxonDate.isValid) { throw new Error(`Invalid ISO date ${JSON.stringify(isoDate)} : ${luxonDate.invalidReason}`); } luxonDate = luxonDate.setZone(timezone); const date = luxonDate.endOf('day').toJSDate(); return date; } //# sourceMappingURL=dateUtils.js.map