Files
avz-site/node_modules/@date-fns/tz/date/mini.js
T
2026-06-10 12:14:46 -03:00

448 lines
18 KiB
JavaScript

import { tzOffset } from "../tzOffset/index.js";
// TZDate stores time in two `Date` objects because native `Date` can only
// expose local fields in the system time zone.
//
// The external date (`this`) stores the real timestamp. It is the value used by
// `getTime()`, `valueOf()`, UTC getters/setters, comparisons, and arithmetic.
// Its local getters are not used for target-zone fields because they would read
// the timestamp through the system time zone.
//
// The internal date (`this.internal`) stores the target-zone wall-clock fields.
// We read and write those fields through UTC getters/setters so the host system
// time zone cannot reinterpret them. For example, `getHours()` returns
// `internal.getUTCHours()`, and `setHours()` writes `internal.setUTCHours(...)`.
//
// Syncing moves data between the two representations:
//
// - `syncToInternal` starts from the external timestamp and rebuilds internal
// wall-clock fields using `this.timeZone` offset.
//
// - `syncFromInternal` starts from internal wall-clock fields, writes them into
// the external date, then adjusts that timestamp for the system/target offset
// difference and DST edge cases.
//
// The public TZDate value is the combination of both: external is the instant,
// internal is the wall-clock view of that instant in `this.timeZone`.
export class TZDateMini extends Date {
//#region static
constructor(...args) {
super();
// Time zone string is always the last string argument unless date string
// is passed (as a single argument).
if (args.length > 1 && typeof args[args.length - 1] === "string") {
this.timeZone = args.pop();
}
this.internal = new Date();
// Validate the time zone by checking its offset.
if (isNaN(tzOffset(this.timeZone, this))) {
this.setTime(NaN);
} else {
if (!args.length) {
// No arguments passed: use current time
this.setTime(Date.now());
} else if (typeof args[0] === "number" && (args.length === 1 || args.length === 2 && typeof args[1] !== "number")) {
// Timestamp passed: use it as is
this.setTime(args[0]);
} else if (typeof args[0] === "string") {
// `Date` string passed: parse it as external date
this.setTime(+new Date(args[0]));
} else if (args[0] instanceof Date) {
// `Date` passed: use its timestamp
this.setTime(+args[0]);
} else {
// `Date` values passed:
// Set it as external date.
this.setTime(+new Date(...args));
// Adjust internal and external dates considering that we might have
// landed on the DST hour.
adjustToSystemTZ(this, args);
}
}
}
static tz(tz, ...args) {
return args.length ? new TZDateMini(...args, tz) : new TZDateMini(Date.now(), tz);
}
//#endregion
//#region time zone
withTimeZone(timeZone) {
return new TZDateMini(+this, timeZone);
}
getTimezoneOffset() {
const offset = -tzOffset(this.timeZone, this);
// Remove the seconds offset using `Math.floor` for negative UTC time zones
// and `Math.ceil` for positive UTC time zones.
return offset > 0 ? Math.floor(offset) : Math.ceil(offset);
}
//#endregion
//#region time
setTime(_time) {
// Use the native `setTime` to the external date time.
Date.prototype.setTime.apply(this, arguments);
// Then apply it to the internal date adjusting to the timezone offset.
syncToInternal(this);
return +this;
}
//#endregion
//#region date-fns integration
[Symbol.for("constructDateFrom")](date) {
return new TZDateMini(+new Date(date), this.timeZone);
}
//#endregion
}
// Assign getters and setters
const re = /^(get|set)(?!UTC)/;
Object.getOwnPropertyNames(Date.prototype).forEach(method => {
if (!re.test(method)) return;
const utcMethod = method.replace(re, "$1UTC");
// Filter out methods without UTC counterparts
if (!TZDateMini.prototype[utcMethod]) return;
if (method.startsWith("get")) {
// Delegate to internal date's UTC method
TZDateMini.prototype[method] = function () {
return this.internal[utcMethod]();
};
} else {
// Assign regular setter
TZDateMini.prototype[method] = function () {
Date.prototype[utcMethod].apply(this.internal, arguments);
syncFromInternal(this);
return +this;
};
// Assign UTC setter
TZDateMini.prototype[utcMethod] = function () {
Date.prototype[utcMethod].apply(this, arguments);
syncToInternal(this);
return +this;
};
}
});
/**
* @internal
* Function syncs time to internal date, applying the current time zone offset.
*
* @param {Date} date - `Date` to sync
*/
function syncToInternal(date) {
// Start internal from the same real timestamp as external.
date.internal.setTime(+date);
// Shift internal by the target offset so its UTC fields become the target-zone
// wall-clock fields for the external timestamp.
//
// o internal UTC fields: 2024-02-11 00:00
// |
// | + Asia/Singapore offset (+08:00)
// v
// x internal UTC fields: 2024-02-11 08:00
//
date.internal.setUTCSeconds(date.internal.getUTCSeconds() -
// Round after converting minutes to seconds to avoid fractional offset
// precision errors from historical offsets.
Math.round(-tzOffset(date.timeZone, date) * 60));
}
/**
* @internal
* Function syncs internal wall-clock fields into the external date.
*
* @param {Date} date - The date to sync
*/
function syncFromInternal(date) {
// Copy target-zone wall-clock fields from internal into external using native
// local setters. At this point external holds the right field values but they
// are interpreted in the system time zone; `adjustToSystemTZ` fixes that.
Date.prototype.setFullYear.call(date, date.internal.getUTCFullYear(), date.internal.getUTCMonth(), date.internal.getUTCDate());
Date.prototype.setHours.call(date, date.internal.getUTCHours(), date.internal.getUTCMinutes(), date.internal.getUTCSeconds(), date.internal.getUTCMilliseconds());
// Now we have to adjust the date to the system time zone
adjustToSystemTZ(date);
}
/**
* @internal
* Function adjusts the date to the system time zone. It uses the time zone
* differences to calculate the offset and adjust the date.
*
* We use it when constructing `TZDate` and syncing the external date from
* the internal one.
*
* @param {TZDate} date - `TZDate` to adjust.
*/
function adjustToSystemTZ(date, constructorArgs) {
// Keep the intended target-zone wall-clock value before native Date/system
// time zone normalization can move it. Later corrections compare against this
// value to distinguish intended DST normalization from accidental drift.
const expectedInternalTime = Array.isArray(constructorArgs) ? constructorArgsToInternalTime(constructorArgs) : +date.internal;
//#region Initial offset calculation
// Current target time zone offset at the native timestamp. It may include
// historical offset seconds, which we preserve for the seconds adjustment.
const offsetWithSeconds = tzOffset(date.timeZone, date);
// Minute-precision target offset used by minute-based timestamp adjustments.
// Historical offsets can contain seconds, so we strip the fractional minute
// here and handle remaining seconds in the seconds adjustment below.
const offset = offsetWithSeconds > 0 ? Math.floor(offsetWithSeconds) : Math.ceil(offsetWithSeconds);
//#endregion
//#region System DST adjustment
// Native `Date` may normalize the requested wall time when the system time
// zone skips that hour for DST.
//
// We compare the system wall-clock hour represented by the external date with
// the wall-clock hour stored in the internal date. If they differ, the next
// offset-diff calculation may need the previous system offset, because the
// current external timestamp is already after the system DST jump.
// Previous-hour reference used to detect whether the system offset changed
// immediately before the current native timestamp.
const prevHour = new Date(+date);
// Use UTC math so subtracting one hour cannot be normalized back into the
// same missing local DST hour.
prevHour.setUTCHours(prevHour.getUTCHours() - 1);
// Current system offset at the native timestamp.
const systemOffset = -new Date(+date).getTimezoneOffset();
// System offset one real hour before the native timestamp.
const prevHourSystemOffset = -new Date(+prevHour).getTimezoneOffset();
// Non-zero when the system offset changed between `prevHour` and `date`.
const systemDSTChange = systemOffset - prevHourSystemOffset;
// System offset to use in the later system-target offset difference.
// Defaults to the current system offset and switches to the previous offset
// only when native `Date` normalized a missing system wall time.
let systemOffsetForDiff = systemOffset;
if (systemDSTChange && systemOffset !== offset) {
// System wall-clock hour represented by the current external timestamp.
const systemHour = Date.prototype.getHours.apply(date);
// Target wall-clock hour currently stored in internal fields. Constructors
// get it from arguments because internal may have already been synced from
// the normalized external timestamp; setters get it directly from internal.
const expectedHour = Array.isArray(constructorArgs) ? constructorArgs[3] || 0 : date.internal.getUTCHours();
if (systemHour !== expectedHour) {
// Check whether using the current system offset would keep the target
// offset unchanged. If so, the only DST jump we crossed is the system
// one, so the diff should use the pre-DST system offset.
const testDate = new Date(+date);
// Difference that the later offset-diff step would apply with the
// current system offset.
const testOffsetDiff = systemOffset - offset;
if (testOffsetDiff) testDate.setUTCMinutes(testDate.getUTCMinutes() + testOffsetDiff);
// Target offset after applying the current system offset difference.
const testOffsetWithSeconds = tzOffset(date.timeZone, testDate);
// Target offset without historical seconds, matching `offset`.
const testOffset = testOffsetWithSeconds > 0 ? Math.floor(testOffsetWithSeconds) : Math.ceil(testOffsetWithSeconds);
if (testOffset === offset) systemOffsetForDiff = prevHourSystemOffset;
}
}
//#endregion
//#region System diff adjustment
// Move external from the system-zone interpretation of internal fields toward
// the timestamp that represents those fields in the target time zone.
//
// At this point native `Date` has treated the internal wall-clock fields as if
// they belonged to the system zone. The system-target offset difference moves
// external by the distance between that system interpretation and the target
// interpretation.
// Difference between the system offset selected above and the minute-precision
// target offset. Positive values move external forward; negative values move
// it backward.
const offsetDiff = systemOffsetForDiff - offset;
if (offsetDiff)
// Apply the system-target minute difference to external. Internal is
// rebuilt from the final external timestamp after all adjustments are
// complete.
//
// o external as target: 2023-01-31 23:00
// |
// | add `offsetDiff`
// v
// x external as target: 2023-02-01 12:00
//
Date.prototype.setUTCMinutes.call(date, Date.prototype.getUTCMinutes.call(date) + offsetDiff);
//#endregion
//#region Seconds system diff adjustment
// Historical time zone offsets can include seconds, but the minute-based
// offset adjustment above intentionally used minute precision.
//
// This adjustment applies the remaining seconds difference to external. For
// example, historical Singapore used UTC+06:55:25, while ISO formatting shows
// only `+06:55`; without the seconds correction, setter paths keep the wrong
// wall-clock seconds.
// Clone external as a native `Date` so we can inspect how the system time zone
// represents the same timestamp.
const systemDate = new Date(+date);
// Zero UTC seconds before reading system seconds. Any remaining local seconds
// then come from the system time zone offset rather than from the timestamp's
// own seconds value.
systemDate.setUTCSeconds(0);
// Seconds part contributed by the system time zone offset. Negative offsets
// need wrapping because `Date#getSeconds()` returns values in the 0..59 range.
const systemSecondsOffset = systemOffset > 0 ? systemDate.getSeconds() : (systemDate.getSeconds() - 60) % 60;
// Seconds part contributed by the target time zone offset.
const secondsOffset = Math.round(-(tzOffset(date.timeZone, date) * 60)) % 60;
if (secondsOffset || systemSecondsOffset)
// Apply the remaining second-level system-target difference to external.
//
// o external as Asia/Singapore: 1900-01-01 00:00:56
// |
// | + `secondsOffset`
// | + `systemSecondsOffset`
// v
// x external as Asia/Singapore: 1900-01-01 00:00:31
//
Date.prototype.setUTCSeconds.call(date, Date.prototype.getUTCSeconds.call(date) + secondsOffset + systemSecondsOffset);
//#endregion
//#region Post-adjustment DST fix
// The first system-target offset move can cross a target-zone DST boundary.
//
// When that happens, the target offset at the new external timestamp differs
// from the target offset used for `offsetDiff`. We compare the original move
// with the move that would be calculated at the new timestamp, then apply the
// difference to external.
// Target offset at the current external timestamp, including historical
// seconds for the later seconds adjustment.
const postOffsetWithSeconds = tzOffset(date.timeZone, date);
// Minute-precision target offset at the current external timestamp.
const postOffset = postOffsetWithSeconds > 0 ? Math.floor(postOffsetWithSeconds) : Math.ceil(postOffsetWithSeconds);
// System offset at the current external timestamp.
const postSystemOffset = -new Date(+date).getTimezoneOffset();
// System-target offset difference at the current external timestamp.
const postOffsetDiff = postSystemOffset - postOffset;
// Whether the target offset changed after the first offset move.
const offsetChanged = postOffset !== offset;
// Difference between the current offset move and the move already applied.
const postDiff = postOffsetDiff - offsetDiff;
// If the first offset move already normalized a target DST gap forward, the
// generic post-DST correction below would undo that valid normalization. This
// happens, for example, with America/New_York 02:00 during spring-forward when
// the system zone also changes offset around the same instant.
const targetDSTShift = postOffset - offset;
// Candidate timestamp that would represent the requested wall-clock time with
// the post-transition target offset. If it still does not round-trip to the
// requested wall-clock fields, the requested time is inside a target DST gap.
const postOffsetCandidate = expectedInternalTime - postOffset * 60 * 1000;
// Only positive target offset shifts are spring-forward gaps. Negative shifts
// are fall-back overlaps and still need the regular post-adjustment path.
const normalizedTargetDSTGap = targetDSTShift > 0 && targetInternalTime(date) - expectedInternalTime === targetDSTShift * 60 * 1000 && targetInternalTime(date, postOffsetCandidate) !== expectedInternalTime;
if (offsetChanged && postDiff && !normalizedTargetDSTGap) {
// Apply the target-DST correction to external. In the backward-crossing case
// shown here, this lands on an intermediate value that needs `offsetChange`
// below.
//
// o external as America/New_York: 2023-03-12 03:00 EDT
// |
// | + `postDiff`
// v
// x external as America/New_York: 2023-03-12 01:00 EST
//
Date.prototype.setUTCMinutes.call(date, Date.prototype.getUTCMinutes.call(date) + postDiff);
// Target offset after applying `postDiff`. It may change again if the
// correction itself crosses a target-zone DST boundary.
const newOffsetWithSeconds = tzOffset(date.timeZone, date);
// Minute-precision target offset after applying `postDiff`.
const newOffset = newOffsetWithSeconds > 0 ? Math.floor(newOffsetWithSeconds) : Math.ceil(newOffsetWithSeconds);
// Offset change caused by the `postDiff` move itself.
const offsetChange = postOffset - newOffset;
// If the correction moved external backward across the target DST boundary,
// apply the boundary change so external lands on the valid target-zone
// timestamp. Forward crossings are already normalized by native Date, and
// applying this correction there would undo the valid post-DST result.
if (offsetChange && postDiff < 0) {
// Apply the second target-DST correction to external.
//
// o external as America/New_York: 2023-03-12 01:00 EST
// |
// | + `offsetChange`
// v
// x external as America/New_York: 2023-03-12 03:00 EDT
//
Date.prototype.setUTCMinutes.call(date, Date.prototype.getUTCMinutes.call(date) + offsetChange);
}
}
//#endregion
// Rebuild internal wall-clock fields from the final external timestamp.
syncToInternal(date);
// Native Date can normalize historical system offsets with minute and second
// precision before we adjust to the target time zone. Correct only small
// historical drift so DST gap normalization (usually one hour) remains intact.
const expectedTime = constructorArgs ? expectedInternalTime : expectedInternalTime + secondsOffset * 1000;
const drift = expectedTime - +date.internal;
if (drift && Math.abs(drift) < 30 * 60 * 1000) {
Date.prototype.setTime.call(date, +date + drift);
syncToInternal(date);
}
}
function constructorArgsToInternalTime(args) {
// Mirror Date's date-value constructor defaults while preserving explicit
// `undefined` behavior. Missing month/day default, but passed `undefined`
// should remain invalid just like `Date.UTC(year, undefined, ...)`.
return Date.UTC(args[0], args.length > 1 ? args[1] : 0, args.length > 2 ? args[2] : 1, ...args.slice(3));
}
function targetInternalTime(date, time) {
// Compute the target-zone wall-clock representation for a timestamp without
// mutating the TZDate. This mirrors syncToInternal for temporary checks.
const internal = new Date(time ?? +date);
internal.setUTCSeconds(internal.getUTCSeconds() - Math.round(-tzOffset(date.timeZone, internal) * 60));
return +internal;
}