UNPKG

datezone

Version:

A lightweight and comprehensive date and timeZone utility library for JavaScript.

283 lines 9.81 kB
import { FULL_TS, formatToParts } from "./format-parts.pub.js"; import { isDST, isUTC } from "./timezone.pub.js"; // Unified offset cache object for all timezones (null = local) export const offsetCache = { checkedLocalDST: false, fixedOffset: new Map(), // Hot cache for most frequently used timezones (faster than LRU lookup) hotCache: new Map(), lastLocalHourStart: null, lastLocalOffset: null, localFixedOffset: null, perHourOffset: new Map(), }; // Hot cache for popular timezones - faster than period lookup const HOT_CACHE_DURATION = 60 * 60 * 1000; // 1 hour validity function createHotCacheKey(ts, tz) { // Round timestamp to hour for efficient caching const hourStart = Math.floor(ts / (60 * 60 * 1000)) * (60 * 60 * 1000); return `${tz}:${hourStart}`; } // Enhanced DST caching with larger cache sizes and smarter eviction const DST_PERIOD_CACHE_MAX_SIZE = 50; // More timezone periods cached // Doubly-linked list node for efficient LRU operations class LRUNode { key; value; prev; next; constructor(key, value, prev = null, next = null) { this.key = key; this.value = value; this.prev = prev; this.next = next; } } /** * High-performance LRU cache with O(1) operations */ class FastLRUOffsetPeriodCache { cache = new Map(); head; tail; size = 0; constructor() { // Create dummy head and tail nodes this.head = new LRUNode("", []); this.tail = new LRUNode("", []); this.head.next = this.tail; this.tail.prev = this.head; } addToHead(node) { node.prev = this.head; node.next = this.head.next; if (this.head.next) { this.head.next.prev = node; } this.head.next = node; } removeNode(node) { if (node.prev) { node.prev.next = node.next; } if (node.next) { node.next.prev = node.prev; } } moveToHead(node) { this.removeNode(node); this.addToHead(node); } popTail() { const lastNode = this.tail.prev; if (lastNode && lastNode !== this.head) { this.removeNode(lastNode); return lastNode; } return null; } get(tz) { const node = this.cache.get(tz); if (node) { // Move to head (most recently used) this.moveToHead(node); return node.value; } return undefined; } set(tz, periods) { const existingNode = this.cache.get(tz); if (existingNode) { // Update existing node existingNode.value = periods; this.moveToHead(existingNode); } else { // Create new node const newNode = new LRUNode(tz, periods); if (this.size >= DST_PERIOD_CACHE_MAX_SIZE) { // Remove least recently used const tail = this.popTail(); if (tail) { this.cache.delete(tail.key); this.size--; } } this.addToHead(newNode); this.cache.set(tz, newNode); this.size++; } } has(tz) { return this.cache.has(tz); } delete(tz) { const node = this.cache.get(tz); if (node) { this.removeNode(node); this.cache.delete(tz); this.size--; } } } // Replace the original Map with LRU cache export const timeZoneOffsetPeriods = new FastLRUOffsetPeriodCache(); export const DST_WINDOW_MS = 15 * 60 * 1000; export function getYearStartEnd(year) { const start = Date.UTC(year, 0, 1, 0, 0, 0, 0); const end = Date.UTC(year + 1, 0, 1, 0, 0, 0, 0); return [start, end]; } export function computeOffsetPeriods(tz, year) { const [start, end] = getYearStartEnd(year); const periods = []; let prevOffset = calcOffset(start, tz, true); let periodStart = start; let lastChecked = start; // Check if timezone has DST transitions // Sample a few key dates to detect if it's a fixed-offset timezone const midYear = Date.UTC(year, 5, 15); const midYearOffset = calcOffset(midYear, tz, true); // If January and June offsets are the same, likely no DST if (prevOffset === midYearOffset) { // Check one more time in winter to be sure const winter = Date.UTC(year, 11, 15); const winterOffset = calcOffset(winter, tz, true); if (prevOffset === winterOffset) { // No DST detected, return single period return [{ end, offset: prevOffset, start }]; } } // DST detected or uncertain - proceed with full analysis // Use larger step size initially for better performance const stepSize = 7 * 24 * 60 * 60 * 1000; for (let ts = start + stepSize; ts <= end; ts += stepSize) { const offset = calcOffset(ts, tz, true); if (offset !== prevOffset) { // Binary search for exact transition with 5-minute precision let lo = lastChecked; let hi = ts; while (hi - lo > 5 * 60 * 1000) { const mid = Math.floor((lo + hi) / 2); const midOffset = calcOffset(mid, tz, true); if (midOffset === prevOffset) lo = mid; else hi = mid; } periods.push({ end: hi, offset: prevOffset, start: periodStart }); periodStart = hi; prevOffset = offset; lastChecked = hi; } else { lastChecked = ts; } } if (periodStart < end) { periods.push({ end, offset: prevOffset, start: periodStart }); } return periods; } export function ensureOffsetPeriods(tz, year) { if (!timeZoneOffsetPeriods.has(tz)) { timeZoneOffsetPeriods.set(tz, computeOffsetPeriods(tz, year)); return; } const periods = timeZoneOffsetPeriods.get(tz); const range = getYearStartEnd(year); const start = range[0]; const end = range[1]; if (!periods || periods.length === 0 || (periods.length > 0 && (periods[0].start > start || periods[periods.length - 1].end < end))) { timeZoneOffsetPeriods.set(tz, computeOffsetPeriods(tz, year)); } } export function getCachedOffsetDST(ts, tz) { // Hot cache lookup first - fastest path const hotKey = createHotCacheKey(ts, tz); const hotCached = offsetCache.hotCache.get(hotKey); if (hotCached && ts < hotCached.validUntil) { return hotCached.offset; } const d = new Date(ts); const year = d.getUTCFullYear(); ensureOffsetPeriods(tz, year); const periods = timeZoneOffsetPeriods.get(tz); if (!periods) { // Fallback: recompute if not found (should not happen) const newPeriods = computeOffsetPeriods(tz, year); timeZoneOffsetPeriods.set(tz, newPeriods); return null; } // Optimized period search: binary search for better performance with many periods let left = 0; let right = periods.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); const period = periods[mid]; if (ts >= period.start && ts < period.end) { // Check if within DST transition window - but more targeted const nearStart = Math.abs(ts - period.start) < DST_WINDOW_MS; const nearEnd = Math.abs(ts - period.end) < DST_WINDOW_MS; if (nearStart || nearEnd) { return null; // fallback to per-hour cache only near transitions } // Cache in hot cache for future lookups const validUntil = Math.min(period.end, ts + HOT_CACHE_DURATION); offsetCache.hotCache.set(hotKey, { offset: period.offset, validUntil }); // Clean hot cache periodically (every ~100 entries) if (offsetCache.hotCache.size > 100) { const now = Date.now(); for (const [key, entry] of offsetCache.hotCache) { if (now >= entry.validUntil) { offsetCache.hotCache.delete(key); } } } return period.offset; } if (ts < period.start) { right = mid - 1; } else { left = mid + 1; } } return null; } export function calcOffset(ts, tz, bypassCache = false) { if (!tz) { return -new Date(ts).getTimezoneOffset(); } if (isUTC(tz)) return 0; if (!bypassCache) { // Check hot cache first for DST timezones (fastest path) if (isDST(tz)) { const hotKey = createHotCacheKey(ts, tz); const hotCached = offsetCache.hotCache.get(hotKey); if (hotCached && ts < hotCached.validUntil) { return hotCached.offset; } // Try DST period cache const cached = getCachedOffsetDST(ts, tz); if (cached !== null) return cached; } } // Fallback to expensive calculation const parts = formatToParts(ts, tz, FULL_TS); const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second, parts.millisecond); const result = (wall - ts) / 60000; // Cache result in hot cache if it's a DST timezone if (!bypassCache && isDST(tz)) { const hotKey = createHotCacheKey(ts, tz); const validUntil = ts + HOT_CACHE_DURATION; offsetCache.hotCache.set(hotKey, { offset: result, validUntil }); } return result; } //# sourceMappingURL=offset.internal.js.map