Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
764c484
fix(states): read the state code ignoring case in every state-taking …
claude Sep 28, 2026
c947d54
perf(states): check a state code against the util's own table
claude Sep 28, 2026
f462c71
ci(tests): run a Chromium browser test once more when it loses a test…
claude Sep 28, 2026
f0279a3
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
19c90f9
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
f367666
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
510b8a9
refactor(states): read the state code through the shared helpers ever…
claude Sep 28, 2026
5a279f8
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
23818d3
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
b644d85
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
b139996
fix(ie): trim the state code, as the other utils that take a state do
claude Sep 28, 2026
1671624
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
d3e27a3
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
448040c
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 28, 2026
0d6bade
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 29, 2026
a0326e7
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 29, 2026
e13bb25
Merge branch 'claude/holiday-state-code' into claude/municipalities-s…
claude Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ parseCpf('746.506.880-00'); // 74650688000
Gera um CPF válido aleatório.

- O argumento opcional `state` (`StateCode`, ex. `"SP"`) fixa o dígito da região fiscal (o 9º) no código desse estado.
- Sem `state`, ou com um código desconhecido, um dígito de região fiscal aleatório é sorteado.
- `state` ignora maiúsculas/minúsculas e espaços nas pontas (`'sp'` é `'SP'`). Sem `state`, ou com um código desconhecido, um dígito de região fiscal aleatório é sorteado.

```javascript
import { generateCpf } from '@brazilian-utils/brazilian-utils'
Expand Down Expand Up @@ -1857,7 +1857,7 @@ Retorna os municípios brasileiros publicados pelo IBGE: todos os municípios, o

- Cada município (`Municipality`) é `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos. Ordenados por nome no locale "pt-BR".
- Só um `stateCode` omitido (ou `undefined`) pede a lista completa: `null` e `''` retornam `[]`.
- `stateCode` diferencia maiúsculas de minúsculas: `'sp'`, como um código desconhecido, retorna `[]`.
- `stateCode` ignora maiúsculas/minúsculas e espaços nas pontas: `'sp'` retorna os municípios de São Paulo, como `'SP'` (até a 2.4.0 retornava `[]`).
- Embute todos os 5571 municípios, os mesmos códigos da [Divisão Territorial Brasileira 2025](https://geoftp.ibge.gov.br/organizacao_do_territorio/estrutura_territorial/divisao_territorial/2025/DTB_2025.zip) do IBGE (data base 31/12/2025). Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle) para carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-municipalities`.

```javascript
Expand Down Expand Up @@ -1918,7 +1918,7 @@ Retorna os nomes das cidades brasileiras: todas as cidades, ou só as de um esta

- Ordenadas no locale "pt-BR".
- Qualquer `state` falsy pede a lista completa, enquanto `getMunicipalities` retorna `[]`.
- `state` diferencia maiúsculas de minúsculas: `'sp'`, como um código desconhecido, retorna `[]`.
- `state` ignora maiúsculas/minúsculas e espaços nas pontas: `'sp'` retorna as cidades de São Paulo, como `'SP'` (até a 2.4.0 retornava `[]`).
- Embute os 5571 nomes (~153,4 KB minificado, ~49,2 KB com gzip). Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle) para carregá-la sob demanda via `@brazilian-utils/brazilian-utils/get-cities`.

```javascript
Expand Down Expand Up @@ -2490,7 +2490,7 @@ parseVoterId('12345 01 59'); // '123450159'

Gera um título de eleitor válido aleatório. O argumento opcional `state` (`StateCode`, ou `"ZZ"` para um título expedido no exterior) define o código de unidade federativa.

- Uma UF desconhecida, ou um valor que não seja string, usa `"ZZ"` (UF `28`).
- `state` ignora maiúsculas/minúsculas e espaços nas pontas (`'sp'` é `'SP'`). Uma UF desconhecida, ou um valor que não seja string, usa `"ZZ"` (UF `28`).
- O resultado sempre tem 12 dígitos, com os zeros à esquerda do número sequencial; o mesmo título sem eles também é válido.

```javascript
Expand Down Expand Up @@ -3615,7 +3615,7 @@ removeAccents(''); // ''

Valida uma inscrição estadual para um estado. **Descontinuada:** a forma posicional `isValidIe(stateCode, ie)` continua funcionando, mas está descontinuada; use a forma com objeto `isValidIe({ value, stateCode })`.

- Recebe um único objeto (`IsValidIeParams`): `value` é a inscrição e `stateCode` o estado ao qual ela pertence (um `StateCode`, sem diferenciar maiúsculas de minúsculas).
- Recebe um único objeto (`IsValidIeParams`): `value` é a inscrição e `stateCode` o estado ao qual ela pertence (um `StateCode`, sem diferenciar maiúsculas de minúsculas e ignorando espaços em volta).
- Alguns estados têm casos especiais, um prefixo ou formato que a página do SINTEGRA não traz ou um desvio proposital dela (detalhes e fontes no JSDoc em `src/is-valid-ie`):
- GO: os prefixos 10, 11, 15 e 20 a 29, a união de fontes que divergem: a norma (IN nº 946/09-GSF, art. 39, I, na redação da IN nº 1.535/22-GSE) traz 10, 20 e 11, a página do SINTEGRA 10, 11 e 20 a 29, o roteiro de crítica de 2012 10, 11 e 15. O dígito verificador segue o roteiro, como na 2.4.0: resto 1 dá 1 na faixa 10103105 a 10119997, e 11094402 aceita os dois dígitos, casos especiais que a página do SINTEGRA (2022) não tem.
- MT: 11 dígitos, ou os 9 dígitos que a Portaria SEFAZ-MT nº 59/2025 (art. 8º, § 1º) prevê, lidos como a forma de 11 dígitos com dois zeros à esquerda (nenhum texto oficial traz a regra do dígito verificador da forma de 9 dígitos).
Expand All @@ -3637,6 +3637,7 @@ isValidIe({ value: '110042490114', stateCode: 'SP' }); // true
isValidIe({ value: 'P011004243002', stateCode: 'SP' }); // true (produtor rural)
isValidIe({ value: '0187634580933', stateCode: 'AC' }); // false
isValidIe({ value: '109161793', stateCode: 'go' }); // true (não diferencia maiúsculas de minúsculas)
isValidIe({ value: '109161793', stateCode: ' GO ' }); // true (espaços em volta são ignorados)
isValidIe({ value: '200000004', stateCode: 'GO' }); // true (prefixo 20)
isValidIe({ value: '130000019', stateCode: 'MT' }); // true (9 dígitos)
```
Expand Down Expand Up @@ -3692,7 +3693,7 @@ Fonte: [ISO/IEC 7812-1](https://www.iso.org/standard/70484.html).

Verifica a estrutura de um número de registro em conselho profissional (registro/inscrição profissional). Só a quantidade de dígitos e a UF são conferidas, nunca o dígito verificador, nem no CRC.

- Recebe um objeto (`IsValidRegistroProfissionalParams`): `value`, `council` (`RegistroProfissionalCouncil`: `"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e `stateCode` opcional (UF esperada).
- Recebe um objeto (`IsValidRegistroProfissionalParams`): `value`, `council` (`RegistroProfissionalCouncil`: `"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e `stateCode` opcional (UF esperada, sem diferenciar maiúsculas/minúsculas e ignorando espaços nas pontas).
- `"OAB"` e `"CRM"`: 4 a 6 dígitos mais a UF (`123456/SP`, `123456-SP`); `"CRO"`: 3 a 6 dígitos (`12345/SP`), ou a forma da Consolidação das Normas do CFO (Resolução CFO-63/2005), art. 115, § 1º: a sigla do Conselho Regional antes, ligada por hífen à categoria (`TPD`, `TSB`, `ASB`, `APD`, `CLM`/`CLF`, `LPM`/`LPF`, `PV`, `T`) quando houver, depois o número, seguido de `-IS` na secundária ou `-R` na remida (`CRO-SP 12345`, `CRO-SP-TPD 1234`, `CRO-SP 12345-IS`). Até a 2.4.0 essa forma era rejeitada.
- `"CRP"`: código regional de 2 dígitos (`01` a `24`) mais 4 a 6 dígitos (`06/12345`); `stateCode` é ignorado. O sistema CFP tem 24 regionais; o CRP-25 (Amapá) é só uma proposta.
- `"CRC"`: UF, 6 dígitos, tipo de registro (`O` ou `P`) e dígito verificador (`SP-123456/O-3`); transferência acrescenta `T` ou `S` e a UF destino (`SP-123456/O-3 T-MG`). `stateCode` confere a UF de origem. Essa forma e os registros `P`/`S` vêm do Manual de Registro de 2009; a Resolução CFC nº 1.707/2023, em vigor, só fixa uma numeração "única e sequencial em cada CRC" e o `T` da transferência, e o algoritmo do dígito verificador não é publicado.
Expand Down
13 changes: 7 additions & 6 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ parseCpf('746.506.880-00'); // 74650688000
Generate a valid random CPF.

- The optional `state` argument (`StateCode`, e.g. `"SP"`) fixes the região fiscal digit (the 9th) to that state's code.
- Without `state`, or with an unknown code, a random região fiscal digit is drawn.
- `state` ignores letter case and surrounding whitespace (`'sp'` is `'SP'`). Without `state`, or with an unknown code, a random região fiscal digit is drawn.

```javascript
import { generateCpf } from '@brazilian-utils/brazilian-utils'
Expand Down Expand Up @@ -1857,7 +1857,7 @@ Get the Brazilian municipalities published by the IBGE: every municipality, or o

- Each municipality (`Municipality`) is `{ code, name, stateCode }`, where `code` is the 7-digit IBGE code. Sorted by name in the "pt-BR" locale.
- Only an omitted (or `undefined`) `stateCode` asks for the full list: `null` and `''` return `[]`.
- `stateCode` is case-sensitive: `'sp'`, like an unknown code, returns `[]`.
- `stateCode` ignores letter case and surrounding whitespace: `'sp'` returns the São Paulo municipalities, as `'SP'` does (up to 2.4.0 it returned `[]`).
- Embeds all 5571 municipalities, the same codes as the IBGE [Divisão Territorial Brasileira 2025](https://geoftp.ibge.gov.br/organizacao_do_territorio/estrutura_territorial/divisao_territorial/2025/DTB_2025.zip) (data base 31/12/2025). See [Bundle size](getting-started.md#bundle-size) to lazy-load it via `@brazilian-utils/brazilian-utils/get-municipalities`.

```javascript
Expand Down Expand Up @@ -1918,7 +1918,7 @@ Get the names of Brazilian cities: every city, or only those of one state. **Dep

- Sorted in the "pt-BR" locale.
- Any falsy `state` asks for the full list, where `getMunicipalities` returns `[]`.
- `state` is case-sensitive: `'sp'`, like an unknown code, returns `[]`.
- `state` ignores letter case and surrounding whitespace: `'sp'` returns the São Paulo cities, as `'SP'` does (up to 2.4.0 it returned `[]`).
- Embeds all 5571 names (~153.4 KB minified, ~49.2 KB gzipped). See [Bundle size](getting-started.md#bundle-size) to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities`.

```javascript
Expand Down Expand Up @@ -2490,7 +2490,7 @@ parseVoterId('12345 01 59'); // '123450159'

Generate a valid random voter ID number. The optional `state` argument (`StateCode`, or `"ZZ"` for a voter ID issued abroad) sets the federative union code.

- An unknown state, or a value that is not a string, falls back to `"ZZ"` (UF `28`).
- `state` ignores letter case and surrounding whitespace (`'sp'` is `'SP'`). An unknown state, or a value that is not a string, falls back to `"ZZ"` (UF `28`).
- The result always has 12 digits, the leading zeros of the sequential number included; the same ID without them is valid too.

```javascript
Expand Down Expand Up @@ -3615,7 +3615,7 @@ removeAccents(''); // ''

Check if an inscrição estadual (state registration) is valid for a state. **Deprecated:** the positional form `isValidIe(stateCode, ie)` still works but is deprecated; use the object form `isValidIe({ value, stateCode })`.

- Takes a single object (`IsValidIeParams`): `value` is the registration and `stateCode` the state it belongs to (a `StateCode`, case-insensitive).
- Takes a single object (`IsValidIeParams`): `value` is the registration and `stateCode` the state it belongs to (a `StateCode`, case-insensitive, with surrounding whitespace ignored).
- Some states have special cases, a prefix or format the SINTEGRA page does not print or a deliberate deviation from it (details and sources in the JSDoc in `src/is-valid-ie`):
- GO: the prefixes 10, 11, 15 and 20 to 29, the union of sources that disagree: the norm (IN nº 946/09-GSF, art. 39, I, as worded by IN nº 1.535/22-GSE) gives 10, 20 and 11, the SINTEGRA page 10, 11 and 20 to 29, the 2012 roteiro de crítica 10, 11 and 15. The check digit follows the roteiro, as in 2.4.0: a remainder of 1 gives 1 in the range 10103105 to 10119997, and 11094402 takes either digit, special cases the SINTEGRA page (2022) does not have.
- MT: 11 digits, or the 9 digits Portaria SEFAZ-MT nº 59/2025 (art. 8º, § 1º) prescribes, read as the 11 digit form padded with two zeros (no official text gives the check digit rule of the 9 digit form).
Expand All @@ -3637,6 +3637,7 @@ isValidIe({ value: '110042490114', stateCode: 'SP' }); // true
isValidIe({ value: 'P011004243002', stateCode: 'SP' }); // true (produtor rural)
isValidIe({ value: '0187634580933', stateCode: 'AC' }); // false
isValidIe({ value: '109161793', stateCode: 'go' }); // true (case-insensitive)
isValidIe({ value: '109161793', stateCode: ' GO ' }); // true (surrounding whitespace ignored)
isValidIe({ value: '200000004', stateCode: 'GO' }); // true (prefix 20)
isValidIe({ value: '130000019', stateCode: 'MT' }); // true (9 digits)
```
Expand Down Expand Up @@ -3692,7 +3693,7 @@ Source: [ISO/IEC 7812-1](https://www.iso.org/standard/70484.html).

Check the structure of a professional council registration number (registro/inscrição profissional). Only the digit count and the UF are checked, never a check digit, even for CRC.

- Takes an object (`IsValidRegistroProfissionalParams`): `value`, `council` (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`, a `RegistroProfissionalCouncil`) and an optional `stateCode` (expected UF).
- Takes an object (`IsValidRegistroProfissionalParams`): `value`, `council` (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`, a `RegistroProfissionalCouncil`) and an optional `stateCode` (expected UF, letter case and surrounding whitespace ignored).
- `"OAB"` and `"CRM"`: 4 to 6 digits plus the UF (`123456/SP`, `123456-SP`); `"CRO"`: 3 to 6 digits (`12345/SP`), or the form of the Consolidação das Normas do CFO (Resolução CFO-63/2005), art. 115, § 1º: the sigla of the Conselho Regional first, joined by a hyphen to the category (`TPD`, `TSB`, `ASB`, `APD`, `CLM`/`CLF`, `LPM`/`LPF`, `PV`, `T`) when there is one, then the number, followed by `-IS` for a secundária or `-R` for a remida (`CRO-SP 12345`, `CRO-SP-TPD 1234`, `CRO-SP 12345-IS`). Up to 2.4.0 this form was rejected.
- `"CRP"`: a 2-digit regional code (`01` to `24`) plus 4 to 6 digits (`06/12345`); `stateCode` is ignored. The CFP system has 24 regionals; the CRP-25 (Amapá) is only a proposal.
- `"CRC"`: UF, 6 digits, tipo de registro (`O` or `P`) and check digit (`SP-123456/O-3`); a transfer appends `T` or `S` and the destination UF (`SP-123456/O-3 T-MG`). `stateCode` matches the originating UF. This shape and the `P`/`S` registrations come from the Manual de Registro of 2009; Resolução CFC nº 1.707/2023, in force, only sets a numbering "única e sequencial em cada CRC" and the `T` of the transfer, and the check digit algorithm is not published.
Expand Down
14 changes: 14 additions & 0 deletions src/_internals/has-own-key/has-own-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, test } from "../test/runtime";
import { hasOwnKey } from "./has-own-key";

describe("hasOwnKey", () => {
test("should find an own key of the table", () => {
expect(hasOwnKey({ SP: "8" }, "SP")).toBe(true);
});

test("should not find a key the table does not have, a prototype-chain key included", () => {
for (const key of ["RJ", "constructor", "__proto__", "toString"]) {
expect(hasOwnKey({ SP: "8" }, key)).toBe(false);
}
});
});
19 changes: 19 additions & 0 deletions src/_internals/has-own-key/has-own-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Checks that a key is an own key of a table, narrowing it to the table's keys. The lookup is an
* own-property one, so a prototype-chain key such as `"__proto__"` or `"constructor"` is not a key
* of any table.
*
* @param {object} table - The table to look the key up in.
* @param {string} key - The key to look up.
* @returns {boolean} True when `key` is an own key of `table`.
*
* @example
* ```typescript
* hasOwnKey({ SP: "8" }, "SP"); // true
* hasOwnKey({ SP: "8" }, "constructor"); // false
* ```
*/
export const hasOwnKey = <Table extends object>(
table: Table,
key: string,
): key is Extract<keyof Table, string> => Object.hasOwn(table, key);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type StateCode } from "../constants/states";
import { isStateCode } from "../is-state-code/is-state-code";
import { readStateCode } from "../read-state-code/read-state-code";

/**
* Reads the `stateCode` option of the holiday and business day utils. The match is
Expand All @@ -22,12 +22,5 @@ import { isStateCode } from "../is-state-code/is-state-code";
* readHolidayStateCode(""); // null
* ```
*/
export const readHolidayStateCode = (value: unknown): StateCode | undefined | null => {
if (value === undefined) return undefined;

if (typeof value !== "string") return null;

const normalized = value.trim().toUpperCase();

return isStateCode(normalized) ? normalized : null;
};
export const readHolidayStateCode = (value: unknown): StateCode | undefined | null =>
value === undefined ? undefined : readStateCode(value);
42 changes: 42 additions & 0 deletions src/_internals/read-state-code/read-state-code.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { STATE_CODES } from "../constants/state-codes";
import { describe, expect, test } from "../test/runtime";
import { normalizeStateCode, readStateCode } from "./read-state-code";

describe("readStateCode", () => {
test("should read every state code as it is", () => {
for (const code of STATE_CODES) {
expect(readStateCode(code)).toBe(code);
}
});

test("should ignore case and surrounding whitespace", () => {
expect(readStateCode("sp")).toBe("SP");
expect(readStateCode(" Rj\t")).toBe("RJ");
});

test("should return null for a string that is not a state code", () => {
for (const value of ["XX", "ZZ", "", " ", "S P", "SPA", "__proto__", "constructor"]) {
expect(readStateCode(value)).toBeNull();
}
});

test("should return null for a value that is not a string", () => {
for (const value of [null, undefined, 35, {}, ["SP"], new String("SP")]) {
expect(readStateCode(value)).toBeNull();
}
});
});

describe("normalizeStateCode", () => {
test("should trim and upper case a string without checking it is a state", () => {
expect(normalizeStateCode(" sp ")).toBe("SP");
expect(normalizeStateCode("xx")).toBe("XX");
expect(normalizeStateCode("")).toBe("");
});

test("should return an empty string for a value that is not a string", () => {
for (const value of [null, undefined, 35, {}, new String("SP")]) {
expect(normalizeStateCode(value)).toBe("");
}
});
});
43 changes: 43 additions & 0 deletions src/_internals/read-state-code/read-state-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type StateCode } from "../constants/states";
import { isStateCode } from "../is-state-code/is-state-code";

/**
* Normalizes a state code given by a caller: its surrounding whitespace removed and its letters
* upper cased, without checking that the result is a state. A util whose own table is keyed by
* the state codes checks the result against that table instead, and does not bundle the list of
* the 27 codes. A value that is not a string is the empty string, which is no state code.
*
* @param {unknown} value - The state code as the caller passed it.
* @returns {string} The normalized text, or `""` when the value is not a string.
*
* @example
* ```typescript
* normalizeStateCode(" sp "); // "SP"
* normalizeStateCode("xx"); // "XX"
* normalizeStateCode(35); // ""
* ```
*/
export const normalizeStateCode = (value: unknown): string =>
typeof value === "string" ? value.trim().toUpperCase() : "";

/**
* Reads a state code given by a caller: a string that, once normalized by `normalizeStateCode`,
* is the two letter code of a Brazilian state. Anything else (an unknown string such as `"XX"`, an
* empty string, a prototype-chain key such as `"__proto__"`, a value that is not a string) is
* `null`, so the caller decides what an unknown state means.
*
* @param {unknown} value - The state code as the caller passed it.
* @returns {StateCode|null} The state code, or `null` when the value is not one.
*
* @example
* ```typescript
* readStateCode(" sp "); // "SP"
* readStateCode("XX"); // null
* readStateCode(35); // null
* ```
*/
export const readStateCode = (value: unknown): StateCode | null => {
const normalized = normalizeStateCode(value);

return isStateCode(normalized) ? normalized : null;
};
7 changes: 7 additions & 0 deletions src/generate-cpf/generate-cpf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import { isValidCpf } from "../is-valid-cpf/is-valid-cpf";
import { generateCpf } from "./generate-cpf";

describe("generateCpf", () => {
test("should read the state code ignoring case and surrounding whitespace", () => {
for (let run = 0; run < 20; run += 1) {
// @ts-expect-error: a lower case state code is read as its upper case form
expect(generateCpf(" sp ").charAt(8)).toBe(CPF_FISCAL_REGION_BY_STATE.SP);
}
});

test(`should have the right length without mask (${CPF_LENGTH})`, () => {
expect(generateCpf().length).toBe(CPF_LENGTH);
});
Expand Down
Loading
Loading