188 lines
8.5 KiB
JavaScript
188 lines
8.5 KiB
JavaScript
|
import { strictParseByte, strictParseDouble, strictParseFloat32, strictParseShort } from "./parse-utils";
|
||
|
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||
|
export function dateToUtcString(date) {
|
||
|
const year = date.getUTCFullYear();
|
||
|
const month = date.getUTCMonth();
|
||
|
const dayOfWeek = date.getUTCDay();
|
||
|
const dayOfMonthInt = date.getUTCDate();
|
||
|
const hoursInt = date.getUTCHours();
|
||
|
const minutesInt = date.getUTCMinutes();
|
||
|
const secondsInt = date.getUTCSeconds();
|
||
|
const dayOfMonthString = dayOfMonthInt < 10 ? `0${dayOfMonthInt}` : `${dayOfMonthInt}`;
|
||
|
const hoursString = hoursInt < 10 ? `0${hoursInt}` : `${hoursInt}`;
|
||
|
const minutesString = minutesInt < 10 ? `0${minutesInt}` : `${minutesInt}`;
|
||
|
const secondsString = secondsInt < 10 ? `0${secondsInt}` : `${secondsInt}`;
|
||
|
return `${DAYS[dayOfWeek]}, ${dayOfMonthString} ${MONTHS[month]} ${year} ${hoursString}:${minutesString}:${secondsString} GMT`;
|
||
|
}
|
||
|
const RFC3339 = new RegExp(/^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?[zZ]$/);
|
||
|
export const parseRfc3339DateTime = (value) => {
|
||
|
if (value === null || value === undefined) {
|
||
|
return undefined;
|
||
|
}
|
||
|
if (typeof value !== "string") {
|
||
|
throw new TypeError("RFC-3339 date-times must be expressed as strings");
|
||
|
}
|
||
|
const match = RFC3339.exec(value);
|
||
|
if (!match) {
|
||
|
throw new TypeError("Invalid RFC-3339 date-time value");
|
||
|
}
|
||
|
const [_, yearStr, monthStr, dayStr, hours, minutes, seconds, fractionalMilliseconds] = match;
|
||
|
const year = strictParseShort(stripLeadingZeroes(yearStr));
|
||
|
const month = parseDateValue(monthStr, "month", 1, 12);
|
||
|
const day = parseDateValue(dayStr, "day", 1, 31);
|
||
|
return buildDate(year, month, day, { hours, minutes, seconds, fractionalMilliseconds });
|
||
|
};
|
||
|
const RFC3339_WITH_OFFSET = new RegExp(/^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(([-+]\d{2}\:\d{2})|[zZ])$/);
|
||
|
export const parseRfc3339DateTimeWithOffset = (value) => {
|
||
|
if (value === null || value === undefined) {
|
||
|
return undefined;
|
||
|
}
|
||
|
if (typeof value !== "string") {
|
||
|
throw new TypeError("RFC-3339 date-times must be expressed as strings");
|
||
|
}
|
||
|
const match = RFC3339_WITH_OFFSET.exec(value);
|
||
|
if (!match) {
|
||
|
throw new TypeError("Invalid RFC-3339 date-time value");
|
||
|
}
|
||
|
const [_, yearStr, monthStr, dayStr, hours, minutes, seconds, fractionalMilliseconds, offsetStr] = match;
|
||
|
const year = strictParseShort(stripLeadingZeroes(yearStr));
|
||
|
const month = parseDateValue(monthStr, "month", 1, 12);
|
||
|
const day = parseDateValue(dayStr, "day", 1, 31);
|
||
|
const date = buildDate(year, month, day, { hours, minutes, seconds, fractionalMilliseconds });
|
||
|
if (offsetStr.toUpperCase() != "Z") {
|
||
|
date.setTime(date.getTime() - parseOffsetToMilliseconds(offsetStr));
|
||
|
}
|
||
|
return date;
|
||
|
};
|
||
|
const IMF_FIXDATE = new RegExp(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{1,2}):(\d{2}):(\d{2})(?:\.(\d+))? GMT$/);
|
||
|
const RFC_850_DATE = new RegExp(/^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{1,2}):(\d{2}):(\d{2})(?:\.(\d+))? GMT$/);
|
||
|
const ASC_TIME = new RegExp(/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( [1-9]|\d{2}) (\d{1,2}):(\d{2}):(\d{2})(?:\.(\d+))? (\d{4})$/);
|
||
|
export const parseRfc7231DateTime = (value) => {
|
||
|
if (value === null || value === undefined) {
|
||
|
return undefined;
|
||
|
}
|
||
|
if (typeof value !== "string") {
|
||
|
throw new TypeError("RFC-7231 date-times must be expressed as strings");
|
||
|
}
|
||
|
let match = IMF_FIXDATE.exec(value);
|
||
|
if (match) {
|
||
|
const [_, dayStr, monthStr, yearStr, hours, minutes, seconds, fractionalMilliseconds] = match;
|
||
|
return buildDate(strictParseShort(stripLeadingZeroes(yearStr)), parseMonthByShortName(monthStr), parseDateValue(dayStr, "day", 1, 31), { hours, minutes, seconds, fractionalMilliseconds });
|
||
|
}
|
||
|
match = RFC_850_DATE.exec(value);
|
||
|
if (match) {
|
||
|
const [_, dayStr, monthStr, yearStr, hours, minutes, seconds, fractionalMilliseconds] = match;
|
||
|
return adjustRfc850Year(buildDate(parseTwoDigitYear(yearStr), parseMonthByShortName(monthStr), parseDateValue(dayStr, "day", 1, 31), {
|
||
|
hours,
|
||
|
minutes,
|
||
|
seconds,
|
||
|
fractionalMilliseconds,
|
||
|
}));
|
||
|
}
|
||
|
match = ASC_TIME.exec(value);
|
||
|
if (match) {
|
||
|
const [_, monthStr, dayStr, hours, minutes, seconds, fractionalMilliseconds, yearStr] = match;
|
||
|
return buildDate(strictParseShort(stripLeadingZeroes(yearStr)), parseMonthByShortName(monthStr), parseDateValue(dayStr.trimLeft(), "day", 1, 31), { hours, minutes, seconds, fractionalMilliseconds });
|
||
|
}
|
||
|
throw new TypeError("Invalid RFC-7231 date-time value");
|
||
|
};
|
||
|
export const parseEpochTimestamp = (value) => {
|
||
|
if (value === null || value === undefined) {
|
||
|
return undefined;
|
||
|
}
|
||
|
let valueAsDouble;
|
||
|
if (typeof value === "number") {
|
||
|
valueAsDouble = value;
|
||
|
}
|
||
|
else if (typeof value === "string") {
|
||
|
valueAsDouble = strictParseDouble(value);
|
||
|
}
|
||
|
else {
|
||
|
throw new TypeError("Epoch timestamps must be expressed as floating point numbers or their string representation");
|
||
|
}
|
||
|
if (Number.isNaN(valueAsDouble) || valueAsDouble === Infinity || valueAsDouble === -Infinity) {
|
||
|
throw new TypeError("Epoch timestamps must be valid, non-Infinite, non-NaN numerics");
|
||
|
}
|
||
|
return new Date(Math.round(valueAsDouble * 1000));
|
||
|
};
|
||
|
const buildDate = (year, month, day, time) => {
|
||
|
const adjustedMonth = month - 1;
|
||
|
validateDayOfMonth(year, adjustedMonth, day);
|
||
|
return new Date(Date.UTC(year, adjustedMonth, day, parseDateValue(time.hours, "hour", 0, 23), parseDateValue(time.minutes, "minute", 0, 59), parseDateValue(time.seconds, "seconds", 0, 60), parseMilliseconds(time.fractionalMilliseconds)));
|
||
|
};
|
||
|
const parseTwoDigitYear = (value) => {
|
||
|
const thisYear = new Date().getUTCFullYear();
|
||
|
const valueInThisCentury = Math.floor(thisYear / 100) * 100 + strictParseShort(stripLeadingZeroes(value));
|
||
|
if (valueInThisCentury < thisYear) {
|
||
|
return valueInThisCentury + 100;
|
||
|
}
|
||
|
return valueInThisCentury;
|
||
|
};
|
||
|
const FIFTY_YEARS_IN_MILLIS = 50 * 365 * 24 * 60 * 60 * 1000;
|
||
|
const adjustRfc850Year = (input) => {
|
||
|
if (input.getTime() - new Date().getTime() > FIFTY_YEARS_IN_MILLIS) {
|
||
|
return new Date(Date.UTC(input.getUTCFullYear() - 100, input.getUTCMonth(), input.getUTCDate(), input.getUTCHours(), input.getUTCMinutes(), input.getUTCSeconds(), input.getUTCMilliseconds()));
|
||
|
}
|
||
|
return input;
|
||
|
};
|
||
|
const parseMonthByShortName = (value) => {
|
||
|
const monthIdx = MONTHS.indexOf(value);
|
||
|
if (monthIdx < 0) {
|
||
|
throw new TypeError(`Invalid month: ${value}`);
|
||
|
}
|
||
|
return monthIdx + 1;
|
||
|
};
|
||
|
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||
|
const validateDayOfMonth = (year, month, day) => {
|
||
|
let maxDays = DAYS_IN_MONTH[month];
|
||
|
if (month === 1 && isLeapYear(year)) {
|
||
|
maxDays = 29;
|
||
|
}
|
||
|
if (day > maxDays) {
|
||
|
throw new TypeError(`Invalid day for ${MONTHS[month]} in ${year}: ${day}`);
|
||
|
}
|
||
|
};
|
||
|
const isLeapYear = (year) => {
|
||
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
||
|
};
|
||
|
const parseDateValue = (value, type, lower, upper) => {
|
||
|
const dateVal = strictParseByte(stripLeadingZeroes(value));
|
||
|
if (dateVal < lower || dateVal > upper) {
|
||
|
throw new TypeError(`${type} must be between ${lower} and ${upper}, inclusive`);
|
||
|
}
|
||
|
return dateVal;
|
||
|
};
|
||
|
const parseMilliseconds = (value) => {
|
||
|
if (value === null || value === undefined) {
|
||
|
return 0;
|
||
|
}
|
||
|
return strictParseFloat32("0." + value) * 1000;
|
||
|
};
|
||
|
const parseOffsetToMilliseconds = (value) => {
|
||
|
const directionStr = value[0];
|
||
|
let direction = 1;
|
||
|
if (directionStr == "+") {
|
||
|
direction = 1;
|
||
|
}
|
||
|
else if (directionStr == "-") {
|
||
|
direction = -1;
|
||
|
}
|
||
|
else {
|
||
|
throw new TypeError(`Offset direction, ${directionStr}, must be "+" or "-"`);
|
||
|
}
|
||
|
const hour = Number(value.substring(1, 3));
|
||
|
const minute = Number(value.substring(4, 6));
|
||
|
return direction * (hour * 60 + minute) * 60 * 1000;
|
||
|
};
|
||
|
const stripLeadingZeroes = (value) => {
|
||
|
let idx = 0;
|
||
|
while (idx < value.length - 1 && value.charAt(idx) === "0") {
|
||
|
idx++;
|
||
|
}
|
||
|
if (idx === 0) {
|
||
|
return value;
|
||
|
}
|
||
|
return value.slice(idx);
|
||
|
};
|