UNPKG

xtutils

Version:

Thuku's assorted general purpose typescript/javascript library.

582 lines 24.6 kB
"use strict"; //=================================================================================== // simple date helpers - consider useful libraries: https://momentjs.com/ //=================================================================================== Object.defineProperty(exports, "__esModule", { value: true }); exports._duration = exports._elapsed = exports.SECOND_MS = exports.MINUTE_MS = exports.HOUR_MS = exports.DAY_MS = exports.MONTH_MS = exports.YEAR_MS = exports._timestr = exports._datestr = exports._datetime = exports._dayTime = exports._dateOnly = exports._yearEnd = exports._yearStart = exports._monthEnd = exports._monthStart = exports._dayEnd = exports._dayStart = exports._monthName = exports.MONTH_NAMES = exports._dayName = exports.DAY_NAMES = exports._time = exports._date = exports._parseIso = exports._isDate = void 0; /** * Validate `Date` instance * * @param value * @returns `boolean` */ const _isDate = (value) => value instanceof Date && !isNaN(value.getTime()); exports._isDate = _isDate; /** * Parse ISO formatted date value to milliseconds timestamp * - borrowed from https://github.com/jquense/yup/blob/1ee9b21c994b4293f3ab338119dc17ab2f4e284c/src/util/parseIsoDate.ts * * @param value - ISO date `string` (i.e. `'2022-12-19T13:12:42.000+0000'`/`'2022-12-19T13:12:42.000Z'` => `1671455562000`) * @returns `number` milliseconds timestamp | `undefined` when invalid */ const _parseIso = (value) => { const regex = /^(\d{4}|[+-]\d{6})(?:-?(\d{2})(?:-?(\d{2}))?)?(?:[ T]?(\d{2}):?(\d{2})(?::?(\d{2})(?:[,.](\d{1,}))?)?(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?)?$/; // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm let struct, timestamp = NaN; try { value = String(value); } catch (e) { value = ''; } if (struct = regex.exec(value)) { for (const k of [1, 4, 5, 6, 7, 10, 11]) struct[k] = +struct[k] || 0; //allow undefined days and months struct[2] = (+struct[2] || 1) - 1; struct[3] = +struct[3] || 1; //allow arbitrary sub-second precision beyond milliseconds struct[7] = struct[7] ? String(struct[7]).substring(0, 3) : 0; //timestamps without timezone identifiers should be considered local time if ((struct[8] === undefined || struct[8] === '') && (struct[9] === undefined || struct[9] === '')) { timestamp = +new Date(struct[1], struct[2], struct[3], struct[4], struct[5], struct[6], struct[7]); } else { let min_offset = 0; if (struct[8] !== 'Z' && struct[9] !== undefined) { min_offset = struct[10] * 60 + struct[11]; if (struct[9] === '+') min_offset = 0 - min_offset; } timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + min_offset, struct[6], struct[7]); } } else timestamp = Date.parse ? Date.parse(value) : NaN; return !isNaN(timestamp) ? timestamp : undefined; }; exports._parseIso = _parseIso; /** * Parse `Date` value ~ accepts valid `Date` instance, timestamp integer, datetime string (see `_strict` param docs) * - when `_strict === false`: `undefined` value returns `new Date()` and `null|false|true|0` = `new Date(null|false|true|0)` * - when `_strict === false`: `null|false|true|0` returns `new Date(value)` * - when `_strict === false`: `'now'|'today'|'tomorrow'|'yesterday'` return date equivalent * * - supports valid `Date` instance, `integer|string` timestamp in milliseconds and other `string` date texts * - when strict parsing, value must be a valid date value with more than `1` timestamp milliseconds * - when strict parsing is disabled, result for `undefined` = `new Date()` and `null|false|true|0` = `new Date(null|false|true|0)` * * @param value - parse date value (accepts `'now'|'today'|'tomorrow'|'yesterday'` as special values) * @param _strict - enable strict parsing (default: `true`) * @returns `Date` instance | `undefined` when invalid */ const _date = (value, _strict = true) => { if (value === undefined) return _strict ? undefined : new Date(); const _parse = (val) => !isNaN(val) && (val || !_strict) ? new Date(val) : undefined; if ([null, false, true, 0].includes(value)) return _parse(value); if (value instanceof Date) return _parse(value.getTime()); if ('number' === typeof value) return _parse(new Date(value).getTime()); try { let text = String(value).trim(); if (!text || /\[object \w+\]/.test(text)) return undefined; if (/^[+-]?\d+$/.test(text)) return _parse(parseInt(text)); // return _parse(Date.parse(text)); let date = _parse(Date.parse(text)); if (!date) { if (!_strict && /^(now|today|tomorrow|yesterday)$/i.test(text)) { const date = new Date(); if (text.toLowerCase() === 'now') return date; if (text.toLowerCase() === 'today') return new Date(date.getFullYear(), date.getMonth(), date.getDate()); if (text.toLowerCase() === 'tomorrow') return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1); if (text.toLowerCase() === 'yesterday') return new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1); } return undefined; } if (/^\d{4}-\d{2}-\d{2}$/.test(text)) { const date2 = new Date(date.getFullYear(), date.getMonth(), date.getDate()); if (date2.getFullYear() !== date.getFullYear()) date2.setFullYear(date.getFullYear()); return date2; } return date; } catch (e) { console.warn('[_date] exception:', e); return undefined; } }; exports._date = _date; /** * Parsed `Date` timestamp value (i.e. `date.getTime()`) * - see `_date()` parsing docs * * @param value - parse date value * @param min - set `min` timestamp limit ~ enabled when `min` is a valid timestamp integer * @param max - set `max` timestamp limit ~ enabled when `max` is a valid timestamp integer * @param _strict - enable strict parsing (default: `true`) * @returns `number` timestamp in milliseconds | `undefined` when invalid */ const _time = (value, min, max, _strict = true) => { const date = (0, exports._date)(value, _strict); if (!date) return undefined; const time = date.getTime(); if (!isNaN(min = parseFloat(min)) && time < min) return undefined; if (!isNaN(max = parseFloat(max)) && time > max) return undefined; return time; }; exports._time = _time; /** * Day names * - `('Sunday'|'Monday'|'Tuesday'|'Wednesday'|'Thursday'|'Friday'|'Saturday')[]` */ exports.DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; /** * Get day name * * @param index - (default: `0`) day index `0-6` ~ `DAY_NAMES[Math.abs(index % DAY_NAMES.length)]` * @returns `string` ~ `'Sunday'|'Monday'|'Tuesday'|'Wednesday'|'Thursday'|'Friday'|'Saturday'` */ const _dayName = (index) => { index = !isNaN(index = parseInt(index)) ? index : 0; if (index < 0) index = 7 - (Math.abs(index) % exports.DAY_NAMES.length); return exports.DAY_NAMES[Math.abs(index % exports.DAY_NAMES.length)]; }; exports._dayName = _dayName; /** * Month names * - `('January'|'February'|'March'|'April'|'May'|'June'|'July'|'August'|'September'|'October'|'November'|'December')[]` */ exports.MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; /** * Get month name * * @param index - (default: `0`) day index `0-11` ~ `MONTH_NAMES[Math.abs(index % DAY_NAMES.length)]` * @returns `string` ~ `'January'|'February'|'March'|'April'|'May'|'June'|'July'|'August'|'September'|'October'|'November'|'December'` */ const _monthName = (index) => { index = !isNaN(index = parseInt(index)) ? index : 0; if (index < 0) index = 12 - (Math.abs(index) % exports.MONTH_NAMES.length); return exports.MONTH_NAMES[Math.abs(index % exports.MONTH_NAMES.length)]; }; exports._monthName = _monthName; /** * Parse `Date` day start ~ at `00:00:00 0` * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) * @returns `Date` */ const _dayStart = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0); }; exports._dayStart = _dayStart; /** * Parse `Date` day end ~ at `23:59:59 999` * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) * @returns `Date` */ const _dayEnd = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); }; exports._dayEnd = _dayEnd; /** * Parse `Date` month's start day ~ at `00:00:00 0` * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `Date` */ const _monthStart = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0); }; exports._monthStart = _monthStart; /** * Parse `Date` month's end day ~ at `23:59:59 999` * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `Date` */ const _monthEnd = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999); }; exports._monthEnd = _monthEnd; /** * Parse `Date` year's start day ~ at `YYYY-01-01 00:00:00 0` * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `Date` */ const _yearStart = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return new Date(date.getFullYear(), 0, 1, 0, 0, 0, 0); }; exports._yearStart = _yearStart; /** * Parse `Date` year's end day ~ at `YYYY-12-31 23:59:59 999` * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `Date` */ const _yearEnd = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return new Date(date.getFullYear(), 12, 0, 23, 59, 59, 999); }; exports._yearEnd = _yearEnd; /** * Parse `Date` value where only date part is considered (e.g. `'2023-05-27 22:11:57' => '1970-01-01 00:00:00'`) * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `Date` */ const _dateOnly = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return new Date(date.getFullYear(), date.getMonth(), date.getDate()); }; exports._dateOnly = _dateOnly; /** * Parse `Date` value to day's time in milliseconds since midnight (e.g. `'2023-05-27 22:11:57' => 79917000`) * - see `_date()` parsing docs * * @param value - parse date value ~ **_(defaults to `new Date()` when invalid)_** * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `number` */ const _dayTime = (value, _strict = false) => { const date = (0, exports._date)(value, _strict) ?? new Date(); return date.getTime() - new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); }; exports._dayTime = _dayTime; /** * Parse `Date` value to `YYYY-MM-DD HH:mm:ss` format (e.g. `'2023-05-27 22:11:57'`) * - see `_date()` parsing docs * * @param value - parse date value * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `string` ~ `'YYYY-MM-DD HH:mm:ss'` | empty `''` when invalid */ const _datetime = (value, _strict = false) => { const date = (0, exports._date)(value, _strict); if (!date) return ''; return String(date.getFullYear()).padStart(4, '0') + '-' + [date.getMonth() + 1, date.getDate()].map(v => String(v).padStart(2, '0')).join('-') + ' ' + [date.getHours(), date.getMinutes(), date.getSeconds()].map(v => String(v).padStart(2, '0')).join(':'); }; exports._datetime = _datetime; /** * Parse `Date` value to `YYYY-MM-DD` format `string` (e.g. `'2023-05-27'`) * - see `_date()` parsing docs * * @param value - parse date value * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `string` ~ `'YYYY-MM-DD'` | empty `''` when invalid */ const _datestr = (value, _strict = false) => (0, exports._datetime)(value, _strict).substring(0, 10); exports._datestr = _datestr; /** * Parse `Date` value to `HH:mm:ss` format `string` (e.g. `'22:11:57'`) * - see `_date()` parsing docs * * @param value - parse date value * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @returns `string` ~ `'HH:mm:ss'` | empty `''` when invalid */ const _timestr = (value, _strict = false) => (0, exports._datetime)(value, _strict).substring(11, 19); exports._timestr = _timestr; /** * Year unit milliseconds ~ close estimate `365.25` days * - `365.25 * 24 * 60 * 60 * 1000` = `31557600000` ms */ exports.YEAR_MS = 365.25 * 24 * 60 * 60 * 1000; /** * Month unit milliseconds ~ close estimate `30.44` days * - `30.44 * 24 * 60 * 60 * 1000` = `2630016000.0000005` ms */ exports.MONTH_MS = 30.44 * 24 * 60 * 60 * 1000; /** * Day unit milliseconds * - `24 * 60 * 60 * 1000` = `86400000` ms */ exports.DAY_MS = 24 * 60 * 60 * 1000; /** * Hour unit milliseconds * - `60 * 60 * 1000` = `3600000` ms */ exports.HOUR_MS = 60 * 60 * 1000; /** * Minute unit milliseconds * - `60 * 1000` = `60000` ms */ exports.MINUTE_MS = 60 * 1000; /** * Second unit milliseconds * - `1000` ms */ exports.SECOND_MS = 1000; /** * **[internal]** Create `IDuration` object * * @param years - elapsed years * @param months - elapsed months * @param days - elapsed days * @param hours - elapsed hours * @param minutes - elapsed minutes * @param seconds - elapsed seconds * @param milliseconds - elapsed milliseconds * @param total_days - elapsed total days * @param total_time - elapsed total time * @param start_time - start timestamp * @param end_time - end timestamp * @returns `IDuration` */ const create_duration = (years, months, days, hours, minutes, seconds, milliseconds, total_days, total_time, start_time, end_time) => ({ years, months, days, hours, minutes, seconds, milliseconds, total_days, total_time, start_time, end_time, toString: function (mode = 0) { mode = [0, 1].includes(mode = parseInt(mode)) ? mode : 0; const buffer_text = [], buffer_time = []; const _add = (val, name) => { if (mode === 0 && ['hour', 'minute', 'second', 'millisecond'].includes(name)) { if (name === 'millisecond') return; buffer_time.push(String(val).padStart(2, '0')); } else if (val) buffer_text.push(val + ' ' + name + (val > 1 ? 's' : '')); }; _add(years, 'year'); _add(months, 'month'); _add(days, 'day'); _add(hours, 'hour'); _add(minutes, 'minute'); _add(seconds, 'second'); _add(milliseconds, 'millisecond'); if (mode === 0) return (buffer_text.length ? buffer_text.join(', ') + ' ' : '') + buffer_time.join(':'); if (!buffer_text.length) buffer_text.push('0 milliseconds'); return buffer_text.join(', ').replace(/,([^,]*)$/, ' and$1'); // return buffer_text.length > 1 ? buffer_text.slice(0, -1).join(', ') + ' and ' + buffer_text[buffer_text.length - 1] : buffer_text.join(''); }, }); /** * Get elapsed duration between two dates/timestamps ~ extra accuracy considering leap years * - start and end values are reordered automatically (i.e. `start <= end`) * * @param start - start date/timestamp * @param end - end date/timestamp (default: `undefined`) * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @throws `TypeError` on invalid start/end time value * @returns `IDuration` */ const _elapsed = (start, end = undefined, _strict = false) => { // FIXME: improve '_elapsed' accuracy (not urgent) // parse date arguments if (!(start = (0, exports._date)(start, _strict))) throw new TypeError('Invalid elapsed start date value.'); if (!(end = (0, exports._date)(end, _strict))) throw new TypeError('Invalid elapsed end date value.'); // ensure start date is earlier than end date if (start > end) [start, end] = [end, start]; // calc time difference const DAY_MS = 24 * 60 * 60 * 1000; const HOUR_MS = 60 * 60 * 1000; const MINUTE_MS = 60 * 1000; const SECOND_MS = 1000; let years = 0, months = 0, days = 0, hours = 0, minutes = 0, seconds = 0, milliseconds = 0; const total_time = end.getTime() - start.getTime(); const total_days = Math.floor(total_time / DAY_MS); if (end > start) { const d1 = new Date(start.getFullYear(), start.getMonth(), start.getDate()); if (d1.getFullYear() !== start.getFullYear()) d1.setFullYear(start.getFullYear()); const d2 = new Date(end.getFullYear(), end.getMonth(), end.getDate()); if (d2.getFullYear() !== end.getFullYear()) d2.setFullYear(end.getFullYear()); // difference: hours, minutes, seconds, milliseconds let ms = (end.getTime() - start.getTime()) - (d2.getTime() - d1.getTime()); if (ms) { if (ms < 0) { ms = Math.abs(ms); d2.setDate(d2.getDate() - 1); } hours = Math.floor(ms / HOUR_MS); ms -= hours * HOUR_MS; minutes = Math.floor(ms / MINUTE_MS); ms -= minutes * MINUTE_MS; seconds = Math.floor(ms / SECOND_MS); milliseconds = ms - seconds * SECOND_MS; } // difference: years, months, days let y1 = d1.getFullYear(), m1 = d1.getMonth(), dd1 = d1.getDate(); let y2 = d2.getFullYear(), m2 = d2.getMonth(), dd2 = d2.getDate(); if (m1 === m2 && m1 === 1 && dd1 === 29 && dd2 === 28 && dd2 === new Date(y2, 2, 0).getDate()) dd1 = 28; //Feb end adjust if (y2 > y1) { if (m2 > m1) { if (dd1 === 1) { days = dd2 - 1; months = m2 - m1; years = y2 - y1; } else if (dd2 > dd1) { days = dd2 - dd1; months = m2 - m1; years = y2 - y1; } else if (dd2 < dd1) { let d = 0; const end1 = new Date(y1, m1, 0).getDate(); const end2 = new Date(y2, m2, 0).getDate(); if (dd1 > end1 && dd1 > end2) d = 1; else if (dd1 <= end1) d = (end1 - dd1) || 1; else d = (end2 - dd1) || 1; days = d + (dd2 > 1 ? dd2 - 1 : 0); if ((months = m2 - new Date(y1, m1 + 1, 1).getMonth()) < 0) { months += 12; y2--; } years = y2 - y1; } else { days = 0; months = m2 - m1; years = y2 - y1; } } else if (m2 < m1) { if (dd1 === 1) { days = dd2 - 1; months = 12 - m1 + m2; years = y2 - (y1 + 1); } else if (dd2 > dd1) { days = dd2 - dd1; months = 12 - m1 + m2; years = y2 - (y1 + 1); } else if (dd2 < dd1) { let d = 0; const end1 = new Date(y1, m1, 0).getDate(); const end2 = new Date(y2, m2, 0).getDate(); if (dd1 > end1 && dd1 > end2) d = 1; else if (dd1 > end2) d = (end1 - dd1) || 1; else d = (end2 - dd1) || 1; days = d + (dd2 > 1 ? dd2 - 1 : 0); if ((months = m2 - new Date(y1, m1 + 1, 1).getMonth()) < 0) { y2--; months += 12; } years = y2 - y1; } else { months = 12 - m1 + m2; years = y2 - (y1 + 1); } } else if (dd2 >= dd1) { days = dd2 - dd1; years = y2 - y1; } else { days = (new Date(y1, m1 + 1, 0).getDate() - dd1) + dd2; months = 12 - new Date(y1, m1 + 1, 1).getMonth() + m2; years = y2 - (y1 + 1); } } else { days = dd2 - dd1; months = m2 - m1; } } return create_duration(years, months, days, hours, minutes, seconds, milliseconds, total_days, total_time, start.getTime(), end.getTime()); }; exports._elapsed = _elapsed; /** * Get elapsed duration between two dates/timestamps ~ closest estimation * - start and end values are reordered automatically (start = min, end = max) * * @param start - start date/ms timestamp * @param end - end date/ms timestamp (default: `0`) * @param _strict - enable strict datetime parsing (default: `false`) ~ see `_date()` * @throws `TypeError` on invalid start/end time value * @returns `IDuration` */ const _duration = (start, end = 0, _strict = false) => { if (!(start = (0, exports._date)(start, _strict))) throw new TypeError('Invalid duration start date value! Pass a valid Date instance, integer timestamp or date string value.'); if (!(end = (0, exports._date)(end, _strict))) throw new TypeError('Invalid duration end date value! Pass a valid Date instance, integer timestamp or date string value.'); if (start > end) { const swap = start; start = end; end = swap; } let diff = 0; const end_time = end.getTime(); const start_time = start.getTime(); const total_time = diff = Math.abs(end_time - start_time); const total_days = Math.floor(total_time / exports.DAY_MS); const years = Math.floor(total_time / exports.YEAR_MS); diff %= exports.YEAR_MS; const months = Math.floor(diff / exports.MONTH_MS); diff %= exports.MONTH_MS; const days = Math.floor(diff / exports.DAY_MS); diff %= exports.DAY_MS; const hours = Math.floor(diff / exports.HOUR_MS); diff %= exports.HOUR_MS; const minutes = Math.floor(diff / exports.MINUTE_MS); diff %= exports.MINUTE_MS; const seconds = Math.floor(diff / exports.SECOND_MS); const milliseconds = diff % exports.SECOND_MS; return create_duration(years, months, days, hours, minutes, seconds, milliseconds, total_days, total_time, start_time, end_time); }; exports._duration = _duration; //# sourceMappingURL=_datetime.js.map