130 lines
6 KiB
JavaScript
130 lines
6 KiB
JavaScript
export class CronosDate {
|
|
constructor(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) {
|
|
this.year = year;
|
|
this.month = month;
|
|
this.day = day;
|
|
this.hour = hour;
|
|
this.minute = minute;
|
|
this.second = second;
|
|
}
|
|
static fromDate(date, timezone) {
|
|
if (!timezone) {
|
|
return new CronosDate(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
|
|
}
|
|
return timezone['nativeDateToCronosDate'](date);
|
|
}
|
|
toDate(timezone) {
|
|
if (!timezone) {
|
|
return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
|
}
|
|
return timezone['cronosDateToNativeDate'](this);
|
|
}
|
|
static fromUTCTimestamp(timestamp) {
|
|
const date = new Date(timestamp);
|
|
return new CronosDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
|
|
}
|
|
toUTCTimestamp() {
|
|
return Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
|
}
|
|
copyWith({ year = this.year, month = this.month, day = this.day, hour = this.hour, minute = this.minute, second = this.second } = {}) {
|
|
return new CronosDate(year, month, day, hour, minute, second);
|
|
}
|
|
}
|
|
// Adapted from Intl.DateTimeFormat timezone handling in https://github.com/moment/luxon
|
|
const ZoneCache = new Map();
|
|
export class CronosTimezone {
|
|
constructor(IANANameOrOffset) {
|
|
if (typeof IANANameOrOffset === 'number') {
|
|
if (IANANameOrOffset > 840 || IANANameOrOffset < -840)
|
|
throw new Error('Invalid offset');
|
|
this.fixedOffset = IANANameOrOffset;
|
|
return this;
|
|
}
|
|
const offsetMatch = IANANameOrOffset.match(/^([+-]?)(0[1-9]|1[0-4])(?::?([0-5][0-9]))?$/);
|
|
if (offsetMatch) {
|
|
this.fixedOffset = (offsetMatch[1] === '-' ? -1 : 1) * ((parseInt(offsetMatch[2], 10) * 60) + (parseInt(offsetMatch[3], 10) || 0));
|
|
return this;
|
|
}
|
|
if (ZoneCache.has(IANANameOrOffset)) {
|
|
return ZoneCache.get(IANANameOrOffset);
|
|
}
|
|
try {
|
|
this.dateTimeFormat = new Intl.DateTimeFormat("en-US", {
|
|
hour12: false,
|
|
timeZone: IANANameOrOffset,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit"
|
|
});
|
|
}
|
|
catch (err) {
|
|
throw new Error('Invalid IANA name or offset');
|
|
}
|
|
this.zoneName = IANANameOrOffset;
|
|
const currentYear = new Date().getUTCFullYear();
|
|
this.winterOffset = this.offset(Date.UTC(currentYear, 0, 1));
|
|
this.summerOffset = this.offset(Date.UTC(currentYear, 5, 1));
|
|
ZoneCache.set(IANANameOrOffset, this);
|
|
}
|
|
toString() {
|
|
if (this.fixedOffset) {
|
|
const absOffset = Math.abs(this.fixedOffset);
|
|
return [
|
|
this.fixedOffset < 0 ? '-' : '+',
|
|
Math.floor(absOffset / 60).toString().padStart(2, '0'),
|
|
(absOffset % 60).toString().padStart(2, '0')
|
|
].join('');
|
|
}
|
|
return this.zoneName;
|
|
}
|
|
offset(ts) {
|
|
if (!this.dateTimeFormat)
|
|
return this.fixedOffset || 0;
|
|
const date = new Date(ts);
|
|
const { year, month, day, hour, minute, second } = this.nativeDateToCronosDate(date);
|
|
const asUTC = Date.UTC(year, month - 1, day, hour, minute, second), asTS = ts - (ts % 1000);
|
|
return (asUTC - asTS) / 60000;
|
|
}
|
|
nativeDateToCronosDate(date) {
|
|
if (!this.dateTimeFormat) {
|
|
return CronosDate['fromUTCTimestamp'](date.getTime() + (this.fixedOffset || 0) * 60000);
|
|
}
|
|
return this.dateTimeFormat['formatToParts']
|
|
? partsOffset(this.dateTimeFormat, date)
|
|
: hackyOffset(this.dateTimeFormat, date);
|
|
}
|
|
cronosDateToNativeDate(date) {
|
|
if (!this.dateTimeFormat) {
|
|
return new Date(date['toUTCTimestamp']() - (this.fixedOffset || 0) * 60000);
|
|
}
|
|
const provisionalOffset = ((date.month > 3 || date.month < 11) ? this.summerOffset : this.winterOffset) || 0;
|
|
const UTCTimestamp = date['toUTCTimestamp']();
|
|
// Find the right offset a given local time.
|
|
// Our UTC time is just a guess because our offset is just a guess
|
|
let utcGuess = UTCTimestamp - provisionalOffset * 60000;
|
|
// Test whether the zone matches the offset for this ts
|
|
const o2 = this.offset(utcGuess);
|
|
// If so, offset didn't change and we're done
|
|
if (provisionalOffset === o2)
|
|
return new Date(utcGuess);
|
|
// If not, change the ts by the difference in the offset
|
|
utcGuess -= (o2 - provisionalOffset) * 60000;
|
|
// If that gives us the local time we want, we're done
|
|
const o3 = this.offset(utcGuess);
|
|
if (o2 === o3)
|
|
return new Date(utcGuess);
|
|
// If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time
|
|
return new Date(UTCTimestamp - Math.min(o2, o3) * 60000);
|
|
}
|
|
}
|
|
function hackyOffset(dtf, date) {
|
|
const formatted = dtf.format(date).replace(/\u200E/g, ""), parsed = formatted.match(/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/), [, month, day, year, hour, minute, second] = (parsed !== null && parsed !== void 0 ? parsed : []).map(n => parseInt(n, 10));
|
|
return new CronosDate(year, month, day, hour % 24, minute, second);
|
|
}
|
|
function partsOffset(dtf, date) {
|
|
const formatted = dtf.formatToParts(date);
|
|
return new CronosDate(parseInt(formatted[4].value, 10), parseInt(formatted[0].value, 10), parseInt(formatted[2].value, 10), parseInt(formatted[6].value, 10) % 24, parseInt(formatted[8].value, 10), parseInt(formatted[10].value, 10));
|
|
}
|