@gusnips/br 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/cep.d.ts +14 -0
- package/dist/cep.d.ts.map +1 -0
- package/dist/cep.js +23 -0
- package/dist/cep.js.map +1 -0
- package/dist/document.d.ts +60 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +155 -0
- package/dist/document.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/phone.d.ts +58 -0
- package/dist/phone.d.ts.map +1 -0
- package/dist/phone.js +122 -0
- package/dist/phone.js.map +1 -0
- package/package.json +62 -0
- package/src/cep.test.ts +24 -0
- package/src/cep.ts +25 -0
- package/src/document.test.ts +118 -0
- package/src/document.ts +150 -0
- package/src/index.ts +24 -0
- package/src/phone.test.ts +93 -0
- package/src/phone.ts +128 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gustavo Salomé
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @gusnips/br
|
|
2
|
+
|
|
3
|
+
Checks and formats Brazilian documents and phone numbers: CPF, CNPJ, phones and CEP. The same
|
|
4
|
+
code runs on a server, in a browser and in React Native. No dependencies.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { isValidCnpj } from "@gusnips/br";
|
|
8
|
+
|
|
9
|
+
isValidCnpj("12.ABC.345/01DE-35"); // true
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## CPF and CNPJ
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { formatCnpj, isValidCpf, maskDocument } from "@gusnips/br";
|
|
16
|
+
|
|
17
|
+
isValidCpf("111.444.777-35"); // true
|
|
18
|
+
formatCnpj("12abc34501de35"); // "12.ABC.345/01DE-35"
|
|
19
|
+
maskDocument("1114447"); // "111.444.7", for a field as somebody types
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Since July 2026 a new CNPJ can have letters.** The first twelve characters can be A-Z as well as
|
|
23
|
+
digits. The last two, the check digits, are still numbers. `isValidCnpj`, `formatCnpj` and
|
|
24
|
+
`maskCnpj` accept both kinds. A checker that strips letters before it checks turns these CNPJs down,
|
|
25
|
+
so a company registered this year can't sign up.
|
|
26
|
+
|
|
27
|
+
Three kinds of function, and they do different jobs:
|
|
28
|
+
|
|
29
|
+
- `formatCpf` and `formatCnpj` display a **complete** value. They add back the leading zeros a
|
|
30
|
+
number column drops (`1144477735` becomes `011.444.777-35`), and anything that isn't a document
|
|
31
|
+
comes back unchanged.
|
|
32
|
+
- `maskCpf`, `maskCnpj` and `maskDocument` format **partial** input as it is typed, and never pad.
|
|
33
|
+
`maskDocument` switches from CPF to CNPJ after 11 digits, or at the first letter.
|
|
34
|
+
- `isValidCpf` and `isValidCnpj` check the check digits. A value made of one repeated digit is always
|
|
35
|
+
refused, because the arithmetic alone would accept it.
|
|
36
|
+
|
|
37
|
+
`classifyDocument` answers "is this a CPF or a CNPJ?" by shape alone, without checking the digits.
|
|
38
|
+
Government datasets contain test CPFs whose check digits fail, and those are still people.
|
|
39
|
+
|
|
40
|
+
`cpfCheckDigits` and `cnpjCheckDigits` compute the last two digits from the rest. Use them to build a
|
|
41
|
+
valid document in a test, or to complete a head office's CNPJ from its 8-character root:
|
|
42
|
+
`root + "0001"` plus the two digits.
|
|
43
|
+
|
|
44
|
+
## Phones
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { brMobileVariants, formatBrPhone, toE164Br } from "@gusnips/br";
|
|
48
|
+
|
|
49
|
+
toE164Br("(11) 98765-4321"); // "+5511987654321"
|
|
50
|
+
formatBrPhone("5511987654321"); // "(11) 98765-4321"
|
|
51
|
+
brMobileVariants("5511987654321"); // ["5511987654321", "551187654321"]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`parseBrPhone` reads a number however it was written: with or without `+55`, with punctuation, or
|
|
55
|
+
with the trunk `0` people dial between cities. It returns `null` for anything it can't read. It
|
|
56
|
+
checks the area code against the 67 in use, and it refuses a subscriber made of one repeated digit,
|
|
57
|
+
like `99999-9999`.
|
|
58
|
+
|
|
59
|
+
**Brazilian numbers only, on purpose.** A general phone library that reads bare digits guesses the
|
|
60
|
+
country from the first few, and for a Brazilian mobile typed without `+55` it often guesses wrong:
|
|
61
|
+
area code 31 reads as the Netherlands and 81 as Japan. If you also take foreign numbers, send the
|
|
62
|
+
ones that start with `+` and aren't `+55` to an international library, and send the rest here.
|
|
63
|
+
|
|
64
|
+
`55` is both the country code and an area code (Rio Grande do Sul), so length decides first. Eleven
|
|
65
|
+
digits starting with 55 are a mobile in area 55. Only a 12- or 13-digit number carries the country
|
|
66
|
+
code.
|
|
67
|
+
|
|
68
|
+
`brMobileVariants` gives both forms a mobile may be registered under on WhatsApp: with the ninth
|
|
69
|
+
digit and without it. An account created before the ninth digit existed can still use the short
|
|
70
|
+
form. Look up both. Pass it an international number, so run `toE164Br` on user input first. Guessing
|
|
71
|
+
that bare digits are Brazilian is how a foreign number gets matched to somebody else.
|
|
72
|
+
|
|
73
|
+
## CEP
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { formatCep, normalizeCep } from "@gusnips/br";
|
|
77
|
+
|
|
78
|
+
formatCep("01310100"); // "01310-100"
|
|
79
|
+
normalizeCep("01310-100"); // "01310100", or null when it isn't eight digits
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Looking a CEP up is a network call to a provider you choose, so it isn't here. A real CEP can also
|
|
83
|
+
cover a whole small town with no street name, so a lookup that finds no street doesn't mean the CEP
|
|
84
|
+
is wrong.
|
|
85
|
+
|
|
86
|
+
## Not here
|
|
87
|
+
|
|
88
|
+
- **Error messages.** Every function returns a boolean, a string or `null`. The sentence the reader
|
|
89
|
+
sees belongs in your own translations:
|
|
90
|
+
`isValidCpf(value) ? undefined : t("errors.cpfInvalid")`.
|
|
91
|
+
- **zod.** Wrapping a check is one line, `z.string().refine(isValidCpf)`, and the message is yours.
|
package/dist/cep.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CEP, the Brazilian postal code: eight digits, shown as `01310-100`.
|
|
3
|
+
*
|
|
4
|
+
* Only the shape lives here. Looking a CEP up is a network call to a provider you choose, and a
|
|
5
|
+
* CEP can be real and still have no street — a whole small town can share one — so "the lookup
|
|
6
|
+
* found no street" is not "this CEP is invalid".
|
|
7
|
+
*/
|
|
8
|
+
/** The eight digits, or `null` when there are not eight. */
|
|
9
|
+
export declare function normalizeCep(value: string): string | null;
|
|
10
|
+
/** `01310-100`. Anything that is not eight digits comes back as it was. */
|
|
11
|
+
export declare function formatCep(value: string): string;
|
|
12
|
+
/** Formats a CEP as it is typed. Stops at eight digits. */
|
|
13
|
+
export declare function maskCep(partial: string): string;
|
|
14
|
+
//# sourceMappingURL=cep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cep.d.ts","sourceRoot":"","sources":["../src/cep.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzD;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,2DAA2D;AAC3D,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG/C"}
|
package/dist/cep.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CEP, the Brazilian postal code: eight digits, shown as `01310-100`.
|
|
3
|
+
*
|
|
4
|
+
* Only the shape lives here. Looking a CEP up is a network call to a provider you choose, and a
|
|
5
|
+
* CEP can be real and still have no street — a whole small town can share one — so "the lookup
|
|
6
|
+
* found no street" is not "this CEP is invalid".
|
|
7
|
+
*/
|
|
8
|
+
/** The eight digits, or `null` when there are not eight. */
|
|
9
|
+
export function normalizeCep(value) {
|
|
10
|
+
const digits = value.replace(/\D/g, "");
|
|
11
|
+
return digits.length === 8 ? digits : null;
|
|
12
|
+
}
|
|
13
|
+
/** `01310-100`. Anything that is not eight digits comes back as it was. */
|
|
14
|
+
export function formatCep(value) {
|
|
15
|
+
const digits = normalizeCep(value);
|
|
16
|
+
return digits === null ? value : `${digits.slice(0, 5)}-${digits.slice(5)}`;
|
|
17
|
+
}
|
|
18
|
+
/** Formats a CEP as it is typed. Stops at eight digits. */
|
|
19
|
+
export function maskCep(partial) {
|
|
20
|
+
const d = partial.replace(/\D/g, "").slice(0, 8);
|
|
21
|
+
return d.length <= 5 ? d : `${d.slice(0, 5)}-${d.slice(5)}`;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=cep.js.map
|
package/dist/cep.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cep.js","sourceRoot":"","sources":["../src/cep.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,4DAA4D;AAC5D,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACxC,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,OAAO,CAAC,OAAe;IACrC,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPF and CNPJ: check digits, display, input masks.
|
|
3
|
+
*
|
|
4
|
+
* Three concerns sit side by side here, and the adopters that merged them kept them apart for a
|
|
5
|
+
* reason: a DISPLAY formatter works on a complete value and zero-pads it, a MASK formats partial
|
|
6
|
+
* input as somebody types and never pads, and a VALIDATOR says whether the check digits hold.
|
|
7
|
+
*
|
|
8
|
+
* The CNPJ is alphanumeric from July 2026 (Receita Federal, IN RFB 2.229/2024): the first twelve
|
|
9
|
+
* characters may be A-Z as well as digits, and the two check digits stay numeric. Every copy this
|
|
10
|
+
* package replaced stripped letters before it checked, so a company registered under the new
|
|
11
|
+
* format was refused everywhere — and one display formatter turned `12.ABC.345/01DE-35` into a
|
|
12
|
+
* different, numeric CNPJ. The check digits are the same mod-11 over the same weights, with each
|
|
13
|
+
* character counting as its ASCII code minus 48, so a digit counts as itself and `A` as 17.
|
|
14
|
+
*/
|
|
15
|
+
export type DocumentKind = "cpf" | "cnpj";
|
|
16
|
+
/**
|
|
17
|
+
* Which document this is, by SHAPE only: 11 digits is a CPF, 14 CNPJ characters is a CNPJ.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately not a validity check. Government datasets carry test CPFs that fail the check digit,
|
|
20
|
+
* and an adopter reading those has to know "this is a person" without calling the record invalid —
|
|
21
|
+
* keep "what is it" and "does it check" as two questions.
|
|
22
|
+
*/
|
|
23
|
+
export declare function classifyDocument(value: string): DocumentKind | null;
|
|
24
|
+
/** The two check digits for the first nine digits of a CPF. */
|
|
25
|
+
export declare function cpfCheckDigits(base9: string): string;
|
|
26
|
+
/** The two check digits for the first twelve characters of a CNPJ, numeric or alphanumeric. */
|
|
27
|
+
export declare function cnpjCheckDigits(base12: string): string;
|
|
28
|
+
/** Whether a CPF's check digits hold. Formatted or bare. Eleven repeated digits never pass. */
|
|
29
|
+
export declare function isValidCpf(value: string): boolean;
|
|
30
|
+
/** Whether a CNPJ's check digits hold — numeric or alphanumeric, formatted or bare. */
|
|
31
|
+
export declare function isValidCnpj(value: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* A complete CPF for display: `123.456.789-09`.
|
|
34
|
+
*
|
|
35
|
+
* Zero-pads, because government datasets store it as a number and drop the leading zeros. Anything
|
|
36
|
+
* that is not a CPF comes back as it was, so a bad row shows as itself rather than as "".
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatCpf(value: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* A complete CNPJ for display: `12.345.678/0001-95` or `12.ABC.345/01DE-35`.
|
|
41
|
+
*
|
|
42
|
+
* Zero-pads a numeric value for the same reason {@link formatCpf} does. An alphanumeric one cannot
|
|
43
|
+
* have lost a zero to a number column, so it is never padded. Anything else comes back as it was.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatCnpj(value: string): string;
|
|
46
|
+
/** A CPF for display with its middle hidden: `123.***.***-09`. Anything else comes back as it was. */
|
|
47
|
+
export declare function redactCpf(value: string): string;
|
|
48
|
+
/** Formats a CPF as it is typed. Never pads; stops at 11 digits. */
|
|
49
|
+
export declare function maskCpf(partial: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Formats a CNPJ as it is typed, letters included. Never pads; stops at 14 characters, and the last
|
|
52
|
+
* two only take digits, because the check digits stay numeric.
|
|
53
|
+
*/
|
|
54
|
+
export declare function maskCnpj(partial: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* One field for either document: a CPF mask up to 11 digits, a CNPJ mask past that — or as soon as
|
|
57
|
+
* a letter is typed, since only a CNPJ can carry one.
|
|
58
|
+
*/
|
|
59
|
+
export declare function maskDocument(partial: string): string;
|
|
60
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,CAAC;AAY1C;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAGnE;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWpD;AAED,+FAA+F;AAC/F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAetD;AAED,+FAA+F;AAC/F,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKjD;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKlD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,sGAAsG;AACtG,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED,oEAAoE;AACpE,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGpD"}
|
package/dist/document.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPF and CNPJ: check digits, display, input masks.
|
|
3
|
+
*
|
|
4
|
+
* Three concerns sit side by side here, and the adopters that merged them kept them apart for a
|
|
5
|
+
* reason: a DISPLAY formatter works on a complete value and zero-pads it, a MASK formats partial
|
|
6
|
+
* input as somebody types and never pads, and a VALIDATOR says whether the check digits hold.
|
|
7
|
+
*
|
|
8
|
+
* The CNPJ is alphanumeric from July 2026 (Receita Federal, IN RFB 2.229/2024): the first twelve
|
|
9
|
+
* characters may be A-Z as well as digits, and the two check digits stay numeric. Every copy this
|
|
10
|
+
* package replaced stripped letters before it checked, so a company registered under the new
|
|
11
|
+
* format was refused everywhere — and one display formatter turned `12.ABC.345/01DE-35` into a
|
|
12
|
+
* different, numeric CNPJ. The check digits are the same mod-11 over the same weights, with each
|
|
13
|
+
* character counting as its ASCII code minus 48, so a digit counts as itself and `A` as 17.
|
|
14
|
+
*/
|
|
15
|
+
/** Digits only. A CPF is numeric and stays numeric, so a pasted label or letter is noise. */
|
|
16
|
+
function cpfCharacters(value) {
|
|
17
|
+
return value.replace(/\D/g, "");
|
|
18
|
+
}
|
|
19
|
+
/** Digits and A-Z, uppercased — the characters a CNPJ can hold. */
|
|
20
|
+
function cnpjCharacters(value) {
|
|
21
|
+
return value.toUpperCase().replace(/[^0-9A-Z]/g, "");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Which document this is, by SHAPE only: 11 digits is a CPF, 14 CNPJ characters is a CNPJ.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately not a validity check. Government datasets carry test CPFs that fail the check digit,
|
|
27
|
+
* and an adopter reading those has to know "this is a person" without calling the record invalid —
|
|
28
|
+
* keep "what is it" and "does it check" as two questions.
|
|
29
|
+
*/
|
|
30
|
+
export function classifyDocument(value) {
|
|
31
|
+
if (cpfCharacters(value).length === 11 && !/[A-Za-z]/.test(value))
|
|
32
|
+
return "cpf";
|
|
33
|
+
return /^[0-9A-Z]{12}\d{2}$/.test(cnpjCharacters(value)) ? "cnpj" : null;
|
|
34
|
+
}
|
|
35
|
+
/** The two check digits for the first nine digits of a CPF. */
|
|
36
|
+
export function cpfCheckDigits(base9) {
|
|
37
|
+
const digits = cpfCharacters(base9);
|
|
38
|
+
if (digits.length !== 9)
|
|
39
|
+
throw new RangeError("A CPF base is 9 digits.");
|
|
40
|
+
const next = (source) => {
|
|
41
|
+
let sum = 0;
|
|
42
|
+
for (let i = 0; i < source.length; i++)
|
|
43
|
+
sum += Number(source[i]) * (source.length + 1 - i);
|
|
44
|
+
const rest = (sum * 10) % 11;
|
|
45
|
+
return rest === 10 ? 0 : rest;
|
|
46
|
+
};
|
|
47
|
+
const first = next(digits);
|
|
48
|
+
return `${first}${next(`${digits}${first}`)}`;
|
|
49
|
+
}
|
|
50
|
+
/** The two check digits for the first twelve characters of a CNPJ, numeric or alphanumeric. */
|
|
51
|
+
export function cnpjCheckDigits(base12) {
|
|
52
|
+
const chars = cnpjCharacters(base12);
|
|
53
|
+
if (!/^[0-9A-Z]{12}$/.test(chars))
|
|
54
|
+
throw new RangeError("A CNPJ base is 12 characters.");
|
|
55
|
+
const next = (source) => {
|
|
56
|
+
let sum = 0;
|
|
57
|
+
// The weights run 2, 3 … 9 from the RIGHT and wrap, which is the 5→2, 9→2 walk read backwards.
|
|
58
|
+
for (let i = 0; i < source.length; i++) {
|
|
59
|
+
const weight = ((source.length - 1 - i) % 8) + 2;
|
|
60
|
+
sum += (source.charCodeAt(i) - 48) * weight;
|
|
61
|
+
}
|
|
62
|
+
const rest = sum % 11;
|
|
63
|
+
return rest < 2 ? 0 : 11 - rest;
|
|
64
|
+
};
|
|
65
|
+
const first = next(chars);
|
|
66
|
+
return `${first}${next(`${chars}${first}`)}`;
|
|
67
|
+
}
|
|
68
|
+
/** Whether a CPF's check digits hold. Formatted or bare. Eleven repeated digits never pass. */
|
|
69
|
+
export function isValidCpf(value) {
|
|
70
|
+
const digits = cpfCharacters(value);
|
|
71
|
+
if (digits.length !== 11 || /[A-Za-z]/.test(value))
|
|
72
|
+
return false;
|
|
73
|
+
if (/^(\d)\1{10}$/.test(digits))
|
|
74
|
+
return false;
|
|
75
|
+
return cpfCheckDigits(digits.slice(0, 9)) === digits.slice(9);
|
|
76
|
+
}
|
|
77
|
+
/** Whether a CNPJ's check digits hold — numeric or alphanumeric, formatted or bare. */
|
|
78
|
+
export function isValidCnpj(value) {
|
|
79
|
+
const chars = cnpjCharacters(value);
|
|
80
|
+
if (!/^[0-9A-Z]{12}\d{2}$/.test(chars))
|
|
81
|
+
return false;
|
|
82
|
+
if (/^(.)\1{13}$/.test(chars))
|
|
83
|
+
return false;
|
|
84
|
+
return cnpjCheckDigits(chars.slice(0, 12)) === chars.slice(12);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A complete CPF for display: `123.456.789-09`.
|
|
88
|
+
*
|
|
89
|
+
* Zero-pads, because government datasets store it as a number and drop the leading zeros. Anything
|
|
90
|
+
* that is not a CPF comes back as it was, so a bad row shows as itself rather than as "".
|
|
91
|
+
*/
|
|
92
|
+
export function formatCpf(value) {
|
|
93
|
+
const digits = cpfCharacters(value);
|
|
94
|
+
if (digits.length === 0 || digits.length > 11 || /[A-Za-z]/.test(value))
|
|
95
|
+
return value;
|
|
96
|
+
return digits.padStart(11, "0").replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A complete CNPJ for display: `12.345.678/0001-95` or `12.ABC.345/01DE-35`.
|
|
100
|
+
*
|
|
101
|
+
* Zero-pads a numeric value for the same reason {@link formatCpf} does. An alphanumeric one cannot
|
|
102
|
+
* have lost a zero to a number column, so it is never padded. Anything else comes back as it was.
|
|
103
|
+
*/
|
|
104
|
+
export function formatCnpj(value) {
|
|
105
|
+
let chars = cnpjCharacters(value);
|
|
106
|
+
if (/^\d{1,13}$/.test(chars))
|
|
107
|
+
chars = chars.padStart(14, "0");
|
|
108
|
+
if (!/^[0-9A-Z]{12}\d{2}$/.test(chars))
|
|
109
|
+
return value;
|
|
110
|
+
return `${chars.slice(0, 2)}.${chars.slice(2, 5)}.${chars.slice(5, 8)}/${chars.slice(8, 12)}-${chars.slice(12)}`;
|
|
111
|
+
}
|
|
112
|
+
/** A CPF for display with its middle hidden: `123.***.***-09`. Anything else comes back as it was. */
|
|
113
|
+
export function redactCpf(value) {
|
|
114
|
+
const digits = cpfCharacters(value);
|
|
115
|
+
if (digits.length !== 11 || /[A-Za-z]/.test(value))
|
|
116
|
+
return value;
|
|
117
|
+
return `${digits.slice(0, 3)}.***.***-${digits.slice(9)}`;
|
|
118
|
+
}
|
|
119
|
+
/** Formats a CPF as it is typed. Never pads; stops at 11 digits. */
|
|
120
|
+
export function maskCpf(partial) {
|
|
121
|
+
const d = cpfCharacters(partial).slice(0, 11);
|
|
122
|
+
if (d.length <= 3)
|
|
123
|
+
return d;
|
|
124
|
+
if (d.length <= 6)
|
|
125
|
+
return `${d.slice(0, 3)}.${d.slice(3)}`;
|
|
126
|
+
if (d.length <= 9)
|
|
127
|
+
return `${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6)}`;
|
|
128
|
+
return `${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6, 9)}-${d.slice(9)}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Formats a CNPJ as it is typed, letters included. Never pads; stops at 14 characters, and the last
|
|
132
|
+
* two only take digits, because the check digits stay numeric.
|
|
133
|
+
*/
|
|
134
|
+
export function maskCnpj(partial) {
|
|
135
|
+
const all = cnpjCharacters(partial);
|
|
136
|
+
const c = all.slice(0, 12) + all.slice(12).replace(/\D/g, "").slice(0, 2);
|
|
137
|
+
if (c.length <= 2)
|
|
138
|
+
return c;
|
|
139
|
+
if (c.length <= 5)
|
|
140
|
+
return `${c.slice(0, 2)}.${c.slice(2)}`;
|
|
141
|
+
if (c.length <= 8)
|
|
142
|
+
return `${c.slice(0, 2)}.${c.slice(2, 5)}.${c.slice(5)}`;
|
|
143
|
+
if (c.length <= 12)
|
|
144
|
+
return `${c.slice(0, 2)}.${c.slice(2, 5)}.${c.slice(5, 8)}/${c.slice(8)}`;
|
|
145
|
+
return `${c.slice(0, 2)}.${c.slice(2, 5)}.${c.slice(5, 8)}/${c.slice(8, 12)}-${c.slice(12)}`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One field for either document: a CPF mask up to 11 digits, a CNPJ mask past that — or as soon as
|
|
149
|
+
* a letter is typed, since only a CNPJ can carry one.
|
|
150
|
+
*/
|
|
151
|
+
export function maskDocument(partial) {
|
|
152
|
+
const chars = cnpjCharacters(partial);
|
|
153
|
+
return chars.length <= 11 && /^\d*$/.test(chars) ? maskCpf(chars) : maskCnpj(chars);
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { classifyDocument, cnpjCheckDigits, cpfCheckDigits, formatCnpj, formatCpf, isValidCnpj, isValidCpf, maskCnpj, maskCpf, maskDocument, redactCpf, } from "./document.ts";
|
|
2
|
+
export type { DocumentKind } from "./document.ts";
|
|
3
|
+
export { brMobileVariants, formatBrPhone, isValidBrPhone, maskBrPhone, parseBrPhone, toE164Br, } from "./phone.ts";
|
|
4
|
+
export type { BrPhone } from "./phone.ts";
|
|
5
|
+
export { formatCep, maskCep, normalizeCep } from "./cep.ts";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,WAAW,EACX,YAAY,EACZ,QAAQ,GACT,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { classifyDocument, cnpjCheckDigits, cpfCheckDigits, formatCnpj, formatCpf, isValidCnpj, isValidCpf, maskCnpj, maskCpf, maskDocument, redactCpf, } from "./document.js";
|
|
2
|
+
export { brMobileVariants, formatBrPhone, isValidBrPhone, maskBrPhone, parseBrPhone, toE164Br, } from "./phone.js";
|
|
3
|
+
export { formatCep, maskCep, normalizeCep } from "./cep.js";
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,WAAW,EACX,YAAY,EACZ,QAAQ,GACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/phone.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brazilian phone numbers: read, check, format, and the two forms of an older mobile.
|
|
3
|
+
*
|
|
4
|
+
* Brazilian only, on purpose. A number that is not Brazilian comes back as `null`, never as a
|
|
5
|
+
* guess: a general parser reading bare digits guesses a country from the first ones, and for a
|
|
6
|
+
* Brazilian mobile typed without `+55` that guess is wrong often — area code 31 reads as the
|
|
7
|
+
* Netherlands and 81 as Japan. If you take international numbers too, send the ones that start
|
|
8
|
+
* with `+` and are not `+55` to a real international library, and keep this for the rest.
|
|
9
|
+
*/
|
|
10
|
+
export interface BrPhone {
|
|
11
|
+
/** The two-digit area code. */
|
|
12
|
+
ddd: string;
|
|
13
|
+
/** Everything after it: nine digits for a mobile, eight for a landline. */
|
|
14
|
+
subscriber: string;
|
|
15
|
+
kind: "mobile" | "landline";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads a Brazilian number in whatever shape a person or a system wrote it — `(11) 98765-4321`,
|
|
19
|
+
* `+55 11 98765-4321`, `5511987654321`, `011 3456-7890` — or returns `null`.
|
|
20
|
+
*
|
|
21
|
+
* **Length decides before the prefix does**, because `55` is ambiguous: it is the country code and
|
|
22
|
+
* also a DDD (Rio Grande do Sul). Eleven digits starting with 55 are a gaúcho mobile, not a country
|
|
23
|
+
* code in front of nine digits; only 12 or 13 digits carry the country. The copy that tested the
|
|
24
|
+
* prefix first also left every São Paulo-state mobile (DDDs 11-19) without its `55`, because it
|
|
25
|
+
* read an 11-digit number starting with 1 as North American.
|
|
26
|
+
*
|
|
27
|
+
* Strict about what it accepts: a real DDD; a mobile is nine digits starting with 9 (every mobile
|
|
28
|
+
* has had the ninth digit since 2016); a landline is eight starting with 2-5; and a subscriber of
|
|
29
|
+
* one repeated digit is a placeholder somebody typed to get past a form.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseBrPhone(input: string): BrPhone | null;
|
|
32
|
+
/** Whether {@link parseBrPhone} reads it. */
|
|
33
|
+
export declare function isValidBrPhone(input: string): boolean;
|
|
34
|
+
/** `+5511987654321`, or `null` when it is not a Brazilian number. Drop the `+` for WhatsApp. */
|
|
35
|
+
export declare function toE164Br(input: string): string | null;
|
|
36
|
+
/** `(11) 98765-4321` or `(11) 3456-7890`. Anything it cannot read comes back as it was. */
|
|
37
|
+
export declare function formatBrPhone(input: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Formats a number as it is typed: `(41) 98822-9199`. Stops at 11 digits, the longest DDD plus
|
|
40
|
+
* mobile, because a longer string would render as a broken Brazilian number rather than a
|
|
41
|
+
* readable foreign one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function maskBrPhone(partial: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Both forms a Brazilian mobile may be registered under, the given one first: with the ninth
|
|
46
|
+
* digit and without it. Anything else comes back alone.
|
|
47
|
+
*
|
|
48
|
+
* An account created before the ninth digit can still be registered without it, and WhatsApp no
|
|
49
|
+
* longer reliably bridges the two, so a lookup that tries one form misses the person — or opens a
|
|
50
|
+
* second conversation with somebody already in one. Which form an account uses cannot be known
|
|
51
|
+
* from the number, so this gives candidates to look up, never a rewrite.
|
|
52
|
+
*
|
|
53
|
+
* Takes an INTERNATIONAL number (`5511987654321`, `+55 11 98765-4321`), not a bare national one:
|
|
54
|
+
* guessing that ten or eleven bare digits are Brazilian is exactly how a foreign number gets a
|
|
55
|
+
* "twin" that belongs to somebody else. Use {@link toE164Br} first for user input.
|
|
56
|
+
*/
|
|
57
|
+
export declare function brMobileVariants(international: string): string[];
|
|
58
|
+
//# sourceMappingURL=phone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phone.d.ts","sourceRoot":"","sources":["../src/phone.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,MAAM,WAAW,OAAO;IACtB,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;CAC7B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAuB1D;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,gGAAgG;AAChG,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGrD;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAchE"}
|
package/dist/phone.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brazilian phone numbers: read, check, format, and the two forms of an older mobile.
|
|
3
|
+
*
|
|
4
|
+
* Brazilian only, on purpose. A number that is not Brazilian comes back as `null`, never as a
|
|
5
|
+
* guess: a general parser reading bare digits guesses a country from the first ones, and for a
|
|
6
|
+
* Brazilian mobile typed without `+55` that guess is wrong often — area code 31 reads as the
|
|
7
|
+
* Netherlands and 81 as Japan. If you take international numbers too, send the ones that start
|
|
8
|
+
* with `+` and are not `+55` to a real international library, and keep this for the rest.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The 67 area codes (DDD) in use. Anything else — 00, 20, 23, 30, 90 — is a typo or a placeholder,
|
|
12
|
+
* never a number somebody can answer.
|
|
13
|
+
*/
|
|
14
|
+
const AREA_CODES = new Set("11 12 13 14 15 16 17 18 19 21 22 24 27 28 31 32 33 34 35 37 38 41 42 43 44 45 46 47 48 49 51 53 54 55 61 62 63 64 65 66 67 68 69 71 73 74 75 77 79 81 82 83 84 85 86 87 88 89 91 92 93 94 95 96 97 98 99".split(" "));
|
|
15
|
+
/**
|
|
16
|
+
* Reads a Brazilian number in whatever shape a person or a system wrote it — `(11) 98765-4321`,
|
|
17
|
+
* `+55 11 98765-4321`, `5511987654321`, `011 3456-7890` — or returns `null`.
|
|
18
|
+
*
|
|
19
|
+
* **Length decides before the prefix does**, because `55` is ambiguous: it is the country code and
|
|
20
|
+
* also a DDD (Rio Grande do Sul). Eleven digits starting with 55 are a gaúcho mobile, not a country
|
|
21
|
+
* code in front of nine digits; only 12 or 13 digits carry the country. The copy that tested the
|
|
22
|
+
* prefix first also left every São Paulo-state mobile (DDDs 11-19) without its `55`, because it
|
|
23
|
+
* read an 11-digit number starting with 1 as North American.
|
|
24
|
+
*
|
|
25
|
+
* Strict about what it accepts: a real DDD; a mobile is nine digits starting with 9 (every mobile
|
|
26
|
+
* has had the ninth digit since 2016); a landline is eight starting with 2-5; and a subscriber of
|
|
27
|
+
* one repeated digit is a placeholder somebody typed to get past a form.
|
|
28
|
+
*/
|
|
29
|
+
export function parseBrPhone(input) {
|
|
30
|
+
const trimmed = input.trim();
|
|
31
|
+
let digits = trimmed.replace(/\D/g, "");
|
|
32
|
+
if (trimmed.startsWith("+")) {
|
|
33
|
+
// A number written with + says its country, and only +55 is ours.
|
|
34
|
+
if (!digits.startsWith("55"))
|
|
35
|
+
return null;
|
|
36
|
+
digits = digits.slice(2);
|
|
37
|
+
}
|
|
38
|
+
else if (digits.startsWith("0") && (digits.length === 11 || digits.length === 12)) {
|
|
39
|
+
// The trunk 0 people dial between cities. No DDD starts with 0, so this cannot be anything else.
|
|
40
|
+
// ponytail: the carrier code form (0 + 2-digit carrier + DDD) is not read; it comes back null.
|
|
41
|
+
digits = digits.slice(1);
|
|
42
|
+
}
|
|
43
|
+
else if (digits.length === 12 || digits.length === 13) {
|
|
44
|
+
if (!digits.startsWith("55"))
|
|
45
|
+
return null;
|
|
46
|
+
digits = digits.slice(2);
|
|
47
|
+
}
|
|
48
|
+
if (digits.length !== 10 && digits.length !== 11)
|
|
49
|
+
return null;
|
|
50
|
+
const ddd = digits.slice(0, 2);
|
|
51
|
+
const subscriber = digits.slice(2);
|
|
52
|
+
if (!AREA_CODES.has(ddd) || /^(\d)\1+$/.test(subscriber))
|
|
53
|
+
return null;
|
|
54
|
+
if (subscriber.length === 9)
|
|
55
|
+
return subscriber.startsWith("9") ? { ddd, subscriber, kind: "mobile" } : null;
|
|
56
|
+
return /^[2-5]/.test(subscriber) ? { ddd, subscriber, kind: "landline" } : null;
|
|
57
|
+
}
|
|
58
|
+
/** Whether {@link parseBrPhone} reads it. */
|
|
59
|
+
export function isValidBrPhone(input) {
|
|
60
|
+
return parseBrPhone(input) !== null;
|
|
61
|
+
}
|
|
62
|
+
/** `+5511987654321`, or `null` when it is not a Brazilian number. Drop the `+` for WhatsApp. */
|
|
63
|
+
export function toE164Br(input) {
|
|
64
|
+
const phone = parseBrPhone(input);
|
|
65
|
+
return phone === null ? null : `+55${phone.ddd}${phone.subscriber}`;
|
|
66
|
+
}
|
|
67
|
+
/** `(11) 98765-4321` or `(11) 3456-7890`. Anything it cannot read comes back as it was. */
|
|
68
|
+
export function formatBrPhone(input) {
|
|
69
|
+
const phone = parseBrPhone(input);
|
|
70
|
+
if (phone === null)
|
|
71
|
+
return input;
|
|
72
|
+
const { ddd, subscriber } = phone;
|
|
73
|
+
return `(${ddd}) ${subscriber.slice(0, -4)}-${subscriber.slice(-4)}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Formats a number as it is typed: `(41) 98822-9199`. Stops at 11 digits, the longest DDD plus
|
|
77
|
+
* mobile, because a longer string would render as a broken Brazilian number rather than a
|
|
78
|
+
* readable foreign one.
|
|
79
|
+
*/
|
|
80
|
+
export function maskBrPhone(partial) {
|
|
81
|
+
const d = partial.replace(/\D/g, "").slice(0, 11);
|
|
82
|
+
if (d.length === 0)
|
|
83
|
+
return "";
|
|
84
|
+
if (d.length <= 2)
|
|
85
|
+
return `(${d}`;
|
|
86
|
+
if (d.length <= 6)
|
|
87
|
+
return `(${d.slice(0, 2)}) ${d.slice(2)}`;
|
|
88
|
+
if (d.length <= 10)
|
|
89
|
+
return `(${d.slice(0, 2)}) ${d.slice(2, 6)}-${d.slice(6)}`;
|
|
90
|
+
return `(${d.slice(0, 2)}) ${d.slice(2, 7)}-${d.slice(7)}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Both forms a Brazilian mobile may be registered under, the given one first: with the ninth
|
|
94
|
+
* digit and without it. Anything else comes back alone.
|
|
95
|
+
*
|
|
96
|
+
* An account created before the ninth digit can still be registered without it, and WhatsApp no
|
|
97
|
+
* longer reliably bridges the two, so a lookup that tries one form misses the person — or opens a
|
|
98
|
+
* second conversation with somebody already in one. Which form an account uses cannot be known
|
|
99
|
+
* from the number, so this gives candidates to look up, never a rewrite.
|
|
100
|
+
*
|
|
101
|
+
* Takes an INTERNATIONAL number (`5511987654321`, `+55 11 98765-4321`), not a bare national one:
|
|
102
|
+
* guessing that ten or eleven bare digits are Brazilian is exactly how a foreign number gets a
|
|
103
|
+
* "twin" that belongs to somebody else. Use {@link toE164Br} first for user input.
|
|
104
|
+
*/
|
|
105
|
+
export function brMobileVariants(international) {
|
|
106
|
+
const digits = international.replace(/\D/g, "");
|
|
107
|
+
const match = /^55(\d{2})(\d{8,9})$/.exec(digits);
|
|
108
|
+
if (match === null)
|
|
109
|
+
return [digits];
|
|
110
|
+
const [, ddd = "", subscriber = ""] = match;
|
|
111
|
+
if (!AREA_CODES.has(ddd))
|
|
112
|
+
return [digits];
|
|
113
|
+
if (subscriber.length === 9 && subscriber.startsWith("9")) {
|
|
114
|
+
return [digits, `55${ddd}${subscriber.slice(1)}`];
|
|
115
|
+
}
|
|
116
|
+
// Eight digits starting 6-9 is a mobile from before the ninth digit; 2-5 is a landline, which
|
|
117
|
+
// never had one.
|
|
118
|
+
if (subscriber.length === 8 && /^[6-9]/.test(subscriber))
|
|
119
|
+
return [digits, `55${ddd}9${subscriber}`];
|
|
120
|
+
return [digits];
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=phone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phone.js","sourceRoot":"","sources":["../src/phone.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gusnips/br",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Brazilian documents and phone numbers: CPF and CNPJ (the alphanumeric CNPJ too), phones, CEP. Check digits, display, input masks. Zero dependencies, no framework.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Gustavo Salomé",
|
|
8
|
+
"homepage": "https://github.com/gusnips/frontkit/tree/main/br#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/gusnips/frontkit/issues"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"brazil",
|
|
34
|
+
"cpf",
|
|
35
|
+
"cnpj",
|
|
36
|
+
"cep",
|
|
37
|
+
"phone",
|
|
38
|
+
"validation",
|
|
39
|
+
"typescript"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "rm -rf dist && tsc",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run --passWithNoTests",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"lint": "eslint src",
|
|
47
|
+
"sync:docs": "cp ../LICENSE .",
|
|
48
|
+
"prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build && bun run sync:docs",
|
|
49
|
+
"release:patch": "bun pm version patch && bun publish --access public",
|
|
50
|
+
"release:minor": "bun pm version minor && bun publish --access public",
|
|
51
|
+
"release:major": "bun pm version major && bun publish --access public"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.1.2"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/gusnips/frontkit.git",
|
|
60
|
+
"directory": "br"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/cep.test.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatCep, maskCep, normalizeCep } from "./cep.ts";
|
|
3
|
+
|
|
4
|
+
describe("CEP", () => {
|
|
5
|
+
it("reads eight digits in any punctuation", () => {
|
|
6
|
+
expect(normalizeCep("01310-100")).toBe("01310100");
|
|
7
|
+
expect(normalizeCep("01.310-100")).toBe("01310100");
|
|
8
|
+
expect(normalizeCep("1310100")).toBe(null);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("formats a complete one and leaves the rest alone", () => {
|
|
12
|
+
expect(formatCep("01310100")).toBe("01310-100");
|
|
13
|
+
expect(formatCep("1310100")).toBe("1310100");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("masks as it is typed", () => {
|
|
17
|
+
expect(["013", "01310", "013101", "0131010099"].map(maskCep)).toEqual([
|
|
18
|
+
"013",
|
|
19
|
+
"01310",
|
|
20
|
+
"01310-1",
|
|
21
|
+
"01310-100",
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/cep.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CEP, the Brazilian postal code: eight digits, shown as `01310-100`.
|
|
3
|
+
*
|
|
4
|
+
* Only the shape lives here. Looking a CEP up is a network call to a provider you choose, and a
|
|
5
|
+
* CEP can be real and still have no street — a whole small town can share one — so "the lookup
|
|
6
|
+
* found no street" is not "this CEP is invalid".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The eight digits, or `null` when there are not eight. */
|
|
10
|
+
export function normalizeCep(value: string): string | null {
|
|
11
|
+
const digits = value.replace(/\D/g, "");
|
|
12
|
+
return digits.length === 8 ? digits : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `01310-100`. Anything that is not eight digits comes back as it was. */
|
|
16
|
+
export function formatCep(value: string): string {
|
|
17
|
+
const digits = normalizeCep(value);
|
|
18
|
+
return digits === null ? value : `${digits.slice(0, 5)}-${digits.slice(5)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Formats a CEP as it is typed. Stops at eight digits. */
|
|
22
|
+
export function maskCep(partial: string): string {
|
|
23
|
+
const d = partial.replace(/\D/g, "").slice(0, 8);
|
|
24
|
+
return d.length <= 5 ? d : `${d.slice(0, 5)}-${d.slice(5)}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
classifyDocument,
|
|
4
|
+
cnpjCheckDigits,
|
|
5
|
+
cpfCheckDigits,
|
|
6
|
+
formatCnpj,
|
|
7
|
+
formatCpf,
|
|
8
|
+
isValidCnpj,
|
|
9
|
+
isValidCpf,
|
|
10
|
+
maskCnpj,
|
|
11
|
+
maskCpf,
|
|
12
|
+
maskDocument,
|
|
13
|
+
redactCpf,
|
|
14
|
+
} from "./document.ts";
|
|
15
|
+
|
|
16
|
+
// The Receita Federal's own example of the alphanumeric format.
|
|
17
|
+
const ALPHANUMERIC = "12.ABC.345/01DE-35";
|
|
18
|
+
|
|
19
|
+
describe("CPF", () => {
|
|
20
|
+
it("accepts real check digits, formatted or bare", () => {
|
|
21
|
+
expect(isValidCpf("111.444.777-35")).toBe(true);
|
|
22
|
+
expect(isValidCpf("52998224725")).toBe(true);
|
|
23
|
+
expect(cpfCheckDigits("111444777")).toBe("35");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("refuses a wrong digit, a wrong length, a letter, and one repeated digit", () => {
|
|
27
|
+
expect(isValidCpf("111.444.777-36")).toBe(false);
|
|
28
|
+
expect(isValidCpf("1114447773")).toBe(false);
|
|
29
|
+
expect(isValidCpf("111.444.777-3A5")).toBe(false);
|
|
30
|
+
// Every repeated digit passes the arithmetic, which is why it is refused on its own.
|
|
31
|
+
for (let d = 0; d <= 9; d++) expect(isValidCpf(String(d).repeat(11))).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("zero-pads for display, because datasets store it as a number", () => {
|
|
35
|
+
expect(formatCpf("1144477735")).toBe("011.444.777-35");
|
|
36
|
+
expect(formatCpf("not a cpf")).toBe("not a cpf");
|
|
37
|
+
expect(redactCpf("111.444.777-35")).toBe("111.***.***-35");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("masks as it is typed, without padding", () => {
|
|
41
|
+
expect(["1", "1114", "1114447", "1114447773", "111444777356"].map(maskCpf)).toEqual([
|
|
42
|
+
"1",
|
|
43
|
+
"111.4",
|
|
44
|
+
"111.444.7",
|
|
45
|
+
"111.444.777-3",
|
|
46
|
+
"111.444.777-35",
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("CNPJ", () => {
|
|
52
|
+
it("checks the alphanumeric CNPJ that every copy it replaces refused", () => {
|
|
53
|
+
expect(isValidCnpj(ALPHANUMERIC)).toBe(true);
|
|
54
|
+
expect(isValidCnpj("12abc34501de35")).toBe(true);
|
|
55
|
+
expect(cnpjCheckDigits("12ABC34501DE")).toBe("35");
|
|
56
|
+
expect(isValidCnpj("12.ABC.345/01DE-36")).toBe(false);
|
|
57
|
+
// The check digits stay numeric.
|
|
58
|
+
expect(isValidCnpj("12.ABC.345/01DE-3A")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("checks a numeric CNPJ the way it always did", () => {
|
|
62
|
+
expect(isValidCnpj("11.222.333/0001-81")).toBe(true);
|
|
63
|
+
expect(isValidCnpj("11222333000182")).toBe(false);
|
|
64
|
+
for (let d = 0; d <= 9; d++) expect(isValidCnpj(String(d).repeat(14))).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// An independent derivation of the numeric weights — the left-to-right 5→2, 9→2 walk the old
|
|
68
|
+
// copies used — so a mistake in the right-to-left formula cannot agree with itself.
|
|
69
|
+
it("agrees with the classic numeric walk on 2,000 random bases", () => {
|
|
70
|
+
const classic = (base: string): string => {
|
|
71
|
+
const next = (s: string): number => {
|
|
72
|
+
let sum = 0;
|
|
73
|
+
let weight = s.length === 12 ? 5 : 6;
|
|
74
|
+
for (const c of s) {
|
|
75
|
+
sum += Number(c) * weight;
|
|
76
|
+
weight = weight === 2 ? 9 : weight - 1;
|
|
77
|
+
}
|
|
78
|
+
return sum % 11 < 2 ? 0 : 11 - (sum % 11);
|
|
79
|
+
};
|
|
80
|
+
const first = next(base);
|
|
81
|
+
return `${first}${next(base + first)}`;
|
|
82
|
+
};
|
|
83
|
+
for (let i = 0; i < 2000; i++) {
|
|
84
|
+
const base = Array.from({ length: 12 }, () => Math.floor(Math.random() * 10)).join("");
|
|
85
|
+
expect(cnpjCheckDigits(base)).toBe(classic(base));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps the letters when it formats, and pads only a numeric value", () => {
|
|
90
|
+
// One copy stripped the letters here and displayed a different, numeric CNPJ.
|
|
91
|
+
expect(formatCnpj("12abc34501de35")).toBe(ALPHANUMERIC);
|
|
92
|
+
expect(formatCnpj("1222333000181")).toBe("01.222.333/0001-81");
|
|
93
|
+
expect(formatCnpj("nope")).toBe("nope");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("masks letters as they are typed and keeps the last two numeric", () => {
|
|
97
|
+
expect(maskCnpj("12abc")).toBe("12.ABC");
|
|
98
|
+
expect(maskCnpj("12ABC34501DE")).toBe("12.ABC.345/01DE");
|
|
99
|
+
expect(maskCnpj("12ABC34501DEX3")).toBe("12.ABC.345/01DE-3");
|
|
100
|
+
expect(maskCnpj("12ABC34501DE35999")).toBe(ALPHANUMERIC);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("either document", () => {
|
|
105
|
+
it("classifies by shape, not by check digits", () => {
|
|
106
|
+
// A dataset's test CPF fails the check digit and is still a person.
|
|
107
|
+
expect(classifyDocument("111.444.777-00")).toBe("cpf");
|
|
108
|
+
expect(classifyDocument(ALPHANUMERIC)).toBe("cnpj");
|
|
109
|
+
expect(classifyDocument("11.222.333/0001-81")).toBe("cnpj");
|
|
110
|
+
expect(classifyDocument("123")).toBe(null);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("switches from the CPF mask to the CNPJ mask past 11 digits, or at the first letter", () => {
|
|
114
|
+
expect(maskDocument("11144477735")).toBe("111.444.777-35");
|
|
115
|
+
expect(maskDocument("112223330001")).toBe("11.222.333/0001");
|
|
116
|
+
expect(maskDocument("12A")).toBe("12.A");
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/document.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPF and CNPJ: check digits, display, input masks.
|
|
3
|
+
*
|
|
4
|
+
* Three concerns sit side by side here, and the adopters that merged them kept them apart for a
|
|
5
|
+
* reason: a DISPLAY formatter works on a complete value and zero-pads it, a MASK formats partial
|
|
6
|
+
* input as somebody types and never pads, and a VALIDATOR says whether the check digits hold.
|
|
7
|
+
*
|
|
8
|
+
* The CNPJ is alphanumeric from July 2026 (Receita Federal, IN RFB 2.229/2024): the first twelve
|
|
9
|
+
* characters may be A-Z as well as digits, and the two check digits stay numeric. Every copy this
|
|
10
|
+
* package replaced stripped letters before it checked, so a company registered under the new
|
|
11
|
+
* format was refused everywhere — and one display formatter turned `12.ABC.345/01DE-35` into a
|
|
12
|
+
* different, numeric CNPJ. The check digits are the same mod-11 over the same weights, with each
|
|
13
|
+
* character counting as its ASCII code minus 48, so a digit counts as itself and `A` as 17.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type DocumentKind = "cpf" | "cnpj";
|
|
17
|
+
|
|
18
|
+
/** Digits only. A CPF is numeric and stays numeric, so a pasted label or letter is noise. */
|
|
19
|
+
function cpfCharacters(value: string): string {
|
|
20
|
+
return value.replace(/\D/g, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Digits and A-Z, uppercased — the characters a CNPJ can hold. */
|
|
24
|
+
function cnpjCharacters(value: string): string {
|
|
25
|
+
return value.toUpperCase().replace(/[^0-9A-Z]/g, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Which document this is, by SHAPE only: 11 digits is a CPF, 14 CNPJ characters is a CNPJ.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately not a validity check. Government datasets carry test CPFs that fail the check digit,
|
|
32
|
+
* and an adopter reading those has to know "this is a person" without calling the record invalid —
|
|
33
|
+
* keep "what is it" and "does it check" as two questions.
|
|
34
|
+
*/
|
|
35
|
+
export function classifyDocument(value: string): DocumentKind | null {
|
|
36
|
+
if (cpfCharacters(value).length === 11 && !/[A-Za-z]/.test(value)) return "cpf";
|
|
37
|
+
return /^[0-9A-Z]{12}\d{2}$/.test(cnpjCharacters(value)) ? "cnpj" : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The two check digits for the first nine digits of a CPF. */
|
|
41
|
+
export function cpfCheckDigits(base9: string): string {
|
|
42
|
+
const digits = cpfCharacters(base9);
|
|
43
|
+
if (digits.length !== 9) throw new RangeError("A CPF base is 9 digits.");
|
|
44
|
+
const next = (source: string): number => {
|
|
45
|
+
let sum = 0;
|
|
46
|
+
for (let i = 0; i < source.length; i++) sum += Number(source[i]) * (source.length + 1 - i);
|
|
47
|
+
const rest = (sum * 10) % 11;
|
|
48
|
+
return rest === 10 ? 0 : rest;
|
|
49
|
+
};
|
|
50
|
+
const first = next(digits);
|
|
51
|
+
return `${first}${next(`${digits}${first}`)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The two check digits for the first twelve characters of a CNPJ, numeric or alphanumeric. */
|
|
55
|
+
export function cnpjCheckDigits(base12: string): string {
|
|
56
|
+
const chars = cnpjCharacters(base12);
|
|
57
|
+
if (!/^[0-9A-Z]{12}$/.test(chars)) throw new RangeError("A CNPJ base is 12 characters.");
|
|
58
|
+
const next = (source: string): number => {
|
|
59
|
+
let sum = 0;
|
|
60
|
+
// The weights run 2, 3 … 9 from the RIGHT and wrap, which is the 5→2, 9→2 walk read backwards.
|
|
61
|
+
for (let i = 0; i < source.length; i++) {
|
|
62
|
+
const weight = ((source.length - 1 - i) % 8) + 2;
|
|
63
|
+
sum += (source.charCodeAt(i) - 48) * weight;
|
|
64
|
+
}
|
|
65
|
+
const rest = sum % 11;
|
|
66
|
+
return rest < 2 ? 0 : 11 - rest;
|
|
67
|
+
};
|
|
68
|
+
const first = next(chars);
|
|
69
|
+
return `${first}${next(`${chars}${first}`)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether a CPF's check digits hold. Formatted or bare. Eleven repeated digits never pass. */
|
|
73
|
+
export function isValidCpf(value: string): boolean {
|
|
74
|
+
const digits = cpfCharacters(value);
|
|
75
|
+
if (digits.length !== 11 || /[A-Za-z]/.test(value)) return false;
|
|
76
|
+
if (/^(\d)\1{10}$/.test(digits)) return false;
|
|
77
|
+
return cpfCheckDigits(digits.slice(0, 9)) === digits.slice(9);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether a CNPJ's check digits hold — numeric or alphanumeric, formatted or bare. */
|
|
81
|
+
export function isValidCnpj(value: string): boolean {
|
|
82
|
+
const chars = cnpjCharacters(value);
|
|
83
|
+
if (!/^[0-9A-Z]{12}\d{2}$/.test(chars)) return false;
|
|
84
|
+
if (/^(.)\1{13}$/.test(chars)) return false;
|
|
85
|
+
return cnpjCheckDigits(chars.slice(0, 12)) === chars.slice(12);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A complete CPF for display: `123.456.789-09`.
|
|
90
|
+
*
|
|
91
|
+
* Zero-pads, because government datasets store it as a number and drop the leading zeros. Anything
|
|
92
|
+
* that is not a CPF comes back as it was, so a bad row shows as itself rather than as "".
|
|
93
|
+
*/
|
|
94
|
+
export function formatCpf(value: string): string {
|
|
95
|
+
const digits = cpfCharacters(value);
|
|
96
|
+
if (digits.length === 0 || digits.length > 11 || /[A-Za-z]/.test(value)) return value;
|
|
97
|
+
return digits.padStart(11, "0").replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A complete CNPJ for display: `12.345.678/0001-95` or `12.ABC.345/01DE-35`.
|
|
102
|
+
*
|
|
103
|
+
* Zero-pads a numeric value for the same reason {@link formatCpf} does. An alphanumeric one cannot
|
|
104
|
+
* have lost a zero to a number column, so it is never padded. Anything else comes back as it was.
|
|
105
|
+
*/
|
|
106
|
+
export function formatCnpj(value: string): string {
|
|
107
|
+
let chars = cnpjCharacters(value);
|
|
108
|
+
if (/^\d{1,13}$/.test(chars)) chars = chars.padStart(14, "0");
|
|
109
|
+
if (!/^[0-9A-Z]{12}\d{2}$/.test(chars)) return value;
|
|
110
|
+
return `${chars.slice(0, 2)}.${chars.slice(2, 5)}.${chars.slice(5, 8)}/${chars.slice(8, 12)}-${chars.slice(12)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A CPF for display with its middle hidden: `123.***.***-09`. Anything else comes back as it was. */
|
|
114
|
+
export function redactCpf(value: string): string {
|
|
115
|
+
const digits = cpfCharacters(value);
|
|
116
|
+
if (digits.length !== 11 || /[A-Za-z]/.test(value)) return value;
|
|
117
|
+
return `${digits.slice(0, 3)}.***.***-${digits.slice(9)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Formats a CPF as it is typed. Never pads; stops at 11 digits. */
|
|
121
|
+
export function maskCpf(partial: string): string {
|
|
122
|
+
const d = cpfCharacters(partial).slice(0, 11);
|
|
123
|
+
if (d.length <= 3) return d;
|
|
124
|
+
if (d.length <= 6) return `${d.slice(0, 3)}.${d.slice(3)}`;
|
|
125
|
+
if (d.length <= 9) return `${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6)}`;
|
|
126
|
+
return `${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6, 9)}-${d.slice(9)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Formats a CNPJ as it is typed, letters included. Never pads; stops at 14 characters, and the last
|
|
131
|
+
* two only take digits, because the check digits stay numeric.
|
|
132
|
+
*/
|
|
133
|
+
export function maskCnpj(partial: string): string {
|
|
134
|
+
const all = cnpjCharacters(partial);
|
|
135
|
+
const c = all.slice(0, 12) + all.slice(12).replace(/\D/g, "").slice(0, 2);
|
|
136
|
+
if (c.length <= 2) return c;
|
|
137
|
+
if (c.length <= 5) return `${c.slice(0, 2)}.${c.slice(2)}`;
|
|
138
|
+
if (c.length <= 8) return `${c.slice(0, 2)}.${c.slice(2, 5)}.${c.slice(5)}`;
|
|
139
|
+
if (c.length <= 12) return `${c.slice(0, 2)}.${c.slice(2, 5)}.${c.slice(5, 8)}/${c.slice(8)}`;
|
|
140
|
+
return `${c.slice(0, 2)}.${c.slice(2, 5)}.${c.slice(5, 8)}/${c.slice(8, 12)}-${c.slice(12)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* One field for either document: a CPF mask up to 11 digits, a CNPJ mask past that — or as soon as
|
|
145
|
+
* a letter is typed, since only a CNPJ can carry one.
|
|
146
|
+
*/
|
|
147
|
+
export function maskDocument(partial: string): string {
|
|
148
|
+
const chars = cnpjCharacters(partial);
|
|
149
|
+
return chars.length <= 11 && /^\d*$/.test(chars) ? maskCpf(chars) : maskCnpj(chars);
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
classifyDocument,
|
|
3
|
+
cnpjCheckDigits,
|
|
4
|
+
cpfCheckDigits,
|
|
5
|
+
formatCnpj,
|
|
6
|
+
formatCpf,
|
|
7
|
+
isValidCnpj,
|
|
8
|
+
isValidCpf,
|
|
9
|
+
maskCnpj,
|
|
10
|
+
maskCpf,
|
|
11
|
+
maskDocument,
|
|
12
|
+
redactCpf,
|
|
13
|
+
} from "./document.ts";
|
|
14
|
+
export type { DocumentKind } from "./document.ts";
|
|
15
|
+
export {
|
|
16
|
+
brMobileVariants,
|
|
17
|
+
formatBrPhone,
|
|
18
|
+
isValidBrPhone,
|
|
19
|
+
maskBrPhone,
|
|
20
|
+
parseBrPhone,
|
|
21
|
+
toE164Br,
|
|
22
|
+
} from "./phone.ts";
|
|
23
|
+
export type { BrPhone } from "./phone.ts";
|
|
24
|
+
export { formatCep, maskCep, normalizeCep } from "./cep.ts";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
brMobileVariants,
|
|
4
|
+
formatBrPhone,
|
|
5
|
+
isValidBrPhone,
|
|
6
|
+
maskBrPhone,
|
|
7
|
+
parseBrPhone,
|
|
8
|
+
toE164Br,
|
|
9
|
+
} from "./phone.ts";
|
|
10
|
+
|
|
11
|
+
describe("parseBrPhone", () => {
|
|
12
|
+
it("reads a number in every shape it gets written in", () => {
|
|
13
|
+
for (const input of [
|
|
14
|
+
"(11) 98765-4321",
|
|
15
|
+
"11987654321",
|
|
16
|
+
"5511987654321",
|
|
17
|
+
"+55 11 98765-4321",
|
|
18
|
+
"011 98765-4321",
|
|
19
|
+
]) {
|
|
20
|
+
expect(toE164Br(input)).toBe("+5511987654321");
|
|
21
|
+
}
|
|
22
|
+
expect(parseBrPhone("(11) 3456-7890")).toEqual({
|
|
23
|
+
ddd: "11",
|
|
24
|
+
subscriber: "34567890",
|
|
25
|
+
kind: "landline",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// One copy tested an 11-digit number starting with 1 as North American before it tested for
|
|
30
|
+
// Brazil, so every mobile in DDDs 11-19 was stored without its 55 and inbound lookups missed.
|
|
31
|
+
it("keeps São Paulo-state mobiles Brazilian", () => {
|
|
32
|
+
for (let ddd = 11; ddd <= 19; ddd++) {
|
|
33
|
+
expect(toE164Br(`${ddd}987654321`)).toBe(`+55${ddd}987654321`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 55 is the country code AND a DDD. Length decides first.
|
|
38
|
+
it("reads 11 digits starting with 55 as DDD 55, and 13 as the country code", () => {
|
|
39
|
+
expect(toE164Br("55987654321")).toBe("+5555987654321");
|
|
40
|
+
expect(toE164Br("5555987654321")).toBe("+5555987654321");
|
|
41
|
+
expect(toE164Br("5534567890")).toBe("+555534567890");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// A general parser guessed a country from bare digits: 31 became the Netherlands, 81 Japan.
|
|
45
|
+
it("never guesses a foreign country", () => {
|
|
46
|
+
expect(toE164Br("31987654321")).toBe("+5531987654321");
|
|
47
|
+
expect(toE164Br("81987654321")).toBe("+5581987654321");
|
|
48
|
+
expect(parseBrPhone("+1 415 555 0100")).toBe(null);
|
|
49
|
+
expect(parseBrPhone("+351 912 345 678")).toBe(null);
|
|
50
|
+
expect(parseBrPhone("14155550100")).toBe(null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("refuses what nobody can answer", () => {
|
|
54
|
+
expect(isValidBrPhone("(20) 98765-4321")).toBe(false); // no such DDD
|
|
55
|
+
expect(isValidBrPhone("(11) 99999-9999")).toBe(false); // a placeholder
|
|
56
|
+
expect(isValidBrPhone("(31) 61234-5678")).toBe(false); // nine digits, not a mobile
|
|
57
|
+
expect(isValidBrPhone("(11) 8765-4321")).toBe(false); // a mobile from before the ninth digit
|
|
58
|
+
expect(isValidBrPhone("12345")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("display", () => {
|
|
63
|
+
it("formats what it can read and leaves the rest alone", () => {
|
|
64
|
+
expect(formatBrPhone("5511987654321")).toBe("(11) 98765-4321");
|
|
65
|
+
expect(formatBrPhone("1134567890")).toBe("(11) 3456-7890");
|
|
66
|
+
expect(formatBrPhone("+1 415 555 0100")).toBe("+1 415 555 0100");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("masks as it is typed", () => {
|
|
70
|
+
expect(
|
|
71
|
+
["4", "41", "41988", "4198822919", "41988229199", "419882291990"].map(maskBrPhone),
|
|
72
|
+
).toEqual(["(4", "(41", "(41) 988", "(41) 9882-2919", "(41) 98822-9199", "(41) 98822-9199"]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("brMobileVariants", () => {
|
|
77
|
+
it("gives both forms of a mobile, the given one first", () => {
|
|
78
|
+
expect(brMobileVariants("5511987654321")).toEqual(["5511987654321", "551187654321"]);
|
|
79
|
+
expect(brMobileVariants("+55 11 8765-4321")).toEqual(["551187654321", "5511987654321"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("gives a landline, a foreign number and an unknown DDD alone", () => {
|
|
83
|
+
expect(brMobileVariants("551134567890")).toEqual(["551134567890"]);
|
|
84
|
+
expect(brMobileVariants("14155550100")).toEqual(["14155550100"]);
|
|
85
|
+
expect(brMobileVariants("5520987654321")).toEqual(["5520987654321"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Guessing that bare digits are Brazilian is how a foreign number got a twin that belongs to
|
|
89
|
+
// someone else.
|
|
90
|
+
it("does not treat a bare national number as international", () => {
|
|
91
|
+
expect(brMobileVariants("11987654321")).toEqual(["11987654321"]);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/src/phone.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brazilian phone numbers: read, check, format, and the two forms of an older mobile.
|
|
3
|
+
*
|
|
4
|
+
* Brazilian only, on purpose. A number that is not Brazilian comes back as `null`, never as a
|
|
5
|
+
* guess: a general parser reading bare digits guesses a country from the first ones, and for a
|
|
6
|
+
* Brazilian mobile typed without `+55` that guess is wrong often — area code 31 reads as the
|
|
7
|
+
* Netherlands and 81 as Japan. If you take international numbers too, send the ones that start
|
|
8
|
+
* with `+` and are not `+55` to a real international library, and keep this for the rest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The 67 area codes (DDD) in use. Anything else — 00, 20, 23, 30, 90 — is a typo or a placeholder,
|
|
13
|
+
* never a number somebody can answer.
|
|
14
|
+
*/
|
|
15
|
+
const AREA_CODES = new Set(
|
|
16
|
+
"11 12 13 14 15 16 17 18 19 21 22 24 27 28 31 32 33 34 35 37 38 41 42 43 44 45 46 47 48 49 51 53 54 55 61 62 63 64 65 66 67 68 69 71 73 74 75 77 79 81 82 83 84 85 86 87 88 89 91 92 93 94 95 96 97 98 99".split(
|
|
17
|
+
" ",
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export interface BrPhone {
|
|
22
|
+
/** The two-digit area code. */
|
|
23
|
+
ddd: string;
|
|
24
|
+
/** Everything after it: nine digits for a mobile, eight for a landline. */
|
|
25
|
+
subscriber: string;
|
|
26
|
+
kind: "mobile" | "landline";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads a Brazilian number in whatever shape a person or a system wrote it — `(11) 98765-4321`,
|
|
31
|
+
* `+55 11 98765-4321`, `5511987654321`, `011 3456-7890` — or returns `null`.
|
|
32
|
+
*
|
|
33
|
+
* **Length decides before the prefix does**, because `55` is ambiguous: it is the country code and
|
|
34
|
+
* also a DDD (Rio Grande do Sul). Eleven digits starting with 55 are a gaúcho mobile, not a country
|
|
35
|
+
* code in front of nine digits; only 12 or 13 digits carry the country. The copy that tested the
|
|
36
|
+
* prefix first also left every São Paulo-state mobile (DDDs 11-19) without its `55`, because it
|
|
37
|
+
* read an 11-digit number starting with 1 as North American.
|
|
38
|
+
*
|
|
39
|
+
* Strict about what it accepts: a real DDD; a mobile is nine digits starting with 9 (every mobile
|
|
40
|
+
* has had the ninth digit since 2016); a landline is eight starting with 2-5; and a subscriber of
|
|
41
|
+
* one repeated digit is a placeholder somebody typed to get past a form.
|
|
42
|
+
*/
|
|
43
|
+
export function parseBrPhone(input: string): BrPhone | null {
|
|
44
|
+
const trimmed = input.trim();
|
|
45
|
+
let digits = trimmed.replace(/\D/g, "");
|
|
46
|
+
if (trimmed.startsWith("+")) {
|
|
47
|
+
// A number written with + says its country, and only +55 is ours.
|
|
48
|
+
if (!digits.startsWith("55")) return null;
|
|
49
|
+
digits = digits.slice(2);
|
|
50
|
+
} else if (digits.startsWith("0") && (digits.length === 11 || digits.length === 12)) {
|
|
51
|
+
// The trunk 0 people dial between cities. No DDD starts with 0, so this cannot be anything else.
|
|
52
|
+
// ponytail: the carrier code form (0 + 2-digit carrier + DDD) is not read; it comes back null.
|
|
53
|
+
digits = digits.slice(1);
|
|
54
|
+
} else if (digits.length === 12 || digits.length === 13) {
|
|
55
|
+
if (!digits.startsWith("55")) return null;
|
|
56
|
+
digits = digits.slice(2);
|
|
57
|
+
}
|
|
58
|
+
if (digits.length !== 10 && digits.length !== 11) return null;
|
|
59
|
+
|
|
60
|
+
const ddd = digits.slice(0, 2);
|
|
61
|
+
const subscriber = digits.slice(2);
|
|
62
|
+
if (!AREA_CODES.has(ddd) || /^(\d)\1+$/.test(subscriber)) return null;
|
|
63
|
+
if (subscriber.length === 9)
|
|
64
|
+
return subscriber.startsWith("9") ? { ddd, subscriber, kind: "mobile" } : null;
|
|
65
|
+
return /^[2-5]/.test(subscriber) ? { ddd, subscriber, kind: "landline" } : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether {@link parseBrPhone} reads it. */
|
|
69
|
+
export function isValidBrPhone(input: string): boolean {
|
|
70
|
+
return parseBrPhone(input) !== null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `+5511987654321`, or `null` when it is not a Brazilian number. Drop the `+` for WhatsApp. */
|
|
74
|
+
export function toE164Br(input: string): string | null {
|
|
75
|
+
const phone = parseBrPhone(input);
|
|
76
|
+
return phone === null ? null : `+55${phone.ddd}${phone.subscriber}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `(11) 98765-4321` or `(11) 3456-7890`. Anything it cannot read comes back as it was. */
|
|
80
|
+
export function formatBrPhone(input: string): string {
|
|
81
|
+
const phone = parseBrPhone(input);
|
|
82
|
+
if (phone === null) return input;
|
|
83
|
+
const { ddd, subscriber } = phone;
|
|
84
|
+
return `(${ddd}) ${subscriber.slice(0, -4)}-${subscriber.slice(-4)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Formats a number as it is typed: `(41) 98822-9199`. Stops at 11 digits, the longest DDD plus
|
|
89
|
+
* mobile, because a longer string would render as a broken Brazilian number rather than a
|
|
90
|
+
* readable foreign one.
|
|
91
|
+
*/
|
|
92
|
+
export function maskBrPhone(partial: string): string {
|
|
93
|
+
const d = partial.replace(/\D/g, "").slice(0, 11);
|
|
94
|
+
if (d.length === 0) return "";
|
|
95
|
+
if (d.length <= 2) return `(${d}`;
|
|
96
|
+
if (d.length <= 6) return `(${d.slice(0, 2)}) ${d.slice(2)}`;
|
|
97
|
+
if (d.length <= 10) return `(${d.slice(0, 2)}) ${d.slice(2, 6)}-${d.slice(6)}`;
|
|
98
|
+
return `(${d.slice(0, 2)}) ${d.slice(2, 7)}-${d.slice(7)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Both forms a Brazilian mobile may be registered under, the given one first: with the ninth
|
|
103
|
+
* digit and without it. Anything else comes back alone.
|
|
104
|
+
*
|
|
105
|
+
* An account created before the ninth digit can still be registered without it, and WhatsApp no
|
|
106
|
+
* longer reliably bridges the two, so a lookup that tries one form misses the person — or opens a
|
|
107
|
+
* second conversation with somebody already in one. Which form an account uses cannot be known
|
|
108
|
+
* from the number, so this gives candidates to look up, never a rewrite.
|
|
109
|
+
*
|
|
110
|
+
* Takes an INTERNATIONAL number (`5511987654321`, `+55 11 98765-4321`), not a bare national one:
|
|
111
|
+
* guessing that ten or eleven bare digits are Brazilian is exactly how a foreign number gets a
|
|
112
|
+
* "twin" that belongs to somebody else. Use {@link toE164Br} first for user input.
|
|
113
|
+
*/
|
|
114
|
+
export function brMobileVariants(international: string): string[] {
|
|
115
|
+
const digits = international.replace(/\D/g, "");
|
|
116
|
+
const match = /^55(\d{2})(\d{8,9})$/.exec(digits);
|
|
117
|
+
if (match === null) return [digits];
|
|
118
|
+
const [, ddd = "", subscriber = ""] = match;
|
|
119
|
+
if (!AREA_CODES.has(ddd)) return [digits];
|
|
120
|
+
if (subscriber.length === 9 && subscriber.startsWith("9")) {
|
|
121
|
+
return [digits, `55${ddd}${subscriber.slice(1)}`];
|
|
122
|
+
}
|
|
123
|
+
// Eight digits starting 6-9 is a mobile from before the ninth digit; 2-5 is a landline, which
|
|
124
|
+
// never had one.
|
|
125
|
+
if (subscriber.length === 8 && /^[6-9]/.test(subscriber))
|
|
126
|
+
return [digits, `55${ddd}9${subscriber}`];
|
|
127
|
+
return [digits];
|
|
128
|
+
}
|