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.

690 lines (689 loc) 22.9 kB
"use strict"; /** * ChronosInterval - Duration/Interval handling * @module ChronosInterval */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ChronosInterval = void 0; const types_1 = require("../types"); const utils_1 = require("../utils"); const locales_1 = require("../locales"); // ============================================================================ // ChronosInterval Class // ============================================================================ /** * ChronosInterval - Represents a duration/interval of time * * @example * ```typescript * // Create intervals * const interval = ChronosInterval.create({ days: 5, hours: 3 }); * const hours = ChronosInterval.hours(24); * const fromISO = ChronosInterval.fromISO('P1Y2M3D'); * * // Arithmetic * const doubled = interval.multiply(2); * const combined = interval.add(hours); * * // Formatting * console.log(interval.forHumans()); // "5 days 3 hours" * console.log(interval.toISO()); // "P5DT3H" * ``` */ class ChronosInterval { // ============================================================================ // Constructor // ============================================================================ constructor(duration = {}, inverted = false) { var _a, _b, _c, _d, _e, _f, _g, _h, _j; this._years = (_a = duration.years) !== null && _a !== void 0 ? _a : 0; this._months = (_b = duration.months) !== null && _b !== void 0 ? _b : 0; this._weeks = (_c = duration.weeks) !== null && _c !== void 0 ? _c : 0; this._days = (_d = duration.days) !== null && _d !== void 0 ? _d : 0; this._hours = (_e = duration.hours) !== null && _e !== void 0 ? _e : 0; this._minutes = (_f = duration.minutes) !== null && _f !== void 0 ? _f : 0; this._seconds = Math.floor((_g = duration.seconds) !== null && _g !== void 0 ? _g : 0); this._milliseconds = (_h = duration.milliseconds) !== null && _h !== void 0 ? _h : (((_j = duration.seconds) !== null && _j !== void 0 ? _j : 0) % 1) * 1000; this._locale = (0, locales_1.getLocale)('en'); this._inverted = inverted; } // ============================================================================ // Static Factory Methods // ============================================================================ /** * Create an interval from a duration object */ static create(duration) { return new ChronosInterval(duration); } /** * Create an interval from years */ static years(years) { return new ChronosInterval({ years }); } /** * Create an interval from months */ static months(months) { return new ChronosInterval({ months }); } /** * Create an interval from weeks */ static weeks(weeks) { return new ChronosInterval({ weeks }); } /** * Create an interval from days */ static days(days) { return new ChronosInterval({ days }); } /** * Create an interval from hours */ static hours(hours) { return new ChronosInterval({ hours }); } /** * Create an interval from minutes */ static minutes(minutes) { return new ChronosInterval({ minutes }); } /** * Create an interval from seconds */ static seconds(seconds) { return new ChronosInterval({ seconds }); } /** * Create an interval from milliseconds */ static milliseconds(milliseconds) { return new ChronosInterval({ milliseconds }); } /** * Create a single unit interval */ static unit(amount, unit) { const normalizedUnit = (0, utils_1.normalizeUnit)(unit); const duration = {}; switch (normalizedUnit) { 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 new ChronosInterval(duration); } /** * Create from ISO 8601 duration string (P1Y2M3DT4H5M6S) */ static fromISO(iso) { const duration = (0, utils_1.parseISODuration)(iso); return new ChronosInterval(duration); } /** * Create from a human-readable string * * @example * ```typescript * ChronosInterval.fromString('2 days 3 hours') * ChronosInterval.fromString('1 year, 6 months') * ``` */ static fromString(input) { const duration = {}; const patterns = [ [/(\d+)\s*(?:years?|y)/i, 'years'], [/(\d+)\s*(?:months?|mo)/i, 'months'], [/(\d+)\s*(?:weeks?|w)/i, 'weeks'], [/(\d+)\s*(?:days?|d)/i, 'days'], [/(\d+)\s*(?:hours?|hrs?|h)/i, 'hours'], [/(\d+)\s*(?:minutes?|mins?|m)(?!\w)/i, 'minutes'], [/(\d+)\s*(?:seconds?|secs?|s)(?!\w)/i, 'seconds'], [/(\d+)\s*(?:milliseconds?|ms)/i, 'milliseconds'], ]; for (const [pattern, key] of patterns) { const match = input.match(pattern); if (match) { duration[key] = parseInt(match[1], 10); } } return new ChronosInterval(duration); } /** * Create a zero-length interval */ static zero() { return new ChronosInterval(); } /** * Create an interval from the difference between two dates * * @example * ```typescript * const start = new Date('2024-01-01'); * const end = new Date('2024-03-15'); * const interval = ChronosInterval.between(start, end); * ``` */ static between(start, end) { const startDate = start instanceof Date ? start : start.toDate(); const endDate = end instanceof Date ? end : end.toDate(); const diffMs = endDate.getTime() - startDate.getTime(); const inverted = diffMs < 0; const absDiffMs = Math.abs(diffMs); // Calculate approximate components const days = Math.floor(absDiffMs / types_1.MILLISECONDS_PER_DAY); const remainingMs = absDiffMs % types_1.MILLISECONDS_PER_DAY; const hours = Math.floor(remainingMs / types_1.MILLISECONDS_PER_HOUR); const remainingAfterHours = remainingMs % types_1.MILLISECONDS_PER_HOUR; const minutes = Math.floor(remainingAfterHours / types_1.MILLISECONDS_PER_MINUTE); const remainingAfterMinutes = remainingAfterHours % types_1.MILLISECONDS_PER_MINUTE; const seconds = Math.floor(remainingAfterMinutes / types_1.MILLISECONDS_PER_SECOND); const milliseconds = remainingAfterMinutes % types_1.MILLISECONDS_PER_SECOND; return new ChronosInterval({ days, hours, minutes, seconds, milliseconds }, inverted); } // ============================================================================ // Getters // ============================================================================ get years() { return this._years; } get months() { return this._months; } get weeks() { return this._weeks; } get days() { return this._days; } get hours() { return this._hours; } get minutes() { return this._minutes; } get seconds() { return this._seconds; } get milliseconds() { return this._milliseconds; } get inverted() { return this._inverted; } // ============================================================================ // Total Calculations // ============================================================================ /** * Get total duration in a specific unit */ total(unit) { const ms = this.totalMilliseconds(); const normalizedUnit = (0, utils_1.normalizeUnit)(unit); switch (normalizedUnit) { case 'millisecond': return ms; case 'second': return ms / types_1.MILLISECONDS_PER_SECOND; case 'minute': return ms / types_1.MILLISECONDS_PER_MINUTE; case 'hour': return ms / types_1.MILLISECONDS_PER_HOUR; case 'day': return ms / types_1.MILLISECONDS_PER_DAY; case 'week': return ms / types_1.MILLISECONDS_PER_WEEK; case 'month': return ms / (types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY); case 'year': return ms / (types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY); default: return ms; } } /** * Get total duration in milliseconds */ totalMilliseconds() { let ms = this._milliseconds; ms += this._seconds * types_1.MILLISECONDS_PER_SECOND; ms += this._minutes * types_1.MILLISECONDS_PER_MINUTE; ms += this._hours * types_1.MILLISECONDS_PER_HOUR; ms += this._days * types_1.MILLISECONDS_PER_DAY; ms += this._weeks * types_1.MILLISECONDS_PER_WEEK; ms += this._months * types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY; ms += this._years * types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY; return this._inverted ? -ms : ms; } /** * Get total duration in seconds */ totalSeconds() { return this.total('seconds'); } /** * Get total duration in minutes */ totalMinutes() { return this.total('minutes'); } /** * Get total duration in hours */ totalHours() { return this.total('hours'); } /** * Get total duration in days */ totalDays() { return this.total('days'); } /** * Get total duration in weeks */ totalWeeks() { return this.total('weeks'); } /** * Get total duration in months (approximate) */ totalMonths() { return this.total('months'); } /** * Get total duration in years (approximate) */ totalYears() { return this.total('years'); } // ============================================================================ // Arithmetic Operations // ============================================================================ /** * Add another interval to this one */ add(other) { const otherInterval = other instanceof ChronosInterval ? other : ChronosInterval.create(other); return new ChronosInterval({ years: this._years + otherInterval._years, months: this._months + otherInterval._months, weeks: this._weeks + otherInterval._weeks, days: this._days + otherInterval._days, hours: this._hours + otherInterval._hours, minutes: this._minutes + otherInterval._minutes, seconds: this._seconds + otherInterval._seconds, milliseconds: this._milliseconds + otherInterval._milliseconds, }); } /** * Subtract another interval from this one */ subtract(other) { const otherInterval = other instanceof ChronosInterval ? other : ChronosInterval.create(other); return new ChronosInterval({ years: this._years - otherInterval._years, months: this._months - otherInterval._months, weeks: this._weeks - otherInterval._weeks, days: this._days - otherInterval._days, hours: this._hours - otherInterval._hours, minutes: this._minutes - otherInterval._minutes, seconds: this._seconds - otherInterval._seconds, milliseconds: this._milliseconds - otherInterval._milliseconds, }); } /** * Multiply the interval by a factor */ multiply(factor) { return new ChronosInterval({ years: this._years * factor, months: this._months * factor, weeks: this._weeks * factor, days: this._days * factor, hours: this._hours * factor, minutes: this._minutes * factor, seconds: this._seconds * factor, milliseconds: this._milliseconds * factor, }); } /** * Divide the interval by a factor */ divide(factor) { if (factor === 0) { throw new Error('Cannot divide by zero'); } return this.multiply(1 / factor); } /** * Negate the interval */ negate() { return new ChronosInterval({ years: this._years, months: this._months, weeks: this._weeks, days: this._days, hours: this._hours, minutes: this._minutes, seconds: this._seconds, milliseconds: this._milliseconds, }, !this._inverted); } /** * Get absolute value of interval */ abs() { return new ChronosInterval({ years: Math.abs(this._years), months: Math.abs(this._months), weeks: Math.abs(this._weeks), days: Math.abs(this._days), hours: Math.abs(this._hours), minutes: Math.abs(this._minutes), seconds: Math.abs(this._seconds), milliseconds: Math.abs(this._milliseconds), }, false); } // ============================================================================ // Comparison Methods // ============================================================================ /** * Check if equal to another interval */ equals(other) { return this.totalMilliseconds() === other.totalMilliseconds(); } /** * Check if greater than another interval */ greaterThan(other) { return this.totalMilliseconds() > other.totalMilliseconds(); } /** * Check if less than another interval */ lessThan(other) { return this.totalMilliseconds() < other.totalMilliseconds(); } /** * Check if greater than or equal to another interval */ greaterThanOrEqual(other) { return this.totalMilliseconds() >= other.totalMilliseconds(); } /** * Check if less than or equal to another interval */ lessThanOrEqual(other) { return this.totalMilliseconds() <= other.totalMilliseconds(); } /** * Check if the interval is zero */ isZero() { return this.totalMilliseconds() === 0; } /** * Check if the interval is positive */ isPositive() { return this.totalMilliseconds() > 0; } /** * Check if the interval is negative */ isNegative() { return this.totalMilliseconds() < 0; } // ============================================================================ // Normalization Methods // ============================================================================ /** * Cascade units to proper values (normalize overflow) * * @example * 90 seconds becomes 1 minute 30 seconds */ cascade() { let ms = Math.abs(this.totalMilliseconds()); const years = Math.floor(ms / (types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY)); ms %= types_1.AVERAGE_DAYS_PER_YEAR * types_1.MILLISECONDS_PER_DAY; const months = Math.floor(ms / (types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY)); ms %= types_1.AVERAGE_DAYS_PER_MONTH * types_1.MILLISECONDS_PER_DAY; const weeks = Math.floor(ms / types_1.MILLISECONDS_PER_WEEK); ms %= types_1.MILLISECONDS_PER_WEEK; const days = Math.floor(ms / types_1.MILLISECONDS_PER_DAY); ms %= types_1.MILLISECONDS_PER_DAY; const hours = Math.floor(ms / types_1.MILLISECONDS_PER_HOUR); ms %= types_1.MILLISECONDS_PER_HOUR; const minutes = Math.floor(ms / types_1.MILLISECONDS_PER_MINUTE); ms %= types_1.MILLISECONDS_PER_MINUTE; const seconds = Math.floor(ms / types_1.MILLISECONDS_PER_SECOND); const milliseconds = ms % types_1.MILLISECONDS_PER_SECOND; return new ChronosInterval({ years, months, weeks, days, hours, minutes, seconds, milliseconds, }, this._inverted); } /** * Cascade without including weeks */ cascadeWithoutWeeks() { const cascaded = this.cascade(); return new ChronosInterval({ years: cascaded._years, months: cascaded._months, days: cascaded._days + cascaded._weeks * 7, hours: cascaded._hours, minutes: cascaded._minutes, seconds: cascaded._seconds, milliseconds: cascaded._milliseconds, }, this._inverted); } // ============================================================================ // Formatting Methods // ============================================================================ /** * Format as ISO 8601 duration */ toISO() { return (0, utils_1.durationToISO)({ years: this._years, months: this._months, weeks: this._weeks, days: this._days, hours: this._hours, minutes: this._minutes, seconds: this._seconds + this._milliseconds / 1000, }); } /** * Format for human reading * * @example * ```typescript * interval.forHumans() // "2 days 3 hours" * interval.forHumans({ short: true }) // "2d 3h" * interval.forHumans({ parts: 2 }) // "2 days 3 hours" (max 2 parts) * ``` */ forHumans(options = {}) { const { short = false, parts = 7, join = ' ', conjunction } = options; const cascaded = this.cascade(); const result = []; const units = [ [cascaded._years, 'year'], [cascaded._months, 'month'], [cascaded._weeks, 'week'], [cascaded._days, 'day'], [cascaded._hours, 'hour'], [cascaded._minutes, 'minute'], [cascaded._seconds, 'second'], ]; for (const [value, unit] of units) { if (value !== 0 && result.length < parts) { const label = short ? unit[0] : ` ${(0, utils_1.pluralizeUnit)(unit, value)}`; result.push(`${value}${label}`); } } if (result.length === 0) { return short ? '0s' : '0 seconds'; } if (conjunction && result.length > 1) { const last = result.pop(); return `${result.join(join)}${conjunction}${last}`; } return result.join(join); } /** * Format using a format string * * Tokens: * - %y: years * - %m: months * - %w: weeks * - %d: days * - %h: hours * - %i: minutes * - %s: seconds * - %f: milliseconds * - %R: +/- sign * - %r: +/- or empty */ format(formatStr) { const sign = this._inverted ? '-' : '+'; return formatStr .replace(/%y/g, String(Math.abs(this._years))) .replace(/%m/g, String(Math.abs(this._months))) .replace(/%w/g, String(Math.abs(this._weeks))) .replace(/%d/g, String(Math.abs(this._days))) .replace(/%h/g, String(Math.abs(this._hours))) .replace(/%H/g, (0, utils_1.padStart)(Math.abs(this._hours), 2)) .replace(/%i/g, String(Math.abs(this._minutes))) .replace(/%I/g, (0, utils_1.padStart)(Math.abs(this._minutes), 2)) .replace(/%s/g, String(Math.abs(this._seconds))) .replace(/%S/g, (0, utils_1.padStart)(Math.abs(this._seconds), 2)) .replace(/%f/g, String(Math.abs(this._milliseconds))) .replace(/%R/g, sign) .replace(/%r/g, this._inverted ? '-' : ''); } // ============================================================================ // Conversion Methods // ============================================================================ /** * Convert to Duration object */ toDuration() { return { years: this._years, months: this._months, weeks: this._weeks, days: this._days, hours: this._hours, minutes: this._minutes, seconds: this._seconds, milliseconds: this._milliseconds, }; } /** * Convert to array */ toArray() { return [ this._years, this._months, this._weeks, this._days, this._hours, this._minutes, this._seconds, this._milliseconds, ]; } /** * Clone this interval */ clone() { const cloned = new ChronosInterval(this.toDuration(), this._inverted); cloned._locale = this._locale; return cloned; } /** * Set locale for this interval */ locale(code) { const cloned = this.clone(); cloned._locale = (0, locales_1.getLocale)(code); return cloned; } /** * Get primitive value (total milliseconds) */ valueOf() { return this.totalMilliseconds(); } /** * Convert to string */ toString() { return this.forHumans(); } /** * Convert to JSON */ toJSON() { return Object.assign(Object.assign({}, this.toDuration()), { iso: this.toISO() }); } } exports.ChronosInterval = ChronosInterval; // ============================================================================ // Export Default // ============================================================================ exports.default = ChronosInterval;