UNPKG

clockwork-tz

Version:

Accurate timezone conversions and DST handling with optional deterministic IANA tzdb support

725 lines (706 loc) 23.4 kB
/** * Core types and interfaces for clockwork-tz */ /** Engine types supported by clockwork-tz */ type EngineKind = 'system-intl' | 'embedded-tzdb'; /** Disambiguation strategy for ambiguous local times during DST transitions */ type Disambiguation = 'earliest' | 'latest' | 'reject'; /** Label style options for timezone listing */ type LabelStyle = 'original' | 'offset' | 'abbrev' | 'altName'; /** * Complete timezone-aware time representation */ interface ClockworkTime { /** ISO 8601 instant in UTC with Z suffix */ utc: string; /** IANA timezone identifier (e.g., "Australia/Lord_Howe") */ zone: string; /** Formatted local civil time */ local: string; /** Offset from UTC in minutes (e.g., 630 for +10:30) */ offsetMinutes: number; /** Whether this time is during Daylight Saving Time */ isDST: boolean; /** The engine that produced this result */ source: EngineKind; /** TZDB version (only available for embedded-tzdb engine) */ tzdbVersion?: string; } /** * Options for listing timezones */ interface ListOptions { /** Style for timezone labels */ labelStyle?: LabelStyle; /** User's timezone for biasing label generation */ forZone?: string; } /** * Timezone list item */ interface TimeZoneItem { /** IANA timezone identifier */ value: string; /** Human-readable label */ label: string; /** Current offset from UTC in minutes */ offsetMinutes: number; } /** * Initialization options for clockwork-tz */ interface ClockworkOptions { /** Engine to use for timezone calculations */ engine?: EngineKind; /** URL for CDN-hosted timezone database manifest */ manifestURL?: string; /** Auto-update interval in milliseconds (default: 24 hours) */ autoUpdateIntervalMs?: number; } /** * Formatting options */ interface FormatOptions { /** Custom format pattern (date-fns compatible) */ format?: string; } /** * Conversion options */ interface ConvertOptions extends FormatOptions { /** Disambiguation strategy for ambiguous times */ disambiguation?: Disambiguation; } /** * Engine interface that all timezone engines must implement */ interface TzEngine { /** Engine identifier */ readonly kind: EngineKind; /** Get current TZDB version (if available) */ getVersion(): string | undefined; /** Check if a timezone is valid */ isValidTimeZone(zone: string): boolean; /** Get user's current timezone */ getUserTimeZone(): string; /** Convert UTC time to timezone */ fromUTC(utc: Date, zone: string): ClockworkTime; /** Interpret local time in timezone */ interpretLocal(localISO: string, zone: string, disambiguation: Disambiguation): ClockworkTime; /** Get timezone offset at specific time */ getOffset(time: Date, zone: string): number; /** Check if time is in DST */ isDST(time: Date, zone: string): boolean; /** List available timezones */ listTimeZones(options: ListOptions): TimeZoneItem[]; /** Format time in timezone */ format(time: Date, zone: string, pattern: string): string; } /** * Environment detection results */ interface Environment { /** Running in Node.js */ isNode: boolean; /** Running in browser */ isBrowser: boolean; /** BroadcastChannel available */ hasBroadcastChannel: boolean; /** localStorage available */ hasLocalStorage: boolean; /** Cache API available */ hasCacheAPI: boolean; /** File system access available */ hasFileSystem: boolean; } /** * Error thrown when embedded TZDB engine is not yet implemented */ declare class NotImplementedError extends Error { constructor(message: string); } /** * Error thrown for invalid timezone operations */ declare class TimezoneError extends Error { constructor(message: string); } /** * Error thrown for ambiguous time operations when disambiguation is 'reject' */ declare class AmbiguousTimeError extends TimezoneError { constructor(localTime: string, zone: string); } /** * Error thrown for invalid local times (e.g., during spring forward) */ declare class InvalidTimeError extends TimezoneError { constructor(localTime: string, zone: string); } /** * Main API implementation for clockwork-tz */ /** * Initialize clockwork-tz with options */ declare function initClockworkTZ(opts?: ClockworkOptions): void; /** * Get the current engine type */ declare function engine(): EngineKind; /** * Get the current TZDB version (if available) */ declare function tzdbVersion(): string | undefined; /** * Get the user's timezone */ declare function getUserTimeZone(): string; /** * Check if a timezone identifier is valid */ declare function isValidTimeZone(zone: string): boolean; /** * Convert UTC time to a specific timezone */ declare function fromUTC(input: string | Date, zone: string, opts?: FormatOptions): ClockworkTime; /** * Interpret a local time string in a specific timezone */ declare function interpretLocal(localISO: string, zone: string, opts?: ConvertOptions): ClockworkTime; /** * Convert time from one timezone to another */ declare function convert(input: string | Date, fromZone: string, toZone: string, opts?: ConvertOptions): ClockworkTime; /** * Format a date/time in a specific timezone */ declare function format$1(input: string | Date, zone: string, pattern?: string): string; /** * Get timezone offset in minutes for a specific date/time */ declare function getOffset(input: string | Date, zone: string): number; /** * Check if a date/time is in Daylight Saving Time */ declare function isDST(input: string | Date, zone: string): boolean; /** * List available timezones with labels and current offsets */ declare function listTimeZones(opts?: ListOptions): TimeZoneItem[]; /** * Listen for timezone data updates */ declare function onDataUpdate(callback: (version: string) => void): () => void; /** * Watch for timezone transitions (DST changes) */ declare function watchZoneTransitions(zone: string, callback: (at: Date) => void): () => void; /** * Watch for time ticks (regular intervals) */ declare function onTick(callback: (now: Date) => void, granularityMs?: number): () => void; /** * Get current engine instance (for advanced usage) */ declare function getCurrentEngine(): TzEngine; /** * Manually set the engine (for testing or advanced usage) */ declare function setEngine(engine: TzEngine): void; /** * Get current initialization options */ declare function getInitOptions(): ClockworkOptions; /** * Force refresh timezone data (for embedded engine) */ declare function refreshTimezoneData(): Promise<void>; /** * Get library version and status information */ declare function getStatus(): { engine: EngineKind; tzdbVersion?: string | undefined; dataLoaded: boolean; lastUpdate?: Date | undefined; }; /** * ISO 8601 date parsing and validation utilities */ /** * Date component structure */ interface DateComponents { year: number; month: number; day: number; hour: number; minute: number; second: number; millisecond: number; } /** * Validate and parse an ISO 8601 date string */ declare function parseISODate(isoString: string): Date; /** * Validate an ISO 8601 date string format */ declare function isValidISOString(isoString: string): boolean; /** * Validate a local ISO string format (without timezone) */ declare function isValidLocalISOString(localString: string): boolean; /** * Parse a local ISO string into date components */ declare function parseLocalISOString(localString: string): DateComponents; /** * Convert date components to a Date object (assumes local timezone) */ declare function componentsToDate(components: DateComponents): Date; /** * Format a Date as ISO 8601 UTC string */ declare function formatAsUTC(date: Date): string; /** * Format a Date as local ISO string (without timezone info) */ declare function formatAsLocalISO(date: Date): string; /** * Normalize input to a Date object */ declare function normalizeToDate(input: string | Date): Date; /** * Check if a year is a leap year */ declare function isLeapYear(year: number): boolean; /** * Get the number of days in a month */ declare function getDaysInMonth(year: number, month: number): number; /** * Validate date components */ declare function validateDateComponents(components: DateComponents): boolean; /** * Add milliseconds to a Date */ declare function addMilliseconds(date: Date, ms: number): Date; /** * Get the difference between two dates in milliseconds */ declare function diffInMilliseconds(date1: Date, date2: Date): number; /** * Date parsing utilities with strict ISO validation */ /** * Parse result with metadata */ interface ParseResult { /** Parsed date */ date: Date; /** Whether the input had timezone information */ hasTimezone: boolean; /** Original input string */ original: string; /** Whether the date is valid */ isValid: boolean; } /** * Parsing options */ interface ParseOptions { /** Strict mode - reject non-ISO formats */ strict?: boolean; /** Default timezone for local times */ defaultTimezone?: string; /** Allow flexible separators (space instead of T) */ allowFlexible?: boolean; } /** * Parse a date string with validation and metadata * * @param input - Date string to parse * @param options - Parsing options * @returns Parse result with date and metadata * @throws Error for invalid input in strict mode */ declare function parse(input: string, options?: ParseOptions): ParseResult; /** * Parse a date string and return only the Date object * * @param input - Date string to parse * @param options - Parsing options * @returns Parsed Date object * @throws Error for invalid input */ declare function parseDate(input: string, options?: ParseOptions): Date; /** * Parse date components from a string * * @param input - Local date string (without timezone) * @returns Date components * @throws Error for invalid input */ declare function parseComponents(input: string): DateComponents; /** * Safely parse a date string without throwing * * @param input - Date string to parse * @param options - Parsing options * @returns Parse result or null if parsing failed */ declare function safeParse(input: string, options?: ParseOptions): ParseResult | null; /** * Check if a string can be parsed as a date * * @param input - String to test * @param options - Parsing options * @returns True if parseable */ declare function isParseable(input: string, options?: ParseOptions): boolean; /** * Parse multiple date formats and return the first successful one * * @param input - Date string to parse * @param formats - Array of format options to try * @returns First successful parse result * @throws Error if none of the formats work */ declare function parseAny(input: string, formats?: ParseOptions[]): ParseResult; /** * Parse a date range from a string * * @param input - Range string (e.g., "2024-01-01 to 2024-01-31") * @param options - Parsing options * @returns Object with start and end dates */ declare function parseRange(input: string, options?: ParseOptions & { separator?: string | RegExp; }): { start: Date; end: Date; }; /** * Parse a time-only string (HH:mm:ss format) * * @param input - Time string * @returns Object with hour, minute, second */ declare function parseTime(input: string): { hour: number; minute: number; second: number; millisecond: number; }; /** * Parse timezone offset from a string * * @param input - Offset string (e.g., "+05:30", "-08:00", "Z") * @returns Offset in minutes */ declare function parseOffset(input: string): number; /** * Normalize various date inputs to a consistent Date object * * @param input - Date, string, or number input * @returns Normalized Date object */ declare function normalize(input: string | Date | number): Date; /** * Date formatting utilities using date-fns-tz */ /** * Default format pattern */ declare const DEFAULT_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss zzz"; /** * Common format patterns */ declare const FORMAT_PATTERNS: { /** ISO-like with timezone abbreviation: 2024-03-15 14:30:00 PST */ readonly default: "yyyy-MM-dd HH:mm:ss zzz"; /** ISO date only: 2024-03-15 */ readonly dateOnly: "yyyy-MM-dd"; /** ISO time only: 14:30:00 */ readonly timeOnly: "HH:mm:ss"; /** ISO datetime without timezone: 2024-03-15 14:30:00 */ readonly dateTime: "yyyy-MM-dd HH:mm:ss"; /** ISO datetime with milliseconds: 2024-03-15 14:30:00.123 */ readonly dateTimeMs: "yyyy-MM-dd HH:mm:ss.SSS"; /** 12-hour format: 2024-03-15 2:30:00 PM PST */ readonly amPm: "yyyy-MM-dd h:mm:ss a zzz"; /** Human readable: March 15, 2024 at 2:30 PM PST */ readonly human: "MMMM d, yyyy 'at' h:mm a zzz"; /** Short format: 3/15/24 2:30 PM */ readonly short: "M/d/yy h:mm a"; /** Long format with full timezone name */ readonly long: "EEEE, MMMM d, yyyy 'at' h:mm:ss a zzzz"; /** RFC 2822 style: Fri, 15 Mar 2024 14:30:00 +0800 */ readonly rfc2822: "EEE, d MMM yyyy HH:mm:ss xx"; /** ISO 8601 with offset: 2024-03-15T14:30:00+08:00 */ readonly iso8601: "yyyy-MM-dd'T'HH:mm:ssxxx"; }; /** * Format a date/time in a specific timezone * * @param input - Date object or ISO string to format * @param zone - IANA timezone identifier * @param pattern - Format pattern (defaults to DEFAULT_FORMAT_PATTERN) * @returns Formatted date string * @throws TimezoneError if timezone is invalid or formatting fails */ declare function format(input: string | Date, zone: string, pattern?: string): string; /** * Format a date using a predefined pattern */ declare function formatWithPattern(input: string | Date, zone: string, patternName: keyof typeof FORMAT_PATTERNS): string; /** * Format current time in a timezone */ declare function formatNow(zone: string, pattern?: string): string; /** * Format a date for different common use cases */ declare function formatFor(input: string | Date, zone: string, useCase: 'display' | 'log' | 'filename' | 'api' | 'human'): string; /** * Format a date range in a timezone */ declare function formatRange(start: string | Date, end: string | Date, zone: string, options?: { pattern?: string; separator?: string; sameDay?: boolean; }): string; /** * Format duration between two dates */ declare function formatDuration(start: string | Date, end: string | Date, options?: { units?: Array<'days' | 'hours' | 'minutes' | 'seconds'>; precision?: number; }): string; /** * Format relative time (e.g., "2 hours ago", "in 3 days") */ declare function formatRelative(date: string | Date, relativeTo: (string | Date) | undefined, zone: string): string; /** * Format timezone offset as a string */ declare function formatOffset(offsetMinutes: number): string; /** * Validate a format pattern */ declare function isValidFormatPattern(pattern: string): boolean; /** * Timezone transition detection and watching */ /** * Get information about the next timezone transition */ declare function getNextTransition(zone: string, from?: Date, engine?: TzEngine): { date: Date; fromOffset: number; toOffset: number; isDSTStart: boolean; } | null; /** * Get information about the previous timezone transition */ declare function getPreviousTransition(zone: string, from?: Date, engine?: TzEngine): { date: Date; fromOffset: number; toOffset: number; isDSTStart: boolean; } | null; /** * Check if a date is near a timezone transition (within 24 hours) */ declare function isNearTransition(date: Date, zone: string, engine?: TzEngine, windowHours?: number): boolean; /** * Cleanup all active watchers (useful for testing or app shutdown) */ declare function cleanupAllWatchers(): void; /** * Get statistics about active watchers */ declare function getWatcherStats(): { transitionWatchers: number; tickWatchers: number; }; /** * Environment detection utilities for cross-platform compatibility */ /** * Detect the current runtime environment and available features */ declare function detectEnvironment(): Environment; /** * Get the appropriate cache storage based on environment */ declare function getCacheStorage(): { get: (key: string) => Promise<string | null>; set: (key: string, value: string, ttl?: number) => Promise<void>; delete: (key: string) => Promise<void>; }; /** * Create a broadcast channel for cross-tab communication if available */ declare function createBroadcastChannel(channelName: string): { postMessage: (data: unknown) => void; addEventListener: (type: string, listener: (event: { data: unknown; }) => void) => void; close: () => void; } | null; declare function getEnvironment(): Environment; /** * Check if we're running in a secure context (required for some APIs) */ declare function isSecureContext(): boolean; /** * CDN updater for timezone database with caching and signature verification */ /** * Default CDN URLs for timezone data */ declare const DEFAULT_MANIFEST_URL = "https://cdn.clockwork-tz.dev/manifest.json"; declare const CACHE_KEY_PREFIX = "clockwork-tz:"; declare const BROADCAST_CHANNEL_NAME = "clockwork-tz-updates"; /** * Ensure fresh timezone database data is available * * This function: * 1. Checks local cache for existing data * 2. Fetches manifest from CDN if needed * 3. Downloads new data if version has changed * 4. Verifies signature (stub implementation) * 5. Updates cache and broadcasts changes */ declare function ensureFreshTzdb(manifestURL?: string): Promise<{ version: string; data: unknown; } | null>; /** * Clear all cached timezone data */ declare function clearCache(): Promise<void>; /** * Get cache statistics */ declare function getCacheStats(): Promise<{ manifestCached: boolean; dataCached: boolean; dataVersion?: string | undefined; dataAge?: number | undefined; }>; /** * System Intl engine implementation using platform Intl API and date-fns-tz */ /** * System Intl timezone engine using platform APIs */ declare class SystemIntlEngine implements TzEngine { readonly kind: "system-intl"; getVersion(): string | undefined; isValidTimeZone(zone: string): boolean; getUserTimeZone(): string; fromUTC(utc: Date, zone: string): ClockworkTime; interpretLocal(localISO: string, zone: string, disambiguation: Disambiguation): ClockworkTime; getOffset(time: Date, zone: string): number; isDST(time: Date, zone: string): boolean; listTimeZones(options?: ListOptions): TimeZoneItem[]; format(time: Date, zone: string, pattern: string): string; private parseOffsetString; private formatZoneLabel; private getCommonTimeZones; } /** * Create and return a new SystemIntl engine instance */ declare function createSystemIntlEngine(): SystemIntlEngine; /** * Embedded TZDB engine stub - deterministic timezone calculations using IANA data * * TODO: This is a stub implementation. Full implementation would include: * - Loading and parsing IANA timezone database JSON * - Calculating transitions and offsets from raw TZDB data * - Handling all edge cases deterministically * - Supporting historical timezone changes */ /** * Embedded TZDB engine for deterministic timezone calculations * * This engine provides deterministic results by using a specific version * of the IANA timezone database, ensuring consistent behavior across * different platforms and over time. */ declare class EmbeddedTzdbEngine implements TzEngine { readonly kind: "embedded-tzdb"; private tzdbVersion?; private tzdbData?; constructor(tzdbData?: unknown, version?: string | undefined); getVersion(): string | undefined; isValidTimeZone(_zone: string): boolean; getUserTimeZone(): string; fromUTC(_utc: Date, _zone: string): ClockworkTime; interpretLocal(_localISO: string, _zone: string, _disambiguation: Disambiguation): ClockworkTime; getOffset(_time: Date, _zone: string): number; isDST(_time: Date, _zone: string): boolean; listTimeZones(_options?: ListOptions): TimeZoneItem[]; format(_time: Date, _zone: string, _pattern: string): string; /** * Load timezone database data * TODO: Implement loading from various sources: * - Embedded JSON data * - CDN-fetched data * - Local cache */ loadTzdbData(data: unknown, version: string): Promise<void>; /** * Get the raw TZDB data (for debugging/inspection) */ getTzdbData(): unknown; /** * Check if TZDB data is loaded */ isDataLoaded(): boolean; } /** * Create and return a new EmbeddedTzdb engine instance */ declare function createEmbeddedTzdbEngine(tzdbData?: unknown, version?: string): EmbeddedTzdbEngine; /** * clockwork-tz - Accurate timezone conversions and DST handling * * @author Adam Turner * @version 0.1.0 * @license MIT */ declare const VERSION = "0.1.0"; /** * Default export for convenience */ declare const _default: { init: typeof initClockworkTZ; engine: typeof engine; tzdbVersion: typeof tzdbVersion; getUserTimeZone: typeof getUserTimeZone; isValidTimeZone: typeof isValidTimeZone; fromUTC: typeof fromUTC; interpretLocal: typeof interpretLocal; convert: typeof convert; format: typeof format$1; getOffset: typeof getOffset; isDST: typeof isDST; listTimeZones: typeof listTimeZones; onDataUpdate: typeof onDataUpdate; watchZoneTransitions: typeof watchZoneTransitions; onTick: typeof onTick; formatTime: typeof format$1; parseDate: typeof parseDate; normalize: typeof normalize; getStatus: typeof getStatus; VERSION: string; }; export { AmbiguousTimeError, BROADCAST_CHANNEL_NAME, CACHE_KEY_PREFIX, type ClockworkOptions, type ClockworkTime, type ConvertOptions, DEFAULT_FORMAT_PATTERN, DEFAULT_MANIFEST_URL, type DateComponents, type Disambiguation, type EngineKind, FORMAT_PATTERNS, type FormatOptions, InvalidTimeError, type LabelStyle, type ListOptions, NotImplementedError, type ParseOptions, type ParseResult, type TimeZoneItem, TimezoneError, type TzEngine, VERSION, addMilliseconds, cleanupAllWatchers, clearCache, componentsToDate, convert, createBroadcastChannel, createEmbeddedTzdbEngine, createSystemIntlEngine, _default as default, detectEnvironment, diffInMilliseconds, engine, ensureFreshTzdb, format$1 as format, formatAsLocalISO, formatAsUTC, formatDuration, formatFor, formatNow, formatOffset, formatRange, formatRelative, format as formatTime, formatWithPattern, fromUTC, getCacheStats, getCacheStorage, getCurrentEngine, getDaysInMonth, getEnvironment, getInitOptions, getNextTransition, getOffset, getPreviousTransition, getStatus, getUserTimeZone, getWatcherStats, initClockworkTZ, interpretLocal, isDST, isLeapYear, isNearTransition, isParseable, isSecureContext, isValidFormatPattern, isValidISOString, isValidLocalISOString, isValidTimeZone, listTimeZones, normalize, normalizeToDate, onDataUpdate, onTick, parse, parseAny, parseComponents, parseDate, parseISODate, parseLocalISOString, parseOffset, parseRange, parseTime, refreshTimezoneData, safeParse, setEngine, tzdbVersion, validateDateComponents, watchZoneTransitions };