Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - gitrecess/za-id: Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call. · GitHub
Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - gitrecess/za-id: Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call. · GitHub
Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - gitrecess/za-id: Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call. · GitHub
Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - gitrecess/za-id: Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call. · GitHub
Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - gitrecess/za-id: Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call. · GitHub
Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - gitrecess/za-id: Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call. · GitHub
Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - gitrecess/za-id: Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call. · GitHub
Skip to content

Repository files navigation

@iamlukia/za-id

Validate and decode South African ID numbers.

  • Zero runtime dependencies. Nothing to audit but this.
  • No network call. Runs entirely in your own process — there is no API behind it, and no dependency that could add one. An ID number is about as sensitive an identifier as there is; it should not leave your machine to be checked.
  • Fully typed, with a discriminated result so a failed parse cannot be read as a successful one.
  • Deterministic. The two-digit year needs a reference date to resolve; that date is an argument, not a hidden read of the system clock.
  • Works in Node, Deno, Bun, Cloudflare Workers and the browser. Published as ESM only; Node ≥ 20.19 is the floor, and CommonJS callers on that same floor can require() it directly.
npm install @iamlukia/za-id

Usage

import{parseZaId,isValidZaId}from'@iamlukia/za-id';isValidZaId('8001015009087');// trueconstresult=parseZaId('800101 5009 08 7');// spaces are fineif(result.valid){result.dateOfBirth.iso;// '1980-01-01'result.dateOfBirth.centuryAssumed;// '1900s'result.sex;// 'male'result.citizenship;// 'citizen'}else{result.reason;// 'checksum'result.message;// human-readable, for display}

Branch on reason, never on message — the wording may change between versions, the reasons will not.

reasonMeaning
not-digitsSomething other than digits and whitespace — or not a string.
wrong-lengthNot exactly thirteen digits.
invalid-monthDigits 3–4 are not a month.
invalid-dateA day that does not exist in that month.
checksumThe Luhn check fails.

Every id comes back as a result rather than an exception. That includes one that is not a string at all — a number straight out of JSON.parse, null, undefined — which reports not-digits instead of raising a TypeError. TypeScript callers will not get that far, since the signature asks for a string; the guard is there for the JavaScript ones.

The second argument is not covered by that: options mistakes throw rather than coming back as a result. parseZaId(id, null) and parseZaId(id, { now: 'today' }) raise a TypeError, and an Invalid Date in now raises a RangeError — every comparison against NaN is false, so without that check the century inference quietly assumes the 1900s and reports it as fact. If you are building options from untrusted input, validate it yourself.

Since 0.3.0. Before that, an Invalid Date was the one case that failed silently.

Pinning the reference date

Two digits of year cannot say which century. The convention — used here and almost everywhere — is that a year at or below the current one means the 2000s, anything above it means the 1900s, and a date landing in the future is pushed back a hundred years.

That makes the result depend on when you ask. Pass now to fix it:

parseZaId('8001015009087',{now: newDate('2020-01-01')});

Useful for reproducible tests, and for backfilling historical records where the capture date is the honest reference rather than today.

centuryAssumed comes back with every successful result, so you can show the assumption rather than presenting a guess as a fact. For anyone born more than about a century ago, that guess is wrong.

What the thirteen digits mean

Reading left to right: 1–6 date of birth as YYMMDD; 7–10 a sequence number within that day, where 0000–4999 was issued to people registered female and 5000–9999 to people registered male; 11 citizenship, 0 for South African citizen and 1 for permanent resident; 12 a historical artefact, see below; 13 a Luhn checksum over the twelve digits in front of it.

The sex digit records what the Department of Home Affairs has on file, which is not necessarily how somebody describes themselves. It can be changed — the Alteration of Sex Description and Sex Status Act 49 of 2003 provides for exactly that, and the number is reissued when it happens. This library reports what the digits say, not what is true about a person. The field is named sex rather than gender for that reason.

Digit 12 is not decoded, and will not be. It was a race classification digit under apartheid and has been unused since 1994. There is nothing in that position anyone needs today, and shipping the lookup table would be a strange thing to publish.

What a valid result does and does not mean

valid: true means the number is well-formed: the format, the date and the checksum all agree. It does not mean the number was ever issued, and it does not mean it belongs to the person presenting it.

A checksum is arithmetic, and arithmetic can be satisfied deliberately — you can invent a number that passes every check here and belongs to nobody. Only the Department of Home Affairs can confirm that a number was issued and to whom, through the National Population Register. If you are verifying somebody's identity for anything that matters, this tells you the number is plausible, and that is all it tells you.

The Luhn check has one further blind spot worth knowing: it catches every single mistyped digit and every adjacent transposition except swapping a 0 and a 9, which doubling leaves unchanged. There is a test asserting exactly that, so it is a known property rather than a surprise.

Alternatives

za-id and za-id-validator on npm cover similar ground. This one exists because it has no dependencies (za-id pulls in moment), ships TypeScript types, takes an injectable reference date, and reports which century it assumed instead of silently picking one. If those do not matter to you, the others work.

Related

The same logic runs as a browser tool at iamlukia.com/tools/sa-id-validator, where you can paste a number and see it decoded without installing anything.

That page carries its own hand-written copy of this logic on purpose — it ships uncompiled so the "nothing leaves your browser" claim is checkable in view-source. The two are kept in step by hand.

Licence

MIT

About

Validate and decode South African ID numbers. Zero dependencies, fully typed, no network call.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages