/** * Composable function for time field logic */ export function useTimeField() { function parseTimeString(str: string) { if (!str) return { hours: '', minutes: '', amPm: '' } const fullMatch = str.match(/^(\d{2}):?(\d{2})?(?:\s*(AM|PM))?$/) const minutesAmPmMatch = str.match(/^:(\d{2})\s*(AM|PM)$/) const hoursMatch = str.match(/^\d{2}$/) const minutesMatch = str.match(/^:(\d{2})$/) const ampmMatch = str.match(/^(AM|PM)$/) if (fullMatch) { return { hours: fullMatch[1] || '', minutes: fullMatch[2] || '', amPm: fullMatch[3] || '', } } if (minutesAmPmMatch) { return { hours: '', minutes: minutesAmPmMatch[1], amPm: minutesAmPmMatch[2], } } if (hoursMatch) { return { hours: str, minutes: '', amPm: '', } } if (minutesMatch) { return { hours: '', minutes: minutesMatch[1], amPm: '', } } if (ampmMatch) { return { hours: '', minutes: '', amPm: ampmMatch[1], } } return { hours: '', minutes: '', amPm: '' } } /** * Parse time string to total minutes since midnight * Handles both 24h and 12h (AM/PM) formats * Returns null if time string is malformed * * @param timeStr - Time string in format "HH:MM" or "HH:MM AM/PM" * @returns Total minutes since midnight, or null if parsing fails */ function parseTimeToMinutes(timeStr: string): number | null { if (!timeStr || typeof timeStr !== 'string') { return null } let hours = 0 let minutes = 0 // Handle AM/PM format const ampmMatch = timeStr.trim().match(/^(\d{1,2}):(\d{2})\s?(AM|PM|am|pm)$/i) if (ampmMatch) { hours = parseInt(ampmMatch[1], 10) minutes = parseInt(ampmMatch[2], 10) const meridiem = ampmMatch[3].toUpperCase() // Validate hour range for 12h format if (hours < 1 || hours > 12) { return null } // Convert to 24h format if (meridiem === 'PM' && hours !== 12) { hours += 12 } else if (meridiem === 'AM' && hours === 12) { hours = 0 } } else { // Handle 24h format const match24h = timeStr.trim().match(/^(\d{1,2}):(\d{2})$/) if (match24h) { hours = parseInt(match24h[1], 10) minutes = parseInt(match24h[2], 10) } else { return null // Malformed time string } } // Validate hour and minute ranges if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { return null } return hours * 60 + minutes } /** * Validate time format based on time format and field type * Returns true if time matches the expected format */ function isValidTimeFormat(timeStr: string, timeFormat: string, timeFieldType?: string): boolean { if (!timeStr || typeof timeStr !== 'string') { return false } const trimmed = timeStr.trim() // Dropdown mode already ensures format, so only validate non-dropdown if (timeFieldType === 'dropdown') { if (timeFormat === 'ampm') { return /^(0?[1-9]|1[0-2]):[0-5]\d ?([AP]M|[ap]m)$/.test(trimmed) } else { return /^(?:[01]?\d|2[0-3]):[0-5]\d$/.test(trimmed) } } // Time-picker mode if (timeFieldType === 'time-picker') { return /^(?:[01]\d|2[0-3]):[0-5]\d(?:\s?(?:AM|PM|am|pm))?$/.test(trimmed) } // Input mode if (timeFormat === 'ampm') { return /^(0?[1-9]|1[0-2]):[0-5]\d (AM|PM|am|pm)$/.test(trimmed) } else { return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(trimmed) } } function isTimeInValidRange( timeStr: string, minTimeValue?: string | null, maxTimeValue?: string | null, ): { isInRange: boolean; error?: string } { // Empty or partial values are allowed if (!timeStr || timeStr === '__PARTIAL__') { return { isInRange: true } } const currentTimeMinutes = parseTimeToMinutes(timeStr) if (currentTimeMinutes === null) { return { isInRange: false, error: 'Invalid time format' } } const hasMin = minTimeValue !== null && minTimeValue !== undefined && minTimeValue !== '' const hasMax = maxTimeValue !== null && maxTimeValue !== undefined && maxTimeValue !== '' if (!hasMin && !hasMax) { return { isInRange: true } } // Parse min time let minTimeMinutes: number | null = null if (hasMin) { minTimeMinutes = parseTimeToMinutes(minTimeValue!) if (minTimeMinutes === null) { return { isInRange: false, error: `Malformed minimum time value: ${minTimeValue}` } } } // Parse max time let maxTimeMinutes: number | null = null if (hasMax) { maxTimeMinutes = parseTimeToMinutes(maxTimeValue!) if (maxTimeMinutes === null) { return { isInRange: false, error: `Malformed maximum time value: ${maxTimeValue}` } } } // Check range if (minTimeMinutes !== null && currentTimeMinutes < minTimeMinutes) { return { isInRange: false, error: `Time must be at or after ${minTimeValue}` } } if (maxTimeMinutes !== null && currentTimeMinutes > maxTimeMinutes) { return { isInRange: false, error: `Time must be at or before ${maxTimeValue}` } } return { isInRange: true } } function formatTimeString(hours: string, minutes: string, amPm: string) { if (!hours && !minutes && amPm) return amPm if (!hours && minutes && amPm) return `:${minutes} ${amPm}` if (hours && minutes) return amPm ? `${hours}:${minutes} ${amPm}` : `${hours}:${minutes}` if (hours) return hours if (minutes) return `:${minutes}` return '' } function getAmPmFromHours(hours: string): 'AM' | 'PM' { const h = parseInt(hours, 10) return h >= 12 ? 'PM' : 'AM' } function to24hFormat(time: string): string { if (!time) return '' const match = time.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i) if (!match) return time let hours = parseInt(match[1], 10) const minutes = match[2] const ampm = match[3]?.toUpperCase() if (ampm === 'PM' && hours < 12) hours += 12 if (ampm === 'AM' && hours === 12) hours = 0 return `${hours.toString().padStart(2, '0')}:${minutes}` } return { parseTimeString, parseTimeToMinutes, isValidTimeFormat, isTimeInValidRange, formatTimeString, getAmPmFromHours, to24hFormat, } }