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.

680 lines (679 loc) 24.3 kB
"use strict"; /** * ChronosTimezone - Timezone handling and conversions * @module ChronosTimezone */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Timezones = exports.ChronosTimezone = exports.TIMEZONES = void 0; // ============================================================================ // Timezone Data // ============================================================================ /** * Common timezone identifiers */ exports.TIMEZONES = { // UTC UTC: 'UTC', GMT: 'GMT', // Americas 'America/New_York': 'America/New_York', 'America/Chicago': 'America/Chicago', 'America/Denver': 'America/Denver', 'America/Los_Angeles': 'America/Los_Angeles', 'America/Phoenix': 'America/Phoenix', 'America/Anchorage': 'America/Anchorage', 'America/Toronto': 'America/Toronto', 'America/Vancouver': 'America/Vancouver', 'America/Mexico_City': 'America/Mexico_City', 'America/Sao_Paulo': 'America/Sao_Paulo', 'America/Buenos_Aires': 'America/Buenos_Aires', 'America/Lima': 'America/Lima', 'America/Bogota': 'America/Bogota', // Europe 'Europe/London': 'Europe/London', 'Europe/Paris': 'Europe/Paris', 'Europe/Berlin': 'Europe/Berlin', 'Europe/Madrid': 'Europe/Madrid', 'Europe/Rome': 'Europe/Rome', 'Europe/Amsterdam': 'Europe/Amsterdam', 'Europe/Brussels': 'Europe/Brussels', 'Europe/Vienna': 'Europe/Vienna', 'Europe/Warsaw': 'Europe/Warsaw', 'Europe/Prague': 'Europe/Prague', 'Europe/Moscow': 'Europe/Moscow', 'Europe/Istanbul': 'Europe/Istanbul', 'Europe/Athens': 'Europe/Athens', 'Europe/Helsinki': 'Europe/Helsinki', 'Europe/Stockholm': 'Europe/Stockholm', 'Europe/Oslo': 'Europe/Oslo', 'Europe/Copenhagen': 'Europe/Copenhagen', 'Europe/Dublin': 'Europe/Dublin', 'Europe/Zurich': 'Europe/Zurich', // Asia 'Asia/Tokyo': 'Asia/Tokyo', 'Asia/Shanghai': 'Asia/Shanghai', 'Asia/Hong_Kong': 'Asia/Hong_Kong', 'Asia/Singapore': 'Asia/Singapore', 'Asia/Seoul': 'Asia/Seoul', 'Asia/Taipei': 'Asia/Taipei', 'Asia/Bangkok': 'Asia/Bangkok', 'Asia/Jakarta': 'Asia/Jakarta', 'Asia/Manila': 'Asia/Manila', 'Asia/Kuala_Lumpur': 'Asia/Kuala_Lumpur', 'Asia/Ho_Chi_Minh': 'Asia/Ho_Chi_Minh', 'Asia/Dubai': 'Asia/Dubai', 'Asia/Kolkata': 'Asia/Kolkata', 'Asia/Mumbai': 'Asia/Mumbai', 'Asia/Karachi': 'Asia/Karachi', 'Asia/Dhaka': 'Asia/Dhaka', 'Asia/Tehran': 'Asia/Tehran', 'Asia/Riyadh': 'Asia/Riyadh', 'Asia/Jerusalem': 'Asia/Jerusalem', // Australia/Pacific 'Australia/Sydney': 'Australia/Sydney', 'Australia/Melbourne': 'Australia/Melbourne', 'Australia/Brisbane': 'Australia/Brisbane', 'Australia/Perth': 'Australia/Perth', 'Australia/Adelaide': 'Australia/Adelaide', 'Pacific/Auckland': 'Pacific/Auckland', 'Pacific/Fiji': 'Pacific/Fiji', 'Pacific/Honolulu': 'Pacific/Honolulu', // Africa 'Africa/Cairo': 'Africa/Cairo', 'Africa/Johannesburg': 'Africa/Johannesburg', 'Africa/Lagos': 'Africa/Lagos', 'Africa/Nairobi': 'Africa/Nairobi', 'Africa/Casablanca': 'Africa/Casablanca', }; // ============================================================================ // ChronosTimezone Class // ============================================================================ /** * ChronosTimezone - Handles timezone operations and conversions * * This class provides comprehensive timezone handling including: * - Timezone information retrieval * - Offset calculations * - DST detection * - Timezone conversions * * @example * ```typescript * // Get timezone info * const tz = ChronosTimezone.create('America/New_York'); * console.log(tz.offset); // -5 or -4 depending on DST * * // Check DST * console.log(tz.isDST(new Date())); // true/false * * // Convert between timezones * const utcDate = new Date(); * const localDate = ChronosTimezone.convert(utcDate, 'UTC', 'America/New_York'); * ``` */ class ChronosTimezone { // ============================================================================ // Constructor // ============================================================================ /** * Create a new ChronosTimezone */ constructor(identifier = 'UTC') { this._originalOffset = null; this._extraMinutes = 0; // For non-whole-hour offsets like +05:30 this._cachedOffset = null; this._cachedDate = null; const normalized = this._normalizeIdentifier(identifier); this._identifier = normalized.identifier; this._originalOffset = normalized.originalOffset; this._extraMinutes = normalized.extraMinutes; } /** * Normalize timezone identifier */ _normalizeIdentifier(identifier) { // Handle UTC aliases if (identifier.toUpperCase() === 'Z' || identifier.toUpperCase() === 'GMT') { return { identifier: 'UTC', originalOffset: null, extraMinutes: 0 }; } // Handle offset strings like +05:30, -08:00 if (/^[+-]\d{2}:\d{2}$/.test(identifier)) { // Store original offset and convert to Etc/GMT for internal use const offsetHours = this._parseOffsetString(identifier); const sign = offsetHours >= 0 ? 1 : -1; const absHours = Math.abs(offsetHours); const wholeHours = Math.floor(absHours); // Calculate extra minutes for non-whole-hour offsets (e.g., +05:30 has 30 extra minutes) const extraMinutes = Math.round((absHours - wholeHours) * 60) * sign; const etcGmt = `Etc/GMT${offsetHours >= 0 ? '-' : '+'}${wholeHours}`; return { identifier: etcGmt, originalOffset: identifier, extraMinutes }; } return { identifier, originalOffset: null, extraMinutes: 0 }; } /** * Parse offset string to hours */ _parseOffsetString(offset) { const match = offset.match(/^([+-])(\d{2}):(\d{2})$/); if (!match) return 0; const sign = match[1] === '+' ? 1 : -1; const hours = parseInt(match[2], 10); const minutes = parseInt(match[3], 10); return sign * (hours + minutes / 60); } // ============================================================================ // Static Factory Methods // ============================================================================ /** * Create a timezone instance */ static create(identifier = 'UTC') { return new ChronosTimezone(identifier); } /** * Create UTC timezone */ static utc() { return new ChronosTimezone('UTC'); } /** * Create timezone from local system timezone */ static local() { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; return new ChronosTimezone(tz); } /** * Create timezone from offset in hours */ static fromOffset(offsetHours) { const sign = offsetHours >= 0 ? '+' : '-'; const absOffset = Math.abs(offsetHours); const hours = Math.floor(absOffset); const minutes = Math.round((absOffset - hours) * 60); const offsetString = `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; return new ChronosTimezone(offsetString); } /** * Get the local system timezone identifier */ static localIdentifier() { return Intl.DateTimeFormat().resolvedOptions().timeZone; } // ============================================================================ // Getters // ============================================================================ /** * Get timezone identifier * Returns the original offset string if created from an offset, otherwise returns the IANA identifier */ get identifier() { var _a; return (_a = this._originalOffset) !== null && _a !== void 0 ? _a : this._identifier; } /** * Get the internal IANA timezone identifier (used for Intl operations) */ get ianaIdentifier() { return this._identifier; } /** * Get timezone name (alias for identifier) */ get name() { var _a; return (_a = this._originalOffset) !== null && _a !== void 0 ? _a : this._identifier; } /** * Get timezone abbreviation for a given date */ getAbbreviation(date = new Date()) { var _a; try { const formatter = new Intl.DateTimeFormat('en-US', { timeZone: this._identifier, timeZoneName: 'short', }); const parts = formatter.formatToParts(date); const tzPart = parts.find((p) => p.type === 'timeZoneName'); return (_a = tzPart === null || tzPart === void 0 ? void 0 : tzPart.value) !== null && _a !== void 0 ? _a : this._identifier; } catch (_b) { return this._identifier; } } /** * Get full timezone name for a given date */ getFullName(date = new Date()) { var _a; try { const formatter = new Intl.DateTimeFormat('en-US', { timeZone: this._identifier, timeZoneName: 'long', }); const parts = formatter.formatToParts(date); const tzPart = parts.find((p) => p.type === 'timeZoneName'); return (_a = tzPart === null || tzPart === void 0 ? void 0 : tzPart.value) !== null && _a !== void 0 ? _a : this._identifier; } catch (_b) { return this._identifier; } } // ============================================================================ // Offset Calculations // ============================================================================ /** * Get UTC offset in minutes for a given date */ getOffsetMinutes(date = new Date()) { try { // Create formatters for UTC and target timezone const utcFormatter = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false, }); const tzFormatter = new Intl.DateTimeFormat('en-US', { timeZone: this._identifier, year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false, }); const utcParts = this._parseIntlParts(utcFormatter.formatToParts(date)); const tzParts = this._parseIntlParts(tzFormatter.formatToParts(date)); const utcDate = new Date(Date.UTC(utcParts.year, utcParts.month - 1, utcParts.day, utcParts.hour, utcParts.minute)); const tzDate = new Date(Date.UTC(tzParts.year, tzParts.month - 1, tzParts.day, tzParts.hour, tzParts.minute)); // Add extra minutes for non-whole-hour offsets (e.g., +05:30) return ((tzDate.getTime() - utcDate.getTime()) / 60000 + this._extraMinutes); } catch (_a) { return this._extraMinutes; } } /** * Parse Intl formatter parts to components */ _parseIntlParts(parts) { const result = { year: 0, month: 0, day: 0, hour: 0, minute: 0 }; for (const part of parts) { switch (part.type) { case 'year': result.year = parseInt(part.value, 10); break; case 'month': result.month = parseInt(part.value, 10); break; case 'day': result.day = parseInt(part.value, 10); break; case 'hour': result.hour = parseInt(part.value, 10); break; case 'minute': result.minute = parseInt(part.value, 10); break; } } return result; } /** * Get UTC offset in hours for a given date */ getOffsetHours(date = new Date()) { return this.getOffsetMinutes(date) / 60; } /** * Get UTC offset as string (e.g., "+05:30", "-08:00") */ getOffsetString(date = new Date()) { const offsetMinutes = this.getOffsetMinutes(date); const sign = offsetMinutes >= 0 ? '+' : '-'; const absMinutes = Math.abs(offsetMinutes); const hours = Math.floor(absMinutes / 60); const minutes = absMinutes % 60; return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; } /** * Get complete offset information */ getOffset(date = new Date()) { const minutes = this.getOffsetMinutes(date); return { minutes, hours: minutes / 60, string: this.getOffsetString(date), }; } // ============================================================================ // DST (Daylight Saving Time) // ============================================================================ /** * Check if DST is in effect for a given date */ isDST(date = new Date()) { const jan = new Date(date.getFullYear(), 0, 1); const jul = new Date(date.getFullYear(), 6, 1); const janOffset = this.getOffsetMinutes(jan); const julOffset = this.getOffsetMinutes(jul); const currentOffset = this.getOffsetMinutes(date); // DST is in effect if current offset matches the larger offset const standardOffset = Math.min(janOffset, julOffset); return currentOffset !== standardOffset; } /** * Check if timezone observes DST */ observesDST() { const currentYear = new Date().getFullYear(); const jan = new Date(currentYear, 0, 15); const jul = new Date(currentYear, 6, 15); return this.getOffsetMinutes(jan) !== this.getOffsetMinutes(jul); } /** * Get the next DST transition */ getNextDSTTransition(from = new Date()) { if (!this.observesDST()) { return null; } const currentOffset = this.getOffsetMinutes(from); const checkDate = new Date(from); // Search forward up to 1 year for (let i = 0; i < 366; i++) { checkDate.setDate(checkDate.getDate() + 1); const newOffset = this.getOffsetMinutes(checkDate); if (newOffset !== currentOffset) { // Found a transition, binary search for exact time const exactDate = this._findExactTransition(new Date(checkDate.getTime() - 24 * 60 * 60 * 1000), checkDate); return { date: exactDate, fromOffset: currentOffset, toOffset: newOffset, isDSTStart: newOffset > currentOffset, }; } } return null; } /** * Binary search to find exact DST transition time */ _findExactTransition(start, end) { const startOffset = this.getOffsetMinutes(start); while (end.getTime() - start.getTime() > 60000) { // Within 1 minute const mid = new Date((start.getTime() + end.getTime()) / 2); const midOffset = this.getOffsetMinutes(mid); if (midOffset === startOffset) { start = mid; } else { end = mid; } } return end; } // ============================================================================ // Conversion // ============================================================================ /** * Convert a date to this timezone (returns formatted string) */ format(date, formatOptions) { const options = Object.assign({ timeZone: this._identifier }, formatOptions); return new Intl.DateTimeFormat('en-US', options).format(date); } /** * Get date components in this timezone */ getComponents(date) { var _a; const formatter = new Intl.DateTimeFormat('en-US', { timeZone: this._identifier, year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', weekday: 'short', hour12: false, }); const parts = formatter.formatToParts(date); const result = { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 0, dayOfWeek: 0, }; const dayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, }; for (const part of parts) { switch (part.type) { case 'year': result.year = parseInt(part.value, 10); break; case 'month': result.month = parseInt(part.value, 10); break; case 'day': result.day = parseInt(part.value, 10); break; case 'hour': result.hour = parseInt(part.value, 10); break; case 'minute': result.minute = parseInt(part.value, 10); break; case 'second': result.second = parseInt(part.value, 10); break; case 'weekday': result.dayOfWeek = (_a = dayMap[part.value]) !== null && _a !== void 0 ? _a : 0; break; } } return result; } /** * Convert a date from one timezone to another */ static convert(date, from, to) { const fromTz = new ChronosTimezone(from); const toTz = new ChronosTimezone(to); const fromOffset = fromTz.getOffsetMinutes(date); const toOffset = toTz.getOffsetMinutes(date); const diffMinutes = toOffset - fromOffset; return new Date(date.getTime() + diffMinutes * 60000); } /** * Convert a UTC date to this timezone */ fromUTC(date) { return ChronosTimezone.convert(date, 'UTC', this._identifier); } /** * Convert a date in this timezone to UTC */ toUTC(date) { return ChronosTimezone.convert(date, this._identifier, 'UTC'); } // ============================================================================ // Information // ============================================================================ /** * Get comprehensive timezone information */ getInfo(date = new Date()) { var _a; return { identifier: (_a = this._originalOffset) !== null && _a !== void 0 ? _a : this._identifier, abbreviation: this.getAbbreviation(date), name: this.getFullName(date), offset: this.getOffset(date), isDST: this.isDST(date), observesDST: this.observesDST(), }; } /** * Check if two timezones are equivalent at a given moment */ equals(other, date = new Date()) { const otherTz = typeof other === 'string' ? new ChronosTimezone(other) : other; return this.getOffsetMinutes(date) === otherTz.getOffsetMinutes(date); } /** * Check if this is the same timezone identifier */ isSame(other) { const otherIdentifier = typeof other === 'string' ? other : other.identifier; return this.identifier === otherIdentifier; } // ============================================================================ // Static Utilities // ============================================================================ /** * Get all available timezone identifiers * Note: This returns common timezones. Use Intl.supportedValuesOf('timeZone') for all. */ static getAvailableTimezones() { // Try to use the native method if available if (typeof Intl !== 'undefined' && 'supportedValuesOf' in Intl) { try { return Intl.supportedValuesOf('timeZone'); } catch (_a) { // Fall back to predefined list } } return Object.values(exports.TIMEZONES); } /** * Check if a timezone identifier is valid */ static isValid(identifier) { try { new Intl.DateTimeFormat('en-US', { timeZone: identifier }); return true; } catch (_a) { return false; } } /** * Get timezones grouped by region */ static getTimezonesByRegion() { const timezones = ChronosTimezone.getAvailableTimezones(); const grouped = {}; for (const tz of timezones) { const parts = tz.split('/'); const region = parts[0]; if (!grouped[region]) { grouped[region] = []; } grouped[region].push(tz); } return grouped; } /** * Find timezones that match a given offset */ static findByOffset(offsetHours, date = new Date()) { const targetMinutes = offsetHours * 60; const result = []; for (const tz of ChronosTimezone.getAvailableTimezones()) { const timezone = new ChronosTimezone(tz); if (timezone.getOffsetMinutes(date) === targetMinutes) { result.push(timezone); } } return result; } /** * Get current time in a specific timezone */ static now(identifier) { const tz = new ChronosTimezone(identifier); return tz.fromUTC(new Date()); } // ============================================================================ // Serialization // ============================================================================ /** * Convert to string */ toString() { var _a; return (_a = this._originalOffset) !== null && _a !== void 0 ? _a : this._identifier; } /** * Convert to JSON */ toJSON() { var _a; const now = new Date(); return { identifier: (_a = this._originalOffset) !== null && _a !== void 0 ? _a : this._identifier, offset: this.getOffsetString(now), isDST: this.isDST(now), observesDST: this.observesDST(), }; } /** * Get primitive value */ valueOf() { var _a; return (_a = this._originalOffset) !== null && _a !== void 0 ? _a : this._identifier; } } exports.ChronosTimezone = ChronosTimezone; // ============================================================================ // Common Timezone Aliases // ============================================================================ /** * Pre-created timezone instances for common timezones */ exports.Timezones = { UTC: ChronosTimezone.utc(), Local: ChronosTimezone.local(), // US Eastern: ChronosTimezone.create('America/New_York'), Central: ChronosTimezone.create('America/Chicago'), Mountain: ChronosTimezone.create('America/Denver'), Pacific: ChronosTimezone.create('America/Los_Angeles'), // Europe London: ChronosTimezone.create('Europe/London'), Paris: ChronosTimezone.create('Europe/Paris'), Berlin: ChronosTimezone.create('Europe/Berlin'), // Asia Tokyo: ChronosTimezone.create('Asia/Tokyo'), Shanghai: ChronosTimezone.create('Asia/Shanghai'), Singapore: ChronosTimezone.create('Asia/Singapore'), Dubai: ChronosTimezone.create('Asia/Dubai'), Mumbai: ChronosTimezone.create('Asia/Kolkata'), // Australia/Pacific Sydney: ChronosTimezone.create('Australia/Sydney'), Auckland: ChronosTimezone.create('Pacific/Auckland'), };