204 lines
6.7 KiB
JavaScript
204 lines
6.7 KiB
JavaScript
import { tzOffset } from "../tzOffset/index.js";
|
|
export class TZDateMini extends Date {
|
|
//#region static
|
|
|
|
constructor(...args) {
|
|
super();
|
|
if (args.length > 1 && typeof args[args.length - 1] === "string") {
|
|
this.timeZone = args.pop();
|
|
}
|
|
this.internal = new Date();
|
|
if (isNaN(tzOffset(this.timeZone, this))) {
|
|
this.setTime(NaN);
|
|
} else {
|
|
if (!args.length) {
|
|
this.setTime(Date.now());
|
|
} else if (typeof args[0] === "number" && (args.length === 1 || args.length === 2 && typeof args[1] !== "number")) {
|
|
this.setTime(args[0]);
|
|
} else if (typeof args[0] === "string") {
|
|
this.setTime(+new Date(args[0]));
|
|
} else if (args[0] instanceof Date) {
|
|
this.setTime(+args[0]);
|
|
} else {
|
|
this.setTime(+new Date(...args));
|
|
adjustToSystemTZ(this, NaN);
|
|
syncToInternal(this);
|
|
}
|
|
}
|
|
}
|
|
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() {
|
|
return -tzOffset(this.timeZone, this);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region time
|
|
|
|
setTime(time) {
|
|
Date.prototype.setTime.apply(this, arguments);
|
|
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;
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Function syncs time to internal date, applying the time zone offset.
|
|
*
|
|
* @param {Date} date - Date to sync
|
|
*/
|
|
function syncToInternal(date) {
|
|
date.internal.setTime(+date);
|
|
date.internal.setUTCMinutes(date.internal.getUTCMinutes() - date.getTimezoneOffset());
|
|
}
|
|
|
|
/**
|
|
* Function syncs the internal date UTC values to the date. It allows to get
|
|
* accurate timestamp value.
|
|
*
|
|
* @param {Date} date - The date to sync
|
|
*/
|
|
function syncFromInternal(date) {
|
|
// First we transpose the internal values
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function adjustToSystemTZ(date) {
|
|
// Save the time zone offset before all the adjustments
|
|
const offset = tzOffset(date.timeZone, date);
|
|
|
|
//#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:
|
|
//
|
|
// $ 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 calculate the previous hour to see if the time zone offset has changed
|
|
// and we have landed on the DST hour.
|
|
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.
|
|
prevHour.setUTCHours(prevHour.getUTCHours() - 1);
|
|
|
|
// Calculate if we are on the system DST hour.
|
|
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);
|
|
|
|
//#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.
|
|
|
|
const offsetDiff = systemOffset - offset;
|
|
if (offsetDiff) Date.prototype.setUTCMinutes.call(date, Date.prototype.getUTCMinutes.call(date) + offsetDiff);
|
|
|
|
//#endregion
|
|
|
|
//#region Post-adjustment DST fix
|
|
|
|
const postOffset = tzOffset(date.timeZone, date);
|
|
const postSystemOffset = -new Date(+date).getTimezoneOffset();
|
|
const postOffsetDiff = postSystemOffset - postOffset;
|
|
const offsetChanged = postOffset !== offset;
|
|
const postDiff = postOffsetDiff - offsetDiff;
|
|
if (offsetChanged && postDiff) {
|
|
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.
|
|
|
|
const newOffset = tzOffset(date.timeZone, date);
|
|
const offsetChange = postOffset - newOffset;
|
|
if (offsetChange) {
|
|
date.internal.setUTCMinutes(date.internal.getUTCMinutes() + offsetChange);
|
|
Date.prototype.setUTCMinutes.call(date, Date.prototype.getUTCMinutes.call(date) + offsetChange);
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
} |