const DATE_ONLY = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/; /** A FHIR date or dateTime holding only a year, a month or a day: a calendar date, never an instant. */ export const isFhirDateOnly = (value: unknown): value is string => typeof value === 'string' && DATE_ONLY.test(value.trim()); /** * A FHIR date-time for storage. A typed day stays a day: converting it to an instant would store UTC * midnight, which every reader west of Greenwich shows as the evening before. */ export const toFhirDateTime = (dateTime: string | Date): string => { if (!dateTime) return ''; if (typeof dateTime === 'string' && DATE_ONLY.test(dateTime.trim())) return dateTime.trim(); return new Date(dateTime).toISOString(); }; /** * A FHIR instant for storage, as `issued` requires: a time and a zone, never a bare day. An * unreadable value is dropped rather than sent, because the server refuses the whole resource for it. */ export const toFhirInstant = (value: string | Date): string => { if (!value) return ''; const at = new Date(value); return Number.isNaN(at.getTime()) ? '' : at.toISOString(); }; /** * A FHIR date-time for display. A date with no time is a calendar day, not UTC midnight: read as an * instant it would show the evening before anywhere west of Greenwich. */ export const formatFhirDateTimeForDisplay = (fhirDateTime: string): string => { if (!fhirDateTime) return ''; const dateOnly = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(fhirDateTime); if (dateOnly) { const [, year, month, day] = dateOnly; const at = new Date(Date.UTC(Number(year), month ? Number(month) - 1 : 0, day ? Number(day) : 1)); return new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', year: 'numeric', ...(month ? { month: 'short' as const } : {}), ...(day ? { day: 'numeric' as const } : {}), }).format(at); } return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short', }).format(new Date(fhirDateTime)); };