{"record":{"id":"1fcbb1234c468bb9","repo":"jackwener/OpenCLI","slug":"name-is-not-a-real-calendar-date-value","errorCode":null,"errorMessage":"--${name} is not a real calendar date: ${value}","messagePattern":"--(.+?) is not a real calendar date: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/ctrip/utils.js","lineNumber":226,"sourceCode":"export function parseIsoDate(name, raw) {\n    if (raw === undefined || raw === null || raw === '' || String(raw).trim() === '') {\n        throw new ArgumentError(`--${name} is required (YYYY-MM-DD)`);\n    }\n    const value = String(raw);\n    const m = ISO_DATE_RE.exec(value);\n    if (!m) {\n        throw new ArgumentError(`--${name} must be YYYY-MM-DD, got ${JSON.stringify(raw)}`);\n    }\n    const year = Number(m[1]);\n    const month = Number(m[2]);\n    const day = Number(m[3]);\n    if (month < 1 || month > 12 || day < 1 || day > 31) {\n        throw new ArgumentError(`--${name} has invalid month/day: ${value}`);\n    }\n    // Cross-check via UTC date math so 2026-02-30 doesn't pass.\n    const parsed = new Date(Date.UTC(year, month - 1, day));\n    if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {\n        throw new ArgumentError(`--${name} is not a real calendar date: ${value}`);\n    }\n    return value;\n}\n\n/**\n * Validate a 3-letter IATA airport / metro code, return uppercase.\n * Ctrip URL accepts both single-airport (PEK / PVG) and metro-group (BJS / SHA) codes.\n */\nexport function parseIataCode(name, raw) {\n    if (raw === undefined || raw === null || raw === '') {\n        throw new ArgumentError(`--${name} is required (3-letter IATA code, e.g. PEK, SHA)`);\n    }\n    const value = String(raw).trim().toUpperCase();\n    if (!/^[A-Z]{3}$/.test(value)) {\n        throw new ArgumentError(`--${name} must be a 3-letter IATA code, got ${JSON.stringify(raw)}`);\n    }\n    return value;\n}","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/ctrip/utils.js#L208-L244","documentation":"parseIsoDate performs a final UTC round-trip check: it constructs Date.UTC(year, month-1, day) and verifies the components round-trip exactly. This catches pattern-valid, range-valid but nonexistent calendar dates such as 2026-02-30 or 2025-02-29 (non-leap year), which would otherwise silently roll over to March.","triggerScenarios":"Calling parseIsoDate(name, raw) with impossible dates like 2026-02-30, 2025-02-29, 2026-04-31, or 2026-06-31 — regex and range checks pass but Date.UTC normalization shifts the date.","commonSituations":"Hand-typed end-of-month dates (31st typed for a 30-day month); leap-year assumption errors; naive date arithmetic that overflowed a month boundary.","solutions":["Pick a date that exists in the calendar (2026-02 has 28 days; 2025 is not a leap year)","Use a date library (e.g. Temporal.PlainDate or dayjs with strict parsing) to validate before calling","Compute dates via Date arithmetic rather than hand-assembling strings","Clamp day to the last day of the month when generating dates"],"exampleFix":"// before\n--checkin 2026-02-30\n// after\n--checkin 2026-02-28","handlingStrategy":"validation","validationCode":"function isRealDate(y, m, d) {\n  const dt = new Date(Date.UTC(y, m - 1, d));\n  return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;\n}\nif (!isRealDate(2026, 2, 30)) throw new Error('not a real calendar date');","typeGuard":"function isRealIsoDate(v) {\n  if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(v)) return false;\n  const [y, m, d] = v.split('-').map(Number);\n  const dt = new Date(Date.UTC(y, m - 1, d));\n  return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;\n}","tryCatchPattern":"try {\n  const date = parseIsoDate('checkout', raw);\n} catch (err) {\n  if (err instanceof ArgumentError && /not a real calendar date/.test(err.message)) {\n    // clamp to last valid day of the month\n  }\n  throw err;\n}","preventionTips":["Verify leap years before using Feb 29 (2025 no, 2028 yes)","Clamp day to the month's last day when computing dates","Use strict date parsing (Temporal.PlainDate, dayjs strict mode)","Never hand-assemble end-of-month dates"],"tags":["argument-validation","date-format","calendar"],"backgroundTag":"invalid-calendar-date","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}