import { addDays, addMonths, addWeeks, addYears, isValid, parse, setHours, setMinutes, startOfDay, } from "date-fns"; import type { ParsedDateTime } from "./types"; const RELATIVE_PATTERNS = [ { pattern: /^today$/i, fn: () => startOfDay(new Date()) }, { pattern: /^tomorrow$/i, fn: () => startOfDay(addDays(new Date(), 1)) }, { pattern: /^yesterday$/i, fn: () => startOfDay(addDays(new Date(), -1)) }, { pattern: /^in (\d+) days?$/i, fn: (match: RegExpMatchArray) => startOfDay(addDays(new Date(), parseInt(match[1]))), }, { pattern: /^in (\d+) weeks?$/i, fn: (match: RegExpMatchArray) => startOfDay(addWeeks(new Date(), parseInt(match[1]))), }, { pattern: /^in (\d+) months?$/i, fn: (match: RegExpMatchArray) => startOfDay(addMonths(new Date(), parseInt(match[1]))), }, { pattern: /^in (\d+) years?$/i, fn: (match: RegExpMatchArray) => startOfDay(addYears(new Date(), parseInt(match[1]))), }, { pattern: /^(\d+) days? ago$/i, fn: (match: RegExpMatchArray) => startOfDay(addDays(new Date(), -parseInt(match[1]))), }, { pattern: /^(\d+) weeks? ago$/i, fn: (match: RegExpMatchArray) => startOfDay(addWeeks(new Date(), -parseInt(match[1]))), }, { pattern: /^(\d+) months? ago$/i, fn: (match: RegExpMatchArray) => startOfDay(addMonths(new Date(), -parseInt(match[1]))), }, { pattern: /^next week$/i, fn: () => startOfDay(addWeeks(new Date(), 1)) }, { pattern: /^next month$/i, fn: () => startOfDay(addMonths(new Date(), 1)) }, { pattern: /^last week$/i, fn: () => startOfDay(addWeeks(new Date(), -1)) }, { pattern: /^last month$/i, fn: () => startOfDay(addMonths(new Date(), -1)) }, ]; const TIME_PATTERNS = [ { pattern: /^(\d{1,2}):(\d{2})\s*(am|pm)?$/i }, { pattern: /^(\d{1,2})\s*(am|pm)$/i }, ]; const DATE_FORMATS = [ "yyyy-MM-dd", "MM/dd/yyyy", "dd/MM/yyyy", "MMM dd, yyyy", "MMMM dd, yyyy", "dd MMM yyyy", "dd MMMM yyyy", "yyyy/MM/dd", ]; function parseTime(input: string): { hours: number; minutes: number } | null { for (const { pattern } of TIME_PATTERNS) { const match = input.match(pattern); if (match) { let hours = parseInt(match[1]); const minutes = match[2] ? parseInt(match[2]) : 0; const meridiem = match[3]?.toLowerCase(); if (meridiem === "pm" && hours < 12) { hours += 12; } else if (meridiem === "am" && hours === 12) { hours = 0; } if (hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) { return { hours, minutes }; } } } return null; } export function parseNaturalLanguageDate(input: string): ParsedDateTime | null { const trimmedInput = input.trim(); if (!trimmedInput) return null; // Try relative patterns first (high confidence) for (const { pattern, fn } of RELATIVE_PATTERNS) { const match = trimmedInput.match(pattern); if (match) { try { const date = fn(match); return { date, confidence: "high", originalInput: input, }; } catch {} } } // Try parsing time with "at" prefix (e.g., "at 3pm", "at 15:30") const atTimeMatch = trimmedInput.match(/^at\s+(.+)$/i); if (atTimeMatch) { const timeResult = parseTime(atTimeMatch[1]); if (timeResult) { const date = setMinutes( setHours(new Date(), timeResult.hours), timeResult.minutes, ); return { date, confidence: "high", originalInput: input, }; } } // Try parsing standalone time const timeResult = parseTime(trimmedInput); if (timeResult) { const date = setMinutes( setHours(new Date(), timeResult.hours), timeResult.minutes, ); return { date, confidence: "medium", originalInput: input, }; } // Try parsing date + time (e.g., "tomorrow at 3pm", "2024-12-25 at 14:30") const dateTimeMatch = trimmedInput.match(/^(.+?)\s+at\s+(.+)$/i); if (dateTimeMatch) { const datePart = dateTimeMatch[1]; const timePart = dateTimeMatch[2]; const dateResult = parseNaturalLanguageDate(datePart); const timeResult = parseTime(timePart); if (dateResult && timeResult) { const date = setMinutes( setHours(dateResult.date, timeResult.hours), timeResult.minutes, ); return { date, confidence: dateResult.confidence, originalInput: input, }; } } // Try standard date formats (medium confidence) for (const format of DATE_FORMATS) { try { const date = parse(trimmedInput, format, new Date()); if (isValid(date)) { return { date: startOfDay(date), confidence: "medium", originalInput: input, }; } } catch {} } // Try ISO format (high confidence) try { const date = new Date(trimmedInput); if (isValid(date) && !isNaN(date.getTime())) { return { date, confidence: "high", originalInput: input, }; } } catch { // Continue to next attempt } return null; }