UNPKG

@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
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;