@tubular/time
Version:
Date/time, IANA timezones, leap seconds, TAI/UTC conversions, calendar with settable Julian/Gregorian switchover
1,062 lines • 50.2 kB
JavaScript
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