UNPKG

@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
"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