You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two deliberate differences from python-stdnum (checked against master 006192e):
id/nik: we accept province codes 65 and 92–96 (#214)
python-stdnum checks the first 4 digits of a NIK against stdnum/id/loc.dat. That table was built from the Perl Locale::ID::Province data and uses the old province codes: West Papua is 91 and Papua is 94. It also has no entry for North Kalimantan (65). As a result, python-stdnum rejects NIKs issued in North Kalimantan and in the Papua provinces created in 2022. We check only the 2-digit province code, against the current list of 38.
Sources for the current codes:
Kepmendagri No. 100.1.1-6117 Tahun 2022 (Minister of Home Affairs decree on administrative area codes), transcribed on Wikisource.
Kepmendagri No. 300.2.2-2430 Tahun 2025, as tabulated in cahyadsn/wilayah: Kalimantan Utara 65, Papua Barat 92, Papua Barat Daya 96 (UU No. 29 Tahun 2022).
A mutation diff of id/npwp (which uses id/nik for 16-digit numbers) against python-stdnum gave 380 differences. All of them are 16-digit NIKs with prefix 65, 92, 93, 95 or 96 that we accept and python-stdnum rejects.
Not verified: I read the 2025 decree only through the cahyadsn/wilayah tables, not the decree PDF itself. Codes 65, 92 and 96 rest on that.
be/vat: we reject check values 00, 98 and 99
The last two digits of a Belgian enterprise number are 97 - (first 8 digits mod 97), so they always fall in 01–97. python-stdnum instead tests (int(number[:-2]) + int(number[-2:])) % 97 == 0. That also passes when the check value is 00, 98 or 99, since those differ from the correct value by a multiple of 97. Our src/be/vat.ts computes 97 - (front % 97) and compares, so it rejects them.
Source: the Peppol BIS Billing 3.0 validation rules (the format for Belgian e-invoicing) check scheme 0208, the Belgian enterprise number, with exactly this formula. See u:mod97-0208 in PEPPOL-EN16931-UBL.sch, used by rule PEPPOL-COMMON-R043:
<variablename="calculated_digits"select="xs:string(97 - (xs:integer(substring($val,1,8)) mod 97))" />
<sequenceselect="number($checkdigits) = number($calculated_digits)" />
Not verified: I could not find a Belgian government (FPS Economy / KBO-BCE) page that states the formula. The EU TIN country sheet for Belgium covers only the 11-digit national number, and the KBO pages showed a bot check.
More deliberate differences from python-stdnum, checked against master 006192e with a mutation diff of each validator. Each entry gives the source, or says it has none.
Birth dates in the future are rejected: cz/rc, kr/rrn, fi/hetu, cn/ric, lt/asmens, ee/ik (#230)
These numbers encode the holder's date of birth, so a date after today can't belong to anyone yet. python-stdnum only checks that the date exists. In the mutation diffs this is the largest single difference: 367 numbers for cz/rc, 135 for kr/rrn, 132 for ee/ik, 123 for fi/hetu and 39 for cn/ric. There is no external source for this rule; it follows from what the field means. The check compares the date with the current time (validBirthdate() in util/isValidDate.ts), with no margin for time zones.
cz/rc: remainder 10 is rejected for numbers from 1985 on
For a 10-digit RČ, when the first 9 digits mod 11 give 10, the check digit was written as 0. python-stdnum accepts this at any date since 852515c (issue #468). We accept it only for birth years before 1985.
Source: the page python-stdnum's commit cites, domzo13.cz: Rodná čísla. It says the exception was used for about 1,000 numbers and ended in 1985 by internal regulation FSÚ Č. Vk. 2898/1985, adding: "it is not ruled out that it occurred in a minimal number after that year". This accounts for 39 differences in the mutation diff. A real post-1985 number of this kind would be rejected; I chose the regulation over that possibility.
(The same diff found a real bug: 9-digit numbers with year digits 80, born in 1880, were rejected because we tested year > 1980 where python-stdnum tests >= 1980. Fixed in #242.)
kr/rrn: numbers issued since October 2020 have a random suffix
For people born in 2020 or later we check only the length, the digits and the birth date. python-stdnum still checks the region digits and the check digit, so it rejects these numbers (72 in the mutation diff). This came in with #202. Sources are in the module doc comment: the Personal Information Protection Act and the Financial News report of 2020-06-29 announcing random numbers from October 2020.
kr/brn: the check digit is verified
python-stdnum checks only the length and three structural fields; it has had no check digit since its online check was removed in 0779d6a. We also verify the check digit: weights 1,3,7,1,3,7,1,3,5 over the first 9 digits, plus ⌊9th digit × 5 / 10⌋, then the check digit is (10 − sum mod 10) mod 10. As a result we reject far more numbers than python-stdnum (49,653 in the mutation diff, almost all random strings).
Evidence that the check is correct: of the 231 numbers in python-stdnum's tests/test_kr_brn.doctest listed as "found online", 228 pass our check. The 3 that fail are 206-18-12345, which looks like a placeholder, plus 306-82-00417 and 728-86-00316.
Not verified: I found no National Tax Service document for the algorithm, only secondary descriptions such as oppadu.com.
ec/ruc: company RUCs get structural checks only
When the third digit is 6 (public) or 9 (private), we don't apply the old mod-11 check digit. python-stdnum still does and rejects them. The SRI says these RUCs have no specific validation algorithm, and the old check rejects real registered numbers. Sources are in the module doc comment: SRI electronic invoicing and minka.gob.ec issue 32. This came in with #196.
be/nn and be/bis: a day past the end of a known month is rejected (#229)
Since #229, we accept day 00 for a known month, as python-stdnum does. python-stdnum also accepts a day past the end of the month (for example 30 February) as "day unknown"; we don't.
When only the year (and month) is known, the unknown parts are 00 (example 40 00 00 953-81).
When the serial numbers 001–997 run out, the day becomes 01, then 02, "enzovoort" (and so on).
So a day past the end of the month could only come from that overflow, after roughly 500 people of one sex had been registered for each day of the month with only the year and month known. We judge that too unlikely to justify accepting impossible dates. That leaves 128 differences for be/nn and 138 for be/bis.
The first 6 digits are a GB/T 2260 administrative division code (GB 11643-1999). We check only the 2-digit province. python-stdnum checks all 6 digits against a dated county table built from Chinese Wikipedia (stdnum/cn/loc.dat). So we accept county codes that aren't in its table (128 in the mutation diff). I didn't port the table: it has about 6,900 lines, changes over time, and is not from an official source.
nz/bank: branches are checked against allocated ranges, not the current register (#241)
python-stdnum accepts only branches listed in its copy of the Payments NZ bank branch register (January 2026). We accept any branch in the allocated range for each bank. #241 adds every branch in the current register to those ranges. We accept 41 numbers python-stdnum rejects: accounts at branches inside our ranges that aren't in the register.
Not verified: the reasoning is that accounts at closed branches keep their numbers. I haven't confirmed that with a Payments NZ source.
us/ssn, us/ein: separators anywhere are allowed
python-stdnum allows dashes only in the standard positions (NNN-NN-NNNN, NN-NNNNNNN). We strip separators wherever they are, as every other validator in this library does. The digit checks are identical.
kr/rrn: no check digit for alien registration numbers (#256)
For a number whose 7th digit is 5–8 (an alien registration number, 외국인등록번호), we check only the length, digits and birth date. python-stdnum applies the RRN check digit formula. No official source gives a check digit formula for ARNs: the Enforcement Decree of the Immigration Act, Article 40-3 defines the number as 13 digits showing birth date, sex and registering office, and leaves the scheme to the Minister of Justice, who hasn't published it. Other validators (Microsoft Presidio, several blogs) use the RRN value + 2 instead, also without a source; the two can't both be right.
A mutation diff over 20,500 inputs against python-stdnum 2.2 gave 4,398 numbers we accept and python-stdnum rejects, all with a 7th digit of 5–8 and a birth year before 2020.
Not verified: the Minister of Justice rule itself (no public copy found), and whether the October 2020 randomisation applies to ARNs.
Activity
koblas commentedon Nov 10, 2026
Two deliberate differences from python-stdnum (checked against master
006192e):id/nik: we accept province codes 65 and 92–96 (#214)python-stdnum checks the first 4 digits of a NIK against
stdnum/id/loc.dat. That table was built from the PerlLocale::ID::Provincedata and uses the old province codes: West Papua is 91 and Papua is 94. It also has no entry for North Kalimantan (65). As a result, python-stdnum rejects NIKs issued in North Kalimantan and in the Papua provinces created in 2022. We check only the 2-digit province code, against the current list of 38.Sources for the current codes:
A mutation diff of
id/npwp(which usesid/nikfor 16-digit numbers) against python-stdnum gave 380 differences. All of them are 16-digit NIKs with prefix 65, 92, 93, 95 or 96 that we accept and python-stdnum rejects.Not verified: I read the 2025 decree only through the cahyadsn/wilayah tables, not the decree PDF itself. Codes 65, 92 and 96 rest on that.
be/vat: we reject check values 00, 98 and 99The last two digits of a Belgian enterprise number are
97 - (first 8 digits mod 97), so they always fall in 01–97. python-stdnum instead tests(int(number[:-2]) + int(number[-2:])) % 97 == 0. That also passes when the check value is 00, 98 or 99, since those differ from the correct value by a multiple of 97. Oursrc/be/vat.tscomputes97 - (front % 97)and compares, so it rejects them.Source: the Peppol BIS Billing 3.0 validation rules (the format for Belgian e-invoicing) check scheme 0208, the Belgian enterprise number, with exactly this formula. See
u:mod97-0208in PEPPOL-EN16931-UBL.sch, used by rule PEPPOL-COMMON-R043:Not verified: I could not find a Belgian government (FPS Economy / KBO-BCE) page that states the formula. The EU TIN country sheet for Belgium covers only the 11-digit national number, and the KBO pages showed a bot check.
koblas commentedon Nov 10, 2026
More deliberate differences from python-stdnum, checked against master
006192ewith a mutation diff of each validator. Each entry gives the source, or says it has none.Birth dates in the future are rejected:
cz/rc,kr/rrn,fi/hetu,cn/ric,lt/asmens,ee/ik(#230)These numbers encode the holder's date of birth, so a date after today can't belong to anyone yet. python-stdnum only checks that the date exists. In the mutation diffs this is the largest single difference: 367 numbers for
cz/rc, 135 forkr/rrn, 132 foree/ik, 123 forfi/hetuand 39 forcn/ric. There is no external source for this rule; it follows from what the field means. The check compares the date with the current time (validBirthdate()inutil/isValidDate.ts), with no margin for time zones.cz/rc: remainder 10 is rejected for numbers from 1985 onFor a 10-digit RČ, when the first 9 digits mod 11 give 10, the check digit was written as 0. python-stdnum accepts this at any date since 852515c (issue #468). We accept it only for birth years before 1985.
Source: the page python-stdnum's commit cites, domzo13.cz: Rodná čísla. It says the exception was used for about 1,000 numbers and ended in 1985 by internal regulation FSÚ Č. Vk. 2898/1985, adding: "it is not ruled out that it occurred in a minimal number after that year". This accounts for 39 differences in the mutation diff. A real post-1985 number of this kind would be rejected; I chose the regulation over that possibility.
(The same diff found a real bug: 9-digit numbers with year digits
80, born in 1880, were rejected because we testedyear > 1980where python-stdnum tests>= 1980. Fixed in #242.)kr/rrn: numbers issued since October 2020 have a random suffixFor people born in 2020 or later we check only the length, the digits and the birth date. python-stdnum still checks the region digits and the check digit, so it rejects these numbers (72 in the mutation diff). This came in with #202. Sources are in the module doc comment: the Personal Information Protection Act and the Financial News report of 2020-06-29 announcing random numbers from October 2020.
kr/brn: the check digit is verifiedpython-stdnum checks only the length and three structural fields; it has had no check digit since its online check was removed in 0779d6a. We also verify the check digit: weights 1,3,7,1,3,7,1,3,5 over the first 9 digits, plus ⌊9th digit × 5 / 10⌋, then the check digit is (10 − sum mod 10) mod 10. As a result we reject far more numbers than python-stdnum (49,653 in the mutation diff, almost all random strings).
Evidence that the check is correct: of the 231 numbers in python-stdnum's
tests/test_kr_brn.doctestlisted as "found online", 228 pass our check. The 3 that fail are206-18-12345, which looks like a placeholder, plus306-82-00417and728-86-00316.Not verified: I found no National Tax Service document for the algorithm, only secondary descriptions such as oppadu.com.
ec/ruc: company RUCs get structural checks onlyWhen the third digit is 6 (public) or 9 (private), we don't apply the old mod-11 check digit. python-stdnum still does and rejects them. The SRI says these RUCs have no specific validation algorithm, and the old check rejects real registered numbers. Sources are in the module doc comment: SRI electronic invoicing and minka.gob.ec issue 32. This came in with #196.
be/nnandbe/bis: a day past the end of a known month is rejected (#229)Since #229, we accept day
00for a known month, as python-stdnum does. python-stdnum also accepts a day past the end of the month (for example 30 February) as "day unknown"; we don't.Source: IT000 Rijksregisternummer, version 15.05.2016, section "Onvolledige geboortedatum":
40 00 00 953-81).So a day past the end of the month could only come from that overflow, after roughly 500 people of one sex had been registered for each day of the month with only the year and month known. We judge that too unlikely to justify accepting impossible dates. That leaves 128 differences for
be/nnand 138 forbe/bis.cn/ric: only the province code is checked (#233)The first 6 digits are a GB/T 2260 administrative division code (GB 11643-1999). We check only the 2-digit province. python-stdnum checks all 6 digits against a dated county table built from Chinese Wikipedia (
stdnum/cn/loc.dat). So we accept county codes that aren't in its table (128 in the mutation diff). I didn't port the table: it has about 6,900 lines, changes over time, and is not from an official source.nz/bank: branches are checked against allocated ranges, not the current register (#241)python-stdnum accepts only branches listed in its copy of the Payments NZ bank branch register (January 2026). We accept any branch in the allocated range for each bank. #241 adds every branch in the current register to those ranges. We accept 41 numbers python-stdnum rejects: accounts at branches inside our ranges that aren't in the register.
Not verified: the reasoning is that accounts at closed branches keep their numbers. I haven't confirmed that with a Payments NZ source.
us/ssn,us/ein: separators anywhere are allowedpython-stdnum allows dashes only in the standard positions (
NNN-NN-NNNN,NN-NNNNNNN). We strip separators wherever they are, as every other validator in this library does. The digit checks are identical.kr/rrn: no check digit for alien registration numbers (#256)For a number whose 7th digit is 5–8 (an alien registration number, 외국인등록번호), we check only the length, digits and birth date. python-stdnum applies the RRN check digit formula. No official source gives a check digit formula for ARNs: the Enforcement Decree of the Immigration Act, Article 40-3 defines the number as 13 digits showing birth date, sex and registering office, and leaves the scheme to the Minister of Justice, who hasn't published it. Other validators (Microsoft Presidio, several blogs) use the RRN value + 2 instead, also without a source; the two can't both be right.
A mutation diff over 20,500 inputs against python-stdnum 2.2 gave 4,398 numbers we accept and python-stdnum rejects, all with a 7th digit of 5–8 and a birth year before 2020.
Not verified: the Minister of Justice rule itself (no public copy found), and whether the October 2020 randomisation applies to ARNs.