From ec51c049c3d25de59cbc0bb52ca16d6ee42ac41e Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 8 Apr 2026 23:14:46 -0500 Subject: [PATCH] Fix Monday.com date parsing and normalize dropdown labels - Rewrite parseToISO to handle many OCR date formats: "july 20, 02", "20/May/1999", "08/JUN/26", "Oct / 02 / 2004", "Feb 19, 2016", etc. - Return null instead of raw string when date parsing fails, preventing Monday.com ColumnValueException for unparseable dates - Normalize long nextStep text from pre-update OCR runs (e.g. "Expressing my faith in Jesus..." -> "Baptism") before sending to Monday.com dropdowns Made-with: Cursor --- src/lib/monday.ts | 100 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 91 insertions(+), 9 deletions(-) diff --git a/src/lib/monday.ts b/src/lib/monday.ts index facebe1..b0d7d54 100644 --- a/src/lib/monday.ts +++ b/src/lib/monday.ts @@ -155,6 +155,20 @@ function toCleanString(val: unknown): string | null { return str; } +const DROPDOWN_NORMALIZATIONS: [RegExp, string][] = [ + [/expressing my faith in jesus/i, "Baptism"], + [/i want to be baptized/i, "Baptism"], + [/learning more about becoming a partner/i, "Next Steps"], + [/attend next steps/i, "Next Steps"], +]; + +function normalizeDropdownLabel(label: string): string { + for (const [pattern, replacement] of DROPDOWN_NORMALIZATIONS) { + if (pattern.test(label)) return replacement; + } + return label; +} + function formatForColumnType(colType: string, val: unknown): unknown | null { switch (colType) { case "boolean": @@ -172,15 +186,20 @@ function formatForColumnType(colType: string, val: unknown): unknown | null { case "dropdown": { const str = toCleanString(val); if (!str) return null; - const labels = str.split(",").map((s) => s.trim()).filter(Boolean); + const labels = str.split(",").map((s) => normalizeDropdownLabel(s.trim())).filter(Boolean); return labels.length > 0 ? { labels } : null; } case "date": { const str = toCleanString(val); if (!str) return null; + if (typeof val === "object" && !Array.isArray(val) && (val as Record).date) { + const inner = String((val as Record).date); + const iso = parseToISO(inner); + return iso ? { date: iso } : null; + } const iso = parseToISO(str); - return iso ? { date: iso } : str; + return iso ? { date: iso } : null; } case "numeric": @@ -212,14 +231,77 @@ function formatForColumnType(colType: string, val: unknown): unknown | null { } } +const MONTH_MAP: Record = { + jan: "01", january: "01", feb: "02", february: "02", + mar: "03", march: "03", apr: "04", april: "04", + may: "05", jun: "06", june: "06", jul: "07", july: "07", + aug: "08", august: "08", sep: "09", september: "09", + oct: "10", october: "10", nov: "11", november: "11", + dec: "12", december: "12", +}; + +function normalizeYear(y: string): string { + if (y.length === 4) return y; + const n = parseInt(y); + return n > 50 ? `19${y.padStart(2, "0")}` : `20${y.padStart(2, "0")}`; +} + function parseToISO(dateStr: string): string | null { - const parts = dateStr.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{2,4})$/); - if (!parts) return null; - let [, m, d, y] = parts; - if (y.length === 2) y = parseInt(y) > 50 ? `19${y}` : `20${y}`; - const month = m.padStart(2, "0"); - const day = d.padStart(2, "0"); - return `${y}-${month}-${day}`; + const s = dateStr.replace(/\s+/g, " ").trim(); + + // MM/DD/YYYY or DD/MM/YYYY (all-numeric) + const numParts = s.match(/^(\d{1,2})\s*[/\-.]\s*(\d{1,2})\s*[/\-.]\s*(\d{2,4})$/); + if (numParts) { + let [, a, b, y] = numParts; + y = normalizeYear(y); + const aNum = parseInt(a), bNum = parseInt(b); + let month: string, day: string; + if (aNum > 12 && bNum <= 12) { + day = a; month = b; + } else { + month = a; day = b; + } + return `${y}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`; + } + + // DD/Mon/YYYY e.g. "20/May/1999", "08/JUN/26" + const dmyNamed = s.match(/^(\d{1,2})\s*[/\-.]\s*([A-Za-z]+)\s*[/\-.]\s*(\d{2,4})$/); + if (dmyNamed) { + const [, d, mName, y] = dmyNamed; + const month = MONTH_MAP[mName.toLowerCase()]; + if (month) return `${normalizeYear(y)}-${month}-${d.padStart(2, "0")}`; + } + + // "Month DD, YY(YY)" e.g. "july 20, 02", "Feb 19, 2016", "October 5, 1990" + const mdyNamed = s.match(/^([A-Za-z]+)\s+(\d{1,2}),?\s*(\d{2,4})$/); + if (mdyNamed) { + const [, mName, d, y] = mdyNamed; + const month = MONTH_MAP[mName.toLowerCase()]; + if (month) return `${normalizeYear(y)}-${month}-${d.padStart(2, "0")}`; + } + + // "Mon / DD / YYYY" with spaces e.g. "Oct / 02 / 2004" + const spacedNamed = s.match(/^([A-Za-z]+)\s*[/\-.]\s*(\d{1,2})\s*[/\-.]\s*(\d{2,4})$/); + if (spacedNamed) { + const [, mName, d, y] = spacedNamed; + const month = MONTH_MAP[mName.toLowerCase()]; + if (month) return `${normalizeYear(y)}-${month}-${d.padStart(2, "0")}`; + } + + // YYYY-MM-DD already ISO + const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (iso) return s; + + // Last resort: try native Date parsing + const d = new Date(s); + if (!isNaN(d.getTime()) && d.getFullYear() > 1900) { + const yyyy = String(d.getFullYear()); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; + } + + return null; } export function mapCardToColumnValues(