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
This commit is contained in:
Randall Stillwell 2026-04-08 23:14:46 -05:00
parent daec81884c
commit ec51c049c3

View file

@ -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<string, unknown>).date) {
const inner = String((val as Record<string, unknown>).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<string, string> = {
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(