@villedemontreal/general-utils
Version:
General utilities library
235 lines • 8.6 kB
JavaScript
;
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 moment = require("moment");
const _1 = require(".");
const lodash_1 = require("lodash");
function isDateEqual(value, expectedDate) {
const _moment = moment(value);
const expectedMoment = moment(expectedDate);
return _moment.isSame(expectedMoment);
}
/** @see https://momentjs.com/docs/#/query/is-between/ */
function isDateBetween(value, expectedDate, inclusivity = '[]') {
const _moment = moment(value);
const from = expectedDate[0];
const to = expectedDate[1];
if (from === null || from === undefined) {
if (inclusivity[1] === ')') {
return _moment.isBefore(to);
}
return !_moment.isAfter(to);
}
if (to === null || to === undefined) {
if (inclusivity[0] === '(') {
return _moment.isAfter(from);
}
return !_moment.isBefore(from);
}
return _moment.isBetween(from, to, null, inclusivity);
}
/**
* 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.
* - `Moment` 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 = moment.utc(dateDefinition).toDate();
}
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) {
let compatible = false;
if (expectedDate instanceof Array) {
compatible = isDateBetween(value, expectedDate);
}
else {
compatible = isDateEqual(value, expectedDate);
}
return compatible;
}
function getDateRangeAround(value, marginValue, marginUnit) {
const _moment = moment(value);
return [_moment.subtract(marginValue, marginUnit), _moment.add(marginValue, marginUnit)];
}
/**
* 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 Moment.js & others.
* Note: It produces ISO-compatible dates, and which also works well with T-SQL.
* @see `#parseDate`
* @see `#formatDate`
* @see https://momentjs.com/docs/#/displaying/format/
*/
exports.DEFAULT_DATE_FORMAT = 'YYYY-MM-DDTHH: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];
return moment.utc(representation, formats).toDate();
}
/**
* Formats the given date using the provided format (or the default ISO format).
*
* @see `#parseDate`
*/
function formatDate(date, format = exports.DEFAULT_DATE_FORMAT) {
return (moment(date)
.format(format)
// 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) {
return formatDate(moment(date).utc(), 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