vremel
Version:
JavaScript date utility library for Temporal API
188 lines (177 loc) • 4.83 kB
text/typescript
import {
isInstantConstructor,
isPlainDateTimeConstructor,
} from "../type-utils.js";
import type { Temporal } from "../types.js";
import { createRecord } from "./_createRecord.js";
import { formatExactTimeIso } from "./_formatExactTimeIso.js";
import { getDayOfWeekFromYmd } from "./_getDayOfWeekFromYmd.js";
import { getDayOfWeekNumberFromAbbreviation } from "./_getDayOfWeekNumberFromAbbreviation.js";
import { getMonthNumberFromAbbreviation } from "./_getMonthNumberFromAbbreviation.js";
// spec: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3 https://datatracker.ietf.org/doc/html/rfc2822#section-4.3
function removeComment(str: string) {
const r = /(?<!\\)[()]/g;
let res = "";
let commentNestLevel = 0;
let lastNonCommentStarted = 0;
for (const m of str.matchAll(r)) {
if (m[0] === "(") {
if (commentNestLevel === 0) {
// comment started
res += str.slice(lastNonCommentStarted, m.index);
}
commentNestLevel++;
} else {
commentNestLevel--;
if (commentNestLevel === 0) {
// comment ended
lastNonCommentStarted = m.index + 1;
}
if (commentNestLevel < 0) {
throw new Error("Unbalanced nested comment");
}
}
}
if (commentNestLevel !== 0) {
throw new Error("Unbalanced nested comment");
}
return res;
}
const dateTimeFormatRegex =
/^[ \t\r\n]*(?:([A-Za-z]{3}),)?[ \t\r\n]*(\d\d)[ \t\r\n]+([A-Za-z]{3})[ \t\r\n]+(\d\d|\d\d\d\d)[ \t\r\n]+(\d\d):(\d\d)(?::(\d\d))?[ \t\r\n]+([+-]\d{4}|[A-Za-z]+)[ \t\r\n]*$/;
function fullYear(year: string) {
const yearNum = parseInt(year, 10);
if (year.length === 4) {
return yearNum;
}
return yearNum >= 50 ? 1900 + yearNum : 2000 + yearNum;
}
function getOffset(timeZone: string): string {
if (["UT", "GMT", "z", "Z"].includes(timeZone)) {
return "+00:00";
}
if (timeZone === "-0000" || /^[A-IK-Za-ik-z]$/.test(timeZone)) {
// according to the spec, military zone except 'Z' should be considered equivalent to "-0000",
// which means the date-time contains no information about the local time zone
throw new Error("No offset info");
}
if (/^[+-]\d{4}$/.test(timeZone)) {
return `${timeZone.slice(0, 3)}:${timeZone.slice(3)}`;
}
const table = createRecord({
EDT: "-04:00",
EST: "-05:00",
CDT: "-05:00",
CST: "-06:00",
MDT: "-06:00",
MST: "-07:00",
PDT: "-07:00",
PST: "-08:00",
});
if (table[timeZone] !== undefined) {
return table[timeZone];
}
throw new Error("Unknown time zone");
}
function parse(
date: string,
): [
year: number,
month: number,
day: number,
hour: number,
minute: number,
second: number,
dayOfWeek: string | undefined,
timeZone: string,
] {
const result = dateTimeFormatRegex.exec(date);
if (result === null) {
throw new Error(`Invalid date and time format: ${date}`);
}
const [
,
dayOfWeek,
day,
monthName,
year,
hour,
minute,
second = "00",
timeZone,
] = result;
if (
day === undefined ||
monthName === undefined ||
year === undefined ||
hour === undefined ||
minute === undefined ||
timeZone === undefined
) {
throw new Error("something wrong");
}
return [
fullYear(year),
getMonthNumberFromAbbreviation(monthName),
parseInt(day),
parseInt(hour, 10),
parseInt(minute, 10),
parseInt(second),
dayOfWeek,
timeZone,
];
}
/**
* Creates Temporal object from datetime string in RFC 2822's format.
*
* @param date datetime string in RFC 2822's format
* @param TemporalClass Temporal class (such as `Temporal.PlainDateTime` or `Temporal.Instant`) which will be returned
* @returns an instance of Temporal class specified in `TemporalClass` argument
*/
export function fromRfc2822<
TemporalClassType extends
| typeof Temporal.Instant
| typeof Temporal.ZonedDateTime
| typeof Temporal.PlainDateTime,
>(
date: string,
TemporalClass: TemporalClassType,
): InstanceType<TemporalClassType> {
const dateWithoutComment = date.includes("(") ? removeComment(date) : date;
const [year, month, day, hour, minute, second, dayOfWeek, timeZone] =
parse(dateWithoutComment);
if (
dayOfWeek !== undefined &&
getDayOfWeekFromYmd(year, month, day) !==
getDayOfWeekNumberFromAbbreviation(dayOfWeek)
) {
throw new Error(`Wrong day of week: ${dayOfWeek}`);
}
if (isPlainDateTimeConstructor(TemporalClass)) {
return TemporalClass.from({
year,
month,
day,
hour,
minute,
second,
calendarId: "iso8601",
}) as InstanceType<TemporalClassType>;
}
const offsetIso = getOffset(timeZone);
if (isInstantConstructor(TemporalClass)) {
return TemporalClass.from(
formatExactTimeIso(year, month, day, hour, minute, second, 0, offsetIso),
) as InstanceType<TemporalClassType>;
}
return TemporalClass.from({
year,
month,
day,
hour,
minute,
second,
calendarId: "iso8601",
timeZone: offsetIso,
}) as InstanceType<TemporalClassType>;
}