From d091f4a8150331d3613cfe262b2e86eb49254d41 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 28 Aug 2026 13:38:12 +0200 Subject: [PATCH] fix(shared): read the regex groups defensively in the datetime helper shared compiles with noUncheckedIndexedAccess, so a capture group is string | undefined there even when the pattern guarantees it. Pull the groups through optional access with explicit fallbacks instead of indexing into the match, which also drops the two throwaway locals. No behaviour change: same inputs, same outputs, the spec is untouched. --- shared/src/datetime/datetime-normalize.ts | 42 ++++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/shared/src/datetime/datetime-normalize.ts b/shared/src/datetime/datetime-normalize.ts index c21aaa1d8..a292cd90a 100644 --- a/shared/src/datetime/datetime-normalize.ts +++ b/shared/src/datetime/datetime-normalize.ts @@ -19,6 +19,7 @@ */ 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'. @@ -27,34 +28,40 @@ const MERIDIEM_CLOCK = /^(\d{1,2})(?::(\d{2}))?(?::\d{2})?\s*([ap])\.?\s?m\.?$/i */ export function parseMeridiemClock(value: string | null | undefined): string | null { const m = MERIDIEM_CLOCK.exec((value || '').trim()); - if (!m) return null; - let hour = Number.parseInt(m[1], 10); - const minute = m[2] ? Number.parseInt(m[2], 10) : 0; + 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 = m[3].toLowerCase() === 'p'; + + 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(); - if (!raw) return { date: null, time: null }; + const date = LEADING_DATE.exec(raw)?.[1]; + if (!date) return { date: null, time: null }; - const dateMatch = /^(\d{4}-\d{2}-\d{2})/.exec(raw); - if (!dateMatch) return { date: null, time: null }; - const date = dateMatch[1]; - - const rest = raw.slice(10).replace(/^[T\s]/, '').trim(); + const rest = clockPart(raw); if (!rest) return { date, time: null }; const meridiem = parseMeridiemClock(rest); if (meridiem) return { date, time: meridiem }; - const clock = /^(\d{2}:\d{2})/.exec(rest); - return { date, time: clock ? clock[1] : null }; + return { date, time: /^(\d{2}:\d{2})/.exec(rest)?.[1] ?? null }; } /** @@ -64,17 +71,18 @@ export function splitLocalDateTime(value: string | null | undefined): { date: st */ export function normalizeLocalDateTime(value: string): string { if (!value) return value; - const dateMatch = /^(\d{4}-\d{2}-\d{2})/.exec(value.trim()); - if (!dateMatch) 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 = value.trim().slice(10).replace(/^[T\s]/, '').trim(); + const rest = clockPart(trimmed); const clock = parseMeridiemClock(rest); if (!clock) return value; - const seconds = /^\d{1,2}:\d{2}:(\d{2})/.exec(rest); - return `${dateMatch[1]}T${clock}:${seconds ? seconds[1] : '00'}`; + const seconds = /^\d{1,2}:\d{2}:(\d{2})/.exec(rest)?.[1] ?? '00'; + return `${date}T${clock}:${seconds}`; }