// ─── parseDate (C# DateTime.ParseExact style) ─── // Parses a date string using an exact format pattern. // Supported tokens: // yyyy — 4-digit year // yy — 2-digit year (assumes 2000+) // MM — 2-digit month (01–12) // M — 1 or 2-digit month // dd — 2-digit day (01–31) // d — 1 or 2-digit day // HH — 2-digit hour 24h (00–23) // H — 1 or 2-digit hour 24h // hh — 2-digit hour 12h (01–12) // h — 1 or 2-digit hour 12h // mm — 2-digit minute (00–59) // m — 1 or 2-digit minute // ss — 2-digit second (00–59) // s — 1 or 2-digit second // fff — 3-digit milliseconds // ff — 2-digit (tenths + hundredths) // f — 1-digit (tenths) // tt — AM/PM // t — A/P // Z — literal 'Z' (UTC marker) // K — timezone offset (+HH:mm, -HH:mm, or Z) // // All other characters are matched literally. // Returns Date on success, null on failure. const TOKEN_ORDER = [ "yyyy", "yy", "MM", "M", "dd", "d", "HH", "H", "hh", "h", "mm", "m", "ss", "s", "fff", "ff", "f", "tt", "t", "K", "Z", ]; const TOKEN_PATTERNS = { yyyy: "(\\d{4})", yy: "(\\d{2})", MM: "(\\d{2})", M: "(\\d{1,2})", dd: "(\\d{2})", d: "(\\d{1,2})", HH: "(\\d{2})", H: "(\\d{1,2})", hh: "(\\d{2})", h: "(\\d{1,2})", mm: "(\\d{2})", m: "(\\d{1,2})", ss: "(\\d{2})", s: "(\\d{1,2})", fff: "(\\d{3})", ff: "(\\d{2})", f: "(\\d{1})", tt: "(AM|PM|am|pm)", t: "([AaPp])", K: "(Z|[+-]\\d{2}:\\d{2})", Z: "(Z)", }; function buildFormatRegex(format) { const groups = []; let regexStr = ""; let i = 0; while (i < format.length) { let matched = false; for (const token of TOKEN_ORDER) { if (format.startsWith(token, i)) { groups.push(token); regexStr += TOKEN_PATTERNS[token]; i += token.length; matched = true; break; } } if (!matched) { // Escape literal character regexStr += format[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); i++; } } return { regex: new RegExp("^" + regexStr + "$"), groups }; } export function parseDate(input, format) { if (input == null || format == null) return null; if (input instanceof Date) return input; const str = String(input); const { regex, groups } = buildFormatRegex(format); const match = str.match(regex); if (!match) return null; let year = 0, month = 1, day = 1; let hour = 0, minute = 0, second = 0, ms = 0; let isPM = false, isAM = false; let tzOffset = null; // minutes for (let g = 0; g < groups.length; g++) { const token = groups[g]; const val = match[g + 1]; switch (token) { case "yyyy": year = parseInt(val, 10); break; case "yy": year = 2000 + parseInt(val, 10); break; case "MM": case "M": month = parseInt(val, 10); break; case "dd": case "d": day = parseInt(val, 10); break; case "HH": case "H": hour = parseInt(val, 10); break; case "hh": case "h": hour = parseInt(val, 10); break; case "mm": case "m": minute = parseInt(val, 10); break; case "ss": case "s": second = parseInt(val, 10); break; case "fff": ms = parseInt(val, 10); break; case "ff": ms = parseInt(val, 10) * 10; break; case "f": ms = parseInt(val, 10) * 100; break; case "tt": isPM = val.toUpperCase() === "PM"; isAM = val.toUpperCase() === "AM"; break; case "t": isPM = val.toUpperCase() === "P"; isAM = val.toUpperCase() === "A"; break; case "Z": tzOffset = 0; break; case "K": if (val === "Z") { tzOffset = 0; } else { const sign = val[0] === "+" ? 1 : -1; const [h, m] = val.substring(1).split(":").map(Number); tzOffset = sign * (h * 60 + m); } break; } } // 12h → 24h conversion if (isPM && hour < 12) hour += 12; if (isAM && hour === 12) hour = 0; if (tzOffset !== null) { // Build UTC date, apply offset const utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms); return new Date(utcMs - tzOffset * 60000); } return new Date(year, month - 1, day, hour, minute, second, ms); } // ─── Input transform presets ─── export const IntInput = { transform: parseInt }; export const FloatInput = { transform: parseFloat }; export const BoolInput = { transform: (v) => { if (typeof v === "boolean") return v; if (typeof v === "string") { if (v.toLowerCase() === "true") return true; if (v.toLowerCase() === "false") return false; } return Boolean(v); }, }; export function DateInput(format) { return { transform: (v) => parseDate(v, format), }; } export const IsoDateInput = DateInput("yyyy-MM-ddTHH:mm:ssK");