UNPKG

@tubular/time

Version:

Date/time, IANA timezones, leap seconds, TAI/UTC conversions, calendar with settable Julian/Gregorian switchover

1,062 lines 50.2 kB
import { abs, div_rd, div_tt0, floor, min, mod2, round } from '@tubular/math'; import { clone, compareStrings, isEqual, last, padLeft, toNumber } from '@tubular/util'; import { dateAndTimeFromMillis_SGC, getDateFromDayNumber_SGC, getDateOfNthWeekdayOfMonth_SGC, getDayNumber_SGC, getDayOnOrAfter_SGC, LAST, millisFromDateTime_SGC } from './calendar'; import { DAY_MSEC, deltaTUpdater, enMonthsShort, enWeekdaysShort, getDateValue, MINUTE_MSEC, parseTimeOffset } from './common'; import { hasIntlDateTime } from './locale-data'; const CLOCK_TYPE_WALL = 0; const CLOCK_TYPE_STD = 1; // noinspection JSUnusedLocalSymbols const CLOCK_TYPE_UTC = 2; // eslint-disable-line @typescript-eslint/no-unused-vars const LAST_DST_YEAR = 2500; const TIME_GAP_AFTER_LAST_TRANSITION = 172800000; // Two days const extendedRegions = /^(America\/Argentina|America\/Indiana)\/(.+)$/; const miscUnique = /^(CST6CDT|EET|EST5EDT|MST7MDT|PST8PDT|SystemV\/AST4ADT|SystemV\/CST6CDT|SystemV\/EST5EDT|SystemV\/MST7MDT|SystemV\/PST8PDT|SystemV\/YST9YDT|WET)$/; const nonZones = new Set(['deltaTs', 'leapSeconds', 'version', 'years']); class Rule { constructor(ruleStr) { const parts = ruleStr.split(/[ :]/); this.startYear = Number(parts[0]); this.month = Number(parts[1]); this.dayOfMonth = Number(parts[2]); this.dayOfWeek = Number(parts[3]); this.atHour = Number(parts[4]); this.atMinute = Number(parts[5]); this.atType = Number(parts[6]); this.save = round(Number(parts[7]) * 60); } getTransitionTime(year, stdOffset, dstOffset) { let date; if (this.dayOfWeek >= 0 && this.dayOfMonth > 0) date = getDayOnOrAfter_SGC(year, this.month, this.dayOfWeek - 1, this.dayOfMonth); else if (this.dayOfWeek >= 0 && this.dayOfMonth < 0) date = getDayOnOrAfter_SGC(year, this.month, this.dayOfWeek - 1, -this.dayOfMonth); else if (this.dayOfWeek >= 0 && this.dayOfMonth === 0) date = getDateOfNthWeekdayOfMonth_SGC(year, this.month, this.dayOfWeek - 1, LAST); else date = this.dayOfMonth; let millis = millisFromDateTime_SGC(year, this.month, date, this.atHour, this.atMinute); if (this.atType === CLOCK_TYPE_WALL) millis -= (stdOffset + dstOffset) * 1000; else if (this.atType === CLOCK_TYPE_STD) millis -= stdOffset * 1000; return millis; } toString() { const month = enMonthsShort[this.month - 1]; const dayOfWeek = enWeekdaysShort[this.dayOfWeek - 1]; let s = ''; if (this.dayOfMonth === 0) s += `last ${dayOfWeek} of ${month}`; else if (this.dayOfWeek < 0) s += `${month} ${this.dayOfMonth}`; else if (this.dayOfMonth > 0) s += `first ${dayOfWeek} on/after ${month} ${this.dayOfMonth}`; else s += `last ${dayOfWeek} on/before ${month} ${-this.dayOfMonth}`; s += `, at ${this.atHour}:${padLeft(this.atMinute, 2, '0')} `; s += ['wall time', 'std time', 'UTC'][this.atType]; if (this.save === 0) s += ' begin std time'; else { if (this.save % 3600 === 0) s += ` save ${div_rd(this.save, 3600)} hour${abs(this.save / 3600) > 1 ? 's' : ''}`; else s += ` save ${div_rd(this.save, 60)} mins`; if (this.save % 60 !== 0) s += ` ${this.save % 60} secs`; } return s; } } let osTransitions = []; let osProbableStdOffset; let osProbableDstOffset; let osUsesDst; let osDstOffset; // Create a transition table (if necessary) for the OS timezone so that it can be handled like other timezones. // It might also be discovered, of course, that the OS timezone is a simple fixed offset from UTC. (function () { const date = new Date(1901, 0, 1, 12, 0, 0, 0); // Sample around local noon, so it's unlikely we'll sample right at a transition. let lastSampleTime = date.getTime(); const now = Date.now(); const MONTH_MSEC = 30 * DAY_MSEC; const aBitLater = now + MONTH_MSEC * 12 * 2; const muchLater = now + MONTH_MSEC * 12 * 50; let lastOffset = -date.getTimezoneOffset() * 60; osTransitions.push({ transitionTime: Number.MIN_SAFE_INTEGER, utcOffset: lastOffset, dstOffset: 0 }); while (date.getTime() < muchLater) { const sampleTime = lastSampleTime + MONTH_MSEC; date.setTime(sampleTime); const currentOffset = -date.getTimezoneOffset() * 60; if (osProbableStdOffset === undefined && sampleTime >= aBitLater) osProbableStdOffset = osProbableDstOffset = currentOffset; if (currentOffset !== lastOffset) { if (sampleTime >= aBitLater) { osProbableStdOffset = Math.min(osProbableStdOffset, currentOffset); osProbableDstOffset = Math.max(osProbableDstOffset, currentOffset); } let low = lastSampleTime; let high = sampleTime; while (high - low > MINUTE_MSEC) { const mid = Math.floor((high + low) / 2 / MINUTE_MSEC) * MINUTE_MSEC; date.setTime(mid); const sampleOffset = -date.getTimezoneOffset() * 60; if (sampleOffset === lastOffset) low = mid; else high = mid; } osTransitions.push({ transitionTime: high, utcOffset: currentOffset, dstOffset: 0 }); lastOffset = currentOffset; } lastSampleTime = sampleTime; } if (osTransitions.length < 2) { osTransitions = null; osUsesDst = false; osDstOffset = 0; } else { osUsesDst = (osProbableDstOffset > osProbableStdOffset); osDstOffset = osProbableDstOffset - osProbableStdOffset; // Not the full UTC offset during DST, just the difference from Standard Time. // If the OS timezone isn't historical, but instead projects DST rules indefinitely backward in time, we might have accidentally // captured a DST offset for the first transition, something that will wrongly make DST look like the starting base UTC offset. if (osUsesDst) { if (osTransitions[0].utcOffset === osProbableDstOffset && osTransitions[1].utcOffset === osProbableStdOffset) { osTransitions.splice(0, 1); osTransitions[0].transitionTime = Number.MIN_SAFE_INTEGER; } osTransitions.forEach((transition, index) => { var _a; if (index > 0 && transition.utcOffset === osProbableDstOffset && ((_a = osTransitions[index - 1]) === null || _a === void 0 ? void 0 : _a.utcOffset) === osProbableStdOffset) transition.dstOffset = osProbableDstOffset - osProbableStdOffset; }); // Make sure last transition is to standard time. if (last(osTransitions).dstOffset !== 0) osTransitions.pop(); } } })(); export class Timezone { constructor(zoneInfo) { var _a, _b; this._countries = new Set(); this._zoneName = zoneInfo.zoneName; this._utcOffset = zoneInfo.currentUtcOffset; this._usesDst = zoneInfo.usesDst; this._dstOffset = zoneInfo.dstOffset; this.displayName = zoneInfo.displayName; this.transitions = clone(zoneInfo.transitions); this._aliasFor = zoneInfo.aliasFor; this._population = (_a = zoneInfo.population) !== null && _a !== void 0 ? _a : 0; this._countries = (_b = zoneInfo.countries) !== null && _b !== void 0 ? _b : new Set(); this._stdRule = zoneInfo.stdRule; this._dstRule = zoneInfo.dstRule; if (this.transitions && this.transitions.length > 0) { let lastOffset = this.transitions[0].utcOffset; let lastBaseOffset = lastOffset; let lastDst = false; // The first transition should never be DST. let baseOffset; let isDst; for (const transition of this.transitions) { isDst = (transition.dstOffset !== 0); baseOffset = transition.utcOffset - transition.dstOffset; transition.deltaOffset = transition.utcOffset - lastOffset; transition.dstFlipped = (isDst !== lastDst); transition.baseOffsetChanged = (baseOffset !== lastBaseOffset); transition.wallTime = transition.transitionTime + transition.utcOffset * 1000; transition.wallTimeDay = getDateFromDayNumber_SGC(floor(transition.wallTime / 86400000)).d; Object.freeze(transition); lastOffset = transition.utcOffset; lastDst = isDst; lastBaseOffset = baseOffset; } } } static get version() { return this._version; } static defineTimezones(encodedTimezones) { const changed = !isEqual(this.encodedTimezones, encodedTimezones); if (encodedTimezones === null || encodedTimezones === void 0 ? void 0 : encodedTimezones.version) this._version = encodedTimezones.version; else this._version = 'unspecified'; this.encodedTimezones = Object.assign({}, encodedTimezones !== null && encodedTimezones !== void 0 ? encodedTimezones : {}); this.extractZoneInfo(); this.extractLeapSeconds(); this.extractDeltaTs(); if (changed) { this.offsetsAndZones = undefined; this.regionAndSubzones = undefined; this.zoneLookup = {}; } return changed; } static getAvailableTimezones() { const zones = []; for (const zone of Object.keys(this.encodedTimezones)) { if (zone.includes('/') || /^[A-Z]/.test(zone)) // Filter out deltaTs, leapSeconds, etc. zones.push(zone); } zones.sort(); return zones; } static getOffsetsAndZones() { var _a; if (this.offsetsAndZones) return this.offsetsAndZones; const zoneHash = {}; for (const zone of Object.keys(this.encodedTimezones)) { if (!zone.includes('/') || zone.startsWith('Etc/') || miscUnique.test(zone)) continue; let etz = this.encodedTimezones[zone]; if (!etz.includes(';')) { const $ = /^!([^,]*)$/.exec(etz) || /^(?:.*,)?(.*)$/.exec(etz); etz = (_a = this.encodedTimezones[$[1]]) !== null && _a !== void 0 ? _a : ''; } const sections = etz.split(/[ ;]/); if (sections.length < 3) continue; const offset = sections[1].split(/([-+]?\d\d)/g).filter(s => !!s).join(':') + this.getDstSymbol(toNumber(sections[2]) * 60); let zones = zoneHash[offset]; if (!zones) { zones = []; zoneHash[offset] = zones; } zones.push(zone.replace(/_/g, ' ')); } const offsets = []; const toNum = (s) => toNumber(s.replace(/[^-+\d]/g, '')); for (const offset of Object.keys(zoneHash)) offsets.push(offset); offsets.sort((a, b) => toNum(a) - toNum(b)); this.offsetsAndZones = []; for (const offset of offsets) { const zones = zoneHash[offset]; zones.sort(); // noinspection NonAsciiCharacters this.offsetsAndZones.push({ offset, offsetSeconds: parseTimeOffset(offset.replace(/[^-+\d]/g, '')), dstOffset: { '^': 1800, '§': 3600, '#': 7200, '\u2744': -3600, '~': 999 }[offset.substr(offset.length - 1)] || 0, zones }); } return this.offsetsAndZones; } static getRegionsAndSubzones() { var _a; if (this.regionAndSubzones) return this.regionAndSubzones; let hasMisc = false; const zoneHash = {}; for (const zone of Object.keys(this.encodedTimezones)) { let region; let locale; const $ = (_a = extendedRegions.exec(zone)) !== null && _a !== void 0 ? _a : /^(.+?)\/(.+)$/.exec(zone); if (!$) { region = zone; locale = null; } else { region = $[1]; locale = $[2].replace(/_/g, ' '); } if (locale == null || miscUnique.test(zone)) { region = '~'; // Force miscellaneous zones to sort to end of region list. locale = zone; hasMisc = true; } let locales = zoneHash[region]; if (!locales) { locales = []; zoneHash[region] = locales; } locales.push(locale); } const regions = []; for (const region of Object.keys(zoneHash)) regions.push(region); regions.sort(); if (hasMisc) { regions[regions.length - 1] = 'MISC'; zoneHash.MISC = zoneHash['~']; delete zoneHash['~']; } this.regionAndSubzones = []; for (const region of regions) { const locales = zoneHash[region]; locales.sort(); this.regionAndSubzones.push({ region, subzones: locales }); } return this.regionAndSubzones; } static guess(recheck = false, testCountry, testZone) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; if (!this._guess || recheck) { if (hasIntlDateTime && !testCountry && !testZone) this._guess = (_a = new Intl.DateTimeFormat().resolvedOptions().timeZone) !== null && _a !== void 0 ? _a : 'OS'; else { let country = testCountry; if (!country) { try { if (typeof process !== 'undefined') country = (_f = ((_e = (_c = (_b = process.env) === null || _b === void 0 ? void 0 : _b.LANG) !== null && _c !== void 0 ? _c : (_d = process.env) === null || _d === void 0 ? void 0 : _d.LC_CTYPE) !== null && _e !== void 0 ? _e : '').split(/[-._]/)[1]) === null || _f === void 0 ? void 0 : _f.toUpperCase(); } catch (_m) { } } if (!country) { try { if (typeof navigator !== 'undefined') country = (_h = ((_g = navigator.language) !== null && _g !== void 0 ? _g : '').split(/[-._]/)[1]) === null || _h === void 0 ? void 0 : _h.toUpperCase(); } catch (_o) { } } const osZone = testZone ? Timezone.from(testZone) : this.OS_ZONE; const zoneKey = this.formatUtcOffset(osZone.utcOffset, true) + ';' + floor(osZone.dstOffset / 60); const candidateZones = Array.from((_j = this.zonesByOffsetAndDst[zoneKey]) !== null && _j !== void 0 ? _j : []) .filter(zone => !country || this.doesZoneMatchCountry(zone, country)) .map(zone => ({ zone, rating: osZone.matchRating(Timezone.from(zone)), pop: this.populationForZone[zone] })) .sort((a, b) => b.rating !== a.rating ? b.rating - a.rating : b.pop - a.pop); this._guess = (_l = (_k = candidateZones[0]) === null || _k === void 0 ? void 0 : _k.zone) !== null && _l !== void 0 ? _l : 'OS'; } } return this._guess; } static has(name) { return !!this.zoneLookup[name] || !!this.zonesByLowercase[name.toLowerCase()] || !!this.encodedTimezones[name] || /^(GMT|OS|UTC?|ZONELESS|DATELESS|TAI)$/i.test(name); } static from(name) { return Timezone.getTimezone(name); } static getTimezone(name, longitude) { if (!name) return this.OS_ZONE; const lcName = name.toLowerCase(); if (lcName === 'tai') return this.TAI_ZONE; else if (lcName === 'dateless') return this.DATELESS; else if (lcName === 'zoneless') return this.ZONELESS; if (this.zonesByLowercase[lcName]) name = this.zonesByLowercase[lcName]; const cached = this.zoneLookup[name]; if (cached) return cached; let zone; const $ = /LMT|OS|(?:(GMT|UTC?)?([-+]\d\d(\d{4}|\d\d|:\d\d(:\d\d)?))?)|(?:.+\/.+)|\w+/.exec(name); if ($ === null || $.length === 0) throw new Error('Unrecognized format for timezone name "' + name + '"'); else if ($[0].toUpperCase() === 'LMT') { longitude = (!longitude ? 0 : longitude); zone = new Timezone({ zoneName: 'LMT', currentUtcOffset: Math.round(mod2(longitude, 360) * 4) * 60, usesDst: false, dstOffset: 0, transitions: null }); } else if ($[0].toUpperCase() === 'OS') zone = this.OS_ZONE; else if ($.length > 1 && (/GMT|UTC?/.test($[1]) || $[2])) { let offset = 0; if (!$[1]) name = 'UT' + name; if ($[2]) offset = parseTimeOffset($[2]); zone = new Timezone({ zoneName: name, currentUtcOffset: offset, usesDst: false, dstOffset: 0, transitions: null }); } else if (this.encodedTimezones[name]) { let encodedTimezone = this.encodedTimezones[name]; let aliasFor = null; let popAndC = null; if (!encodedTimezone.includes(';')) { // If no semicolon, must be a link to a (close) duplicate timezone. const $ = /^!(.*,)?(.*)$/.exec(encodedTimezone); // Not an alias timezone, just similar, with possibly different population and country info? if ($) { popAndC = $[1]; encodedTimezone = $[2]; } else aliasFor = encodedTimezone; encodedTimezone = this.encodedTimezones[encodedTimezone]; } zone = new Timezone(this.parseEncodedTimezone(name, encodedTimezone, aliasFor, popAndC)); } else { // Create a timezone equivalent to the OS zone, except with the requested name and an attached error condition. zone = new Timezone({ zoneName: name, currentUtcOffset: osProbableStdOffset, usesDst: osUsesDst, dstOffset: osDstOffset, transitions: osTransitions }); zone._error = 'Unrecognized timezone'; } if (name !== 'LMT' && !zone._error) // Don't cache LMT because of variable longitude-dependent offsets for the same name. this.zoneLookup[name] = zone; return zone; } static getAliasesForZone(zone) { zone = this.zonesByLowercase[zone === null || zone === void 0 ? void 0 : zone.toLowerCase()]; if (!this.zonesAliases[zone]) return []; else return Array.from(this.zonesAliases[zone]); } static hasShortName(name) { return !!this.shortZoneNames[name]; } static getShortZoneNameInfo(shortName) { return clone(this.shortZoneNames[shortName]); } static getPopulation(zoneName) { let population = this.populationForZone[zoneName]; if (population == null) { const aliases = this.getAliasesForZone(zoneName); for (const alias of aliases) { population = this.populationForZone[alias]; if (population != null && population > 0) { this.populationForZone[zoneName] = population; break; } } } if (population == null) this.populationForZone[zoneName] = 0; return population !== null && population !== void 0 ? population : 0; } static getCountries(zoneName) { let countries = this.countriesForZone[zoneName]; if (countries == null) { const aliases = this.getAliasesForZone(zoneName); for (const alias of aliases) { countries = this.countriesForZone[alias]; if (countries.size != null) { this.countriesForZone[zoneName] = countries; break; } } } if (countries == null) this.countriesForZone[zoneName] = new Set(); return new Set(countries); } static doesZoneMatchCountry(zoneName, country) { return this.getCountries(zoneName).has(country.toUpperCase()); } static parseTimeOffset(offset) { let sign = 1; if (offset.startsWith('-')) { sign = -1; offset = offset.substr(1); } else if (offset.startsWith('+')) offset = offset.substr(1); if (offset === '0') return 0; else if (offset === '1') return 3600; else { let offsetSeconds = 60 * (60 * Number(offset.substr(0, 2)) + Number(offset.substr(2, 2))); if (offset.length === 6) offsetSeconds += Number(offset.substr(4, 2)); return sign * offsetSeconds; } } static fromBase60(x) { let sign = 1; let result = 0; let inFractionalPart = false; let power = 1; if (x.startsWith('-')) { sign = -1; x = x.substr(1); } else if (x.startsWith('+')) x = x.substr(1); for (let i = 0; i < x.length; ++i) { let digit = x.charCodeAt(i); if (digit === 46) { // "decimal" point (sexagesimal point, in this case) inFractionalPart = true; continue; } else if (digit > 96) // a-z -> 10-35 digit -= 87; else if (digit > 64) // A-X -> 36-60 digit -= 29; else // 0-9 digit -= 48; if (inFractionalPart) { power /= 60; result += power * digit; } else { result *= 60; result += digit; } } return result * sign; } static extractTimezoneTransitionsFromIntl(zone, endYear) { const transitions = []; const timeOptions = { timeZone: zone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }; const zoneDTF = new Intl.DateTimeFormat('en', timeOptions); let lastSampleTime = millisFromDateTime_SGC(1901, 1, 1, 0, 0, 0, 0); let hour; do { lastSampleTime += 3600000; hour = getDateValue(zoneDTF, lastSampleTime, 'hour'); } while (hour !== 0 && hour !== 1); lastSampleTime += 43200000; const getUtcOffset = (millis) => { const fields = zoneDTF.formatToParts(millis); return floor((millisFromDateTime_SGC(getDateValue(fields, 'year'), getDateValue(fields, 'month'), getDateValue(fields, 'day'), getDateValue(fields, 'hour'), getDateValue(fields, 'minute'), getDateValue(fields, 'second')) - millis) / 1000); }; const MONTH_MSEC = 30 * DAY_MSEC; const aBitLater = lastSampleTime + MONTH_MSEC * 12 * 2; const muchLater = millisFromDateTime_SGC(endYear + 1, 1, 1, 0, 0, 0, 0); let lastOffset = getUtcOffset(lastSampleTime); let probableStdOffset; let probableDstOffset; while (lastSampleTime < muchLater) { const sampleTime = lastSampleTime + MONTH_MSEC; const currentOffset = getUtcOffset(sampleTime); if (probableStdOffset === undefined && sampleTime >= aBitLater) probableStdOffset = probableDstOffset = currentOffset; if (currentOffset !== lastOffset) { if (sampleTime >= aBitLater) { probableStdOffset = Math.min(probableStdOffset, currentOffset); probableDstOffset = Math.max(probableDstOffset, currentOffset); } let low = lastSampleTime; let high = sampleTime; while (high - low > MINUTE_MSEC) { const mid = Math.floor((high + low) / 2 / MINUTE_MSEC) * MINUTE_MSEC; const sampleOffset = getUtcOffset(mid); if (sampleOffset === lastOffset) low = mid; else high = mid; } transitions.push({ transitionTime: high, utcOffset: currentOffset, dstOffset: 0 }); lastOffset = currentOffset; } lastSampleTime = sampleTime; } if (transitions.length < 2 || probableDstOffset <= probableStdOffset) return []; // If the timezone isn't historical, but instead projects DST rules indefinitely backward in time, we might have accidentally // captured a DST offset for the first transition, something that will wrongly make DST look like the starting base UTC offset. if (transitions[0].utcOffset === probableDstOffset && transitions[1].utcOffset === probableStdOffset) { transitions.splice(0, 1); transitions[0].transitionTime = Number.MIN_SAFE_INTEGER; } transitions.forEach((transition, index) => { var _a; if (transition.utcOffset === probableDstOffset && ((_a = transitions[index - 1]) === null || _a === void 0 ? void 0 : _a.utcOffset) === probableStdOffset) transition.dstOffset = probableDstOffset - probableStdOffset; }); return transitions; } static applyTransitionRules(transitions, startYear, endYear, currentUtcOffset, stdRule, dstRule, lastTTime, dstOffset, dstName, stdName, backfill = false) { for (let year = startYear; year < endYear; ++year) { const stdTime = stdRule.getTransitionTime(year, currentUtcOffset, dstOffset); const dstTime = dstRule.getTransitionTime(year, currentUtcOffset, 0); const firstRule = (dstTime < stdTime ? dstRule : stdRule); const firstTime = (dstTime < stdTime ? dstTime : stdTime); const secondRule = (dstTime > stdTime ? dstRule : stdRule); const secondTime = (dstTime > stdTime ? dstTime : stdTime); if (firstTime > lastTTime + TIME_GAP_AFTER_LAST_TRANSITION && (backfill || year >= firstRule.startYear)) transitions.push({ transitionTime: firstTime, utcOffset: currentUtcOffset + firstRule.save, dstOffset: firstRule.save, name: firstRule.save ? dstName : stdName }); if (secondTime > lastTTime + TIME_GAP_AFTER_LAST_TRANSITION && (backfill || year >= secondRule.startYear)) transitions.push({ transitionTime: secondTime, utcOffset: currentUtcOffset + secondRule.save, dstOffset: secondRule.save, name: secondRule.save ? dstName : stdName }); } } static countriesStringToSet(s) { return s.includes(' ') ? new Set(s.split(/\s+/)) : new Set(s.split(/(\w\w)/).filter(s => !!s)); } static parseEncodedTimezone(name, etz, aliasFor, popAndC) { var _a, _b; let transitions = []; const sections = etz.split(';'); let parts = sections[0].split(' '); const baseUtcOffset = this.parseTimeOffset(parts[0]); const currentUtcOffset = this.parseTimeOffset(parts[1]); const dstOffset = round(Number(parts[2]) * 60); let displayName; let lastStdName; let lastDstName; let firstTTime = Number.MIN_SAFE_INTEGER; let population = 0; let countries = ''; let stdRule; let dstRule; transitions.push({ transitionTime: Number.MIN_SAFE_INTEGER, utcOffset: baseUtcOffset, dstOffset: 0 }); if (sections.length > 5) { if (!popAndC) popAndC = sections[5] + ',' + ((_a = sections[6]) !== null && _a !== void 0 ? _a : ''); sections.length = 5; while (!last(sections)) --sections.length; } if (popAndC) { const parts = popAndC.split(','); population = toNumber(parts[0]); countries = (_b = parts[1]) !== null && _b !== void 0 ? _b : ''; } if (sections.length > 1) { const offsets = sections[1].split(' '); const utcOffsets = []; const dstOffsets = []; const names = []; for (let i = 0; i < offsets.length; ++i) { const offset = offsets[i]; parts = offset.split('/'); utcOffsets[i] = round(this.fromBase60(parts[0]) * 60); dstOffsets[i] = round(this.fromBase60(parts[1]) * 60); if (parts.length > 2) names[i] = parts[2]; else names[i] = null; } transitions[0].name = names[0]; if (sections.length > 3) { const offsetIndices = sections[2]; const transitionTimes = sections[3].split(' '); let lastTTime = 0; for (let i = 0; i < offsetIndices.length; ++i) { const offsetIndex = this.fromBase60(offsetIndices.substr(i, 1)); const ttime = lastTTime + round(this.fromBase60(transitionTimes[i]) * 60); transitions.push({ transitionTime: ttime * 1000, utcOffset: utcOffsets[offsetIndex], dstOffset: dstOffsets[offsetIndex], name: names[offsetIndex] }); lastTTime = ttime; if (i === 0) firstTTime = ttime; if (dstOffsets[offsetIndex] !== 0) lastDstName = names[offsetIndex]; else lastStdName = names[offsetIndex]; } if (sections.length > 4) { // Extend transitions table with rules-based Daylight Saving Time changes. lastTTime *= 1000; const rules = sections[4].split(','); stdRule = new Rule(rules[0]); dstRule = new Rule(rules[1]); const startYear = dateAndTimeFromMillis_SGC(lastTTime).y - 1; this.applyTransitionRules(transitions, startYear, LAST_DST_YEAR, currentUtcOffset, stdRule, dstRule, lastTTime, dstOffset, lastDstName, lastStdName); // Make sure last transition isn't DST if (transitions[transitions.length - 1].dstOffset !== 0) transitions.length -= 1; const firstExplicitTransitionYear = dateAndTimeFromMillis_SGC(firstTTime * 1000).y; // Backfill transitions table with Intl-extracted transitions or rules-based Daylight Saving Time changes. if (firstExplicitTransitionYear > 2000 && transitions.length > 1) { const insertTransitions = this.extractTimezoneTransitionsFromIntl(name, firstExplicitTransitionYear); let fromRules = false; if (insertTransitions.length === 0 && currentUtcOffset === baseUtcOffset) { fromRules = true; this.applyTransitionRules(insertTransitions, 1925, firstExplicitTransitionYear + 1, currentUtcOffset, stdRule, dstRule, Number.MIN_SAFE_INTEGER + 1, dstOffset, lastDstName, lastStdName, true); } if (insertTransitions.length > 0) { // Make sure first added transition isn't to standard time. if (fromRules && insertTransitions.length > 1 && insertTransitions[0].dstOffset === 0 && insertTransitions[1].dstOffset !== 0) insertTransitions.splice(0, 1); // Make sure last added transition IS to standard time, and doesn't overlap already-created transitions. while (insertTransitions.length > 0 && last(insertTransitions).dstOffset !== 0 || last(insertTransitions).transitionTime >= transitions[1].transitionTime) insertTransitions.splice(insertTransitions.length - 1, 1); if (insertTransitions[0].transitionTime === transitions[0].transitionTime) insertTransitions.splice(0, 1); transitions.splice(1, 0, ...insertTransitions); } } } } } if (transitions.length === 1) { displayName = transitions[0].name; transitions = null; } return { zoneName: name, currentUtcOffset: currentUtcOffset, usesDst: dstOffset !== 0, dstOffset: dstOffset, displayName: displayName, transitions: transitions, population, countries: this.countriesStringToSet(countries), aliasFor, stdRule, dstRule }; } static buildAliases(srcZone, dstZone) { let source = this.zonesAliases[srcZone]; let destination = this.zonesAliases[dstZone]; if (!source) source = this.zonesAliases[srcZone] = new Set(); if (!destination) destination = this.zonesAliases[dstZone] = new Set(); source.add(dstZone); destination.add(srcZone); source.forEach(zone => { if (zone !== dstZone) { destination.add(zone); this.zonesAliases[zone].add(dstZone); } }); } static extractZoneInfo() { this.shortZoneNames = {}; this.zonesByLowercase = { gmt: 'GMT', lmt: 'LMT', os: 'OS', tai: 'TAI', ut: 'UT', utc: 'UTC' }; this.zonesByOffsetAndDst = {}; this.countriesForZone = {}; this.zonesAliases = {}; this.zonesForCountry = {}; this.populationForZone = {}; const preferredZones = new Set([ 'Australia/ACT', 'Australia/Adelaide', 'Asia/Tokyo', 'Asia/Hong_Kong', 'Asia/Jakarta', 'Asia/Novosibirsk', 'Asia/Calcutta', 'Asia/Karachi', 'Europe/Moscow', 'Africa/Cairo', 'Europe/Paris', 'Europe/London', 'Atlantic/Azores', 'America/Scoresbysund', 'America/Godthab', 'America/St_Johns', 'America/Halifax', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'America/Anchorage', 'Pacific/Honolulu', 'America/Adak', 'Pacific/Apia' ]); const sortKey = (key) => preferredZones.has(key) ? '!' + key : key; const keys = Object.keys(this.encodedTimezones) .filter(key => !nonZones.has(key) && !key.startsWith('_')) .sort((a, b) => compareStrings(sortKey(a), sortKey(b))); keys.forEach(ianaName => { var _a, _b; let etz = this.encodedTimezones[ianaName]; let otherZone; let popAndC; let done = false; this.zonesByLowercase[ianaName.toLowerCase()] = ianaName; if (!etz.includes(';')) { const $ = /^!(.*,)?(.*)$/.exec(etz); if ($) { popAndC = $[1]; otherZone = $[2]; etz = this.encodedTimezones[otherZone]; } else { otherZone = etz; done = true; } } if (otherZone) { this.buildAliases(ianaName, otherZone); this.buildAliases(otherZone, ianaName); } if (done) return; const sections = etz.split(';'); let parts = sections[0].split(' '); const currentUtcOffset = this.parseTimeOffset(parts[1]); const currentDstOffset = round(Number(parts[2]) * 60); if (sections.length > 1) { const baseOffset = sections[0].split(' '); const offsetKey = (baseOffset.length > 2 ? baseOffset[1] + ';' + baseOffset[2] : null); const offsets = sections[1].split(' '); for (let i = 0; i < offsets.length; ++i) { const offset = offsets[i]; parts = offset.split('/'); if (parts.length > 2) { const name = parts[2]; const info = this.shortZoneNames[name]; const utcOffset = round(this.fromBase60(parts[0]) * 60); const dstOffset = round(this.fromBase60(parts[1]) * 60); if ((!info || ianaName.startsWith('America/') && !info.ianaName.startsWith('America/')) && utcOffset - dstOffset === currentUtcOffset && (!dstOffset || (dstOffset && dstOffset === currentDstOffset))) { this.shortZoneNames[name] = { utcOffset, dstOffset, ianaName }; } } if (!popAndC && sections.length > 5) popAndC = sections[5] + ',' + ((_a = sections[6]) !== null && _a !== void 0 ? _a : ''); if (offsetKey) { let zones = this.zonesByOffsetAndDst[offsetKey]; if (!zones) this.zonesByOffsetAndDst[offsetKey] = zones = new Set(); zones.add(ianaName); } if (popAndC) { const parts = popAndC.split(','); const countries = this.countriesStringToSet((_b = parts[1]) !== null && _b !== void 0 ? _b : ''); if (countries.size > 0) this.countriesForZone[ianaName] = countries; this.populationForZone[ianaName] = toNumber(parts[0]); countries.forEach(country => { let zones = this.zonesForCountry[country]; if (!zones) this.zonesForCountry[country] = zones = new Set(); zones.add(ianaName); }); } } } }); } static extractDeltaTs() { var _a; const deltaTs = (_a = this.encodedTimezones) === null || _a === void 0 ? void 0 : _a.deltaTs; const lastLeap = this.getDateAfterLastKnownLeapSecond(); if (deltaTs) deltaTUpdater(deltaTs.split(/\s+/).map(dt => toNumber(dt)), lastLeap); else deltaTUpdater(null, lastLeap); } static extractLeapSeconds() { var _a; this.leapSeconds = []; this.lastLeapSecond = undefined; const leaps = (_a = this.encodedTimezones) === null || _a === void 0 ? void 0 : _a.leapSeconds; if (!leaps) return; let deltaTai = -1; this.leapSeconds.push({ utcMillis: Number.MIN_SAFE_INTEGER, taiMillis: Number.MIN_SAFE_INTEGER + 10000, dateAfter: null, deltaTai: 0, isNegative: false }); // Proleptic extension of leap seconds back to 1958, per Tony Finch, https://fanf.livejournal.com/69586.html. const leapSecondDays = [-4383, -3837, -3106, -2376, -1826, -1280, -915, -549, -184, 181, 546]; leapSecondDays.push(...leaps.split(/\s+/).map(day => toNumber(day))); leapSecondDays.forEach((signCodedDay, index) => { const day = (index < 11 ? signCodedDay : abs(signCodedDay)); const utcMillis = day * DAY_MSEC; deltaTai += (index > 10 && signCodedDay < 0 ? -1 : 1); this.leapSeconds.push({ utcMillis, taiMillis: utcMillis + deltaTai * 1000, dateAfter: getDateFromDayNumber_SGC(day), deltaTai, isNegative: index > 10 && signCodedDay < 0 }); }); this.lastLeapSecond = last(this.leapSeconds).dateAfter; } static formatUtcOffset(offsetSeconds, noColons = false) { if (offsetSeconds == null) return '?'; let result = offsetSeconds < 0 ? '-' : '+'; const colon = noColons ? '' : ':'; offsetSeconds = Math.abs(offsetSeconds); const hours = div_tt0(offsetSeconds, 3600); offsetSeconds -= hours * 3600; const minutes = div_tt0(offsetSeconds, 60); offsetSeconds -= minutes * 60; result += padLeft(hours, 2, '0') + colon + padLeft(minutes, 2, '0'); if (offsetSeconds !== 0) { result += colon + padLeft(floor(offsetSeconds), 2, '0'); if (offsetSeconds % 1 !== 0) { result += '.' + offsetSeconds.toFixed(3).substr(2); result = result.replace(/\.000$/, ''); } } return result; } static getDstSymbol(dstOffsetSeconds) { if (dstOffsetSeconds == null) return ''; switch (dstOffsetSeconds) { case 0: return ''; case 1800: return '^'; case 3600: return '§'; case 7200: return '#'; default: return (dstOffsetSeconds < 0 ? '\u2744' : '~'); // Snowflake character for negative/winter DST } } get zoneName() { return this._zoneName; } get utcOffset() { return this._utcOffset; } get usesDst() { return this._usesDst; } get dstOffset() { return this._dstOffset; } get error() { return this._error; } get aliasFor() { return this._aliasFor; } get countries() { return new Set(this._countries); } get population() { return this._population; } get stdRule() { var _a; return (_a = this._stdRule) === null || _a === void 0 ? void 0 : _a.toString(); } get dstRule() { var _a; return (_a = this._dstRule) === null || _a === void 0 ? void 0 : _a.toString(); } getOffset(utcTime, day = 0) { if (!this.transitions || this.transitions.length === 0) return this._utcOffset; else { let transition = this.findTransitionByUtc(utcTime); if (day !== 0 && transition.wallTimeDay !== day) transition = this.findTransitionByUtc(utcTime - 1); return transition.utcOffset; } } getDisplayName(utcTime) { let name; if (!this.transitions || this.transitions.length === 0) { name = this.displayName; if (!name) name = Timezone.formatUtcOffset(this.utcOffset); } else { const transition = this.findTransitionByUtc(utcTime); name = transition.name; if (!name) name = Timezone.formatUtcOffset(transition.utcOffset); } let match = /^[+-]\d\d$/.exec(name); if (match) name = match[0] + ':00'; else { match = /^([+-]\d\d)(\d\d)$/.exec(name); if (match) name = match[1] + ':' + match[2]; else { match = /^([+-]\d\d)(\d\d)(\d\d)$/.exec(name); if (match) name = match[1] + ':' + match[2] + ':' + match[3]; } } return name; } supportsCountry(country) { return this._countries.has(country.toUpperCase()); } getOffsetForWallTime(wallTime) { if (!this.transitions || this.transitions.length === 0) return this._utcOffset; else { const transition = this.findTransitionByWallTime(wallTime); return transition.utcOffset; } } getFormattedOffset(utcTime, noColons = false) { return Timezone.formatUtcOffset(this.getOffset(utcTime), noColons); } getOffsets(utcTime) { if (!this.transitions || this.transitions.length === 0) return [this._utcOffset, this._dstOffset]; else { const transition = this.findTransitionByUtc(utcTime); return [transition.utcOffset, transition.dstOffset]; } } isDuringDst(utcTime) { if (!this.transitions || this.transitions.length === 0) return false; else { const transition = this.findTransitionByUtc(utcTime); return (transition.dstOffset !== 0); } } getAllTransitions() { return !this.transitions || this.transitions.length === 0 ? null : clone(this.transitions); } findTransitionByUtc(utcTime) { if (!this.transitions || this.transitions.length === 0) return null; for (let i = 0; i < this.transitions.length - 1; ++i) { if (this.transitions[i].transitionTime <= utcTime && utcTime < this.transitions[i + 1].transitionTime) return this.transitions[i]; } return last(this.transitions); } static findDeltaTaiFromUtc(utcTime) { if (!this.leapSeconds || this.leapSeconds.length === 0) return null; for (let i = this.leapSeconds.length - 1; i >= 0; --i) { let leapInfo = this.leapSeconds[i]; const next = this.leapSeconds[i + 1]; if (utcTime >= leapInfo.utcMillis) { leapInfo = clone(leapInfo); leapInfo.inLeap = (next && !next.isNegative && utcTime >= next.utcMillis - 1000); leapInfo.inNegativeLeap = (next && next.isNegative && utcTime >= next.utcMillis - 2000 && utcTime < next.utcMillis - 1000); return leapInfo; } } return Object.assign({ inLeap: false }, this.leapSeconds[0]); } static getLeapSecondList() { return clone(this.leapSeconds); } static getDateAfterLastKnownLeapSecond() { return this.lastLeapSecond; } static getUpcomingLeapSecond() { if (!this.lastLeapSecond) return null; else if (getDayNumber_SGC(this.lastLeapSecond) * DAY_MSEC > Date.now()) return this.lastLeapSecond; else return null; } static findDeltaTaiFromTai(taiTime) { if (!this.leapSeconds || this.leapSeconds.length === 0) return null; for (let i = this.leapSeconds.length - 1; i >= 0; --i) { let leapInfo = this.leapSeconds[i]; const next = this.leapSeconds[i + 1]; if (taiTime >= leapInfo.taiMillis) { leapInfo = clone(leapInfo); leapInfo.inLeap = (next && !next.isNegative && taiTime >= next.taiMillis - 1000); return leapInfo; } } return Object.assign({ inLeap: false }, this.leapSeconds[0]); } findTransitionByWallTime(wallTime) { if (!this.transitions || this.transitions.length === 0) return null; for (let i = 0; i < this.transitions.length - 1; ++i) { if (this.transitions[i].wallTime <= wallTime && wallTime < this.transitions[i + 1].wallTime) return this.transitions[i]; } return last(this.transitions); } matchRating(other) { if (other === this) return Number.MAX_SAFE_INTEGER; else if (other.utcOffset !== this.utcOffset || other.dstOffset !== this.dstOffset) return 0; else if ((this.transitions == null && other.transitions == null) || (this.transitions.length < 25 && isEqual(this.transitions, other.transitions))) return Number.MAX_SAFE_INTEGER; let thisIndex = this.transitions.length - 1; let otherIndex = other.transitions.length - 1; while (this.transitions[thisIndex].transitionTime > other.transitions[otherIndex].transitionTime) --thisIndex; while (other.transitions[otherIndex].transitionTime > this.transitions[thisIndex].transitionTime) --otherIndex; for (let i = 0; i < thisIndex && i < otherIndex; ++i) { const tt = this.transitions[thisIndex - 1]; const to = other.transitions[otherIndex - 1]; if (tt.transitionTime !== to.transitionTime || tt.utcOffset !== to.utcOffset || tt.dstOffset !== to.dstOffset || tt.baseOffsetChanged !== to.baseOffsetChanged) return i; } return thisIndex === otherIndex ? Number.MAX_SAFE_INTEGER : min(thisIndex, otherIndex); } } Timezone.encodedTimezones = {}; Timezone.shortZoneNames = {}; Timezone.zonesByLowercase = {}; Timezone.zonesByOffsetAndDst = {}; Timezone.countriesForZone = {}; Timezone.zonesForCountry = {}; Timezone.populationForZone = {}; Timezone.leapSeconds = []; Timezone._version = 'unspecified'; Timezone.OS_ZONE = new Timezone({ zoneName: 'OS', currentUtcOffset: osProbableStdOffset, usesDst: osUsesDst, dstOffset: osDstOffset, transitions: osTransitions }); Timezone.UT_ZONE = new Timezone({ zoneName: 'UT', currentUtcOffset: 0, usesDst: false, dstOffset: 0, transitions: null }); Timezone.TAI_ZONE = new Timezone({ zoneName: 'TAI', currentUtcOffset: 0, usesDst: false, dstOffset: 0, transitions: null }); Timezone.ZONELESS = new Timezone({ zoneName: 'ZONELESS', currentUtcOffset: 0, usesDst: false, dstOffset: 0, transitions: null }); Timezone.DATELESS = new Timezone({ zoneName: 'DATE