From c4118babd083606fc4c862048724996e412a44fe Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sun, 26 Jul 2026 19:23:21 +0200 Subject: [PATCH] fix: accept numbers written with their usual separators (se, cl, uy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clean() in these four modules does not normalise the input to the shape validate()'s length check expects. se/personnummer required the -/+ separator, so the bare 10 and 12 digit forms were InvalidLength even though format() accepted them. The separator is written between the birth date and the birth number, it is not part of the number (Folkbokforingslagen 1991:481 §18); python-stdnum inserts the default '-' for those two lengths in compact() and that step was not ported. cl/rut, uy/cedula and uy/nie did not strip the '.' that their own format() emits, so each rejected its own standard presentation. --- src/cl/rut.spec.ts | 26 ++++++++++++++++++++++++++ src/cl/rut.ts | 2 +- src/se/personnummer.spec.ts | 27 +++++++++++++++++++++++++++ src/se/personnummer.ts | 11 ++++++++++- src/uy/cedula.spec.ts | 19 +++++++++++++++++++ src/uy/cedula.ts | 2 +- src/uy/nie.spec.ts | 18 ++++++++++++++++++ src/uy/nie.ts | 2 +- 8 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/cl/rut.spec.ts b/src/cl/rut.spec.ts index c099e962..f42ad99b 100644 --- a/src/cl/rut.spec.ts +++ b/src/cl/rut.spec.ts @@ -30,12 +30,38 @@ describe('cl/rut', () => { expect(result.isValid && result.compact).toEqual('125319092'); }); + // The number is normally printed with the thousands separators, which is + // also what format() emits. + it('validate:12.531.909-2', () => { + const result = validate('12.531.909-2'); + + expect(result.isValid && result.compact).toEqual('125319092'); + }); + + it('validate:CL 7.727.230-5', () => { + const result = validate('CL 7.727.230-5'); + + expect(result.isValid && result.compact).toEqual('77272305'); + }); + + it('validate:format:76086428-5', () => { + const result = validate(format('76086428-5')); + + expect(result.isValid && result.compact).toEqual('760864285'); + }); + it('validate:12531909-3', () => { const result = validate('12531909-3'); expect(result.error).toBeInstanceOf(InvalidChecksum); }); + it('validate:12.531.909-3', () => { + const result = validate('12.531.909-3'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); + it('validate:76086A28-5', () => { const result = validate('76086A28-5'); diff --git a/src/cl/rut.ts b/src/cl/rut.ts index 06824e2f..9f75896e 100644 --- a/src/cl/rut.ts +++ b/src/cl/rut.ts @@ -16,7 +16,7 @@ import { strings, weightedSum } from '../util'; import { Validator, ValidateReturn } from '../types'; function clean(input: string): ReturnType { - const [v, err] = strings.cleanUnicode(input, ' -'); + const [v, err] = strings.cleanUnicode(input, ' -.'); if (err) { return ['', err]; diff --git a/src/se/personnummer.spec.ts b/src/se/personnummer.spec.ts index 688f1d22..e490eefc 100644 --- a/src/se/personnummer.spec.ts +++ b/src/se/personnummer.spec.ts @@ -38,6 +38,21 @@ describe('se/personnummer', () => { }, ); + // The separator is only written between the birth date and the birth number, + // it is not part of the number (Folkbokföringslagen 1991:481 §18), so the + // bare 10 and 12 digit forms are valid too. + test.each([ + ['8803200016', '880320-0016'], + ['8112289874', '811228-9874'], + ['6709199530', '670919-9530'], + ['7010632391', '701063-2391'], // coordination number + ['119001022384', '900102+2384'], // 4digit year, no hyphen + ])('validate:%s', (given, want) => { + const result = validate(given); + + expect(result.isValid && result.compact).toEqual(want); + }); + it('validate:12345678', () => { const result = validate('12345678'); @@ -49,4 +64,16 @@ describe('se/personnummer', () => { expect(result.error).toBeInstanceOf(InvalidChecksum); }); + + it('validate:8803200018', () => { + const result = validate('8803200018'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); + + it('validate:198803200018', () => { + const result = validate('198803200018'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); }); diff --git a/src/se/personnummer.ts b/src/se/personnummer.ts index a9dd6184..91ecf6ee 100644 --- a/src/se/personnummer.ts +++ b/src/se/personnummer.ts @@ -26,7 +26,16 @@ function clean(input: string): ReturnType { return [value, err]; } - const [a, b, c] = strings.splitAt(value, -5, -4); + // A 10 or 12 digit number is written without the separator, so put the + // default one back. '-' is only replaced by '+' the year someone turns 100, + // which the bare form cannot express. + const separated = + (value.length === 10 || value.length === 12) && + !'-+'.includes(value[value.length - 5]) + ? `${value.slice(0, -4)}-${value.slice(-4)}` + : value; + + const [a, b, c] = strings.splitAt(separated, -5, -4); return [`${a.replace(/[-+]/g, '')}${b}${c}`, null]; } diff --git a/src/uy/cedula.spec.ts b/src/uy/cedula.spec.ts index b7de6d00..8765a5b4 100644 --- a/src/uy/cedula.spec.ts +++ b/src/uy/cedula.spec.ts @@ -14,6 +14,19 @@ describe('uy/cedula', () => { expect(result.isValid && result.compact).toEqual('12345672'); }); + // The cédula is printed as a.bcd.efg-h, which is also what format() emits. + it('validate:1.234.567-2', () => { + const result = validate('1.234.567-2'); + + expect(result.isValid && result.compact).toEqual('12345672'); + }); + + it('validate:format:12345672', () => { + const result = validate(format('12345672')); + + expect(result.isValid && result.compact).toEqual('12345672'); + }); + it('validate:1121123', () => { const result = validate('1121123'); @@ -25,4 +38,10 @@ describe('uy/cedula', () => { expect(result.error).toBeInstanceOf(InvalidChecksum); }); + + it('validate:1.234.567-3', () => { + const result = validate('1.234.567-3'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); }); diff --git a/src/uy/cedula.ts b/src/uy/cedula.ts index f4b81932..51cb4ae8 100644 --- a/src/uy/cedula.ts +++ b/src/uy/cedula.ts @@ -9,7 +9,7 @@ import { strings, weightedSum } from '../util'; import { Validator, ValidateReturn } from '../types'; function clean(input: string): ReturnType { - return strings.cleanUnicode(input, ' -/'); + return strings.cleanUnicode(input, ' -/.'); } const impl: Validator = { diff --git a/src/uy/nie.spec.ts b/src/uy/nie.spec.ts index bec07b97..4bebdd29 100644 --- a/src/uy/nie.spec.ts +++ b/src/uy/nie.spec.ts @@ -14,6 +14,18 @@ describe('uy/nie', () => { expect(result.isValid && result.compact).toEqual('912345670'); }); + it('validate:91.234.567-0', () => { + const result = validate('91.234.567-0'); + + expect(result.isValid && result.compact).toEqual('912345670'); + }); + + it('validate:format:912345670', () => { + const result = validate(format('912345670')); + + expect(result.isValid && result.compact).toEqual('912345670'); + }); + it('validate:1121123', () => { const result = validate('1121123'); @@ -25,4 +37,10 @@ describe('uy/nie', () => { expect(result.error).toBeInstanceOf(InvalidChecksum); }); + + it('validate:91.234.567-3', () => { + const result = validate('91.234.567-3'); + + expect(result.error).toBeInstanceOf(InvalidChecksum); + }); }); diff --git a/src/uy/nie.ts b/src/uy/nie.ts index 110491da..00aac8c3 100644 --- a/src/uy/nie.ts +++ b/src/uy/nie.ts @@ -18,7 +18,7 @@ import { strings, weightedSum } from '../util'; import { Validator, ValidateReturn } from '../types'; function clean(input: string): ReturnType { - return strings.cleanUnicode(input, ' -'); + return strings.cleanUnicode(input, ' -.'); } const impl: Validator = {