@open-rlb/date-tz
Version:
A lightweight JavaScript/TypeScript date-time utility with full timezone support, custom formatting, parsing, and manipulation features.
447 lines • 16.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DateTz = void 0;
const timezones_1 = require("./timezones");
const MS_PER_MINUTE = 60000;
const MS_PER_HOUR = 3600000;
const MS_PER_DAY = 86400000;
const epochYear = 1970;
const daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
class DateTz {
constructor(value, tz) {
if (typeof value === 'object') {
this.timestamp = value.timestamp;
this.timezone = value.timezone || 'UTC';
if (!timezones_1.timezones[this.timezone]) {
throw new Error(`Invalid timezone: ${value.timezone}`);
}
}
else {
this.timezone = tz || 'UCT';
if (!timezones_1.timezones[this.timezone]) {
throw new Error(`Invalid timezone: ${tz}`);
}
this.timestamp = this.stripSMs(value);
}
}
get timezoneOffset() {
return timezones_1.timezones[this.timezone];
}
compare(other) {
if (this.isComparable(other)) {
return this.timestamp - other.timestamp;
}
throw new Error('Cannot compare dates with different timezones');
}
isComparable(other) {
return this.timezone === other.timezone;
}
toString(pattern, locale) {
if (!pattern)
pattern = 'YYYY-MM-DD HH:mm:ss';
const offset = (this.isDst ? timezones_1.timezones[this.timezone].dst : timezones_1.timezones[this.timezone].sdt) * 1000;
let remainingMs = this.timestamp + offset;
let year = epochYear;
while (true) {
const daysInYear = this.isLeapYear(year) ? 366 : 365;
const msInYear = daysInYear * MS_PER_DAY;
if (remainingMs >= msInYear) {
remainingMs -= msInYear;
year++;
}
else {
break;
}
}
let month = 0;
while (month < 12) {
const daysInMonth = month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
const msInMonth = daysInMonth * MS_PER_DAY;
if (remainingMs >= msInMonth) {
remainingMs -= msInMonth;
month++;
}
else {
break;
}
}
const day = Math.floor(remainingMs / MS_PER_DAY) + 1;
remainingMs %= MS_PER_DAY;
const hour = Math.floor(remainingMs / MS_PER_HOUR);
remainingMs %= MS_PER_HOUR;
const minute = Math.floor(remainingMs / MS_PER_MINUTE);
remainingMs %= MS_PER_MINUTE;
const second = Math.floor(remainingMs / 1000);
const pm = hour >= 12 ? 'PM' : 'AM';
const hour12 = hour % 12 || 12;
if (!locale)
locale = 'en';
let monthStr = new Date(year, month, 3).toLocaleString(locale || 'en', { month: 'long' });
monthStr = monthStr.charAt(0).toUpperCase() + monthStr.slice(1);
const tokens = {
YYYY: year,
YY: String(year).slice(-2),
yyyy: year.toString(),
yy: String(year).slice(-2),
MM: String(month + 1).padStart(2, '0'),
LM: monthStr,
DD: String(day).padStart(2, '0'),
HH: String(hour).padStart(2, '0'),
mm: String(minute).padStart(2, '0'),
ss: String(second).padStart(2, '0'),
aa: pm.toLowerCase(),
AA: pm,
hh: hour12.toString().padStart(2, '0'),
tz: this.timezone,
};
return pattern.replace(/YYYY|yyyy|YY|yy|MM|LM|DD|HH|hh|mm|ss|aa|AA|tz/g, (match) => tokens[match]);
}
add(value, unit) {
let remainingMs = this.timestamp;
let year = 1970;
let days = Math.floor(remainingMs / MS_PER_DAY);
remainingMs %= MS_PER_DAY;
let hour = Math.floor(remainingMs / MS_PER_HOUR);
remainingMs %= MS_PER_HOUR;
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
while (days >= this.daysInYear(year)) {
days -= this.daysInYear(year);
year++;
}
let month = 0;
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
month++;
}
let day = days + 1;
switch (unit) {
case 'minute':
minute += value;
break;
case 'hour':
hour += value;
break;
case 'day':
day += value;
break;
case 'month':
month += value;
break;
case 'year':
year += value;
break;
default:
throw new Error(`Unsupported unit: ${unit}`);
}
while (minute >= 60) {
minute -= 60;
hour++;
}
while (hour >= 24) {
hour -= 24;
day++;
}
while (month >= 12) {
month -= 12;
year++;
}
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
month++;
if (month >= 12) {
month = 0;
year++;
}
}
const newTimestamp = (() => {
let totalMs = 0;
for (let y = 1970; y < year; y++) {
totalMs += this.daysInYear(y) * MS_PER_DAY;
}
for (let m = 0; m < month; m++) {
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
}
totalMs += (day - 1) * MS_PER_DAY;
totalMs += hour * MS_PER_HOUR;
totalMs += minute * MS_PER_MINUTE;
totalMs += second * 1000;
return totalMs;
})();
this.timestamp = newTimestamp;
return this;
}
_year(considerDst = false) {
const offset = considerDst ? (timezones_1.timezones[this.timezone].dst * 1000) : (timezones_1.timezones[this.timezone].sdt * 1000);
let remainingMs = this.timestamp + offset;
let year = 1970;
let days = Math.floor(remainingMs / MS_PER_DAY);
while (days >= this.daysInYear(year)) {
days -= this.daysInYear(year);
year++;
}
return year;
}
_month(considerDst = false) {
const offset = considerDst ? (timezones_1.timezones[this.timezone].dst * 1000) : (timezones_1.timezones[this.timezone].sdt * 1000);
let remainingMs = this.timestamp + offset;
let year = 1970;
let days = Math.floor(remainingMs / MS_PER_DAY);
while (days >= this.daysInYear(year)) {
days -= this.daysInYear(year);
year++;
}
let month = 0;
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
month++;
}
return month;
}
_day(considerDst = false) {
const offset = considerDst ? (timezones_1.timezones[this.timezone].dst * 1000) : (timezones_1.timezones[this.timezone].sdt * 1000);
let remainingMs = this.timestamp + offset;
let year = 1970;
let days = Math.floor(remainingMs / MS_PER_DAY);
while (days >= this.daysInYear(year)) {
days -= this.daysInYear(year);
year++;
}
let month = 0;
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
month++;
}
return days + 1;
}
_hour(considerDst = false) {
const offset = considerDst ? (timezones_1.timezones[this.timezone].dst * 1000) : (timezones_1.timezones[this.timezone].sdt * 1000);
let remainingMs = this.timestamp + offset;
remainingMs %= MS_PER_DAY;
let hour = Math.floor(remainingMs / MS_PER_HOUR);
return hour;
}
_minute(considerDst = false) {
const offset = considerDst ? (timezones_1.timezones[this.timezone].dst * 1000) : (timezones_1.timezones[this.timezone].sdt * 1000);
let remainingMs = this.timestamp + offset;
remainingMs %= MS_PER_HOUR;
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
return minute;
}
_dayOfWeek(considerDst = false) {
const offset = considerDst ? (timezones_1.timezones[this.timezone].dst * 1000) : (timezones_1.timezones[this.timezone].sdt * 1000);
let remainingMs = this.timestamp + offset;
const date = new Date(remainingMs);
return date.getDay();
}
convertToTimezone(tz) {
if (!timezones_1.timezones[tz]) {
throw new Error(`Invalid timezone: ${tz}`);
}
this.timezone = tz;
return this;
}
cloneToTimezone(tz) {
if (!timezones_1.timezones[tz]) {
throw new Error(`Invalid timezone: ${tz}`);
}
const clone = new DateTz(this);
clone.timezone = tz;
return clone;
}
stripSMs(timestamp) {
const days = Math.floor(timestamp / MS_PER_DAY);
const remainingAfterDays = timestamp % MS_PER_DAY;
const hours = Math.floor(remainingAfterDays / MS_PER_HOUR);
const remainingAfterHours = remainingAfterDays % MS_PER_HOUR;
const minutes = Math.floor(remainingAfterHours / MS_PER_MINUTE);
return days * MS_PER_DAY + hours * MS_PER_HOUR + minutes * MS_PER_MINUTE;
}
set(value, unit) {
let remainingMs = this.timestamp;
let year = 1970;
let days = Math.floor(remainingMs / MS_PER_DAY);
remainingMs %= MS_PER_DAY;
let hour = Math.floor(remainingMs / MS_PER_HOUR);
remainingMs %= MS_PER_HOUR;
let minute = Math.floor(remainingMs / MS_PER_MINUTE);
let second = Math.floor((remainingMs % MS_PER_MINUTE) / 1000);
while (days >= this.daysInYear(year)) {
days -= this.daysInYear(year);
year++;
}
let month = 0;
while (days >= (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
days -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
month++;
}
let day = days + 1;
switch (unit) {
case 'year':
year = value;
break;
case 'month':
month = value - 1;
break;
case 'day':
day = value;
break;
case 'hour':
hour = value;
break;
case 'minute':
minute = value;
break;
default:
throw new Error(`Unsupported unit: ${unit}`);
}
while (month >= 12) {
month -= 12;
year++;
}
while (day > (month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month])) {
day -= month === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[month];
month++;
if (month >= 12) {
month = 0;
year++;
}
}
const newTimestamp = (() => {
let totalMs = 0;
for (let y = 1970; y < year; y++) {
totalMs += this.daysInYear(y) * MS_PER_DAY;
}
for (let m = 0; m < month; m++) {
totalMs += (m === 1 && this.isLeapYear(year) ? 29 : daysPerMonth[m]) * MS_PER_DAY;
}
totalMs += (day - 1) * MS_PER_DAY;
totalMs += hour * MS_PER_HOUR;
totalMs += minute * MS_PER_MINUTE;
totalMs += second * 1000;
return totalMs;
})();
this.timestamp = newTimestamp;
return this;
}
isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
daysInYear(year) {
return this.isLeapYear(year) ? 366 : 365;
}
static parse(dateString, pattern, tz) {
if (!pattern)
pattern = DateTz.defaultFormat;
if (!tz)
tz = 'UTC';
if (!timezones_1.timezones[tz]) {
throw new Error(`Invalid timezone: ${tz}`);
}
if (pattern.includes('hh') && (!pattern.includes('aa') || !pattern.includes('AA'))) {
throw new Error('AM/PM marker (aa or AA) is required when using 12-hour format (hh)');
}
const regex = /YYYY|yyyy|MM|DD|HH|hh|mm|ss|aa|AA/g;
const dateComponents = {
YYYY: 1970,
yyyy: 1970,
MM: 0,
DD: 0,
HH: 0,
hh: 0,
aa: 'am',
AA: "AM",
mm: 0,
ss: 0,
};
let match;
let index = 0;
while ((match = regex.exec(pattern)) !== null) {
const token = match[0];
const value = parseInt(dateString.substring(match.index, match.index + token.length), 10);
dateComponents[token] = value;
index += token.length + 1;
}
const year = dateComponents.YYYY || dateComponents.yyyy;
const month = dateComponents.MM - 1;
const day = dateComponents.DD;
let hour = 0;
const ampm = (dateComponents.a || dateComponents.A);
if (ampm) {
hour = ampm.toUpperCase() === 'AM' ? dateComponents.hh : dateComponents.hh + 12;
}
else {
hour = dateComponents.HH;
}
const minute = dateComponents.mm;
const second = dateComponents.ss;
const daysInYear = (year) => (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0) ? 366 : 365;
const daysInMonth = (year, month) => month === 1 && daysInYear(year) === 366 ? 29 : daysPerMonth[month];
let timestamp = 0;
for (let y = 1970; y < year; y++) {
timestamp += daysInYear(y) * MS_PER_DAY;
}
for (let m = 0; m < month; m++) {
timestamp += daysInMonth(year, m) * MS_PER_DAY;
}
timestamp += (day - 1) * MS_PER_DAY;
timestamp += hour * MS_PER_HOUR;
timestamp += minute * MS_PER_MINUTE;
timestamp += second * 1000;
const offset = (timezones_1.timezones[tz].sdt) * 1000;
let remainingMs = timestamp - offset;
const date = new DateTz(remainingMs, tz);
date.timestamp -= date.isDst ? (timezones_1.timezones[tz].dst - timezones_1.timezones[tz].sdt) * 1000 : 0;
return date;
}
static now(tz) {
if (!tz)
tz = 'UTC';
const timezone = timezones_1.timezones[tz];
if (!timezone) {
throw new Error(`Invalid timezone: ${tz}`);
}
const date = new DateTz(Date.now(), tz);
return date;
}
get isDst() {
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: this.timezone,
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const janD = Date.UTC(this._year(), 0, 1, this._hour() - timezones_1.timezones[this.timezone].sdt / 3600, this._minute(), 0);
const jan = formatter.format(+janD);
const now = formatter.format(this.timestamp);
const janMinutes = this.hhmmToMinutes(jan);
const nowMinutes = this.hhmmToMinutes(now);
return nowMinutes !== janMinutes;
}
hhmmToMinutes(hhmm) {
const [hours, minutes] = hhmm.split(':').map(Number);
return hours * 60 + minutes;
}
get year() {
return this._year(true);
}
get month() {
return this._month(true);
}
get day() {
return this._day(true);
}
get hour() {
return this._hour(true);
}
get minute() {
return this._minute(true);
}
get dayOfWeek() {
return this._dayOfWeek(true);
}
}
exports.DateTz = DateTz;
DateTz.defaultFormat = 'YYYY-MM-DD HH:mm:ss';
//# sourceMappingURL=date-tz.js.map