UNPKG

south-african-id-validator

Version:

Validate South African ID numbers and extract date of birth, gender, and citizenship. TypeScript-native, zero dependencies, runs anywhere.

143 lines (142 loc) 4.82 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region src/luhn.ts /** * Verify that the final digit of a numeric string is the correct Luhn check * digit over the preceding digits. * * Knows nothing about South African ID numbers — it is the bare algorithm. * * @param digits A string of ASCII digits. Anything else (empty string, * non-digit characters) returns `false`. * @returns `true` when the Luhn checksum is valid, `false` otherwise. */ function isValidLuhn(digits) { if (digits.length < 2) return false; if (!/^\d+$/.test(digits)) return false; let sum = 0; let shouldDouble = false; for (let i = digits.length - 1; i >= 0; i--) { let value = digits.charCodeAt(i) - 48; if (shouldDouble) { value *= 2; if (value > 9) value -= 9; } sum += value; shouldDouble = !shouldDouble; } return sum % 10 === 0; } //#endregion //#region src/parse-citizenship.ts /** * Extract the citizenship status from a South African ID number. * * The `C` digit (index 11) is `0` for a citizen and `1` for a permanent * resident. Any other value indicates a structurally invalid ID and yields * `null`, which `validate()` maps to `INVALID_FORMAT`. */ function parseCitizenship(idNumber) { const code = idNumber.charAt(10); if (code === "0") return "citizen"; if (code === "1") return "permanent_resident"; return null; } //#endregion //#region src/parse-date-of-birth.ts const ELIGIBILITY_AGE_YEARS = 16; /** * Extract and validate the date of birth from a South African ID number. * * The first six digits encode `YYMMDD`. The century is inferred using the * 16-year eligibility rule: a year that would make the holder younger than 16 * on `referenceDate` is treated as belonging to the previous century. * * @param idNumber A 13-digit ID number. The caller is responsible for length * and character-class validation. * @param referenceDate The date used as "today" for the eligibility rule. * Inject a fixed value for deterministic tests; defaults to `new Date()`. * @returns The parsed `Date`, or `null` if the encoded date is not a real * calendar date (e.g. Feb 30, Feb 29 of a non-leap year). */ function parseDateOfBirth(idNumber, referenceDate = /* @__PURE__ */ new Date()) { const yy = Number(idNumber.slice(0, 2)); const mm = Number(idNumber.slice(2, 4)); const dd = Number(idNumber.slice(4, 6)); const refYear = referenceDate.getFullYear(); let year = Math.floor(refYear / 100) * 100 + yy; if (new Date(year, mm - 1, dd) > new Date(refYear - ELIGIBILITY_AGE_YEARS, referenceDate.getMonth(), referenceDate.getDate())) year -= 100; const dateOfBirth = new Date(year, mm - 1, dd); if (dateOfBirth.getFullYear() !== year || dateOfBirth.getMonth() !== mm - 1 || dateOfBirth.getDate() !== dd) return null; return dateOfBirth; } //#endregion //#region src/parse-gender.ts /** * Extract the gender from a South African ID number. * * The `SSSS` block (digits 7–10) is `< 5000` for female and `>= 5000` for * male. The caller is responsible for length and character-class validation. */ function parseGender(idNumber) { return Number(idNumber.slice(6, 10)) < 5e3 ? "female" : "male"; } //#endregion //#region src/validate.ts const ID_LENGTH = 13; /** * Validate a South African ID number and, on success, extract the encoded * date of birth, gender, and citizenship. * * The result is a discriminated union — narrow with `result.valid` to access * the data fields, or with `!result.valid` to access the typed `error` code. * * The pipeline short-circuits on the first failure and reports the most * fundamental problem first: length, then character class, then Luhn * checksum, then date validity, then citizenship digit. * * @example * ```ts * const result = validate('7311190013080'); * if (result.valid) { * console.log(result.dateOfBirth, result.gender, result.citizenship); * } else { * console.error(result.error); * } * ``` * * @param idNumber The 13-digit ID number to validate. * @param options Optional overrides — see {@link ValidateOptions}. */ function validate(idNumber, options = {}) { if (typeof idNumber !== "string" || idNumber.length !== ID_LENGTH) return { valid: false, error: "INVALID_LENGTH" }; if (!/^\d{13}$/.test(idNumber)) return { valid: false, error: "INVALID_FORMAT" }; if (!isValidLuhn(idNumber)) return { valid: false, error: "INVALID_CHECKSUM" }; const dateOfBirth = parseDateOfBirth(idNumber, options.referenceDate); if (dateOfBirth === null) return { valid: false, error: "INVALID_DATE" }; const citizenship = parseCitizenship(idNumber); if (citizenship === null) return { valid: false, error: "INVALID_FORMAT" }; return { valid: true, dateOfBirth, gender: parseGender(idNumber), citizenship }; } //#endregion exports.validate = validate; //# sourceMappingURL=index.cjs.map