@tubular/time
Version:
Date/time, IANA timezones, leap seconds, TAI/UTC conversions, calendar with settable Julian/Gregorian switchover
1,118 lines (1,117 loc) • 76.9 kB
JavaScript
import { abs, div_rd, floor, max, min, mod, mod2, round, sign } from '@tubular/math';
import { clone, forEach, isArray, isEqual, isNumber, isObject, isString, toNumber } from '@tubular/util';
import { getDayNumber_SGC, handleVariableDateArgs, isGregorianType, Calendar, getDateFromDayNumberGregorian } from './calendar';
import { formatter, DAY_MSEC, DELTA_MJD, HOUR_MSEC, MAX_YEAR, MIN_YEAR, MINUTE_MSEC, orderFields, parseISODateTime, parseTimeOffset, purgeAliasFields, syncDateAndTime, UNIX_TIME_ZERO_AS_JULIAN_DAY, validateDateAndTime, minimizeFields } from './common';
import { Timezone } from './timezone';
import { getMinDaysInWeek, getStartOfWeek, hasIntlDateTime, normalizeLocale } from './locale-data';
import { taiMillisToTdt, taiToUtMillis, tdtDaysToTaiMillis, tdtToUt, utToTaiMillis, utToTdt } from './ut-converter';
export var DateTimeField;
(function (DateTimeField) {
DateTimeField[DateTimeField["BAD_FIELD"] = Number.NEGATIVE_INFINITY] = "BAD_FIELD";
DateTimeField[DateTimeField["FULL"] = -1] = "FULL";
DateTimeField[DateTimeField["MILLI"] = 0] = "MILLI";
DateTimeField[DateTimeField["MILLI_TAI"] = 1] = "MILLI_TAI";
DateTimeField[DateTimeField["SECOND"] = 2] = "SECOND";
DateTimeField[DateTimeField["SECOND_TAI"] = 3] = "SECOND_TAI";
DateTimeField[DateTimeField["MINUTE"] = 4] = "MINUTE";
DateTimeField[DateTimeField["MINUTE_TAI"] = 5] = "MINUTE_TAI";
DateTimeField[DateTimeField["HOUR_12"] = 6] = "HOUR_12";
DateTimeField[DateTimeField["HOUR"] = 7] = "HOUR";
DateTimeField[DateTimeField["HOUR_TAI"] = 8] = "HOUR_TAI";
DateTimeField[DateTimeField["AM_PM"] = 9] = "AM_PM";
DateTimeField[DateTimeField["DAY"] = 10] = "DAY";
DateTimeField[DateTimeField["DAY_TAI"] = 11] = "DAY_TAI";
DateTimeField[DateTimeField["DAY_BY_WEEK"] = 12] = "DAY_BY_WEEK";
DateTimeField[DateTimeField["DAY_BY_WEEK_LOCALE"] = 13] = "DAY_BY_WEEK_LOCALE";
DateTimeField[DateTimeField["DAY_OF_YEAR"] = 14] = "DAY_OF_YEAR";
DateTimeField[DateTimeField["WEEK"] = 15] = "WEEK";
DateTimeField[DateTimeField["WEEK_LOCALE"] = 16] = "WEEK_LOCALE";
DateTimeField[DateTimeField["MONTH"] = 17] = "MONTH";
DateTimeField[DateTimeField["QUARTER"] = 18] = "QUARTER";
DateTimeField[DateTimeField["YEAR"] = 19] = "YEAR";
DateTimeField[DateTimeField["YEAR_WEEK"] = 20] = "YEAR_WEEK";
DateTimeField[DateTimeField["YEAR_WEEK_LOCALE"] = 21] = "YEAR_WEEK_LOCALE";
DateTimeField[DateTimeField["ERA"] = 22] = "ERA";
})(DateTimeField || (DateTimeField = {}));
function dtfToString(field) {
if (isString(field))
return `"${field}"`;
else if (DateTimeField[field])
return `"${DateTimeField[field]}"`;
else
return `#${field}`;
}
const fieldNames = {
milli: DateTimeField.MILLI,
millis: DateTimeField.MILLI,
millisecond: DateTimeField.MILLI,
milliseconds: DateTimeField.MILLI,
second: DateTimeField.SECOND,
seconds: DateTimeField.SECOND,
minute: DateTimeField.MINUTE,
minutes: DateTimeField.MINUTE,
hour12: DateTimeField.HOUR_12,
hours12: DateTimeField.HOUR_12,
hour: DateTimeField.HOUR,
hours: DateTimeField.HOUR,
ampm: DateTimeField.AM_PM,
am_pm: DateTimeField.AM_PM,
day: DateTimeField.DAY,
days: DateTimeField.DAY,
date: DateTimeField.DAY,
dayByWeek: DateTimeField.DAY_BY_WEEK,
dayByWeekLocale: DateTimeField.DAY_BY_WEEK_LOCALE,
dayOfYear: DateTimeField.DAY_OF_YEAR,
week: DateTimeField.WEEK,
weeks: DateTimeField.WEEK,
weekLocale: DateTimeField.WEEK_LOCALE,
month: DateTimeField.MONTH,
months: DateTimeField.MONTH,
quarter: DateTimeField.QUARTER,
quarters: DateTimeField.QUARTER,
year: DateTimeField.YEAR,
years: DateTimeField.YEAR,
yearWeek: DateTimeField.YEAR_WEEK,
yearWeekLocale: DateTimeField.YEAR_WEEK_LOCALE,
era: DateTimeField.ERA,
milli_tai: DateTimeField.MILLI_TAI,
millis_tai: DateTimeField.MILLI_TAI,
millisecond_tai: DateTimeField.MILLI_TAI,
milliseconds_tai: DateTimeField.MILLI_TAI,
second_tai: DateTimeField.SECOND_TAI,
seconds_tai: DateTimeField.SECOND_TAI,
minute_tai: DateTimeField.MINUTE_TAI,
minutes_tai: DateTimeField.MINUTE_TAI,
hour_tai: DateTimeField.HOUR_TAI,
hours_tai: DateTimeField.HOUR_TAI,
day_tai: DateTimeField.DAY_TAI,
days_tai: DateTimeField.DAY_TAI
};
forEach(fieldNames, (key, value) => { if (key !== key.toLowerCase())
fieldNames[key.toLowerCase()] = value; });
function fieldNameToField(field) {
var _a;
if (isString(field))
return (_a = fieldNames[field.toLowerCase()]) !== null && _a !== void 0 ? _a : DateTimeField.BAD_FIELD;
else
return field;
}
const localeTest = /^[a-z][a-z][-_a-z]*$/i;
const lockError = new Error('This DateTime instance is locked and immutable');
const nonIntError = new Error('Amounts for add/roll must be integers');
// noinspection SpellCheckingInspection
const fullIsoFormat = 'Y-MM-DDTHH:mm:ss.SSSZ';
// noinspection SpellCheckingInspection
const fullAltFormat = 'Y-MM-DDTHH:mm:ss.SSSRZv';
const timeOnlyFormat = 'HH:mm:ss.SSS';
const DATELESS = Timezone.DATELESS;
const ZONELESS = Timezone.ZONELESS;
export class DateTime extends Calendar {
constructor(initialTime, timezone, gregorianOrLocale, gregorianChange) {
var _a;
super(gregorianChange !== null && gregorianChange !== void 0 ? gregorianChange : (isGregorianType(gregorianOrLocale) ? gregorianOrLocale : undefined));
this._deltaTaiMillis = 0;
this._epochMillis = 0;
this._leapSecondMillis = 0;
this._locale = DateTime.defaultLocale;
this._timezone = DateTime.defaultTimezone;
this.wallTimeCounter = 0;
if (!DateTime.defaultTimezoneExplicit && !timezone) {
if (hasIntlDateTime && Timezone.guess() !== 'OS')
this._timezone = DateTime.defaultTimezone = Timezone.from(Timezone.guess());
if (this._timezone.error)
this._timezone = Timezone.OS_ZONE;
else
DateTime.defaultTimezoneExplicit = true;
}
let parseZone;
if (isObject(initialTime) && initialTime.tai != null) {
initialTime = initialTime.tai;
parseZone = Timezone.TAI_ZONE;
}
if (isArray(initialTime)) {
const t = {};
[t.y, t.m, t.d, t.hrs, t.min, t.sec, t.millis] = initialTime;
forEach((initialTime = t), (key, value) => value === undefined ? delete t[key] : null);
}
if (isEqual(initialTime, {}))
initialTime = null;
let occurrence = 0;
if (isString(initialTime)) {
if (initialTime.includes('₂'))
occurrence = 2;
initialTime = initialTime.replace(/[\u00AD\u2010-\u2014\u2212]/g, '-').replace(/\s+/g, ' ').replace(/₂/g, '').trim();
let $ = /^\/Date\((\d+)([-+]\d\d\d\d)?\)\/$/i.exec(initialTime);
if ($) {
const offset = $[2] ? parseTimeOffset($[2]) * 1000 : 0;
initialTime = new Date(toNumber($[1]) + offset).toISOString().slice(0, -1) + ((_a = $[2]) !== null && _a !== void 0 ? _a : '');
}
const saveTime = initialTime;
let zone;
$ = /(Z|\bEtc\/GMT(?:0|[-+]\d{1,2})|\bTAI|\b[_/a-z]+)$/i.exec(initialTime);
if ($) {
zone = $[1];
initialTime = initialTime.slice(0, -zone.length).trim() || null;
if (/^(Z|UTC?|GMT)$/i.test(zone))
zone = 'UT';
else if (/^TAI$/i.test(zone))
zone = 'TAI';
parseZone = Timezone.has(zone) ? Timezone.from(zone) : null;
if (!parseZone || (parseZone instanceof Timezone && parseZone.error)) {
const szni = Timezone.hasShortName(zone) ? Timezone.getShortZoneNameInfo(zone) : null;
if (szni) {
if (szni.ianaName && Timezone.has(szni.ianaName))
parseZone = Timezone.from(szni.ianaName);
if (initialTime)
initialTime += ' ' + Timezone.formatUtcOffset(szni.utcOffset);
}
else if ($.index === 0) {
initialTime = saveTime;
parseZone = null;
}
else {
this._error = `Bad timezone: ${zone}`;
this._epochMillis = null;
return;
}
}
}
if (initialTime) {
try {
initialTime = parseISODateTime(initialTime, true);
if (initialTime.y == null && initialTime.yw == null && initialTime.ywl == null && initialTime.n == null &&
initialTime.jde == null && initialTime.mjde == null && initialTime.jdu == null && initialTime.mjdu == null) {
parseZone = DATELESS;
delete initialTime.utcOffset;
timezone = null;
}
else if (occurrence)
initialTime.occurrence = occurrence;
if (initialTime.utcOffset && !timezone) {
if (parseZone)
timezone = parseZone;
parseZone = new Timezone({
dstOffset: 0,
transitions: null,
usesDst: false,
zoneName: 'UT' + Timezone.formatUtcOffset(initialTime.utcOffset),
currentUtcOffset: initialTime.utcOffset
});
}
}
catch (e) {
initialTime = Date.parse(initialTime + (zone ? ' ' + zone : ''));
if (isNaN(initialTime)) {
this._error = e.message;
this._epochMillis = null;
return;
}
}
}
else
initialTime = null;
}
if (isString(timezone))
timezone = Timezone.from(timezone);
if (timezone === null || timezone === void 0 ? void 0 : timezone.error) {
this._error = `Bad timezone: ${timezone.zoneName}`;
this._epochMillis = null;
return;
}
if (parseZone)
this._timezone = parseZone;
else if (timezone)
this._timezone = timezone;
if (!isNumber(gregorianOrLocale) &&
((isString(gregorianOrLocale) && localeTest.test(gregorianOrLocale)) ||
(isArray(gregorianOrLocale) && isString(gregorianOrLocale[0]))))
this._locale = normalizeLocale(gregorianOrLocale);
if (initialTime instanceof Date)
this.epochMillis = +initialTime;
else if (isObject(initialTime)) {
if (!parseZone && !timezone && initialTime.utcOffset != null && initialTime.utcOffset !== 0)
this._timezone = Timezone.from(Timezone.formatUtcOffset(initialTime.utcOffset));
try {
this.wallTime = initialTime;
}
catch (e) {
this._error = e.message;
this._epochMillis = null;
return;
}
}
else
this.epochMillis = (isNumber(initialTime) ? initialTime :
(parseZone === Timezone.TAI_ZONE || (parseZone == null && timezone === Timezone.TAI_ZONE) ?
utToTaiMillis(Date.now(), true) : Date.now()));
if (parseZone && timezone)
this.timezone = timezone;
}
static julianDay(millis) {
return millis / DAY_MSEC + UNIX_TIME_ZERO_AS_JULIAN_DAY;
}
static millisFromJulianDay(jd) {
return round(DAY_MSEC * (jd - UNIX_TIME_ZERO_AS_JULIAN_DAY));
}
static julianDay_SGC(year, month, day, hour = 0, minute = 0, second = 0) {
return getDayNumber_SGC(year, month, day) + UNIX_TIME_ZERO_AS_JULIAN_DAY +
(hour + (minute + second / 60.0) / 60.0) / 24.0;
}
static getDefaultCenturyBase() { return DateTime.defaultCenturyBase; }
static setDefaultCenturyBase(newBase) { DateTime.defaultCenturyBase = newBase; }
static getDefaultLocale() { return DateTime.defaultLocale; }
static setDefaultLocale(newLocale) { DateTime.defaultLocale = newLocale; }
static getDefaultTimezone() { return DateTime.defaultTimezone; }
static setDefaultTimezone(newZone) {
if (isString(newZone))
newZone = Timezone.from(newZone);
this.defaultTimezone = newZone;
this.defaultTimezoneExplicit = true;
}
static isDateTime(obj) { return obj instanceof DateTime; }
static compare(d1, d2, resolution = DateTimeField.FULL) {
resolution = fieldNameToField(resolution);
if (isString(d2) || isNumber(d2) || d2 instanceof Date)
d2 = new DateTime(d2, d1.timezone, d1.locale, d1.getGregorianChange());
if (d1.type !== d2.type)
throw new Error(`Mismatched DateTime types ${d1.type}/${d2.type}`);
else if (d1._timezone === DATELESS && resolution > DateTimeField.HOUR)
throw new Error(`Resolution ${dtfToString(resolution)} not valid for time-only values`);
if (resolution === DateTimeField.FULL || resolution === DateTimeField.MILLI)
return this.milliCompare(d1, d2);
const divisor = [1, 1, 1000, 1000, MINUTE_MSEC, MINUTE_MSEC, undefined, HOUR_MSEC, HOUR_MSEC][resolution];
if (divisor != null && divisor < DateTimeField.MINUTE) {
const diff = (d1.taiMillis - d2.taiMillis) / divisor;
return abs(diff) < 0.1 ? 0 : sign(diff);
}
else if (divisor != null) // Use _epochMillis here so minutes and higher round off correctly
return floor(d1._epochMillis / divisor) - floor(d2._epochMillis / divisor);
else if (resolution === DateTimeField.DAY)
return floor(d1._wallTime.n) - floor(d2._wallTime.n);
else if (resolution === DateTimeField.MONTH)
return (d1.wallTime.y !== d2.wallTime.y ? d1.wallTime.y - d2.wallTime.y : d1.wallTime.m - d2.wallTime.m);
else if (resolution === DateTimeField.YEAR)
return d1.wallTime.y - d2.wallTime.y;
throw new Error(`Resolution ${dtfToString(resolution)} not valid`);
}
/**
* While TAI millis is generally the best way to compare two dates for sort order, two non-TAI DateTime instances
* might have different epochMillis, but identical taiMillis, because of rounding errors. On the other hand, two
* non-TAI instances might have identical epochMillis during a leap second, with leapSecondMillis being the only
* distinguishing difference. TAI is the only reasonable way to compare a TAI DateTime and a non-TAI DateTime.
*/
static milliCompare(d1, d2) {
if (!d1.isTai() && !d2.isTai()) {
const diff = d1.epochMillis - d2.epochMillis;
if (diff !== 0)
return diff;
else
return d1._leapSecondMillis - d2._leapSecondMillis;
}
return d1.taiMillis - d2.taiMillis;
}
clone(cloneLock = true) {
const copy = clone(this, new Set([Timezone]));
copy._locked = cloneLock ? this._locked : false;
return copy;
}
get type() {
if (this._timezone === ZONELESS)
return 'ZONELESS';
else if (this._timezone === DATELESS)
return 'DATELESS';
else
return 'DATETIME';
}
get valid() { return this._epochMillis != null && !isNaN(this._epochMillis); }
get error() { return this._error || ((!this.valid && 'general error') || undefined); }
throwIfInvalid() {
if (!this.valid)
throw new Error(this.error);
return this;
}
get epochMillis() {
if (this._leapSecondMillis === 0)
return this._epochMillis;
else
return floor(this._epochMillis / 1000) * 1000 + 999;
}
set epochMillis(newTime) {
if (this.locked)
throw lockError;
if (!this.isTai())
newTime = round(newTime);
if (this._epochMillis !== newTime || !this.wallTime || this._leapSecondMillis !== 0) {
this._epochMillis = newTime;
this._leapSecondMillis = 0;
this.updateWallTimeFromEpochMillis();
}
}
get epochSeconds() { return floor(this._epochMillis / 1000); }
set epochSeconds(newTime) { this.epochMillis = newTime * 1000; }
get leapSecondMillis() { return this._leapSecondMillis; }
get deltaTaiMillis() { return this._deltaTaiMillis; }
isJustBeforeNegativeLeapSecond() {
var _a;
return this.isUtcBased() && !!((_a = Timezone.findDeltaTaiFromUtc(this._epochMillis)) === null || _a === void 0 ? void 0 : _a.inNegativeLeap);
}
isInLeapSecond() { return this.leapSecondMillis > 0; }
get utcMillis() { return this.isTai() ? round(this._epochMillis - this._deltaTaiMillis) : this.epochMillis; }
set utcMillis(newTime) {
if (this.locked)
throw lockError;
if (this.isTai())
newTime = utToTaiMillis(newTime, true);
if (this._epochMillis !== newTime || !this.wallTime) {
this._epochMillis = newTime;
this._deltaTaiMillis = 0;
this.updateWallTimeFromEpochMillis();
}
}
setUtcMillis(newTime, leapSecondMillis = 0) {
var _a;
const result = this.locked ? this.clone(false) : this;
if (result.isTai()) {
newTime = utToTaiMillis(newTime + leapSecondMillis, true);
leapSecondMillis = 0;
}
else if (!result.isUtcBased())
leapSecondMillis = 0;
if (result._epochMillis !== newTime || !result.wallTime || result._leapSecondMillis !== leapSecondMillis) {
if (leapSecondMillis) {
const sec59 = floor(newTime / 60000) * 60000 + 59000;
if (newTime >= sec59 && ((_a = Timezone.findDeltaTaiFromUtc(sec59)) === null || _a === void 0 ? void 0 : _a.inLeap)) {
leapSecondMillis -= sec59 + 999 - newTime;
if (leapSecondMillis < 0)
leapSecondMillis = 0;
else
leapSecondMillis = min(leapSecondMillis, 999);
}
else {
newTime = newTime + min(leapSecondMillis, 999);
leapSecondMillis = 0;
}
}
result._epochMillis = newTime;
result._leapSecondMillis = leapSecondMillis;
result.updateWallTimeFromEpochMillis();
}
return result;
}
// noinspection JSUnusedGlobalSymbols /** @deprecated */
get utcTimeMillis() { return this.utcMillis; }
// noinspection JSUnusedGlobalSymbols /** @deprecated */
set utcTimeMillis(newTime) { this.utcMillis = newTime; }
get utcSeconds() { return floor(this.utcMillis / 1000); }
set utcSeconds(newTime) { this.utcMillis = newTime * 1000; }
// noinspection JSUnusedGlobalSymbols /** @deprecated */
get utcTimeSeconds() { return this.utcSeconds; }
// noinspection JSUnusedGlobalSymbols /** @deprecated */
set utcTimeSeconds(newTime) { this.utcSeconds = newTime; }
get taiMillis() {
return !this.isTai()
? this._epochMillis + this._leapSecondMillis + this._deltaTaiMillis
: this._epochMillis;
}
set taiMillis(newTime) {
var _a;
if (this.locked)
throw lockError;
let leapMillis = 0;
if (!this.isTai()) {
if (this.isUtcBased() && ((_a = Timezone.findDeltaTaiFromTai(newTime)) === null || _a === void 0 ? void 0 : _a.inLeap))
leapMillis = mod(newTime, 1000) + 1;
newTime = round(taiToUtMillis(newTime, true) - leapMillis);
}
if (this._epochMillis !== newTime || !this.wallTime || leapMillis !== this._leapSecondMillis) {
this._epochMillis = newTime;
this._leapSecondMillis = leapMillis;
this.updateWallTimeFromEpochMillis();
}
}
get taiSeconds() { return floor(this.taiMillis / 1000); }
set taiSeconds(newTime) { this.taiMillis = newTime * 1000; }
toDate() {
return new Date(this._epochMillis);
}
isTai() {
return this._timezone === Timezone.TAI_ZONE;
}
isUtcBased() {
return this._timezone !== Timezone.TAI_ZONE &&
this._timezone !== Timezone.DATELESS && this._timezone !== Timezone.ZONELESS;
}
getWallTime(purge, minimal = false) {
if (!this.valid)
return { error: this.error };
const w = clone(this._wallTime);
if (this._timezone === DATELESS)
['y', 'year', 'q', 'quarter', 'm', 'month', 'd', 'day', 'dy', 'dayOfYear', 'dow', 'dayOfWeek',
'dowmi', 'dayOfWeekMonthIndex', 'n', 'epochDay', 'j', 'isJulian',
'yw', 'yearByWeek', 'w', 'week', 'dw', 'dayByWeek',
'ywl', 'yearByWeekLocale', 'wl', 'weekLocale', 'dwl', 'dayByWeekLocale',
'utcOffset', 'dstOffset', 'occurrence', 'deltaTai', 'jde', 'mjde', 'jdu', 'mjdu'].forEach(key => delete w[key]);
if (purge != null)
purgeAliasFields(w, purge);
if (minimal)
minimizeFields(w);
return w;
}
get wallTime() { return this.getWallTime(); }
set wallTime(newTime) {
if (this.locked)
throw lockError;
newTime = syncDateAndTime(clone(newTime));
delete newTime.error;
validateDateAndTime(newTime);
if (!isEqual(this._wallTime, newTime)) {
if (newTime.y == null && newTime.yw == null && newTime.ywl == null && newTime.n == null &&
newTime.jde == null && newTime.mjde == null && newTime.jdu == null && newTime.mjdu == null) {
newTime.y = 1970;
newTime.m = 1;
newTime.d = 1;
this._timezone = DATELESS;
}
else if (this._timezone === DATELESS && (newTime.y != null || newTime.yw != null || newTime.ywl != null || newTime.n != null))
this._timezone = ZONELESS;
const counter = this.wallTimeCounter;
this._wallTime = newTime;
this.updateEpochMillisFromWallTime(!this.isTai());
if (this.wallTimeCounter === counter)
this.updateWallTimeFromEpochMillis(this._wallTime.d);
}
}
get wallTimeShort() { return this.getWallTime(false); }
get wallTimeSparse() { return this.getWallTime(false, true); }
get wallTimeLong() { return this.getWallTime(true); }
get timezone() { return this._timezone; }
set timezone(newZone) {
if (this.locked)
throw lockError;
if (isString(newZone))
newZone = Timezone.from(newZone);
if (this._timezone !== newZone) {
const wasTai = this.isTai();
this._timezone = newZone;
this.updateWallTimeFromEpochMillis(0, wasTai, !wasTai && this.isTai());
}
}
tz(newZone, keepLocalTime = false) {
if (isString(newZone)) {
const zone = Timezone.from(newZone);
if (zone.error) {
const szni = Timezone.getShortZoneNameInfo(newZone);
if (szni)
newZone = Timezone.from(szni.ianaName);
else
throw new Error(`Bad timezone: ${newZone}`);
}
else
newZone = zone;
}
const result = this.clone(false);
const wallTime = result.wallTime; // copy
result.timezone = newZone;
if (keepLocalTime) {
delete wallTime.utcOffset;
delete wallTime.occurrence;
delete wallTime.deltaTai;
result._leapSecondMillis = 0;
result.wallTime = wallTime;
}
return result._lock(this.locked);
}
utc(keepLocalTime = false) {
return this.tz(Timezone.UT_ZONE, keepLocalTime);
}
local(keepLocalTime = false) {
return this.tz(Timezone.guess(), keepLocalTime);
}
toLocale(newLocale) {
const result = this.clone();
result._locale = newLocale;
return result;
}
get locale() { return this._locale; }
set locale(newLocale) {
if (this.locked)
throw lockError;
newLocale = newLocale || DateTime.getDefaultLocale();
if (this._locale !== newLocale)
this._locale = newLocale;
}
get utcOffsetSeconds() {
return this._timezone.getOffset(this._epochMillis);
}
get utcOffsetMinutes() {
return round(this._timezone.getOffset(this._epochMillis) / 60);
}
get dstOffsetSeconds() {
return this._timezone.getOffsets(this._epochMillis)[1];
}
get dstOffsetMinutes() {
return round(this._timezone.getOffsets(this._epochMillis)[1] / 60);
}
isDST() {
return this.dstOffsetSeconds !== 0;
}
getTimezoneDisplayName() {
return this._timezone.getDisplayName(this._epochMillis);
}
checkDateless(field) {
if (this._timezone === DATELESS)
throw new Error(`${dtfToString(field)} cannot be used with a dateless time value`);
}
undefinedIfDateless(value) {
return this._timezone === DATELESS ? undefined : value;
}
add(field, amount, variableDays = false) {
const result = this.locked ? this.clone(false) : this;
if (!this.valid)
throw new Error('Cannot perform add()/subtract() on invalid DateTime');
else if (amount === 0)
return result._lock(this.locked);
else if (amount !== floor(amount))
throw nonIntError;
let updateFromWall = false;
let updateFromTai = false;
let normalized;
let weekCount;
const wallTime = result._wallTime;
const fieldN = fieldNameToField(field);
switch (fieldN) {
case DateTimeField.MILLI:
result._epochMillis += amount;
break;
case DateTimeField.MILLI_TAI:
updateFromTai = true;
break;
case DateTimeField.SECOND:
result._epochMillis += amount * 1000;
break;
case DateTimeField.SECOND_TAI:
amount *= 1000;
updateFromTai = true;
break;
case DateTimeField.MINUTE:
result._epochMillis += amount * 60000;
break;
case DateTimeField.MINUTE_TAI:
amount *= 60000;
updateFromTai = true;
break;
case DateTimeField.HOUR:
result._epochMillis += amount * 3600000;
break;
case DateTimeField.HOUR_TAI:
amount *= 3600000;
updateFromTai = true;
break;
case DateTimeField.DAY:
this.checkDateless(fieldN);
if (variableDays) {
updateFromWall = true;
wallTime.n += amount;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.ywl;
}
else
result._epochMillis += amount * 86400000;
break;
case DateTimeField.DAY_TAI:
amount *= DAY_MSEC;
updateFromTai = true;
break;
case DateTimeField.WEEK:
this.checkDateless(fieldN);
result._epochMillis += amount * 604800000;
break;
case DateTimeField.QUARTER:
amount *= 3;
// eslint-disable-next-line no-fallthrough
case DateTimeField.MONTH:
this.checkDateless(fieldN);
// eslint-disable-next-line no-case-declarations
const m = wallTime.m;
updateFromWall = true;
wallTime.m = mod(m - 1 + amount, 12) + 1;
wallTime.y += div_rd(m - 1 + amount, 12);
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.n;
break;
case DateTimeField.YEAR:
this.checkDateless(fieldN);
updateFromWall = true;
wallTime.y += amount;
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.n;
break;
case DateTimeField.YEAR_WEEK:
this.checkDateless(fieldN);
updateFromWall = true;
wallTime.yw += amount;
if (wallTime.w > (weekCount = this.getWeeksInYear(wallTime.yw)))
wallTime.w = weekCount;
delete wallTime.y;
delete wallTime.ywl;
delete wallTime.n;
break;
case DateTimeField.YEAR_WEEK_LOCALE:
this.checkDateless(fieldN);
updateFromWall = true;
wallTime.ywl += amount;
if (wallTime.wl > (weekCount = this.getWeeksInYearLocale(wallTime.ywl)))
wallTime.wl = weekCount;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.n;
break;
default:
throw new Error(`${dtfToString(field)} is not a valid add()/subtract() field`);
}
if (updateFromTai) {
const millis = (result.isTai() ? result._epochMillis : result.taiMillis) + amount;
if (result.isTai())
result._epochMillis = millis;
else {
result.taiMillis = millis;
return result._lock(this.locked);
}
}
else if (updateFromWall) {
delete wallTime.dy;
delete wallTime.occurrence;
delete wallTime.deltaTai;
delete wallTime.utcOffset;
delete wallTime.j;
delete wallTime.jde;
delete wallTime.mjde;
delete wallTime.jdu;
delete wallTime.mjdu;
result._leapSecondMillis = 0;
result.updateEpochMillisFromWallTime();
}
if (this._timezone === DATELESS)
this._epochMillis = mod(this._epochMillis, DAY_MSEC);
result.updateWallTimeFromEpochMillis();
return result._lock(this.locked);
}
subtract(field, amount, variableDays = false) {
return this.add(field, -amount, variableDays);
}
roll(field, amount, minYear = 1900, maxYear = 2099) {
var _a, _b;
const result = this.locked ? this.clone(false) : this;
if (!this.valid)
throw new Error('Cannot perform roll() on invalid DateTime');
else if (amount === 0)
return result._lock(this.locked);
else if (amount !== floor(amount))
throw nonIntError;
let normalized;
const wallTime = result._wallTime;
const fieldN = fieldNameToField(field);
let clearOccurrence = true;
switch (fieldN) {
case DateTimeField.MILLI:
wallTime.millis = mod(wallTime.millis + amount, 1000);
break;
case DateTimeField.SECOND:
wallTime.sec = mod(min(wallTime.sec, 59) + amount, 60);
break;
case DateTimeField.MINUTE:
wallTime.min = mod(wallTime.min + amount, 60);
break;
case DateTimeField.HOUR:
{
const hoursInDay = floor(result.getSecondsInDay() / 3600);
wallTime.hrs = mod(wallTime.hrs + amount, hoursInDay);
if (amount > 0 && ((_a = wallTime.occurrence) !== null && _a !== void 0 ? _a : 1) === 1) {
clearOccurrence = false;
wallTime.occurrence = 2;
}
else if (amount < 0 && ((_b = wallTime.occurrence) !== null && _b !== void 0 ? _b : 1) > 1) {
clearOccurrence = false;
wallTime.occurrence = 1;
}
}
break;
case DateTimeField.AM_PM:
// Normally straight-forward, but this can get weird if the AM/PM roll crosses a Daylight Saving Time change.
{
const targetHour = mod(wallTime.hrs + 12, 24);
result.roll(DateTimeField.HOUR, 12 * mod(amount, 2));
if (result._wallTime.hrs === targetHour)
return result._lock(this.locked);
else if (mod2(result._wallTime.hrs - targetHour, 24) < 0)
return result.add(DateTimeField.HOUR, 1)._lock(this.locked);
else
return result.add(DateTimeField.HOUR, -1)._lock(this.locked);
}
case DateTimeField.DAY:
this.checkDateless(fieldN);
{
const missing = result.getMissingDateRange();
const daysInMonth = result.getLastDateInMonth();
wallTime.d = mod(wallTime.d + amount - 1, daysInMonth) + 1;
if (missing && (missing[0] <= wallTime.d && wallTime.d <= missing[1]))
wallTime.d = amount < 0 ? missing[0] - 1 : missing[1] + 1;
wallTime.d = min(max(wallTime.d, result.getFirstDateInMonth()), daysInMonth);
delete wallTime.dy;
delete wallTime.utcOffset;
}
break;
case DateTimeField.DAY_BY_WEEK:
this.checkDateless(fieldN);
wallTime.dw = mod(wallTime.dw + amount - 1, 7) + 1;
delete wallTime.y;
delete wallTime.ywl;
delete wallTime.utcOffset;
break;
case DateTimeField.DAY_BY_WEEK_LOCALE:
this.checkDateless(fieldN);
wallTime.dwl = mod(wallTime.dwl + amount - 1, 7) + 1;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.utcOffset;
break;
case DateTimeField.DAY_OF_YEAR:
this.checkDateless(fieldN);
wallTime.dy = mod(wallTime.dy + amount - 1, this.getDaysInYear(wallTime.y)) + 1;
delete wallTime.m;
delete wallTime.d;
delete wallTime.utcOffset;
break;
case DateTimeField.WEEK:
this.checkDateless(fieldN);
{
const weeksInYear = result.getWeeksInYear(wallTime.yw);
wallTime.w = mod(wallTime.w + amount - 1, weeksInYear) + 1;
delete wallTime.y;
delete wallTime.ywl;
delete wallTime.utcOffset;
}
break;
case DateTimeField.WEEK_LOCALE:
this.checkDateless(fieldN);
{
const weeksInYear = result.getWeeksInYear(wallTime.ywl, getStartOfWeek(this.locale), getMinDaysInWeek(this.locale));
wallTime.wl = mod(wallTime.wl + amount - 1, weeksInYear) + 1;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.utcOffset;
}
break;
case DateTimeField.QUARTER:
amount *= 3;
// eslint-disable-next-line no-fallthrough
case DateTimeField.MONTH:
this.checkDateless(fieldN);
wallTime.m = mod(wallTime.m + amount - 1, 12) + 1;
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.dy;
delete wallTime.utcOffset;
break;
case DateTimeField.YEAR:
this.checkDateless(fieldN);
wallTime.y = mod(wallTime.y - minYear + amount, maxYear - minYear + 1) + minYear;
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.dy;
delete wallTime.utcOffset;
break;
case DateTimeField.YEAR_WEEK:
this.checkDateless(fieldN);
wallTime.yw = mod(wallTime.yw - minYear + amount, maxYear - minYear + 1) + minYear;
delete wallTime.y;
delete wallTime.ywl;
delete wallTime.utcOffset;
break;
case DateTimeField.YEAR_WEEK_LOCALE:
this.checkDateless(fieldN);
wallTime.ywl = mod(wallTime.ywl - minYear + amount, maxYear - minYear + 1) + minYear;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.utcOffset;
break;
case DateTimeField.ERA:
this.checkDateless(fieldN);
if (amount % 2 === 0)
return result._lock(this.locked);
wallTime.y = -wallTime.y + 1;
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.utcOffset;
break;
default:
throw new Error(`${dtfToString(field)} is not a valid roll() field`);
}
delete wallTime.n;
delete wallTime.j;
delete wallTime.deltaTai;
delete wallTime.jde;
delete wallTime.mjde;
delete wallTime.jdu;
delete wallTime.mjdu;
result._leapSecondMillis = 0;
if (clearOccurrence)
delete wallTime.occurrence;
result.updateEpochMillisFromWallTime();
if (this._timezone === DATELESS)
this._epochMillis = mod(this._epochMillis, 86400000);
result.updateWallTimeFromEpochMillis();
return result._lock(this.locked);
}
get(field) {
const fieldN = fieldNameToField(field);
const wallTime = this._wallTime;
switch (fieldN) {
case DateTimeField.MILLI: return wallTime.millis;
case DateTimeField.SECOND: return wallTime.sec;
case DateTimeField.MINUTE: return wallTime.min;
case DateTimeField.HOUR: return wallTime.hrs;
case DateTimeField.HOUR_12: return wallTime.hrs === 0 ? 12 : wallTime.hrs < 13 ? wallTime.hrs : wallTime.hrs - 12;
case DateTimeField.AM_PM: return wallTime.hrs < 12 ? 0 : 1;
case DateTimeField.DAY: return this.undefinedIfDateless(wallTime.d);
case DateTimeField.DAY_BY_WEEK: return this.undefinedIfDateless(wallTime.dw);
case DateTimeField.DAY_BY_WEEK_LOCALE: return this.undefinedIfDateless(wallTime.dwl);
case DateTimeField.DAY_OF_YEAR: return this.undefinedIfDateless(wallTime.dy);
case DateTimeField.WEEK: return this.undefinedIfDateless(wallTime.w);
case DateTimeField.WEEK_LOCALE: return this.undefinedIfDateless(wallTime.wl);
case DateTimeField.MONTH: return this.undefinedIfDateless(wallTime.m);
case DateTimeField.YEAR: return this.undefinedIfDateless(wallTime.y);
case DateTimeField.YEAR_WEEK: return this.undefinedIfDateless(wallTime.yw);
case DateTimeField.YEAR_WEEK_LOCALE: return this.undefinedIfDateless(wallTime.ywl);
case DateTimeField.ERA: return this.undefinedIfDateless(wallTime.y <= 0 ? 0 : 1);
default:
throw new Error(`${dtfToString(field)} is not a valid get() field`);
}
}
set(field, value, loose = false) {
var _a;
const result = this.locked ? this.clone(false) : this;
if (!this.valid)
throw new Error('Cannot perform set() on invalid DateTime');
else if (value !== floor(value))
throw nonIntError;
let normalized;
const wallTime = result._wallTime;
let min = 0;
let max = 59;
const fieldN = fieldNameToField(field);
switch (fieldN) {
case DateTimeField.MILLI:
max = 999;
wallTime.millis = value;
break;
case DateTimeField.SECOND:
wallTime.sec = value;
if ((_a = Timezone.findDeltaTaiFromUtc(floor(this.epochMillis / 60000) * 60000 + 59000)) === null || _a === void 0 ? void 0 : _a.inLeap)
max = 60;
break;
case DateTimeField.MINUTE:
wallTime.min = value;
break;
case DateTimeField.HOUR:
max = 23;
wallTime.hrs = value;
break;
case DateTimeField.HOUR_12:
min = 1;
max = 12;
if (wallTime.hrs < 12)
wallTime.hrs = (value === 12 ? 0 : value);
else
wallTime.hrs = (value === 12 ? 12 : value + 12);
break;
case DateTimeField.AM_PM:
max = 1;
if (value === 0 && wallTime.hrs >= 12)
wallTime.hrs -= 12;
else if (value === 1 && wallTime.hrs < 12)
wallTime.hrs += 12;
break;
case DateTimeField.DAY:
this.checkDateless(fieldN);
min = loose ? 0 : 1;
max = loose ? 32 : this.getLastDateInMonth();
wallTime.d = value;
delete wallTime.dy;
if (!loose) {
const missing = this.getMissingDateRange();
if (missing && (missing[0] <= value && value <= missing[1]))
throw new Error(`${value} is an invalid date in the month ${wallTime.m}/${wallTime.y}`);
}
break;
case DateTimeField.DAY_BY_WEEK:
this.checkDateless(fieldN);
min = loose ? 0 : 1;
max = loose ? 8 : 7;
wallTime.dw = value;
delete wallTime.y;
delete wallTime.ywl;
delete wallTime.utcOffset;
break;
case DateTimeField.DAY_BY_WEEK_LOCALE:
this.checkDateless(fieldN);
min = loose ? 0 : 1;
max = loose ? 8 : 7;
wallTime.dwl = value;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.utcOffset;
break;
case DateTimeField.DAY_OF_YEAR:
this.checkDateless(fieldN);
min = loose ? 0 : 1;
max = loose ? 367 : this.getDaysInYear(wallTime.y);
wallTime.dy = value;
delete wallTime.m;
delete wallTime.d;
delete wallTime.utcOffset;
break;
case DateTimeField.WEEK:
this.checkDateless(fieldN);
min = loose ? 0 : 1;
max = loose ? 54 : this.getWeeksInYear(wallTime.yw);
wallTime.w = value;
delete wallTime.y;
delete wallTime.ywl;
delete wallTime.utcOffset;
break;
case DateTimeField.WEEK_LOCALE:
this.checkDateless(fieldN);
min = loose ? 0 : 1;
max = loose ? 54 : result.getWeeksInYearLocale(wallTime.ywl);
wallTime.wl = value;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.utcOffset;
break;
case DateTimeField.MONTH:
this.checkDateless(fieldN);
min = loose ? 0 : 1;
max = loose ? 13 : 12;
wallTime.m = value;
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.dy;
delete wallTime.utcOffset;
break;
case DateTimeField.YEAR:
this.checkDateless(fieldN);
min = MIN_YEAR;
max = MAX_YEAR;
wallTime.y = value;
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.dy;
delete wallTime.utcOffset;
break;
case DateTimeField.YEAR_WEEK:
this.checkDateless(fieldN);
min = MIN_YEAR;
max = MAX_YEAR;
wallTime.yw = value;
delete wallTime.y;
delete wallTime.ywl;
delete wallTime.utcOffset;
break;
case DateTimeField.YEAR_WEEK_LOCALE:
this.checkDateless(fieldN);
min = MIN_YEAR;
max = MAX_YEAR;
wallTime.ywl = value;
delete wallTime.y;
delete wallTime.yw;
delete wallTime.utcOffset;
break;
case DateTimeField.ERA:
this.checkDateless(fieldN);
max = 1;
if ((value === 0 && wallTime.y > 0) || (value === 1 && wallTime.y <= 0)) {
wallTime.y = -wallTime.y + 1;
normalized = result.normalizeDate(wallTime);
[wallTime.y, wallTime.m, wallTime.d] = [normalized.y, normalized.m, normalized.d];
delete wallTime.dy;
delete wallTime.utcOffset;
}
break;
default:
throw new Error(`${dtfToString(field)} is not a valid set() field`);
}
if (value < min || value > max)
throw new Error(`${DateTimeField[fieldN]} (${value}) must be in the range [${min}, ${max}]`);
delete wallTime.n;
delete wallTime.j;
delete wallTime.occurrence;
delete wallTime.deltaTai;
delete wallTime.jde;
delete wallTime.mjde;
delete wallTime.jdu;
delete wallTime.mjdu;
const counter = this.wallTimeCounter;
result.updateEpochMillisFromWallTime(this.isUtcBased() && wallTime.sec === 60);
if (result._timezone === DATELESS)
result._epochMillis = mod(result._epochMillis, DAY_MSEC);
if (this.wallTimeCounter === counter)
result.updateWallTimeFromEpochMillis();
return result._lock(this.locked);
}
startOf(field) {
const result = this.locked ? this.clone(false) : this;
if (!this.valid)
throw new Error('Cannot perform startOf() on invalid DateTime');
const wallTime = result._wallTime;
const fieldN = fieldNameToField(field);
switch (fieldN) {
case DateTimeField.SECOND:
wallTime.millis = 0;
break;
case DateTimeField.MINUTE:
wallTime.millis = wallTime.sec = 0;
break;
case DateTimeField.HOUR:
wallTime.millis = wallTime.sec = wallTime.min = 0;
break;
case DateTimeField.DAY:
this.checkDateless(fieldN);
wallTime.millis = wallTime.sec = wallTime.min = wallTime.hrs = 0;
break;
case DateTimeField.WEEK:
this.checkDateless(fieldN);
wallTime.millis = wallTime.sec = wallTime.min = wallTime.hrs = 0;
wallTime.dw = 1;
delete wallTime.y;
delete wallTime.ywl;
break;
case DateTimeField.WEEK_LOCALE:
this.checkDateless(fieldN);
wallTime.millis = wallTime.sec = wallTime.min = wallTime.hrs = 0;
wallTime.dwl = 1;
delete wallTime.y;
delete wallTime.yw;
break;
case DateTimeField.MONTH:
this.checkDateless(fieldN);
wallTime.millis = wallTime.sec = wallTime.min = wallTime.hrs = 0;
wallTime.d = 1;
break;
case DateTimeField.QUARTER:
this.checkDateless(fieldN);
wallTime.millis = wallTime.sec = wallTime.min = wallTime.hrs = 0;
wallTime.d = 1;
wallTime.m = floor((wallTime.m - 1) / 3) * 3 + 1;
break;
case DateTimeField.YEAR:
this.checkDateless(fieldN);
wallTime.millis = wallTime.sec = wallTime.min = wallTime.hrs = 0;
wallTime.d = wallTime.m = 1;
break;
case DateTimeField.YEAR_WEEK:
this.checkDateless(fieldN);
wallTime.millis = wallTime.sec = wallTime.min = wallTime.hrs = 0;