UEA-PRODEM
This commit is contained in:
+8
@@ -8,6 +8,14 @@ This change log follows the format documented in [Keep a CHANGELOG].
|
||||
[semantic versioning]: http://semver.org/
|
||||
[keep a changelog]: http://keepachangelog.com/
|
||||
|
||||
## v1.5.0 - 2026-05-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed incorrect calculations when the date lands on the specified time zone DST hour when the system time zone is west of it.
|
||||
|
||||
- Fixed historical date calculations for time zones with seconds offset in certain time zones (namely `Asia/Colombo`, `Asia/Jerusalem`, `Europe/Istanbul`, `Europe/Chisinau`, `Europe/Minsk`, `Europe/Simferopol`, `Europe/Sofia`, `Europe/Vilnius`).
|
||||
|
||||
## v1.4.1 - 2025-08-12
|
||||
|
||||
### Fixed
|
||||
|
||||
+21
-21
@@ -2,11 +2,11 @@
|
||||
|
||||
The package provides `Date` extensions `TZDate` and `TZDateMini` that perform all calculations in the given time zone rather than the system time zone.
|
||||
|
||||
Using it makes [date-fns](https://github.com/date-fns/date-fns) operate in given time zone but can also be used without it.
|
||||
Using it makes [date-fns](https://github.com/date-fns/date-fns) operate in the given time zone but can also be used without it.
|
||||
|
||||
Like everything else in the date-fns ecosystem, the library is build-size aware. The smallest component, `TZDateMini,` is only `916 B`.
|
||||
Like everything else in the date-fns ecosystem, the library is build-size aware. The smallest component, `TZDateMini,` is only `1.34 KB`.
|
||||
|
||||
**Need only UTC?** See [@date-fns/utc](https://github.com/date-fns/utc) that provides lighter solution.
|
||||
**Need only UTC?** See [@date-fns/utc](https://github.com/date-fns/utc) that provides a lighter solution.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -16,7 +16,7 @@ npm install @date-fns/tz --save
|
||||
|
||||
## Usage
|
||||
|
||||
`TZDate` and `TZDateMini` have API similar to `Date`, but perform all calculations in the given time zone, which might be essential when operating across different time zones, calculating dates for users in different regions, or rendering chart or calendar component:
|
||||
`TZDate` and `TZDateMini` have the API similar to `Date`, but perform all calculations in the given time zone, which might be essential when operating across different time zones, calculating dates for users in different regions, or rendering chart or calendar components:
|
||||
|
||||
```ts
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
@@ -50,9 +50,9 @@ new TZDate(2022, 2, 13, "-2359");
|
||||
|
||||
### Difference between `TZDate` and `TZDateMini`
|
||||
|
||||
The main difference between `TZDate` and `TZDateMini` is the build footprint. The `TZDateMini` is `916 B`, and the `TZDate` is `1.2 kB`. While the difference is slight it might be essential in some environments and use cases.
|
||||
The main difference between `TZDate` and `TZDateMini` is the build footprint. The `TZDateMini` is `1.34 KB KB`, and the `TZDate` is `1.69 KB`. While the difference is slight, it might be essential in some environments and use cases.
|
||||
|
||||
Unlike `TZDateMini` which implements only getters, setters, and `getTimezoneOffset`, `TZDate` also provides formatter functions, mirroring all original `Date` functionality:
|
||||
Unlike `TZDateMini`, which implements only getters, setters, and `getTimezoneOffset`, `TZDate` also provides formatter functions, mirroring all original `Date` functionality:
|
||||
|
||||
```ts
|
||||
import { TZDateMini, TZDate } from "@date-fns/tz";
|
||||
@@ -70,19 +70,19 @@ Even though `TZDate` has a complete API, developers rarely use the formatter fun
|
||||
|
||||
### React Native / Hermes JS Engine
|
||||
|
||||
Starting with [v1.3.0](https://github.com/date-fns/tz/releases/tag/v1.3.0), `@date-fns/tz` supports [Format.JS polyfills](https://formatjs.github.io/docs/polyfills/intl-datetimeformat/) that are required for [Hermes JS Engine](https://github.com/facebook/hermes/blob/main/README.md) powering React Native runtime to work correctly.
|
||||
Starting with [v1.3.0](https://github.com/date-fns/tz/releases/tag/v1.3.0), `@date-fns/tz` supports [Format.JS polyfills](https://formatjs.github.io/docs/polyfills/intl-datetimeformat/) that are required for the [Hermes JS Engine](https://github.com/facebook/hermes/blob/main/README.md) powering React Native runtime to work correctly.
|
||||
|
||||
To use it, you need to import the following polyfills in your entry point:
|
||||
|
||||
```ts
|
||||
import "@formatjs/intl-getcanonicallocales/polyfill";
|
||||
import "@formatjs/intl-locale/polyfill";
|
||||
import "@formatjs/intl-pluralrules/polyfill";
|
||||
import "@formatjs/intl-numberformat/polyfill";
|
||||
import "@formatjs/intl-numberformat/locale-data/en";
|
||||
import "@formatjs/intl-datetimeformat/polyfill";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/en";
|
||||
import "@formatjs/intl-datetimeformat/add-golden-tz"; // or: "@formatjs/intl-datetimeformat/add-all-tz"
|
||||
import "@formatjs/intl-getcanonicallocales/polyfill.js";
|
||||
import "@formatjs/intl-locale/polyfill.js";
|
||||
import "@formatjs/intl-pluralrules/polyfill.js";
|
||||
import "@formatjs/intl-numberformat/polyfill.js";
|
||||
import "@formatjs/intl-numberformat/locale-data/en.js";
|
||||
import "@formatjs/intl-datetimeformat/polyfill.js";
|
||||
import "@formatjs/intl-datetimeformat/locale-data/en.js";
|
||||
import "@formatjs/intl-datetimeformat/add-golden-tz.js"; // or: "@formatjs/intl-datetimeformat/add-all-tz.js"
|
||||
```
|
||||
|
||||
[The JavaScriptCore engine](https://github.com/apple-opensource/JavaScriptCore) is also supported and tested but does not require any polyfills.
|
||||
@@ -114,7 +114,7 @@ The constructor mirrors the original `Date` parameters except for the last time
|
||||
|
||||
#### `TZDate.tz`
|
||||
|
||||
The static `tz` function allows to construct `TZDate` instance with just a time zone:
|
||||
The static `tz` function allows you to construct a `TZDate` instance with just a time zone:
|
||||
|
||||
```ts
|
||||
// Create now in Singapore time zone:
|
||||
@@ -125,7 +125,7 @@ new TZDate("Asia/Singapore");
|
||||
//=> Invalid Date
|
||||
```
|
||||
|
||||
Just like the constructor, the function accepts all parameters variants:
|
||||
Just like the constructor, the function accepts all parameter variants:
|
||||
|
||||
```ts
|
||||
TZDate.tz("Asia/Singapore", 2022, 2);
|
||||
@@ -153,7 +153,7 @@ new TZDate().timeZone;
|
||||
|
||||
#### `withTimeZone`
|
||||
|
||||
The `withTimeZone` method allows to create a new `TZDate` instance with a different time zone:
|
||||
The `withTimeZone` method allows you to create a new `TZDate` instance with a different time zone:
|
||||
|
||||
```ts
|
||||
const sg = new TZDate(2022, 2, 13, "Asia/Singapore");
|
||||
@@ -189,7 +189,7 @@ import { constructFromSymbol } from "@date-fns/tz";
|
||||
|
||||
### `tz`
|
||||
|
||||
The `tz` function allows to specify the context for the [date-fns] functions (**starting from date-fns@4**):
|
||||
The `tz` function allows you to specify the context for the [date-fns] functions (**starting from date-fns@4**):
|
||||
|
||||
```ts
|
||||
import { isSameDay } from "date-fns";
|
||||
@@ -203,7 +203,7 @@ isSameDay("2024-09-09T23:00:00-04:00", "2024-09-10T10:00:00+08:00", {
|
||||
|
||||
### `tzOffset`
|
||||
|
||||
The `tzOffset` function allows to get the time zone UTC offset in minutes from the given time zone and a date:
|
||||
The `tzOffset` function allows you to get the time zone UTC offset in minutes from the given time zone and a date:
|
||||
|
||||
```ts
|
||||
import { tzOffset } from "@date-fns/tz";
|
||||
@@ -248,7 +248,7 @@ tzScan("America/New_York", {
|
||||
|
||||
### `tzName`
|
||||
|
||||
The function returns time zone name in human-readable format, e.g. `"Singapore Standard Time"` in the give date and time.
|
||||
The function returns the time zone name in human-readable format, e.g. `"Singapore Standard Time"` in the given date and time.
|
||||
|
||||
```ts
|
||||
import { tzName } from "@date-fns/tz";
|
||||
|
||||
-1
@@ -3,4 +3,3 @@
|
||||
* the provided value. It helps date-fns to inherit the time zone.
|
||||
*/
|
||||
export declare const constructFromSymbol: unique symbol;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -3,4 +3,3 @@
|
||||
* the provided value. It helps date-fns to inherit the time zone.
|
||||
*/
|
||||
export declare const constructFromSymbol: unique symbol;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,mBAAmB,eAAkC,CAAC"}
|
||||
+288
-73
@@ -2,30 +2,69 @@
|
||||
|
||||
exports.TZDateMini = void 0;
|
||||
var _index = require("../tzOffset/index.cjs");
|
||||
// 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`.
|
||||
|
||||
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((0, _index.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));
|
||||
adjustToSystemTZ(this, NaN);
|
||||
syncToInternal(this);
|
||||
|
||||
// Adjust internal and external dates considering that we might have
|
||||
// landed on the DST hour.
|
||||
adjustToSystemTZ(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,8 +81,8 @@ class TZDateMini extends Date {
|
||||
}
|
||||
getTimezoneOffset() {
|
||||
const offset = -(0, _index.tzOffset)(this.timeZone, this);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -51,8 +90,11 @@ class TZDateMini extends Date {
|
||||
|
||||
//#region time
|
||||
|
||||
setTime(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;
|
||||
}
|
||||
@@ -99,23 +141,40 @@ Object.getOwnPropertyNames(Date.prototype).forEach(method => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Function syncs time to internal date, applying the time zone offset.
|
||||
* @internal
|
||||
* Function syncs time to internal date, applying the current time zone offset.
|
||||
*
|
||||
* @param {Date} date - Date to sync
|
||||
* @param {Date} date - `Date` to sync
|
||||
*/
|
||||
function syncToInternal(date) {
|
||||
// Start internal from the same real timestamp as external.
|
||||
date.internal.setTime(+date);
|
||||
date.internal.setUTCSeconds(date.internal.getUTCSeconds() - Math.round(-(0, _index.tzOffset)(date.timeZone, date) * 60));
|
||||
|
||||
// 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(-(0, _index.tzOffset)(date.timeZone, date) * 60));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function syncs the internal date UTC values to the date. It allows to get
|
||||
* accurate timestamp value.
|
||||
* @internal
|
||||
* Function syncs internal wall-clock fields into the external date.
|
||||
*
|
||||
* @param {Date} date - The date to sync
|
||||
*/
|
||||
function syncFromInternal(date) {
|
||||
// First we transpose the internal values
|
||||
// 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());
|
||||
|
||||
@@ -124,113 +183,269 @@ function syncFromInternal(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.
|
||||
*
|
||||
* @param {Date} date - Date to adjust
|
||||
* We use it when constructing `TZDate` and syncing the external date from
|
||||
* the internal one.
|
||||
*
|
||||
* @param {TZDate} date - `TZDate` to adjust.
|
||||
*/
|
||||
function adjustToSystemTZ(date) {
|
||||
// Save the time zone offset before all the adjustments
|
||||
const baseOffset = (0, _index.tzOffset)(date.timeZone, date);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
const offset = baseOffset > 0 ? Math.floor(baseOffset) : Math.ceil(baseOffset);
|
||||
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 = (0, _index.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
|
||||
|
||||
// The biggest problem with using the system time zone is that when we create
|
||||
// a date from internal values stored in UTC, the system time zone might end
|
||||
// up on the DST hour:
|
||||
// Native `Date` may normalize the requested wall time when the system time
|
||||
// zone skips that hour for DST.
|
||||
//
|
||||
// $ TZ=America/New_York node
|
||||
// > new Date(2020, 2, 8, 1).toString()
|
||||
// 'Sun Mar 08 2020 01:00:00 GMT-0500 (Eastern Standard Time)'
|
||||
// > new Date(2020, 2, 8, 2).toString()
|
||||
// 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
// > new Date(2020, 2, 8, 3).toString()
|
||||
// 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
// > new Date(2020, 2, 8, 4).toString()
|
||||
// 'Sun Mar 08 2020 04:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
//
|
||||
// Here we get the same hour for both 2 and 3, because the system time zone
|
||||
// has DST beginning at 8 March 2020, 2 a.m. and jumps to 3 a.m. So we have
|
||||
// to adjust the internal date to reflect that.
|
||||
//
|
||||
// However we want to adjust only if that's the DST hour the change happenes,
|
||||
// not the hour where DST moves to.
|
||||
// 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.
|
||||
|
||||
// We calculate the previous hour to see if the time zone offset has changed
|
||||
// and we have landed on the DST hour.
|
||||
// Previous-hour reference used to detect whether the system offset changed
|
||||
// immediately before the current native timestamp.
|
||||
const prevHour = new Date(+date);
|
||||
// We use UTC methods here as we don't want to land on the same hour again
|
||||
// in case of DST.
|
||||
|
||||
// Use UTC math so subtracting one hour cannot be normalized back into the
|
||||
// same missing local DST hour.
|
||||
prevHour.setUTCHours(prevHour.getUTCHours() - 1);
|
||||
|
||||
// Calculate if we are on the system DST hour.
|
||||
// Current system offset at the native timestamp.
|
||||
const systemOffset = -new Date(+date).getTimezoneOffset();
|
||||
const prevHourSystemOffset = -new Date(+prevHour).getTimezoneOffset();
|
||||
const systemDSTChange = systemOffset - prevHourSystemOffset;
|
||||
// Detect the DST shift. System DST change will occur both on
|
||||
const dstShift = Date.prototype.getHours.apply(date) !== date.internal.getUTCHours();
|
||||
|
||||
// Move the internal date when we are on the system DST hour.
|
||||
if (systemDSTChange && dstShift) date.internal.setUTCMinutes(date.internal.getUTCMinutes() + systemDSTChange);
|
||||
// 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 = (0, _index.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
|
||||
|
||||
// Now we need to adjust the date, since we just applied internal values.
|
||||
// We need to calculate the difference between the system and date time zones
|
||||
// and apply it to the date.
|
||||
// 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.
|
||||
|
||||
const offsetDiff = systemOffset - offset;
|
||||
if (offsetDiff) Date.prototype.setUTCMinutes.call(date, Date.prototype.getUTCMinutes.call(date) + offsetDiff);
|
||||
// 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
|
||||
//#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);
|
||||
// Set the UTC seconds to 0 to isolate the timezone offset in seconds.
|
||||
|
||||
// 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);
|
||||
// For negative systemOffset, invert the seconds.
|
||||
|
||||
// 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;
|
||||
|
||||
// Calculate the seconds offset based on the timezone offset.
|
||||
// Seconds part contributed by the target time zone offset.
|
||||
const secondsOffset = Math.round(-((0, _index.tzOffset)(date.timeZone, date) * 60)) % 60;
|
||||
if (secondsOffset || systemSecondsOffset) {
|
||||
date.internal.setUTCSeconds(date.internal.getUTCSeconds() + secondsOffset);
|
||||
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
|
||||
|
||||
const postBaseOffset = (0, _index.tzOffset)(date.timeZone, date);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
const postOffset = postBaseOffset > 0 ? Math.floor(postBaseOffset) : Math.ceil(postBaseOffset);
|
||||
// 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 = (0, _index.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 (offsetChanged && postDiff) {
|
||||
|
||||
// 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);
|
||||
|
||||
// Now we need to check if got offset change during the post-adjustment.
|
||||
// If so, we also need both dates to reflect that.
|
||||
// Target offset after applying `postDiff`. It may change again if the
|
||||
// correction itself crosses a target-zone DST boundary.
|
||||
const newOffsetWithSeconds = (0, _index.tzOffset)(date.timeZone, date);
|
||||
|
||||
const newBaseOffset = (0, _index.tzOffset)(date.timeZone, date);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
const newOffset = newBaseOffset > 0 ? Math.floor(newBaseOffset) : Math.ceil(newBaseOffset);
|
||||
// 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 (offsetChange) {
|
||||
date.internal.setUTCMinutes(date.internal.getUTCMinutes() + offsetChange);
|
||||
|
||||
// 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(-(0, _index.tzOffset)(date.timeZone, internal) * 60));
|
||||
return +internal;
|
||||
}
|
||||
+289
-73
@@ -1,28 +1,68 @@
|
||||
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));
|
||||
adjustToSystemTZ(this, NaN);
|
||||
syncToInternal(this);
|
||||
|
||||
// Adjust internal and external dates considering that we might have
|
||||
// landed on the DST hour.
|
||||
adjustToSystemTZ(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,8 +79,8 @@ export class TZDateMini extends Date {
|
||||
}
|
||||
getTimezoneOffset() {
|
||||
const offset = -tzOffset(this.timeZone, this);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -48,8 +88,11 @@ export class TZDateMini extends Date {
|
||||
|
||||
//#region time
|
||||
|
||||
setTime(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;
|
||||
}
|
||||
@@ -95,23 +138,40 @@ Object.getOwnPropertyNames(Date.prototype).forEach(method => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Function syncs time to internal date, applying the time zone offset.
|
||||
* @internal
|
||||
* Function syncs time to internal date, applying the current time zone offset.
|
||||
*
|
||||
* @param {Date} date - Date to sync
|
||||
* @param {Date} date - `Date` to sync
|
||||
*/
|
||||
function syncToInternal(date) {
|
||||
// Start internal from the same real timestamp as external.
|
||||
date.internal.setTime(+date);
|
||||
date.internal.setUTCSeconds(date.internal.getUTCSeconds() - Math.round(-tzOffset(date.timeZone, date) * 60));
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function syncs the internal date UTC values to the date. It allows to get
|
||||
* accurate timestamp value.
|
||||
* @internal
|
||||
* Function syncs internal wall-clock fields into the external date.
|
||||
*
|
||||
* @param {Date} date - The date to sync
|
||||
*/
|
||||
function syncFromInternal(date) {
|
||||
// First we transpose the internal values
|
||||
// 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());
|
||||
|
||||
@@ -120,113 +180,269 @@ function syncFromInternal(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.
|
||||
*
|
||||
* @param {Date} date - Date to adjust
|
||||
* We use it when constructing `TZDate` and syncing the external date from
|
||||
* the internal one.
|
||||
*
|
||||
* @param {TZDate} date - `TZDate` to adjust.
|
||||
*/
|
||||
function adjustToSystemTZ(date) {
|
||||
// Save the time zone offset before all the adjustments
|
||||
const baseOffset = tzOffset(date.timeZone, date);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
const offset = baseOffset > 0 ? Math.floor(baseOffset) : Math.ceil(baseOffset);
|
||||
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
|
||||
|
||||
// The biggest problem with using the system time zone is that when we create
|
||||
// a date from internal values stored in UTC, the system time zone might end
|
||||
// up on the DST hour:
|
||||
// Native `Date` may normalize the requested wall time when the system time
|
||||
// zone skips that hour for DST.
|
||||
//
|
||||
// $ TZ=America/New_York node
|
||||
// > new Date(2020, 2, 8, 1).toString()
|
||||
// 'Sun Mar 08 2020 01:00:00 GMT-0500 (Eastern Standard Time)'
|
||||
// > new Date(2020, 2, 8, 2).toString()
|
||||
// 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
// > new Date(2020, 2, 8, 3).toString()
|
||||
// 'Sun Mar 08 2020 03:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
// > new Date(2020, 2, 8, 4).toString()
|
||||
// 'Sun Mar 08 2020 04:00:00 GMT-0400 (Eastern Daylight Time)'
|
||||
//
|
||||
// Here we get the same hour for both 2 and 3, because the system time zone
|
||||
// has DST beginning at 8 March 2020, 2 a.m. and jumps to 3 a.m. So we have
|
||||
// to adjust the internal date to reflect that.
|
||||
//
|
||||
// However we want to adjust only if that's the DST hour the change happenes,
|
||||
// not the hour where DST moves to.
|
||||
// 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.
|
||||
|
||||
// We calculate the previous hour to see if the time zone offset has changed
|
||||
// and we have landed on the DST hour.
|
||||
// Previous-hour reference used to detect whether the system offset changed
|
||||
// immediately before the current native timestamp.
|
||||
const prevHour = new Date(+date);
|
||||
// We use UTC methods here as we don't want to land on the same hour again
|
||||
// in case of DST.
|
||||
|
||||
// Use UTC math so subtracting one hour cannot be normalized back into the
|
||||
// same missing local DST hour.
|
||||
prevHour.setUTCHours(prevHour.getUTCHours() - 1);
|
||||
|
||||
// Calculate if we are on the system DST hour.
|
||||
// Current system offset at the native timestamp.
|
||||
const systemOffset = -new Date(+date).getTimezoneOffset();
|
||||
const prevHourSystemOffset = -new Date(+prevHour).getTimezoneOffset();
|
||||
const systemDSTChange = systemOffset - prevHourSystemOffset;
|
||||
// Detect the DST shift. System DST change will occur both on
|
||||
const dstShift = Date.prototype.getHours.apply(date) !== date.internal.getUTCHours();
|
||||
|
||||
// Move the internal date when we are on the system DST hour.
|
||||
if (systemDSTChange && dstShift) date.internal.setUTCMinutes(date.internal.getUTCMinutes() + systemDSTChange);
|
||||
// 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
|
||||
|
||||
// Now we need to adjust the date, since we just applied internal values.
|
||||
// We need to calculate the difference between the system and date time zones
|
||||
// and apply it to the date.
|
||||
// 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.
|
||||
|
||||
const offsetDiff = systemOffset - offset;
|
||||
if (offsetDiff) Date.prototype.setUTCMinutes.call(date, Date.prototype.getUTCMinutes.call(date) + offsetDiff);
|
||||
// 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
|
||||
//#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);
|
||||
// Set the UTC seconds to 0 to isolate the timezone offset in seconds.
|
||||
|
||||
// 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);
|
||||
// For negative systemOffset, invert the seconds.
|
||||
|
||||
// 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;
|
||||
|
||||
// Calculate the seconds offset based on the timezone offset.
|
||||
// Seconds part contributed by the target time zone offset.
|
||||
const secondsOffset = Math.round(-(tzOffset(date.timeZone, date) * 60)) % 60;
|
||||
if (secondsOffset || systemSecondsOffset) {
|
||||
date.internal.setUTCSeconds(date.internal.getUTCSeconds() + secondsOffset);
|
||||
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
|
||||
|
||||
const postBaseOffset = tzOffset(date.timeZone, date);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
const postOffset = postBaseOffset > 0 ? Math.floor(postBaseOffset) : Math.ceil(postBaseOffset);
|
||||
// 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 (offsetChanged && postDiff) {
|
||||
|
||||
// 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);
|
||||
|
||||
// Now we need to check if got offset change during the post-adjustment.
|
||||
// If so, we also need both dates to reflect that.
|
||||
// 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);
|
||||
|
||||
const newBaseOffset = tzOffset(date.timeZone, date);
|
||||
// Remove the seconds offset
|
||||
// use Math.floor for negative GMT timezones and Math.ceil for positive GMT timezones.
|
||||
const newOffset = newBaseOffset > 0 ? Math.floor(newBaseOffset) : Math.ceil(newBaseOffset);
|
||||
// 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 (offsetChange) {
|
||||
date.internal.setUTCMinutes(date.internal.getUTCMinutes() + offsetChange);
|
||||
|
||||
// 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;
|
||||
}
|
||||
-1
@@ -5,4 +5,3 @@ export * from "./tz/index.cts";
|
||||
export * from "./tzOffset/index.cts";
|
||||
export * from "./tzScan/index.cts";
|
||||
export * from "./tzName/index.cts";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -5,4 +5,3 @@ export * from "./tz/index.ts";
|
||||
export * from "./tzOffset/index.ts";
|
||||
export * from "./tzScan/index.ts";
|
||||
export * from "./tzName/index.ts";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
|
||||
+20
-45
@@ -1,9 +1,26 @@
|
||||
{
|
||||
"name": "@date-fns/tz",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.0",
|
||||
"description": "date-fns timezone utils",
|
||||
"keywords": [
|
||||
"date",
|
||||
"date-fns",
|
||||
"datetime",
|
||||
"time",
|
||||
"timezones",
|
||||
"tz"
|
||||
],
|
||||
"homepage": "https://github.com/date-fns/date-fns#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/date-fns/date-fns/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Sasha Koss <koss@nocorp.me>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/date-fns/date-fns.git"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "index.cjs",
|
||||
"module": "index.js",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
@@ -88,47 +105,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/date-fns/tz.git"
|
||||
},
|
||||
"keywords": [
|
||||
"date-fns",
|
||||
"tz",
|
||||
"timezones",
|
||||
"date",
|
||||
"time",
|
||||
"datetime"
|
||||
],
|
||||
"author": "Sasha Koss <koss@nocorp.me>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/date-fns/tz/issues"
|
||||
},
|
||||
"homepage": "https://github.com/date-fns/tz#readme",
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.18.2",
|
||||
"@babel/cli": "^7.28.0",
|
||||
"@babel/core": "^7.28.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
|
||||
"@babel/preset-env": "^7.28.0",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@sinonjs/fake-timers": "^11.2.2",
|
||||
"@swc/core": "^1.4.13",
|
||||
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||
"babel-plugin-replace-import-extension": "^1.1.5",
|
||||
"bytes-iec": "^3.1.1",
|
||||
"date-fns": "4.0.0-alpha.1",
|
||||
"glob": "^10.3.12",
|
||||
"minimatch": "^10.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tinybench": "^2.7.0",
|
||||
"typescript": "5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
}
|
||||
"main": "index.cjs"
|
||||
}
|
||||
-1
@@ -9,4 +9,3 @@ import { TZDate } from "../date/index.cjs";
|
||||
* @returns Function that creates a new `TZDate` instance in the time zone
|
||||
*/
|
||||
export declare const tz: (timeZone: string) => (value: Date | number | string) => TZDate;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -9,4 +9,3 @@ import { TZDate } from "../date/index.js";
|
||||
* @returns Function that creates a new `TZDate` instance in the time zone
|
||||
*/
|
||||
export declare const tz: (timeZone: string) => (value: Date | number | string) => TZDate;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tz/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;;;GAQG;AACH,eAAO,MAAM,EAAE,GAAI,UAAU,MAAM,MAAM,OAAO,IAAI,GAAG,MAAM,GAAG,MAAM,WAC/B,CAAC"}
|
||||
-1
@@ -26,4 +26,3 @@ export type TZNameFormat = "short" | "long" | "shortGeneric" | "longGeneric";
|
||||
* @returns Time zone name (e.g. "Singapore Standard Time")
|
||||
*/
|
||||
export declare function tzName(timeZone: string, date: Date, format?: TZNameFormat): string;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -26,4 +26,3 @@ export type TZNameFormat = "short" | "long" | "shortGeneric" | "longGeneric";
|
||||
* @returns Time zone name (e.g. "Singapore Standard Time")
|
||||
*/
|
||||
export declare function tzName(timeZone: string, date: Date, format?: TZNameFormat): string;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tzName/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,cAAc,GAAG,aAAa,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,MAAM,CACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,IAAI,EACV,MAAM,GAAE,YAAqB,GAC5B,MAAM,CAWR"}
|
||||
+19
-7
@@ -5,30 +5,42 @@ const offsetFormatCache = {};
|
||||
const offsetCache = {};
|
||||
|
||||
/**
|
||||
* The function extracts UTC offset in minutes from the given date in specified
|
||||
* The function returns UTC offset in minutes for the given date in specified
|
||||
* time zone.
|
||||
*
|
||||
* Unlike `Date.prototype.getTimezoneOffset`, this function returns the value
|
||||
* mirrored to the sign of the offset in the time zone. For Asia/Singapore
|
||||
* (UTC+8), `tzOffset` returns 480, while `getTimezoneOffset` returns -480.
|
||||
* (UTC+8), `tzOffset` returns `480`, while `getTimezoneOffset` returns `-480`.
|
||||
*
|
||||
* @param timeZone - Time zone name (IANA or UTC offset)
|
||||
* @param date - Date to check the offset for
|
||||
* It uses `Intl.DateTimeFormat` internally to access otherwise unavailable
|
||||
* runtime's time zone data, and falls back to basic manual parsing if Intl API
|
||||
* is not supported (e.g., older Node.js versions, React Native's Hermes, etc.).
|
||||
*
|
||||
* @returns UTC offset in minutes
|
||||
* @param timeZone - Time zone name (IANA or UTC offset).
|
||||
* @param date - Date to check the offset for.
|
||||
*
|
||||
* @returns UTC offset in minutes (e.g., `480` for date in UTC+8).
|
||||
*/
|
||||
function tzOffset(timeZone, date) {
|
||||
try {
|
||||
// Create `Intl.DateTimeFormat` with `"longOffset"` that gives us consistent
|
||||
// date strings like `"5/19/2026, GMT-08:00"`.
|
||||
const format = offsetFormatCache[timeZone] ||= new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
timeZoneName: "longOffset"
|
||||
}).format;
|
||||
|
||||
// Get `"-08:00"`.
|
||||
const offsetStr = format(date).split("GMT")[1];
|
||||
|
||||
// Avoid parsing the same offset string.
|
||||
if (offsetStr in offsetCache) return offsetCache[offsetStr];
|
||||
|
||||
// Calculate offset from `["-08", "00"]` and cache it.
|
||||
return calcOffset(offsetStr, offsetStr.split(":"));
|
||||
} catch {
|
||||
// Fallback to manual parsing if the runtime doesn't support ±HH:MM/±HHMM/±HH
|
||||
// See: https://github.com/nodejs/node/issues/53419
|
||||
// Fallback to manual parsing if the runtime doesn't support
|
||||
// `±HH:MM/±HHMM/±HH`. See: https://github.com/nodejs/node/issues/53419.
|
||||
if (timeZone in offsetCache) return offsetCache[timeZone];
|
||||
const captures = timeZone?.match(offsetRe);
|
||||
if (captures) return calcOffset(timeZone, captures.slice(1));
|
||||
|
||||
+9
-6
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* The function extracts UTC offset in minutes from the given date in specified
|
||||
* The function returns UTC offset in minutes for the given date in specified
|
||||
* time zone.
|
||||
*
|
||||
* Unlike `Date.prototype.getTimezoneOffset`, this function returns the value
|
||||
* mirrored to the sign of the offset in the time zone. For Asia/Singapore
|
||||
* (UTC+8), `tzOffset` returns 480, while `getTimezoneOffset` returns -480.
|
||||
* (UTC+8), `tzOffset` returns `480`, while `getTimezoneOffset` returns `-480`.
|
||||
*
|
||||
* @param timeZone - Time zone name (IANA or UTC offset)
|
||||
* @param date - Date to check the offset for
|
||||
* It uses `Intl.DateTimeFormat` internally to access otherwise unavailable
|
||||
* runtime's time zone data, and falls back to basic manual parsing if Intl API
|
||||
* is not supported (e.g., older Node.js versions, React Native's Hermes, etc.).
|
||||
*
|
||||
* @returns UTC offset in minutes
|
||||
* @param timeZone - Time zone name (IANA or UTC offset).
|
||||
* @param date - Date to check the offset for.
|
||||
*
|
||||
* @returns UTC offset in minutes (e.g., `480` for date in UTC+8).
|
||||
*/
|
||||
export declare function tzOffset(timeZone: string | undefined, date: Date): number;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+9
-6
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* The function extracts UTC offset in minutes from the given date in specified
|
||||
* The function returns UTC offset in minutes for the given date in specified
|
||||
* time zone.
|
||||
*
|
||||
* Unlike `Date.prototype.getTimezoneOffset`, this function returns the value
|
||||
* mirrored to the sign of the offset in the time zone. For Asia/Singapore
|
||||
* (UTC+8), `tzOffset` returns 480, while `getTimezoneOffset` returns -480.
|
||||
* (UTC+8), `tzOffset` returns `480`, while `getTimezoneOffset` returns `-480`.
|
||||
*
|
||||
* @param timeZone - Time zone name (IANA or UTC offset)
|
||||
* @param date - Date to check the offset for
|
||||
* It uses `Intl.DateTimeFormat` internally to access otherwise unavailable
|
||||
* runtime's time zone data, and falls back to basic manual parsing if Intl API
|
||||
* is not supported (e.g., older Node.js versions, React Native's Hermes, etc.).
|
||||
*
|
||||
* @returns UTC offset in minutes
|
||||
* @param timeZone - Time zone name (IANA or UTC offset).
|
||||
* @param date - Date to check the offset for.
|
||||
*
|
||||
* @returns UTC offset in minutes (e.g., `480` for date in UTC+8).
|
||||
*/
|
||||
export declare function tzOffset(timeZone: string | undefined, date: Date): number;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tzOffset/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAoBzE"}
|
||||
+19
-7
@@ -2,30 +2,42 @@ const offsetFormatCache = {};
|
||||
const offsetCache = {};
|
||||
|
||||
/**
|
||||
* The function extracts UTC offset in minutes from the given date in specified
|
||||
* The function returns UTC offset in minutes for the given date in specified
|
||||
* time zone.
|
||||
*
|
||||
* Unlike `Date.prototype.getTimezoneOffset`, this function returns the value
|
||||
* mirrored to the sign of the offset in the time zone. For Asia/Singapore
|
||||
* (UTC+8), `tzOffset` returns 480, while `getTimezoneOffset` returns -480.
|
||||
* (UTC+8), `tzOffset` returns `480`, while `getTimezoneOffset` returns `-480`.
|
||||
*
|
||||
* @param timeZone - Time zone name (IANA or UTC offset)
|
||||
* @param date - Date to check the offset for
|
||||
* It uses `Intl.DateTimeFormat` internally to access otherwise unavailable
|
||||
* runtime's time zone data, and falls back to basic manual parsing if Intl API
|
||||
* is not supported (e.g., older Node.js versions, React Native's Hermes, etc.).
|
||||
*
|
||||
* @returns UTC offset in minutes
|
||||
* @param timeZone - Time zone name (IANA or UTC offset).
|
||||
* @param date - Date to check the offset for.
|
||||
*
|
||||
* @returns UTC offset in minutes (e.g., `480` for date in UTC+8).
|
||||
*/
|
||||
export function tzOffset(timeZone, date) {
|
||||
try {
|
||||
// Create `Intl.DateTimeFormat` with `"longOffset"` that gives us consistent
|
||||
// date strings like `"5/19/2026, GMT-08:00"`.
|
||||
const format = offsetFormatCache[timeZone] ||= new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
timeZoneName: "longOffset"
|
||||
}).format;
|
||||
|
||||
// Get `"-08:00"`.
|
||||
const offsetStr = format(date).split("GMT")[1];
|
||||
|
||||
// Avoid parsing the same offset string.
|
||||
if (offsetStr in offsetCache) return offsetCache[offsetStr];
|
||||
|
||||
// Calculate offset from `["-08", "00"]` and cache it.
|
||||
return calcOffset(offsetStr, offsetStr.split(":"));
|
||||
} catch {
|
||||
// Fallback to manual parsing if the runtime doesn't support ±HH:MM/±HHMM/±HH
|
||||
// See: https://github.com/nodejs/node/issues/53419
|
||||
// Fallback to manual parsing if the runtime doesn't support
|
||||
// `±HH:MM/±HHMM/±HH`. See: https://github.com/nodejs/node/issues/53419.
|
||||
if (timeZone in offsetCache) return offsetCache[timeZone];
|
||||
const captures = timeZone?.match(offsetRe);
|
||||
if (captures) return calcOffset(timeZone, captures.slice(1));
|
||||
|
||||
-1
@@ -27,4 +27,3 @@ export interface TZChange {
|
||||
* @returns Array of time zone changes
|
||||
*/
|
||||
export declare function tzScan(timeZone: string, interval: TZChangeInterval): TZChange[];
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -27,4 +27,3 @@ export interface TZChange {
|
||||
* @returns Array of time zone changes
|
||||
*/
|
||||
export declare function tzScan(timeZone: string, interval: TZChangeInterval): TZChange[];
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tzScan/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB;IAClB,KAAK,EAAE,IAAI,CAAC;IACZ,gBAAgB;IAChB,GAAG,EAAE,IAAI,CAAC;CACX;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,kCAAkC;IAClC,IAAI,EAAE,IAAI,CAAC;IACX,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,gBAAgB,GACzB,QAAQ,EAAE,CA+DZ"}
|
||||
Reference in New Issue
Block a user