1
0
forked from baron/baron-sso
Files
baron-sso/devfront/src/features/clients/rpClaimDateTime.ts

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);
}