79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
const BRUSSELS_TIME_ZONE = "Europe/Brussels";
|
|
|
|
const partsFormatter = new Intl.DateTimeFormat("en-CA", {
|
|
timeZone: BRUSSELS_TIME_ZONE,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
hourCycle: "h23",
|
|
});
|
|
|
|
function partsAt(value: Date): Record<string, string> {
|
|
return Object.fromEntries(
|
|
partsFormatter.formatToParts(value)
|
|
.filter((part) => part.type !== "literal")
|
|
.map((part) => [part.type, part.value]),
|
|
);
|
|
}
|
|
|
|
export function toBrusselsDateTimeLocal(value: Date): string {
|
|
const parts = partsAt(value);
|
|
return `${toBrusselsDate(value)}T${parts.hour}:${parts.minute}`;
|
|
}
|
|
|
|
/** Render an instant as the Fleet Ops Europe/Brussels calendar date (YYYY-MM-DD). */
|
|
export function toBrusselsDate(value: Date): string {
|
|
const parts = partsAt(value);
|
|
return `${parts.year}-${parts.month}-${parts.day}`;
|
|
}
|
|
|
|
export function brusselsDateTimeFromNow(hours: number): string {
|
|
const value = new Date(Date.now() + hours * 60 * 60 * 1000);
|
|
value.setUTCMinutes(0, 0, 0);
|
|
return toBrusselsDateTimeLocal(value);
|
|
}
|
|
|
|
/** Convert a Fleet Ops Europe/Brussels wall-clock value to an unambiguous UTC instant. */
|
|
export function brusselsLocalToIso(value: string): string {
|
|
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(value);
|
|
if (!match) throw new Error("Invalid Brussels date-time");
|
|
const [, year, month, day, hour, minute] = match;
|
|
const wallClockUtc = Date.UTC(+year, +month - 1, +day, +hour, +minute);
|
|
let candidate = wallClockUtc;
|
|
for (let iteration = 0; iteration < 3; iteration += 1) {
|
|
const parts = partsAt(new Date(candidate));
|
|
const represented = Date.UTC(+parts.year, +parts.month - 1, +parts.day, +parts.hour, +parts.minute);
|
|
candidate += wallClockUtc - represented;
|
|
}
|
|
const result = new Date(candidate);
|
|
// Date.UTC normalises impossible calendar values and Brussels' spring transition
|
|
// contains a wall-clock hour that does not exist. Never silently shift either one.
|
|
if (toBrusselsDateTimeLocal(result) !== value) {
|
|
throw new Error("Invalid or non-existent Brussels date-time");
|
|
}
|
|
return result.toISOString();
|
|
}
|
|
|
|
export function tryBrusselsLocalToIso(value: string): string | null {
|
|
try {
|
|
return brusselsLocalToIso(value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Start of the given Brussels calendar day (YYYY-MM-DD) as a UTC ISO instant. */
|
|
export function brusselsDayStartIso(day: string): string {
|
|
return brusselsLocalToIso(`${day}T00:00`);
|
|
}
|
|
|
|
/** Start of the day *after* the given Brussels calendar day, i.e. an exclusive upper bound. */
|
|
export function brusselsNextDayStartIso(day: string): string {
|
|
const [year, month, date] = day.split("-").map(Number);
|
|
const next = new Date(Date.UTC(year, month - 1, date + 1));
|
|
return brusselsLocalToIso(`${next.toISOString().slice(0, 10)}T00:00`);
|
|
}
|