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.

1 lines 7.38 kB
{"version":3,"file":"index.mjs","names":[],"sources":["../src/luhn.ts","../src/parse-citizenship.ts","../src/parse-date-of-birth.ts","../src/parse-gender.ts","../src/validate.ts"],"sourcesContent":["/**\n * Verify that the final digit of a numeric string is the correct Luhn check\n * digit over the preceding digits.\n *\n * Knows nothing about South African ID numbers — it is the bare algorithm.\n *\n * @param digits A string of ASCII digits. Anything else (empty string,\n * non-digit characters) returns `false`.\n * @returns `true` when the Luhn checksum is valid, `false` otherwise.\n */\nexport function isValidLuhn(digits: string): boolean {\n if (digits.length < 2) return false;\n if (!/^\\d+$/.test(digits)) return false;\n\n let sum = 0;\n let shouldDouble = false;\n\n for (let i = digits.length - 1; i >= 0; i--) {\n let value = digits.charCodeAt(i) - 48;\n if (shouldDouble) {\n value *= 2;\n if (value > 9) value -= 9;\n }\n sum += value;\n shouldDouble = !shouldDouble;\n }\n\n return sum % 10 === 0;\n}\n","import type { Citizenship } from \"./types.js\";\n\n/**\n * Extract the citizenship status from a South African ID number.\n *\n * The `C` digit (index 11) is `0` for a citizen and `1` for a permanent\n * resident. Any other value indicates a structurally invalid ID and yields\n * `null`, which `validate()` maps to `INVALID_FORMAT`.\n */\nexport function parseCitizenship(idNumber: string): Citizenship | null {\n const code = idNumber.charAt(10);\n if (code === \"0\") return \"citizen\";\n if (code === \"1\") return \"permanent_resident\";\n return null;\n}\n","const ELIGIBILITY_AGE_YEARS = 16;\n\n/**\n * Extract and validate the date of birth from a South African ID number.\n *\n * The first six digits encode `YYMMDD`. The century is inferred using the\n * 16-year eligibility rule: a year that would make the holder younger than 16\n * on `referenceDate` is treated as belonging to the previous century.\n *\n * @param idNumber A 13-digit ID number. The caller is responsible for length\n * and character-class validation.\n * @param referenceDate The date used as \"today\" for the eligibility rule.\n * Inject a fixed value for deterministic tests; defaults to `new Date()`.\n * @returns The parsed `Date`, or `null` if the encoded date is not a real\n * calendar date (e.g. Feb 30, Feb 29 of a non-leap year).\n */\nexport function parseDateOfBirth(idNumber: string, referenceDate: Date = new Date()): Date | null {\n const yy = Number(idNumber.slice(0, 2));\n const mm = Number(idNumber.slice(2, 4));\n const dd = Number(idNumber.slice(4, 6));\n\n const refYear = referenceDate.getFullYear();\n const currentCentury = Math.floor(refYear / 100) * 100;\n let year = currentCentury + yy;\n\n const candidate = new Date(year, mm - 1, dd);\n const eligibilityCutoff = new Date(\n refYear - ELIGIBILITY_AGE_YEARS,\n referenceDate.getMonth(),\n referenceDate.getDate(),\n );\n\n if (candidate > eligibilityCutoff) {\n year -= 100;\n }\n\n const dateOfBirth = new Date(year, mm - 1, dd);\n\n if (\n dateOfBirth.getFullYear() !== year ||\n dateOfBirth.getMonth() !== mm - 1 ||\n dateOfBirth.getDate() !== dd\n ) {\n return null;\n }\n\n return dateOfBirth;\n}\n","import type { Gender } from \"./types.js\";\n\n/**\n * Extract the gender from a South African ID number.\n *\n * The `SSSS` block (digits 7–10) is `< 5000` for female and `>= 5000` for\n * male. The caller is responsible for length and character-class validation.\n */\nexport function parseGender(idNumber: string): Gender {\n return Number(idNumber.slice(6, 10)) < 5000 ? \"female\" : \"male\";\n}\n","import { isValidLuhn } from \"./luhn.js\";\nimport { parseCitizenship } from \"./parse-citizenship.js\";\nimport { parseDateOfBirth } from \"./parse-date-of-birth.js\";\nimport { parseGender } from \"./parse-gender.js\";\nimport type { ValidateOptions, ValidationResult } from \"./types.js\";\n\nconst ID_LENGTH = 13;\n\n/**\n * Validate a South African ID number and, on success, extract the encoded\n * date of birth, gender, and citizenship.\n *\n * The result is a discriminated union — narrow with `result.valid` to access\n * the data fields, or with `!result.valid` to access the typed `error` code.\n *\n * The pipeline short-circuits on the first failure and reports the most\n * fundamental problem first: length, then character class, then Luhn\n * checksum, then date validity, then citizenship digit.\n *\n * @example\n * ```ts\n * const result = validate('7311190013080');\n * if (result.valid) {\n * console.log(result.dateOfBirth, result.gender, result.citizenship);\n * } else {\n * console.error(result.error);\n * }\n * ```\n *\n * @param idNumber The 13-digit ID number to validate.\n * @param options Optional overrides — see {@link ValidateOptions}.\n */\nexport function validate(idNumber: string, options: ValidateOptions = {}): ValidationResult {\n if (typeof idNumber !== \"string\" || idNumber.length !== ID_LENGTH) {\n return { valid: false, error: \"INVALID_LENGTH\" };\n }\n\n if (!/^\\d{13}$/.test(idNumber)) {\n return { valid: false, error: \"INVALID_FORMAT\" };\n }\n\n if (!isValidLuhn(idNumber)) {\n return { valid: false, error: \"INVALID_CHECKSUM\" };\n }\n\n const dateOfBirth = parseDateOfBirth(idNumber, options.referenceDate);\n if (dateOfBirth === null) {\n return { valid: false, error: \"INVALID_DATE\" };\n }\n\n const citizenship = parseCitizenship(idNumber);\n if (citizenship === null) {\n return { valid: false, error: \"INVALID_FORMAT\" };\n }\n\n const gender = parseGender(idNumber);\n\n return { valid: true, dateOfBirth, gender, citizenship };\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,YAAY,QAAyB;CACnD,IAAI,OAAO,SAAS,GAAG,OAAO;CAC9B,IAAI,CAAC,QAAQ,KAAK,MAAM,GAAG,OAAO;CAElC,IAAI,MAAM;CACV,IAAI,eAAe;CAEnB,KAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;EAC3C,IAAI,QAAQ,OAAO,WAAW,CAAC,IAAI;EACnC,IAAI,cAAc;GAChB,SAAS;GACT,IAAI,QAAQ,GAAG,SAAS;EAC1B;EACA,OAAO;EACP,eAAe,CAAC;CAClB;CAEA,OAAO,MAAM,OAAO;AACtB;;;;;;;;;;ACnBA,SAAgB,iBAAiB,UAAsC;CACrE,MAAM,OAAO,SAAS,OAAO,EAAE;CAC/B,IAAI,SAAS,KAAK,OAAO;CACzB,IAAI,SAAS,KAAK,OAAO;CACzB,OAAO;AACT;;;ACdA,MAAM,wBAAwB;;;;;;;;;;;;;;;AAgB9B,SAAgB,iBAAiB,UAAkB,gCAAsB,IAAI,KAAK,GAAgB;CAChG,MAAM,KAAK,OAAO,SAAS,MAAM,GAAG,CAAC,CAAC;CACtC,MAAM,KAAK,OAAO,SAAS,MAAM,GAAG,CAAC,CAAC;CACtC,MAAM,KAAK,OAAO,SAAS,MAAM,GAAG,CAAC,CAAC;CAEtC,MAAM,UAAU,cAAc,YAAY;CAE1C,IAAI,OADmB,KAAK,MAAM,UAAU,GAAG,IAAI,MACvB;CAS5B,IAAI,IAPkB,KAAK,MAAM,KAAK,GAAG,EAO7B,IAAI,IANc,KAC5B,UAAU,uBACV,cAAc,SAAS,GACvB,cAAc,QAAQ,CAGQ,GAC9B,QAAQ;CAGV,MAAM,cAAc,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE;CAE7C,IACE,YAAY,YAAY,MAAM,QAC9B,YAAY,SAAS,MAAM,KAAK,KAChC,YAAY,QAAQ,MAAM,IAE1B,OAAO;CAGT,OAAO;AACT;;;;;;;;;ACvCA,SAAgB,YAAY,UAA0B;CACpD,OAAO,OAAO,SAAS,MAAM,GAAG,EAAE,CAAC,IAAI,MAAO,WAAW;AAC3D;;;ACJA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;AA0BlB,SAAgB,SAAS,UAAkB,UAA2B,CAAC,GAAqB;CAC1F,IAAI,OAAO,aAAa,YAAY,SAAS,WAAW,WACtD,OAAO;EAAE,OAAO;EAAO,OAAO;CAAiB;CAGjD,IAAI,CAAC,WAAW,KAAK,QAAQ,GAC3B,OAAO;EAAE,OAAO;EAAO,OAAO;CAAiB;CAGjD,IAAI,CAAC,YAAY,QAAQ,GACvB,OAAO;EAAE,OAAO;EAAO,OAAO;CAAmB;CAGnD,MAAM,cAAc,iBAAiB,UAAU,QAAQ,aAAa;CACpE,IAAI,gBAAgB,MAClB,OAAO;EAAE,OAAO;EAAO,OAAO;CAAe;CAG/C,MAAM,cAAc,iBAAiB,QAAQ;CAC7C,IAAI,gBAAgB,MAClB,OAAO;EAAE,OAAO;EAAO,OAAO;CAAiB;CAKjD,OAAO;EAAE,OAAO;EAAM;EAAa,QAFpB,YAAY,QAEa;EAAG;CAAY;AACzD"}