Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion context7.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"rules": [
"The package has zero runtime dependencies and ships as ESM plus a UMD build; nothing else needs to be installed to use it.",
"Import from the root: import { isValidCpf } from '@brazilian-utils/brazilian-utils'. Every util is also a kebab-case subpath, e.g. '@brazilian-utils/brazilian-utils/is-valid-cpf'.",
"Use the subpaths to lazy-load the dataset-backed utils (getMunicipalities, getMunicipalityByCode, getCnae, getCbo, getCest, getCfop, getCid10, isValidCid10, getClassTrib, isValidNcm, getBanks, getNbs, getServiceItem): each one embeds a large official table.",
"Use the subpaths to lazy-load the dataset-backed utils (getMunicipalities, getMunicipalityByCode, getMunicipalitiesByAreaCode, getAreaCodeByMunicipalityCode, getCnae, getCbo, getCest, getCfop, getCid10, isValidCid10, getClassTrib, isValidNcm, getBanks, getNbs, getServiceItem): each one embeds a large official table.",
"Never import the same util from both the root and its subpath in one app: a bundler treats them as two unrelated modules and bundles the dataset twice.",
"Public functions never throw on bad input (null, undefined, wrong type): isValid* return false, format* and parse* return '', single-item getters return null, list getters return [].",
"The only utils that reject are the async getAddressInfoByCep (GetAddressInfoByCepError: NotFound, Validation, Service) and getCepInfoByAddress (GetCepInfoByAddressError: NotFound, Validation).",
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ A few utils embed an official dataset and weigh far more than everything else co
| Util | Dataset | Minified | Gzipped |
| --- | --- | --- | --- |
| `getCid10` | CID-10 V2008 categories and subcategories plus the SIM `U07` codes, with the DATASUS descriptions | 988.3 KB | 123.6 KB |
| `getMunicipalitiesByAreaCode` · `getAreaCodeByMunicipalityCode` | 5571 IBGE municipalities, with the DDD of each (Anatel) | 165.0 - 167.8 KB | 52.1 - 52.8 KB |
| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 153.6 - 154.0 KB | 49.4 - 49.7 KB |
| `getCities` | 5571 IBGE municipality names | 153.4 KB | 49.2 KB |
| `getCbo` | CBO 2002 occupation titles | 115.7 KB | 29.5 KB |
Expand Down
1 change: 1 addition & 0 deletions docs/pt-br/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Alguns utilitários embutem uma base de dados oficial e pesam muito mais que tod
| Utilitário | Base de dados | Minificado | Gzip |
| --- | --- | --- | --- |
| `getCid10` | categorias e subcategorias da CID-10 V2008 mais os códigos `U07` do SIM, com as descrições do DATASUS | 988,3 KB | 123,6 KB |
| `getMunicipalitiesByAreaCode` · `getAreaCodeByMunicipalityCode` | 5571 municípios do IBGE, com o DDD de cada um (Anatel) | 165,0 - 167,8 KB | 52,1 - 52,8 KB |
| `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 153,6 - 154,0 KB | 49,4 - 49,7 KB |
| `getCities` | nomes dos 5571 municípios do IBGE | 153,4 KB | 49,2 KB |
| `getCbo` | títulos das ocupações da CBO 2002 | 115,7 KB | 29,5 KB |
Expand Down
38 changes: 38 additions & 0 deletions docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,25 @@ isValidServicePhone('11987654321'); // false (número geográfico)

Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [página de SUP da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais/servicos-de-utilidade-publica-e-de-emergencia), [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151), [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86).

### getAreaCodeByMunicipalityCode

Retorna o DDD (código de área) de um município brasileiro a partir do código IBGE de 7 dígitos, segundo a tabela da Anatel dos Códigos Nacionais em vigor.

- Aceita o código como o `getMunicipalityByCode`: string (com todo caractere que não é dígito removido) ou número inteiro não negativo.
- Retorna o DDD como número, ou `null` quando o código não é de um município. Cada um dos 5.571 municípios tem exatamente um DDD.
- O DDD quase sempre segue a divisa dos estados. As exceções: o 61 também cobre 12 municípios de Goiás no entorno de Brasília, e Porto União (SC) usa o 42, Rio Negro (PR) o 47 e Barracão (PR) o 49.

```javascript
import { getAreaCodeByMunicipalityCode } from '@brazilian-utils/brazilian-utils';

getAreaCodeByMunicipalityCode('3550308'); // 11 (São Paulo/SP)
getAreaCodeByMunicipalityCode(3304557); // 21 (Rio de Janeiro/RJ)
getAreaCodeByMunicipalityCode('4122305'); // 47 (Rio Negro/PR)
getAreaCodeByMunicipalityCode('0000000'); // null
```

Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [tabela da Anatel dos Códigos Nacionais por município (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais).

### getAreaCodeInfo

Retorna o estado e a região a que um DDD brasileiro (código de área) pertence, dentre os 67 DDDs em uso no Plano Geral de Numeração da Anatel. Aceita string ou número inteiro não negativo.
Expand Down Expand Up @@ -1020,6 +1039,25 @@ getAreaCodesByState('XX'); // []

Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [tabela da Anatel dos Códigos Nacionais por município (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais).

### getMunicipalitiesByAreaCode

Lista os municípios brasileiros que usam um DDD (código de área), segundo a tabela da Anatel dos Códigos Nacionais em vigor.

- Aceita o DDD como o `getAreaCodeInfo`: string (com todo caractere que não é dígito removido) ou número inteiro não negativo.
- Retorna um array de `{ code, name, stateCode }` (`Municipality`): primeiro os municípios do estado sede, depois os do outro estado em que o DDD entra, cada estado em ordem alfabética. Retorna `[]` quando o DDD não está em uso.

```javascript
import { getMunicipalitiesByAreaCode } from '@brazilian-utils/brazilian-utils';

getMunicipalitiesByAreaCode(68).length; // 22 (todos os municípios do Acre)
getMunicipalitiesByAreaCode('(61)').length; // 13 (Brasília e 12 municípios de Goiás)
getMunicipalitiesByAreaCode('47').at(-1); // { code: '4122305', name: 'Rio Negro', stateCode: 'PR' }
getMunicipalitiesByAreaCode('20'); // []
```

Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [tabela da Anatel dos Códigos Nacionais por município (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais).


## Placa de veículo

### isValidLicensePlate
Expand Down
38 changes: 38 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,25 @@ isValidServicePhone('11987654321'); // false (geographic number)

Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel SUP page](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais/servicos-de-utilidade-publica-e-de-emergencia), [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151), [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86).

### getAreaCodeByMunicipalityCode

Get the DDD (area code) a Brazilian municipality dials, given its 7-digit IBGE code, from the Anatel table of the Códigos Nacionais in force.

- Accepts the code the way `getMunicipalityByCode` does: a string (any non-digit characters stripped) or a non-negative integer.
- Returns the DDD as a number, or `null` when the code is not a municipality. Every one of the 5,571 municipalities has exactly one DDD.
- A DDD mostly follows state lines. The exceptions: 61 also covers 12 municipalities of Goiás around Brasília, and Porto União (SC) dials 42, Rio Negro (PR) 47 and Barracão (PR) 49.

```javascript
import { getAreaCodeByMunicipalityCode } from '@brazilian-utils/brazilian-utils';

getAreaCodeByMunicipalityCode('3550308'); // 11 (São Paulo/SP)
getAreaCodeByMunicipalityCode(3304557); // 21 (Rio de Janeiro/RJ)
getAreaCodeByMunicipalityCode('4122305'); // 47 (Rio Negro/PR)
getAreaCodeByMunicipalityCode('0000000'); // null
```

Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [Anatel table of the Códigos Nacionais by municipality (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais).

### getAreaCodeInfo

Get the state and region a Brazilian DDD (area code) belongs to, out of the 67 DDDs in use under the Anatel Plano Geral de Numeração. Accepts a string or a non-negative integer.
Expand Down Expand Up @@ -1020,6 +1039,25 @@ getAreaCodesByState('XX'); // []

Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [Anatel table of the Códigos Nacionais by municipality (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais).

### getMunicipalitiesByAreaCode

List the Brazilian municipalities that dial a given DDD (area code), from the Anatel table of the Códigos Nacionais in force.

- Accepts the DDD the way `getAreaCodeInfo` does: a string (any non-digit characters stripped) or a non-negative integer.
- Returns an array of `{ code, name, stateCode }` (`Municipality`): the seat state's municipalities first, then those of the other state the DDD crosses into, each state's sorted by name. Returns `[]` when the DDD is not in use.

```javascript
import { getMunicipalitiesByAreaCode } from '@brazilian-utils/brazilian-utils';

getMunicipalitiesByAreaCode(68).length; // 22 (every municipality of Acre)
getMunicipalitiesByAreaCode('(61)').length; // 13 (Brasília and 12 municipalities of Goiás)
getMunicipalitiesByAreaCode('47').at(-1); // { code: '4122305', name: 'Rio Negro', stateCode: 'PR' }
getMunicipalitiesByAreaCode('20'); // []
```

Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [Anatel table of the Códigos Nacionais by municipality (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais).


## License plate

### isValidLicensePlate
Expand Down
2 changes: 2 additions & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"./generate-suframa": "./src/generate-suframa/generate-suframa.ts",
"./generate-voter-id": "./src/generate-voter-id/generate-voter-id.ts",
"./get-address-info-by-cep": "./src/get-address-info-by-cep/get-address-info-by-cep.ts",
"./get-area-code-by-municipality-code": "./src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts",
"./get-area-code-info": "./src/get-area-code-info/get-area-code-info.ts",
"./get-area-codes-by-state": "./src/get-area-codes-by-state/get-area-codes-by-state.ts",
"./get-bank-by-code": "./src/get-bank-by-code/get-bank-by-code.ts",
Expand All @@ -79,6 +80,7 @@
"./get-legal-natures": "./src/get-legal-natures/get-legal-natures.ts",
"./get-legal-natures-by-category": "./src/get-legal-natures-by-category/get-legal-natures-by-category.ts",
"./get-municipalities": "./src/get-municipalities/get-municipalities.ts",
"./get-municipalities-by-area-code": "./src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts",
"./get-municipality": "./src/get-municipality/get-municipality.ts",
"./get-municipality-by-code": "./src/get-municipality-by-code/get-municipality-by-code.ts",
"./get-nbs": "./src/get-nbs/get-nbs.ts",
Expand Down
178 changes: 178 additions & 0 deletions scripts/area-codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env node

import { DATA as STATES } from "../src/_internals/constants/states.ts";
import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts";
import { runAsEntryPoint, writeGeneratedFiles } from "./lookup-table.ts";
import { readZipFile } from "./read-xlsx-sheet.ts";

const PGCN_URL = "https://www.anatel.gov.br/dadosabertos/paineis_de_dados/areastarifarias/pgcn.zip";

const PGCN_CSV = "Codigos_Nacionais.csv";

const PGCN_HEADER = [
"CO_MUNICIPIO",
"SG_UF",
"NO_UF",
"NO_MUNICIPIO",
"CN",
"DT_INICIO_VIGENCIA",
"DT_FIM_VIGENCIA",
"DE_ALTERACAO_REGULAMENTAR",
"VIGENTE",
];

const MUNICIPALITY_CODE_REGEX = /^\d{7}$/;

const AREA_CODE_REGEX = /^[1-9][1-9]$/;

const STATE_CODES = new Set<string>(STATES.map((state) => state.code));

/**
* Splits one `;` separated row of the Anatel CSV, whose fields may be quoted (`"Alta Floresta
* D'Oeste"`), a quote inside a quoted field being written twice.
* @param {string} row - The row.
* @returns {string[]} The fields of the row.
*/
const splitRow = (row: string): string[] => {
const fields: string[] = [];
let field = "";
let quoted = false;

for (let index = 0; index < row.length; index++) {
const character = row[index];

if (quoted) {
if (character === '"' && row[index + 1] === '"') {
field += '"';
index++;
} else if (character === '"') {
quoted = false;
} else {
field += character;
}
} else if (character === '"') {
quoted = true;
} else if (character === ";") {
fields.push(field);
field = "";
} else {
field += character;
}
}

fields.push(field);

return fields;
};

/**
* Reads the Código Nacional (DDD) in force for every municipality of the Anatel
* `Codigos_Nacionais.csv`. A municipality's earlier codes stay in the file with `VIGENTE` set to
* `Não`, so only the rows in force are kept, and a municipality with more than one of them is an
* error.
* @param {string} csv - The decoded CSV.
* @returns {Record<string, Record<string, number>>} The DDD of every municipality, by state and
* by 7 digit IBGE municipality code.
*/
export const parseAreaCodes = (csv: string): Record<string, Record<string, number>> => {
const [header, ...rows] = csv.replace(/^/, "").split(/\r?\n/);

if (header === undefined || splitRow(header).join(";") !== PGCN_HEADER.join(";")) {
throw new Error(`${PGCN_CSV} header is not "${PGCN_HEADER.join(";")}"`);
}

const areaCodes: Record<string, Record<string, number>> = {};

for (const row of rows) {
if (row.trim() === "") continue;

const fields = splitRow(row);
const code = fields[0] ?? "";
const stateCode = fields[1] ?? "";
const areaCode = fields[4] ?? "";
const inForce = fields[8] ?? "";

if (inForce !== "Sim") continue;

if (
!MUNICIPALITY_CODE_REGEX.test(code) ||
!STATE_CODES.has(stateCode) ||
!AREA_CODE_REGEX.test(areaCode)
) {
throw new Error(`${PGCN_CSV} has an unexpected row: ${row}`);
}

const state = areaCodes[stateCode] ?? {};

areaCodes[stateCode] = state;

if (Object.hasOwn(state, code)) {
throw new Error(`${PGCN_CSV} has two codes in force for the municipality ${code}`);
}

state[code] = Number(areaCode);
}

return areaCodes;
};

/**
* Renders the module the DDD of every municipality is shipped as: for each state, one string
* holding the two digits of each municipality's DDD, in ascending order of the municipality code.
* @param {Record<string, Record<string, number>>} areaCodes - What `parseAreaCodes` returns.
* @returns {Record<string, string>} The content of the generated file, by its path.
*/
export const renderAreaCodes = (
areaCodes: Record<string, Record<string, number>>,
): Record<string, string> => {
const entries = [...STATE_CODES].sort().map((stateCode) => {
const state = areaCodes[stateCode];

if (state === undefined) throw new Error(`${PGCN_CSV} has no municipality of ${stateCode}`);

const digits = Object.keys(state)
.sort()
.map((code) => String(state[code]))
.join("");

return `\t${stateCode}: "${digits}",`;
});

return {
"./src/_internals/constants/municipality-area-codes.ts": `import { type StateCode } from "./states";

/**
* The DDD (Código Nacional) in force for every Brazilian municipality. For each state, the two
* digits of each municipality's DDD follow one another in ascending order of the 7 digit IBGE
* municipality code, so the municipality with the n-th smallest code of the state has its DDD at
* characters \`2n\` and \`2n + 1\`. The municipalities are the same 5,571 as \`DATA\` in
* \`municipalities.ts\`, which a test checks.
*
* Generated by \`node ./scripts/area-codes.ts\` from the \`Codigos_Nacionais.csv\` Anatel
* publishes, keeping only the rows in force (\`VIGENTE\` is \`Sim\`). Do not edit by hand.
*
* @see Official: https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais
* Anatel, Painel de Dados de Áreas Tarifárias, "Códigos Nacionais".
* @see Official: ${PGCN_URL}
* \`Codigos_Nacionais.csv\`, the Código Nacional of every municipality.
*/
export const MUNICIPALITY_AREA_CODES: Record<StateCode, string> = {
${entries.join("\n")}
};
`,
};
};

const main = async (): Promise<void> => {
const response = await fetchWithRetry(PGCN_URL);

if (!response.ok) throw new Error(`Anatel PGCN request failed with status ${response.status}`);

const archive = Buffer.from(await response.arrayBuffer());

const areaCodes = parseAreaCodes(readZipFile(archive, PGCN_CSV));

await writeGeneratedFiles(renderAreaCodes(areaCodes));
};

await runAsEntryPoint(import.meta.filename, main);
2 changes: 2 additions & 0 deletions scripts/data-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const DATASETS: Record<string, string> = {
"src/_internals/constants/ibs-cbs.ts":
"CST-IBS/CBS and cClassTrib (Portal Nacional da NF-e, Informe Técnico 2025.002)",
"src/_internals/constants/municipalities.ts": "Municipalities (IBGE)",
"src/_internals/constants/municipality-area-codes.ts":
"DDD of every municipality (Anatel, Códigos Nacionais)",
"src/_internals/constants/nbs-descriptions.ts": "NBS 2.0 descriptions (MDIC)",
"src/_internals/constants/nbs.ts": "NBS 2.0 codes (MDIC)",
"src/_internals/constants/service-item-descriptions.ts":
Expand Down
2 changes: 2 additions & 0 deletions scripts/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const run = (command: string, args: string[]): Promise<number | null> =>
});

const generators = [
"area-codes.ts",
"banks.ts",
"cbo.ts",
"cest.ts",
Expand All @@ -36,6 +37,7 @@ const generators = [
];

const generatedFiles = [
"./src/_internals/constants/municipality-area-codes.ts",
"./src/_internals/constants/banks.ts",
"./src/_internals/constants/cbo-descriptions.ts",
"./src/_internals/constants/cbo.ts",
Expand Down
10 changes: 10 additions & 0 deletions scripts/read-xlsx-sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,13 @@ export const readXlsxSheets = (workbook: Buffer): Map<string, string[][]> => {

return new Map(opened.sheets.map((sheet) => [sheet.name, readSheet(opened, sheet)]));
};

/**
* Reads one file of a zip archive as UTF-8, for a dataset published as a zipped CSV rather than
* as a workbook.
* @param {Buffer} archive - The `.zip` file.
* @param {string} path - The path of the file inside the archive.
* @returns {string} The content of the file.
*/
export const readZipFile = (archive: Buffer, path: string): string =>
readFile(unzip(archive), path);
Loading
Loading