forked from baron/baron-sso
138 lines
3.9 KiB
TypeScript
138 lines
3.9 KiB
TypeScript
export type RPClaimDateTimeValueType = "date" | "datetime";
|
|
|
|
export const FALLBACK_TIME_ZONE = "UTC";
|
|
|
|
export function getBrowserTimeZone(): string {
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || FALLBACK_TIME_ZONE;
|
|
}
|
|
|
|
export function getSupportedTimeZones(currentTimeZone = getBrowserTimeZone()) {
|
|
const supported =
|
|
typeof Intl.supportedValuesOf === "function"
|
|
? Intl.supportedValuesOf("timeZone")
|
|
: [];
|
|
return Array.from(
|
|
new Set([currentTimeZone, FALLBACK_TIME_ZONE, ...supported]),
|
|
);
|
|
}
|
|
|
|
function getTimeZoneOffsetMs(date: Date, timeZone: string) {
|
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
timeZone,
|
|
hour12: false,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
}).formatToParts(date);
|
|
const values = Object.fromEntries(
|
|
parts
|
|
.filter((part) => part.type !== "literal")
|
|
.map((part) => [part.type, part.value]),
|
|
);
|
|
const hour = values.hour === "24" ? "00" : values.hour;
|
|
const asUTC = Date.UTC(
|
|
Number(values.year),
|
|
Number(values.month) - 1,
|
|
Number(values.day),
|
|
Number(hour),
|
|
Number(values.minute),
|
|
Number(values.second),
|
|
);
|
|
return asUTC - date.getTime();
|
|
}
|
|
|
|
function zonedDateTimeToUnixSeconds(
|
|
year: number,
|
|
month: number,
|
|
day: number,
|
|
hour: number,
|
|
minute: number,
|
|
timeZone: string,
|
|
) {
|
|
const utcGuess = Date.UTC(year, month - 1, day, hour, minute, 0);
|
|
let instant = utcGuess - getTimeZoneOffsetMs(new Date(utcGuess), timeZone);
|
|
const corrected = utcGuess - getTimeZoneOffsetMs(new Date(instant), timeZone);
|
|
if (corrected !== instant) {
|
|
instant = corrected;
|
|
}
|
|
return Math.trunc(instant / 1000);
|
|
}
|
|
|
|
export function dateTimeInputToUnixSeconds(
|
|
value: string,
|
|
valueType: RPClaimDateTimeValueType,
|
|
timeZone: string,
|
|
): number | null {
|
|
const trimmed = value.trim();
|
|
const match =
|
|
valueType === "date"
|
|
? /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed)
|
|
: /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(trimmed);
|
|
if (!match) return null;
|
|
|
|
const year = Number(match[1]);
|
|
const month = Number(match[2]);
|
|
const day = Number(match[3]);
|
|
const hour = valueType === "datetime" ? Number(match[4]) : 0;
|
|
const minute = valueType === "datetime" ? Number(match[5]) : 0;
|
|
const unixSeconds = zonedDateTimeToUnixSeconds(
|
|
year,
|
|
month,
|
|
day,
|
|
hour,
|
|
minute,
|
|
timeZone || FALLBACK_TIME_ZONE,
|
|
);
|
|
return Number.isFinite(unixSeconds) ? unixSeconds : null;
|
|
}
|
|
|
|
export function unixSecondsToDateTimeInput(
|
|
value: number,
|
|
valueType: RPClaimDateTimeValueType,
|
|
timeZone: string,
|
|
) {
|
|
const date = new Date(value * 1000);
|
|
if (Number.isNaN(date.getTime())) return "";
|
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
timeZone: timeZone || FALLBACK_TIME_ZONE,
|
|
hour12: false,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
}).formatToParts(date);
|
|
const values = Object.fromEntries(
|
|
parts
|
|
.filter((part) => part.type !== "literal")
|
|
.map((part) => [part.type, part.value]),
|
|
);
|
|
const hour = values.hour === "24" ? "00" : values.hour;
|
|
const dateText = `${values.year}-${values.month}-${values.day}`;
|
|
if (valueType === "date") return dateText;
|
|
return `${dateText}T${hour}:${values.minute}`;
|
|
}
|
|
|
|
export function claimDateTimeValueToInputString(
|
|
value: unknown,
|
|
fallback: string,
|
|
valueType: RPClaimDateTimeValueType,
|
|
timeZone: string,
|
|
) {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return unixSecondsToDateTimeInput(value, valueType, timeZone);
|
|
}
|
|
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
return unixSecondsToDateTimeInput(
|
|
Number(value.trim()),
|
|
valueType,
|
|
timeZone,
|
|
);
|
|
}
|
|
const text = typeof value === "string" ? value : fallback;
|
|
return valueType === "date" ? text.slice(0, 10) : text.slice(0, 16);
|
|
}
|