UNPKG

chronos-ts

Version:

A comprehensive TypeScript library for date and time manipulation, inspired by Carbon PHP. Features immutable API, intervals, periods, timezones, and i18n support.

659 lines (658 loc) 21.6 kB
"use strict"; /** * Utility functions for Chronos * @module utils */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MILLISECONDS_PER_YEAR = exports.MILLISECONDS_PER_MONTH = exports.MILLISECONDS_PER_WEEK = exports.MILLISECONDS_PER_DAY = exports.MILLISECONDS_PER_HOUR = exports.MILLISECONDS_PER_MINUTE = exports.MILLISECONDS_PER_SECOND = void 0; exports.isDate = isDate; exports.isValidDateInput = isValidDateInput; exports.isChronosLike = isChronosLike; exports.isDuration = isDuration; exports.isISODuration = isISODuration; exports.isLeapYear = isLeapYear; exports.normalizeUnit = normalizeUnit; exports.pluralizeUnit = pluralizeUnit; exports.getMillisecondsPerUnit = getMillisecondsPerUnit; exports.getDaysInMonth = getDaysInMonth; exports.getDaysInYear = getDaysInYear; exports.getDayOfYear = getDayOfYear; exports.getISOWeek = getISOWeek; exports.getISOWeekYear = getISOWeekYear; exports.getQuarter = getQuarter; exports.startOf = startOf; exports.endOf = endOf; exports.addDuration = addDuration; exports.subtractDuration = subtractDuration; exports.addUnits = addUnits; exports.diffInUnits = diffInUnits; exports.parseISODuration = parseISODuration; exports.durationToISO = durationToISO; exports.compareAtGranularity = compareAtGranularity; exports.isSameAt = isSameAt; exports.cloneDate = cloneDate; exports.isValidDate = isValidDate; exports.clamp = clamp; exports.ordinalSuffix = ordinalSuffix; exports.padStart = padStart; const types_1 = require("../types"); Object.defineProperty(exports, "MILLISECONDS_PER_SECOND", { enumerable: true, get: function () { return types_1.MILLISECONDS_PER_SECOND; } }); Object.defineProperty(exports, "MILLISECONDS_PER_MINUTE", { enumerable: true, get: function () { return types_1.MILLISECONDS_PER_MINUTE; } }); Object.defineProperty(exports, "MILLISECONDS_PER_HOUR", { enumerable: true, get: function () { return types_1.MILLISECONDS_PER_HOUR; } }); Object.defineProperty(exports, "MILLISECONDS_PER_DAY", { enumerable: true, get: function () { return types_1.MILLISECONDS_PER_DAY; } }); Object.defineProperty(exports, "MILLISECONDS_PER_WEEK", { enumerable: true, get: function () { return types_1.MILLISECONDS_PER_WEEK; } }); Object.defineProperty(exports, "MILLISECONDS_PER_MONTH", { enumerable: true, get: function () { return types_1.MILLISECONDS_PER_MONTH; } }); Object.defineProperty(exports, "MILLISECONDS_PER_YEAR", { enumerable: true, get: function () { return types_1.MILLISECONDS_PER_YEAR; } }); // ============================================================================ // Type Guards // ============================================================================ /** * Check if value is a Date object */ function isDate(value) { return value instanceof Date && !isNaN(value.getTime()); } /** * Check if value is a valid date input */ function isValidDateInput(value) { if (value === null || value === undefined) return true; if (typeof value === 'string' || typeof value === 'number') return true; if (isDate(value)) return true; if (isChronosLike(value)) return true; return false; } /** * Check if value implements ChronosLike interface */ function isChronosLike(value) { if (!value || typeof value !== 'object') return false; const obj = value; return typeof obj.toDate === 'function' && typeof obj.valueOf === 'function'; } /** * Check if value is a Duration object */ function isDuration(value) { if (!value || typeof value !== 'object') return false; const obj = value; const keys = [ 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', ]; return keys.some((key) => typeof obj[key] === 'number'); } /** * Check if value is a valid ISO 8601 duration string */ function isISODuration(value) { if (typeof value !== 'string') return false; const pattern = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; return pattern.test(value); } /** * Check if a year is a leap year */ function isLeapYear(year) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } // ============================================================================ // Time Unit Utilities // ============================================================================ /** * Unit aliases mapping */ const UNIT_ALIASES = { // Singular millisecond: 'millisecond', second: 'second', minute: 'minute', hour: 'hour', day: 'day', week: 'week', month: 'month', quarter: 'quarter', year: 'year', decade: 'decade', century: 'century', millennium: 'millennium', // Plural milliseconds: 'millisecond', seconds: 'second', minutes: 'minute', hours: 'hour', days: 'day', weeks: 'week', months: 'month', quarters: 'quarter', years: 'year', decades: 'decade', centuries: 'century', millennia: 'millennium', // Short ms: 'millisecond', s: 'second', m: 'minute', h: 'hour', d: 'day', w: 'week', M: 'month', Q: 'quarter', y: 'year', }; /** * Normalize a time unit to its canonical form */ function normalizeUnit(unit) { var _a; // Check original case first for case-sensitive short codes (M vs m, etc.) const normalized = (_a = UNIT_ALIASES[unit]) !== null && _a !== void 0 ? _a : UNIT_ALIASES[unit.toLowerCase()]; if (!normalized) { throw new Error(`Invalid time unit: ${unit}`); } return normalized; } /** * Get the plural form of a time unit */ function pluralizeUnit(unit, count) { const plurals = { millisecond: 'milliseconds', second: 'seconds', minute: 'minutes', hour: 'hours', day: 'days', week: 'weeks', month: 'months', quarter: 'quarters', year: 'years', decade: 'decades', century: 'centuries', millennium: 'millennia', }; return Math.abs(count) === 1 ? unit : plurals[unit]; } /** * Get milliseconds for a given time unit */ function getMillisecondsPerUnit(unit) { switch (unit) { case 'millisecond': return 1; case 'second': return types_1.MILLISECONDS_PER_SECOND; case 'minute': return types_1.MILLISECONDS_PER_MINUTE; case 'hour': return types_1.MILLISECONDS_PER_HOUR; case 'day': return types_1.MILLISECONDS_PER_DAY; case 'week': return types_1.MILLISECONDS_PER_WEEK; case 'month': return types_1.MILLISECONDS_PER_MONTH; case 'quarter': return types_1.MILLISECONDS_PER_MONTH * 3; case 'year': return types_1.MILLISECONDS_PER_YEAR; case 'decade': return types_1.MILLISECONDS_PER_YEAR * 10; case 'century': return types_1.MILLISECONDS_PER_YEAR * 100; case 'millennium': return types_1.MILLISECONDS_PER_YEAR * 1000; default: throw new Error(`Unknown unit: ${unit}`); } } // ============================================================================ // Date Utilities // ============================================================================ /** * Get the number of days in a specific month */ function getDaysInMonth(year, month) { // Create date for first day of next month, then subtract one day return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); } /** * Get the number of days in a year */ function getDaysInYear(year) { return isLeapYear(year) ? 366 : 365; } /** * Get the day of year (1-366) */ function getDayOfYear(date) { // Use local time consistently (not UTC) to avoid timezone issues const start = new Date(date.getFullYear(), 0, 0); const diff = date.getTime() - start.getTime(); return Math.floor(diff / types_1.MILLISECONDS_PER_DAY); } /** * Get the ISO week number (1-53) */ function getISOWeek(date) { const target = new Date(date.valueOf()); // Set to nearest Thursday: current date + 4 - current day number (make Sunday=7) const dayNr = (date.getDay() + 6) % 7; target.setDate(target.getDate() - dayNr + 3); // Store first Thursday of year const firstThursday = target.valueOf(); // Set to start of year target.setMonth(0, 1); // If not Thursday, set to first Thursday of year if (target.getDay() !== 4) { target.setMonth(0, 1 + ((4 - target.getDay() + 7) % 7)); } // Calculate week number return (1 + Math.ceil((firstThursday - target.valueOf()) / types_1.MILLISECONDS_PER_WEEK)); } /** * Get the ISO week year */ function getISOWeekYear(date) { const target = new Date(date.valueOf()); target.setDate(target.getDate() + 3 - ((date.getDay() + 6) % 7)); return target.getFullYear(); } /** * Get the quarter (1-4) */ function getQuarter(date) { return Math.floor(date.getMonth() / 3) + 1; } /** * Get start of a time unit */ function startOf(date, unit) { const result = new Date(date); switch (unit) { case 'second': result.setMilliseconds(0); break; case 'minute': result.setSeconds(0, 0); break; case 'hour': result.setMinutes(0, 0, 0); break; case 'day': result.setHours(0, 0, 0, 0); break; case 'week': result.setHours(0, 0, 0, 0); result.setDate(result.getDate() - result.getDay()); break; case 'month': result.setHours(0, 0, 0, 0); result.setDate(1); break; case 'quarter': result.setHours(0, 0, 0, 0); result.setMonth(Math.floor(result.getMonth() / 3) * 3, 1); break; case 'year': result.setHours(0, 0, 0, 0); result.setMonth(0, 1); break; case 'decade': result.setHours(0, 0, 0, 0); result.setMonth(0, 1); result.setFullYear(Math.floor(result.getFullYear() / 10) * 10); break; case 'century': result.setHours(0, 0, 0, 0); result.setMonth(0, 1); result.setFullYear(Math.floor(result.getFullYear() / 100) * 100); break; case 'millennium': result.setHours(0, 0, 0, 0); result.setMonth(0, 1); result.setFullYear(Math.floor(result.getFullYear() / 1000) * 1000); break; case 'millisecond': default: break; } return result; } /** * Get end of a time unit */ function endOf(date, unit) { const result = new Date(date); switch (unit) { case 'second': result.setMilliseconds(999); break; case 'minute': result.setSeconds(59, 999); break; case 'hour': result.setMinutes(59, 59, 999); break; case 'day': result.setHours(23, 59, 59, 999); break; case 'week': result.setHours(23, 59, 59, 999); result.setDate(result.getDate() + (6 - result.getDay())); break; case 'month': result.setHours(23, 59, 59, 999); result.setMonth(result.getMonth() + 1, 0); break; case 'quarter': result.setHours(23, 59, 59, 999); result.setMonth(Math.floor(result.getMonth() / 3) * 3 + 3, 0); break; case 'year': result.setHours(23, 59, 59, 999); result.setMonth(11, 31); break; case 'decade': result.setHours(23, 59, 59, 999); result.setFullYear(Math.floor(result.getFullYear() / 10) * 10 + 9, 11, 31); break; case 'century': result.setHours(23, 59, 59, 999); result.setFullYear(Math.floor(result.getFullYear() / 100) * 100 + 99, 11, 31); break; case 'millennium': result.setHours(23, 59, 59, 999); result.setFullYear(Math.floor(result.getFullYear() / 1000) * 1000 + 999, 11, 31); break; case 'millisecond': default: break; } return result; } // ============================================================================ // Arithmetic Utilities // ============================================================================ /** * Add a duration to a date * Handles month overflow by clamping to the last day of the month */ function addDuration(date, duration) { const result = new Date(date); if (duration.years) { const originalDay = result.getDate(); result.setFullYear(result.getFullYear() + duration.years); // Handle year overflow (e.g., Feb 29 in a leap year to Feb 28 in non-leap year) if (result.getDate() !== originalDay) { result.setDate(0); // Go to last day of previous month } } if (duration.months) { const originalDay = result.getDate(); result.setMonth(result.getMonth() + duration.months); // Handle month overflow (e.g., Jan 31 + 1 month = Feb 28/29, not March 2/3) if (result.getDate() !== originalDay) { result.setDate(0); // Go to last day of previous month } } if (duration.weeks) { result.setDate(result.getDate() + duration.weeks * 7); } if (duration.days) { result.setDate(result.getDate() + duration.days); } if (duration.hours) { result.setHours(result.getHours() + duration.hours); } if (duration.minutes) { result.setMinutes(result.getMinutes() + duration.minutes); } if (duration.seconds) { result.setSeconds(result.getSeconds() + duration.seconds); } if (duration.milliseconds) { result.setMilliseconds(result.getMilliseconds() + duration.milliseconds); } return result; } /** * Subtract a duration from a date */ function subtractDuration(date, duration) { const negated = {}; for (const [key, value] of Object.entries(duration)) { if (typeof value === 'number') { negated[key] = -value; } } return addDuration(date, negated); } /** * Add a specific number of units to a date */ function addUnits(date, amount, unit) { const duration = {}; switch (unit) { case 'millisecond': duration.milliseconds = amount; break; case 'second': duration.seconds = amount; break; case 'minute': duration.minutes = amount; break; case 'hour': duration.hours = amount; break; case 'day': duration.days = amount; break; case 'week': duration.weeks = amount; break; case 'month': duration.months = amount; break; case 'quarter': duration.months = amount * 3; break; case 'year': duration.years = amount; break; case 'decade': duration.years = amount * 10; break; case 'century': duration.years = amount * 100; break; case 'millennium': duration.years = amount * 1000; break; } return addDuration(date, duration); } // ============================================================================ // Difference Utilities // ============================================================================ /** * Calculate the difference between two dates in a specific unit */ function diffInUnits(date1, date2, unit) { const diff = date1.getTime() - date2.getTime(); switch (unit) { case 'millisecond': return diff; case 'second': return Math.floor(diff / types_1.MILLISECONDS_PER_SECOND); case 'minute': return Math.floor(diff / types_1.MILLISECONDS_PER_MINUTE); case 'hour': return Math.floor(diff / types_1.MILLISECONDS_PER_HOUR); case 'day': return Math.floor(diff / types_1.MILLISECONDS_PER_DAY); case 'week': return Math.floor(diff / types_1.MILLISECONDS_PER_WEEK); case 'month': return monthDiff(date2, date1); case 'quarter': return Math.floor(monthDiff(date2, date1) / 3); case 'year': return date1.getFullYear() - date2.getFullYear(); case 'decade': return Math.floor((date1.getFullYear() - date2.getFullYear()) / 10); case 'century': return Math.floor((date1.getFullYear() - date2.getFullYear()) / 100); case 'millennium': return Math.floor((date1.getFullYear() - date2.getFullYear()) / 1000); default: throw new Error(`Unknown unit: ${unit}`); } } /** * Calculate month difference between two dates */ function monthDiff(from, to) { const years = to.getFullYear() - from.getFullYear(); const months = to.getMonth() - from.getMonth(); const days = to.getDate() - from.getDate(); let diff = years * 12 + months; // Adjust for day difference if (days < 0) { diff--; } return diff; } // ============================================================================ // Parsing Utilities // ============================================================================ /** * Parse an ISO 8601 duration string */ function parseISODuration(duration) { const pattern = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; const match = duration.match(pattern); if (!match) { throw new Error(`Invalid ISO 8601 duration: ${duration}`); } return { years: match[1] ? parseInt(match[1], 10) : undefined, months: match[2] ? parseInt(match[2], 10) : undefined, weeks: match[3] ? parseInt(match[3], 10) : undefined, days: match[4] ? parseInt(match[4], 10) : undefined, hours: match[5] ? parseInt(match[5], 10) : undefined, minutes: match[6] ? parseInt(match[6], 10) : undefined, seconds: match[7] ? parseFloat(match[7]) : undefined, }; } /** * Convert a duration to ISO 8601 format */ function durationToISO(duration) { var _a, _b; let result = 'P'; if (duration.years) result += `${duration.years}Y`; if (duration.months) result += `${duration.months}M`; if (duration.weeks) result += `${duration.weeks}W`; if (duration.days) result += `${duration.days}D`; const hasTime = duration.hours || duration.minutes || duration.seconds || duration.milliseconds; if (hasTime) { result += 'T'; if (duration.hours) result += `${duration.hours}H`; if (duration.minutes) result += `${duration.minutes}M`; const seconds = ((_a = duration.seconds) !== null && _a !== void 0 ? _a : 0) + ((_b = duration.milliseconds) !== null && _b !== void 0 ? _b : 0) / 1000; if (seconds) result += `${seconds}S`; } return result === 'P' ? 'PT0S' : result; } // ============================================================================ // Comparison Utilities // ============================================================================ /** * Compare two dates at a specific granularity */ function compareAtGranularity(date1, date2, unit) { const d1 = startOf(date1, unit); const d2 = startOf(date2, unit); if (d1.getTime() < d2.getTime()) return -1; if (d1.getTime() > d2.getTime()) return 1; return 0; } /** * Check if two dates are the same at a specific granularity */ function isSameAt(date1, date2, unit) { return compareAtGranularity(date1, date2, unit) === 0; } // ============================================================================ // Cloning Utilities // ============================================================================ /** * Create a deep clone of a date */ function cloneDate(date) { return new Date(date.getTime()); } // ============================================================================ // Validation Utilities // ============================================================================ /** * Check if a date is valid */ function isValidDate(date) { return date instanceof Date && !isNaN(date.getTime()); } /** * Ensure a value is within bounds */ function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } // ============================================================================ // Ordinal Utilities // ============================================================================ /** * Get ordinal suffix for a number (1st, 2nd, 3rd, etc.) */ function ordinalSuffix(n) { const s = ['th', 'st', 'nd', 'rd']; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } // ============================================================================ // Padding Utilities // ============================================================================ /** * Pad a number with leading zeros */ function padStart(value, length, char = '0') { const str = String(value); if (str.length >= length) return str; return char.repeat(length - str.length) + str; }