mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
/**
|
|
* Twelve-hour clock handling for imported booking documents (#2094).
|
|
*
|
|
* A confirmation mail prints "01:11 pm". Every downstream consumer in TREK
|
|
* expects a naive 24-hour local string, and the mapper used to reach it by
|
|
* slicing: `'2026-06-11T02:30 PM'.slice(11, 16)` yields '02:30'. The hour
|
|
* survives, the half of the day does not, and the booking silently lands
|
|
* twelve hours early. Because the arrival then sorts before the departure, the
|
|
* router also rolls it onto the next day, so a three-hour domestic flight is
|
|
* rendered as an overnight.
|
|
*
|
|
* The client has carried the same fix since #1725 (client/src/utils/formatters.ts);
|
|
* this is the server half, which was never pulled across. The regex here is
|
|
* deliberately looser than the client's: the server also sees 'HH:MM:SS AM' and
|
|
* 'a.m.' straight out of a PDF.
|
|
*
|
|
* Everything here is total: a value that cannot be read comes back unchanged
|
|
* rather than becoming null, so no format that works today can start failing.
|
|
*/
|
|
|
|
const MERIDIEM_CLOCK = /^(\d{1,2})(?::(\d{2}))?(?::\d{2})?\s*([ap])\.?\s?m\.?$/i;
|
|
const LEADING_DATE = /^(\d{4}-\d{2}-\d{2})/;
|
|
|
|
/**
|
|
* '3:00 PM' / '02:30:00 pm' / '3 PM' → '15:00'.
|
|
* Returns null when there is no meridiem to resolve, so callers can tell
|
|
* "not my format" from "midnight".
|
|
*/
|
|
export function parseMeridiemClock(value: string | null | undefined): string | null {
|
|
const m = MERIDIEM_CLOCK.exec((value || '').trim());
|
|
const rawHour = m?.[1];
|
|
const half = m?.[3];
|
|
if (!rawHour || !half) return null;
|
|
|
|
let hour = Number.parseInt(rawHour, 10);
|
|
const minute = Number.parseInt(m?.[2] ?? '0', 10);
|
|
if (!Number.isFinite(hour) || hour > 12) return null;
|
|
|
|
const isPm = half.toLowerCase() === 'p';
|
|
// 12 AM is midnight and 12 PM is noon: the hour does not shift at 12.
|
|
if (hour === 12) hour = isPm ? 12 : 0;
|
|
else if (isPm) hour += 12;
|
|
|
|
return `${String(hour).padStart(2, '0')}:${String(Math.min(59, minute)).padStart(2, '0')}`;
|
|
}
|
|
|
|
/** The part after a leading `YYYY-MM-DD`, with its `T` or space removed. */
|
|
function clockPart(value: string): string {
|
|
return value
|
|
.slice(10)
|
|
.replace(/^[T\s]/, '')
|
|
.trim();
|
|
}
|
|
|
|
/** Split a naive date-time string into its date and its 24-hour clock. */
|
|
export function splitLocalDateTime(value: string | null | undefined): { date: string | null; time: string | null } {
|
|
const raw = (value || '').trim();
|
|
const date = LEADING_DATE.exec(raw)?.[1];
|
|
if (!date) return { date: null, time: null };
|
|
|
|
const rest = clockPart(raw);
|
|
if (!rest) return { date, time: null };
|
|
|
|
const meridiem = parseMeridiemClock(rest);
|
|
if (meridiem) return { date, time: meridiem };
|
|
|
|
return { date, time: /^(\d{2}:\d{2})/.exec(rest)?.[1] ?? null };
|
|
}
|
|
|
|
/**
|
|
* Return the same naive date-time with a 24-hour clock. Anything the parser
|
|
* cannot read comes back byte for byte, which is what keeps this additive:
|
|
* a value that is already correct is never rewritten.
|
|
*/
|
|
export function normalizeLocalDateTime(value: string): string {
|
|
if (!value) return value;
|
|
const trimmed = value.trim();
|
|
const date = LEADING_DATE.exec(trimmed)?.[1];
|
|
if (!date) return value;
|
|
|
|
// Only a printed 12-hour clock is rewritten. Everything else comes back
|
|
// untouched, which is what makes this safe to drop into the middle of the
|
|
// mapper: a value carrying a UTC offset, a date-only value, or a shape this
|
|
// does not know keeps whatever it meant before.
|
|
const rest = clockPart(trimmed);
|
|
const clock = parseMeridiemClock(rest);
|
|
if (!clock) return value;
|
|
|
|
const seconds = /^\d{1,2}:\d{2}:(\d{2})/.exec(rest)?.[1] ?? '00';
|
|
return `${date}T${clock}:${seconds}`;
|
|
}
|