UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.

799 lines (798 loc) 28.1 kB
var _a; import { isNumber, isString } from '../guards/primitives.js'; import { DAYS, INTERNALS, MONTHS, MS_PER_DAY } from './constants.js'; import { isLeapYear } from './guards.js'; import { _dateArgsToDate, _formatDate, _hasChronosProperties, _normalizeOffset, _resolveNativeTzName, } from './helpers.js'; import { extractMinutesFromUTC, getNativeTimeZoneId } from './utils.js'; export class Chronos { #date; #offset; #ORIGIN; static #plugins = new Set(); static [INTERNALS] = { internalDate(instance) { return instance.#date; }, offset(instance) { return instance.#offset; }, withOrigin(instance, method, offset, tzName, tzId, tzTracker) { return instance.#withOrigin(method, offset, tzName, tzId, tzTracker); }, toNewDate(instance, value) { return instance.#toNewDate(value); }, cast(date) { return _a.#cast(date); }, }; origin; native; utcOffset; timeZoneName; timeZoneId; $tzTracker; constructor(valueOrYear, month, date, hours, minutes, seconds, ms) { if (isNumber(valueOrYear) && isNumber(month)) { this.#date = new Date(valueOrYear, month - 1, date ?? 1, hours ?? 0, minutes ?? 0, seconds ?? 0, ms ?? 0); this.native = this.#date; } else { this.#date = this.#toNewDate(valueOrYear); this.native = this.#date; } this.#ORIGIN = 'root'; this.origin = this.#ORIGIN; this.#offset = `UTC${this.getUTCOffset()}`; this.utcOffset = this.#offset; this.timeZoneName = this.$getNativeTimeZoneName(); this.timeZoneId = this.$getNativeTimeZoneId(); } *[Symbol.iterator]() { yield ['year', this.year]; yield ['month', this.month]; yield ['isoMonth', this.isoMonth]; yield ['date', this.date]; yield ['weekDay', this.weekDay]; yield ['isoWeekDay', this.isoWeekDay]; yield ['hour', this.hour]; yield ['minute', this.minute]; yield ['second', this.second]; yield ['millisecond', this.millisecond]; yield ['timestamp', this.getTimeStamp()]; yield ['unix', this.unix]; } [Symbol.toPrimitive](hint) { if (hint === 'number') return this.getTimeStamp(); return this.toLocalISOString(); } [Symbol.replace](string, replacement) { return string.replace(this.#removeUTCFromISO(), replacement); } [Symbol.search](string) { return string.indexOf(this.#removeUTCFromISO()); } [Symbol.split](string) { return string.split(this.#removeUTCFromISO()); } [Symbol.match](string) { const [datePart, timePart] = this.toLocalISOString().split('.')[0].split('T'); const fuzzyDate = datePart.replace(/-/g, '[-/]?'); const fuzzyTime = timePart?.replace(/:/g, '[:.]?'); const pattern = timePart ? `${fuzzyDate}(?:[T ]?${fuzzyTime})?` : fuzzyDate; return string.match(new RegExp(pattern)); } get [Symbol.toStringTag]() { return this.toLocalISOString(); } get [Symbol.isConcatSpreadable]() { return true; } $getNativeTimeZoneName(tzId) { const $tzId = tzId || getNativeTimeZoneId(); return _resolveNativeTzName($tzId, 'long', this.#date) ?? $tzId; } $getNativeTimeZoneId() { return getNativeTimeZoneId(); } get #timestamp() { return this.#date.getTime(); } #toNewDate(value) { const date = value instanceof _a ? value.toDate() : _dateArgsToDate(value); if (Number.isNaN(date.getTime())) { throw new TypeError('Provided date is invalid!'); } return date; } #withOrigin(origin, offset, tzName, tzId, tzTracker) { const instance = new _a(this.#date); instance.#ORIGIN = origin; instance.origin = origin; if (offset) { instance.#offset = offset; instance.utcOffset = offset; } if (tzName) instance.timeZoneName = tzName; if (tzId) instance.timeZoneId = tzId; if (tzTracker) instance.$tzTracker = tzTracker; instance.native = instance.toDate(); return instance; } #cloneStates(instance, origin) { return instance.#withOrigin(origin, this.#offset, this.timeZoneName, this.timeZoneId, this.$tzTracker); } #format(format, useUTC = false) { const $date = this.#date; const $utcDate = this.toDate(); const _getUnitValue = (suffix) => { return useUTC ? $utcDate[`getUTC${suffix}`]() : $date[`get${suffix}`](); }; const y = _getUnitValue('FullYear'), mo = _getUnitValue('Month'), d = _getUnitValue('Day'), dt = _getUnitValue('Date'), h = _getUnitValue('Hours'), m = _getUnitValue('Minutes'), s = _getUnitValue('Seconds'), ms = _getUnitValue('Milliseconds'); const offset = useUTC ? 'Z' : this.getTimeZoneOffset(); return _formatDate(format, y, mo, d, dt, h, m, s, ms, offset); } #removeUTCFromISO(local = true) { const search = /\.\d+(Z|[+-]\d{2}:\d{2})?$/; return local ? this.toLocalISOString().replace(search, '') : this.toISOString().replace(search, ''); } get year() { return this.#date.getFullYear(); } get month() { return this.#date.getMonth(); } get date() { return this.#date.getDate(); } get weekDay() { return this.#date.getDay(); } get hour() { return this.#date.getHours(); } get minute() { return this.#date.getMinutes(); } get second() { return this.#date.getSeconds(); } get millisecond() { return this.#date.getMilliseconds(); } get isoWeekDay() { const day = this.weekDay; return day === 0 ? 7 : day; } get isoMonth() { return (this.month + 1); } get unix() { return Math.floor(this.getTimeStamp() / 1000); } get timestamp() { return this.getTimeStamp(); } get lastDateOfMonth() { return this.daysInMonth(); } inspect() { return `[Chronos ${this.toLocalISOString()}]`; } toJSON() { return this.toLocalISOString(); } valueOf() { return this.getTimeStamp(); } clone() { return new _a(this.#date).#withOrigin(this.#ORIGIN, this.#offset, this.timeZoneName, this.timeZoneId, this.$tzTracker); } toDate() { const targetOffset = extractMinutesFromUTC(this.#offset); const systemOffset = this.getUTCOffsetMinutes(); const adjustmentMs = (targetOffset - systemOffset) * 60_000; return new Date(this.#timestamp - adjustmentMs); } toString() { return this.#format('dd mmm DD YYYY HH:mm:ss ') .concat(this.#offset.replace('UTC', 'GMT').replace(':', '')) .concat(` (${this.timeZoneName})`); } toLocalISOString() { return this.#format('YYYY-MM-DDTHH:mm:ss.mssZZ'); } toISOString() { return this.toDate().toISOString(); } toLocaleString(locales, options) { return this.#date.toLocaleString(locales, options); } getTimeStamp() { return this.toDate().getTime(); } format(format, useUTC = false) { return this.#format(format || 'dd, mmm DD, YYYY HH:mm:ss', useUTC); } formatStrict(format, useUTC = false) { return this.#format(format || 'dd, mmm DD, YYYY HH:mm:ss', useUTC); } formatUTC(format = 'dd, mmm DD, YYYY HH:mm:ss:mss') { return this.#format(format, true); } addSeconds(seconds) { return this.#cloneStates(this.add(seconds, 'second'), 'addSeconds'); } addMinutes(minutes) { return this.#cloneStates(this.add(minutes, 'minute'), 'addMinutes'); } addHours(hours) { return this.#cloneStates(this.add(hours, 'hour'), 'addHours'); } addDays(days) { return this.#cloneStates(this.add(days, 'day'), 'addDays'); } addWeeks(weeks) { return this.#cloneStates(this.add(weeks, 'week'), 'addWeeks'); } addMonths(months) { return this.#cloneStates(this.add(months, 'month'), 'addMonths'); } addYears(years) { return this.#cloneStates(this.add(years, 'year'), 'addYears'); } isLeapYear(year) { return isLeapYear(year ?? this.year); } isEqual(other) { return this.#timestamp === _a.#cast(other).#timestamp; } isEqualOrBefore(other) { return this.#timestamp <= _a.#cast(other).#timestamp; } isEqualOrAfter(other) { return this.#timestamp >= _a.#cast(other).#timestamp; } isSame(other, unit, weekStartsOn = 0) { return (this.startOf(unit, weekStartsOn).#timestamp === _a.#cast(other).startOf(unit, weekStartsOn).#timestamp); } isBefore(other, unit, weekStartsOn = 0) { return (this.startOf(unit, weekStartsOn).#timestamp < _a.#cast(other).startOf(unit, weekStartsOn).#timestamp); } isAfter(other, unit, weekStartsOn = 0) { return (this.startOf(unit, weekStartsOn).#timestamp > _a.#cast(other).startOf(unit, weekStartsOn).#timestamp); } isSameOrBefore(other, unit, weekStartsOn = 0) { return (this.isSame(other, unit, weekStartsOn) || this.isBefore(other, unit, weekStartsOn)); } isSameOrAfter(other, unit, weekStartsOn = 0) { return (this.isSame(other, unit, weekStartsOn) || this.isAfter(other, unit, weekStartsOn)); } isBetween(start, end, inclusive = '()') { const s = _a.#cast(start).getTimeStamp(); const e = _a.#cast(end).getTimeStamp(); const t = this.getTimeStamp(); switch (inclusive) { case '[]': return t >= s && t <= e; case '[)': return t >= s && t < e; case '(]': return t > s && t <= e; case '()': return t > s && t < e; default: return false; } } isDST() { const year = this.year; const jan = new Date(year, 0, 1).getTimezoneOffset(); const jul = new Date(year, 6, 1).getTimezoneOffset(); return this.#date.getTimezoneOffset() < Math.max(jan, jul); } isFirstDayOfMonth() { return this.isSame(this.firstDayOfMonth(), 'day'); } isLastDayOfMonth() { return this.isSame(this.lastDayOfMonth(), 'day'); } firstDayOfMonth() { const firstDate = new Date(this.year, this.month, 1); return this.#cloneStates(new _a(firstDate), 'firstDayOfMonth'); } lastDayOfMonth() { const lastDate = new Date(this.year, this.month + 1, 0); return this.#cloneStates(new _a(lastDate), 'lastDayOfMonth'); } startOf(unit, weekStartsOn = 0) { const d = new Date(this.#date); switch (unit) { case 'year': d.setMonth(0, 1); d.setHours(0, 0, 0, 0); break; case 'month': d.setDate(1); d.setHours(0, 0, 0, 0); break; case 'week': { const day = d.getDay(); const diff = (day - weekStartsOn + 7) % 7; d.setDate(d.getDate() - diff); d.setHours(0, 0, 0, 0); break; } case 'day': d.setHours(0, 0, 0, 0); break; case 'hour': d.setMinutes(0, 0, 0); break; case 'minute': d.setSeconds(0, 0); break; case 'second': d.setMilliseconds(0); break; case 'millisecond': break; } return this.#cloneStates(new _a(d), 'startOf'); } endOf(unit, weekStartsOn = 0) { const instance = this.startOf(unit, weekStartsOn).add(1, unit).add(-1, 'millisecond'); return this.#cloneStates(instance, 'endOf'); } add(number, unit) { const d = new Date(this.#date); switch (unit) { case 'millisecond': d.setMilliseconds(d.getMilliseconds() + number); break; case 'second': d.setSeconds(d.getSeconds() + number); break; case 'minute': d.setMinutes(d.getMinutes() + number); break; case 'hour': d.setHours(d.getHours() + number); break; case 'day': d.setDate(d.getDate() + number); break; case 'week': d.setDate(d.getDate() + number * 7); break; case 'month': d.setMonth(d.getMonth() + number); break; case 'year': d.setFullYear(d.getFullYear() + number); break; } return this.#cloneStates(new _a(d), 'add'); } subtract(number, unit) { return this.#cloneStates(this.add(-number, unit), 'subtract'); } get(unit) { switch (unit) { case 'year': return this.year; case 'month': return this.isoMonth; case 'day': return this.date; case 'week': return this.getWeek(); case 'hour': return this.hour; case 'minute': return this.minute; case 'second': return this.second; case 'millisecond': return this.millisecond; } } set(unit, value) { const d = new Date(this.#date); switch (unit) { case 'year': d.setFullYear(value); break; case 'month': d.setMonth(value - 1); break; case 'day': d.setDate(value); break; case 'week': return this.setWeek(value); case 'hour': d.setHours(value); break; case 'minute': d.setMinutes(value); break; case 'second': d.setSeconds(value); break; case 'millisecond': d.setMilliseconds(value); break; } return this.#cloneStates(new _a(d), 'set'); } diff(other, unit) { const time = _a.#cast(other); const msDiff = this.#timestamp - time.#timestamp; switch (unit) { case 'millisecond': return msDiff; case 'second': return msDiff / 1e3; case 'minute': return msDiff / 6e4; case 'hour': return msDiff / 3.6e6; case 'day': return msDiff / 8.64e7; case 'week': return msDiff / 6.048e8; case 'month': { const yearDiff = this.year - time.year; const monthDiff = this.month - time.month; const totalMonthDiff = yearDiff * 12 + monthDiff; const dayDiff = this.date - time.date; return totalMonthDiff + dayDiff / this.daysInMonth(); } case 'year': return this.diff(time, 'month') / 12; } } calendar(baseDate) { const base = baseDate ? _a.#cast(baseDate) : new _a(); const input = this.startOf('day'); const comparison = base.startOf('day'); const diff = input.diff(comparison, 'day'); const timeStr = this.toDate().toLocaleString('en', { hour: 'numeric', minute: '2-digit', }); if (diff === 0) return `Today at ${timeStr}`; if (diff === 1) return `Tomorrow at ${timeStr}`; if (diff === -1) return `Yesterday at ${timeStr}`; return this.toDate().toLocaleString('en', { month: 'long', day: '2-digit', year: 'numeric', weekday: 'long', hour: 'numeric', minute: '2-digit', }); } fromNowShort() { const now = new _a(); const diffInSeconds = this.diff(now, 'second'); const abs = Math.abs(diffInSeconds); const prefix = diffInSeconds >= 0 ? 'in ' : ''; const suffix = diffInSeconds < 0 ? ' ago' : ''; if (abs < 60) { return `${prefix}${Math.floor(abs)}s${suffix}`; } else if (abs < 3600) { return `${prefix}${Math.floor(abs / 60)}m${suffix}`; } else if (abs < 86400) { return `${prefix}${Math.floor(abs / 3600)}h${suffix}`; } else if (abs < 2592000) { return `${prefix}${Math.floor(abs / 86400)}d${suffix}`; } else if (abs < 31536000) { return `${prefix}${Math.floor(abs / 2592000)}mo${suffix}`; } else { return `${prefix}${Math.floor(abs / 31536000)}y${suffix}`; } } setWeek(week) { const d = new Date(this.#date); const year = d.getFullYear(); const jan4 = new Date(year, 0, 4); const dayOfWeek = jan4.getDay() || 7; const weekStart = new Date(jan4); weekStart.setDate(jan4.getDate() - (dayOfWeek - 1)); weekStart.setDate(weekStart.getDate() + (week - 1) * 7); d.setFullYear(weekStart.getFullYear()); d.setMonth(weekStart.getMonth()); d.setDate(weekStart.getDate()); return this.#cloneStates(new _a(d), 'setWeek'); } getWeek() { const target = this.startOf('week', 1).add(3, 'day'); const firstThursday = new _a(target.year, 1, 4) .startOf('week', 1) .add(3, 'day'); return (target.diff(firstThursday, 'week') + 1); } getWeekOfYear(weekStartsOn = 0) { const startOfFirstWeek = this.startOf('year').startOf('week', weekStartsOn); const week = this.startOf('week', weekStartsOn).diff(startOfFirstWeek, 'week'); return (week + 1); } getWeekYear(weekStartsOn = 0) { const d = this.startOf('week', weekStartsOn).add(3, 'day'); return d.year; } getDayOfYear() { const diff = this.startOf('day').diff(this.startOf('year'), 'day'); return (diff + 1); } daysInMonth() { return new Date(this.year, this.month + 1, 0).getDate(); } toObject() { return Object.fromEntries([...this]); } toArray() { return Object.values(this.toObject()); } toQuarter() { const month = this.#date.getMonth(); return (Math.floor(month / 3) + 1); } getUTCOffset() { const offset = -this.#date.getTimezoneOffset(); const sign = offset >= 0 ? '+' : '-'; const pad = (n) => String(Math.floor(Math.abs(n))).padStart(2, '0'); return `${sign}${pad(offset / 60)}:${pad(offset % 60)}`; } getTimeZoneOffset() { return this.#offset.replace('UTC', ''); } getUTCOffsetMinutes() { return -this.#date.getTimezoneOffset(); } getTimeZoneOffsetMinutes() { return extractMinutesFromUTC(this.#offset); } toUTC() { const offset = this.getTimeZoneOffsetMinutes(); const utc = new Date(this.#timestamp - offset * 60 * 1000); return new _a(utc).#withOrigin('toUTC', 'UTC+00:00', 'Greenwich Mean Time', 'UTC'); } toLocal() { const offset = this.getTimeZoneOffsetMinutes() - this.getUTCOffsetMinutes(); const localTime = new Date(this.#timestamp - offset * 60 * 1000); return new _a(localTime).#withOrigin('toLocal'); } day(index) { return DAYS[index ?? this.weekDay]; } monthName(index) { return MONTHS[index ?? this.month]; } static parse(dateStr, format) { const tokenPatterns = { YYYY: '(?<YYYY>\\d{4})', YY: '(?<YY>\\d{2})', MM: '(?<MM>\\d{2})', M: '(?<M>\\d{1,2})', DD: '(?<DD>\\d{2})', D: '(?<D>\\d{1,2})', HH: '(?<HH>\\d{2})', H: '(?<H>\\d{1,2})', mm: '(?<mm>\\d{2})', m: '(?<m>\\d{1,2})', ss: '(?<ss>\\d{2})', s: '(?<s>\\d{1,2})', mss: '(?<mss>\\d{3})', ms: '(?<ms>\\d{1,3})', }; const tokenToComponent = { YYYY: 'year', YY: 'year', MM: 'month', M: 'month', DD: 'date', D: 'date', HH: 'hour', H: 'hour', mm: 'minute', m: 'minute', ss: 'second', s: 'second', mss: 'millisecond', ms: 'millisecond', }; const tokenRegExp = new RegExp(Object.keys(tokenPatterns) .sort((a, b) => b.length - a.length) .join('|'), 'g'); const regexStr = format ?.trim() ?.replace(tokenRegExp, (token) => tokenPatterns[token] ?? token) ?.replace(/\s+/g, '\\s*'); const match = new RegExp(`^${regexStr}\\s*$`).exec(dateStr.trim()); if (!match?.groups) { throw new Error('Invalid date format!'); } const parts = {}; for (const [token, value] of Object.entries(match.groups)) { const key = tokenToComponent[token]; if (key) { let num = Number(value); if (token === 'YY') { num += num < 100 ? 2000 : 0; } parts[key] = num; } } return new _a(parts?.year ?? 1970, parts?.month ?? 1, parts?.date ?? 1, parts?.hour ?? 0, parts?.minute ?? 0, parts?.second ?? 0, parts?.millisecond ?? 0).#withOrigin('parse'); } static with(options) { const now = new _a(); const { year, month, date, hour, minute, second, millisecond } = options ?? {}; const nextLDoM = () => { return now .startOf('month') .set('year', year ?? now.year) .set('month', month ?? now.isoMonth).lastDateOfMonth; }; return new _a(year ?? now.year, month ?? now.isoMonth, date ? date : now.isLastDayOfMonth() && now.date >= nextLDoM() ? nextLDoM() : now.date, hour ?? now.hour, minute ?? now.minute, second ?? now.second, millisecond ?? now.millisecond).#withOrigin('with'); } static today(options) { const { format = 'dd, mmm DD, YYYY HH:mm:ss', useUTC = false } = options || {}; return new _a().#format(format, useUTC); } static yesterday() { const today = new Date(); const yesterday = today.setDate(today.getDate() - 1); return new _a(yesterday).#withOrigin('yesterday'); } static tomorrow() { const today = new Date(); const yesterday = today.setDate(today.getDate() + 1); return new _a(yesterday).#withOrigin('tomorrow'); } static now() { return Date.now(); } static utc(dateLike) { const chronos = new _a(dateLike); const offset = chronos.getTimeZoneOffsetMinutes(); const utc = new Date(chronos.#timestamp - offset * 60 * 1000); return new _a(utc).#withOrigin('utc', 'UTC+00:00', 'Greenwich Mean Time', 'UTC'); } static formatTimePart(time, format) { const timeWithDate = `${new _a().#format('YYYY-MM-DD')}T${_normalizeOffset(time)}`; return new _a(timeWithDate).#format(format || 'hh:mm:ss a'); } static getDatesForDay(day, options) { let startDate = new _a(), endDate = startDate.addWeeks(4); const { format = 'local', roundDate = false } = options ?? {}; if (options) { if ('from' in options || 'to' in options) { if (options?.from) startDate = _a.#cast(options?.from); if (options?.to) endDate = _a.#cast(options?.to); } else if ('span' in options || 'unit' in options) { const { span = 4, unit = 'week' } = options ?? {}; endDate = startDate.add(span, unit); } } if (roundDate) { startDate = startDate.startOf('day'); endDate = endDate.startOf('day'); } const result = []; const step = (startDate.isBefore(endDate, 'day') ? 1 : -1) * MS_PER_DAY; const totalDays = Math.abs(endDate.diff(startDate, 'day')); const currentTime = startDate.#timestamp; let firstOffset = 0; while (new Date(currentTime + firstOffset * step).getDay() !== DAYS.indexOf(day)) { firstOffset++; } for (let i = firstOffset; i <= totalDays; i += 7) { const ts = currentTime + i * step; const chr = new _a(ts).#withOrigin('clone', startDate.#offset, startDate.timeZoneName, startDate.timeZoneId, startDate.$tzTracker); result.push(format === 'local' ? chr.toLocalISOString() : chr.toISOString()); } return result; } static min(...dates) { let winner = _a.#cast(dates[0]); for (const d of dates) { const c = _a.#cast(d); if (c.getTimeStamp() < winner.getTimeStamp()) { winner = c; } } return winner.#cloneStates(winner, winner.#ORIGIN !== 'root' ? winner.#ORIGIN : 'min'); } static max(...dates) { let winner = _a.#cast(dates[0]); for (const d of dates) { const c = _a.#cast(d); if (c.getTimeStamp() > winner.getTimeStamp()) { winner = c; } } return winner.#cloneStates(winner, winner.#ORIGIN !== 'root' ? winner.#ORIGIN : 'max'); } static isLeapYear(date) { let year; if (isNumber(date)) { if (date > 0 && date <= 9999) { year = date; } else { year = new Date(date).getFullYear(); } } else { year = _a.#cast(date).year; } return isLeapYear(year); } static isValidDate(value) { return value instanceof Date; } static isDateString(value) { return isString(value) && !Number.isNaN(Date.parse(value)); } static isValidChronos(value) { return value instanceof _a; } static isReconstructable(value) { return _hasChronosProperties(value); } static reconstruct(value) { if (!_hasChronosProperties(value)) { throw new TypeError('Invalid input for reconstruction!'); } const { native, origin, utcOffset, timeZoneName, timeZoneId, $tzTracker } = value; const offsetMins = extractMinutesFromUTC(utcOffset); const chr = new _a(native); const diffMins = chr.getTimeZoneOffsetMinutes() - offsetMins; const target = chr.utcOffset === utcOffset ? chr : chr.add(-diffMins, 'minute'); return target.#withOrigin(origin, utcOffset, timeZoneName, timeZoneId, $tzTracker); } static use(plugin) { if (!_a.#plugins.has(plugin)) { _a.#plugins.add(plugin); plugin(_a); } } static register(plugin) { _a.use(plugin); } static #cast(date) { return date instanceof _a ? date : new _a(date); } } _a = Chronos; export { chronos, chronosjs, chronosts, chronus, chronusjs, chronusts } from './chronos-fn.js'; export { INTERNALS } from './constants.js'; export { Chronos as Chronus };