← back to Nationalrealestate
src/ingest/parcels/date_guard.ts
109 lines
// Shared deed-date sanity guard for every parcel-deed ingester.
//
// Why this exists: each county feed carries occasional bad-entry dates — a
// future sale ("2027-04-01"), a mainframe near-epoch artifact ("1901-01-01"),
// or a typo'd century ("2921-06-23"). Before this util the future-date guard
// lived as a one-off inline check in arcgis_sales.ts (`> TODAY`), while
// wake_nc.ts had only a 1971 lower bound and miami_dade.ts had NO sanity guard
// at all. A bad-data run against Miami-Dade or Wake could therefore re-introduce
// impossible dates into parcel_event, exactly the class the arcgis guard was
// added to stop. This centralizes ONE tested rule so all four deed ingesters
// (arcgis_sales, miami_dade, wake_nc, sandiego_ca) reject the same defects.
//
// Contract: input is an already-format-normalized 'YYYY-MM-DD' string (or null).
// Output is that same string iff it is a plausible real recorded-deed date, else
// null. It never throws and never mutates its input — a rejected date simply
// drops the event (the parcel's last_sale_date should be nulled alongside it).
// Oldest plausible digitized US deed record. Nothing in these county feeds
// predates ~1900; anything earlier is an epoch/precision artifact, not a sale.
const MIN_DEED_YEAR = 1900;
/**
* Return `iso` iff it is a well-formed 'YYYY-MM-DD' calendar date that is not
* in the future and not older than MIN_DEED_YEAR; otherwise null.
*
* @param iso an ISO date string 'YYYY-MM-DD', or null/undefined/''
* @param today today's date as 'YYYY-MM-DD' (injectable for deterministic tests)
*/
export function sanitizeEventDate(
iso: string | null | undefined,
today: string = new Date().toISOString().slice(0, 10),
): string | null {
if (!iso) return null;
const t = String(iso).trim();
// strict shape: 4-digit year, 2-digit month, 2-digit day
const m = t.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!m) return null;
const year = Number(m[1]);
const month = Number(m[2]);
const day = Number(m[3]);
if (year < MIN_DEED_YEAR) return null;
// reject non-calendar dates (month 00/13, day 00/32, Feb 30, etc.) by
// round-tripping through Date and checking the parts survived unchanged.
const d = new Date(Date.UTC(year, month - 1, day));
if (
d.getUTCFullYear() !== year ||
d.getUTCMonth() !== month - 1 ||
d.getUTCDate() !== day
) {
return null;
}
// future-date guard: a recorded sale cannot be dated after today.
if (t > today) return null;
return t;
}
// --- self-test (run: `tsx src/ingest/parcels/date_guard.ts`) -----------------
// No test framework is installed in this repo, so the util ships its own
// deterministic self-test that exits non-zero on any failure. Runs only when
// executed directly, never on import.
const isMain = (() => {
try {
// import.meta.url is defined in ESM; process.argv[1] is the entry file.
return typeof process !== 'undefined'
&& process.argv[1] != null
&& import.meta.url === new URL(`file://${process.argv[1]}`).href;
} catch { return false; }
})();
if (isMain) {
const TODAY = '2026-07-27';
const cases: Array<[string | null | undefined, string | null, string]> = [
// [input, expected, label]
['2021-06-23', '2021-06-23', 'ordinary valid deed date'],
['2026-07-27', '2026-07-27', 'today is allowed (boundary)'],
['2026-07-28', null, 'tomorrow rejected (future by 1 day)'],
['2027-04-01', null, 'future year rejected'],
['2921-06-23', null, 'typo century (2921) rejected'],
['1899-12-31', null, 'below 1900 floor rejected'],
['1900-01-01', '1900-01-01', '1900 floor allowed (boundary)'],
['1901-01-01', '1901-01-01', 'near-epoch-looking but valid 1901'],
['2021-13-01', null, 'month 13 rejected'],
['2021-00-10', null, 'month 00 rejected'],
['2021-02-30', null, 'Feb 30 (non-calendar) rejected'],
['2021-06-31', null, 'June 31 (non-calendar) rejected'],
['2021-6-3', null, 'unpadded parts rejected (shape)'],
['20210623', null, 'compact form rejected (shape)'],
['2021-06-23T00:00:00', null, 'datetime rejected (shape)'],
['', null, 'empty string -> null'],
[null, null, 'null -> null'],
[undefined, null, 'undefined -> null'],
[' 2021-06-23 ', '2021-06-23', 'surrounding whitespace trimmed'],
];
let failed = 0;
for (const [input, expected, label] of cases) {
const got = sanitizeEventDate(input, TODAY);
const ok = got === expected;
if (!ok) {
failed++;
console.error(`FAIL: ${label}\n input=${JSON.stringify(input)} expected=${JSON.stringify(expected)} got=${JSON.stringify(got)}`);
}
}
if (failed) {
console.error(`\n${failed}/${cases.length} sanitizeEventDate cases FAILED`);
process.exit(1);
}
console.log(`sanitizeEventDate: all ${cases.length} cases passed`);
}