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
27 changes: 27 additions & 0 deletions docs/pt-br/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,33 @@ getStates();

Fonte: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades)

### getStateByCep

Retorna o estado brasileiro ao qual um CEP pertence, a partir das faixas de CEP que os Correios atribuem a cada UF (a "Faixa de CEP" de cada UF).

- Funciona offline: nenhuma API de CEP é chamada, então a resposta diz qual estado é dono da faixa, não se o CEP está em uso.
- Aceita o que o `isValidCep` aceita: 8 dígitos, como string ou número, ignorando espaços, pontos e hifens. Um CEP que começa com `0` precisa ser uma string, e um número negativo ou fracionário é rejeitado.
- Amazonas, Distrito Federal e Goiás têm duas faixas cada, e nenhuma faixa estadual cobre `00000-000` a `00999-999` nem `78900-000` a `78999-999`.
- A faixa é o bloco que pertence ao estado, não uma garantia de que todo CEP dentro dela está em uso: `10000-000` está sem uso dentro da faixa de São Paulo e ainda assim responde São Paulo.
- Retorna `null` para um CEP inválido ou fora de todas as faixas. Exporta o tipo `State`.

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

getStateByCep('01310-100');
// { code: 'SP', name: 'São Paulo', regionCode: 'SE', regionName: 'Sudeste', ibgeCode: 35 }

getStateByCep(20040020);
// { code: 'RJ', name: 'Rio de Janeiro', regionCode: 'SE', regionName: 'Sudeste', ibgeCode: 33 }

getStateByCep('69300-000')?.code; // 'RR'
getStateByCep('72800-000')?.code; // 'GO'
getStateByCep('00999-999'); // null
getStateByCep('12345'); // null
```

Fonte: [Correios, Busca Faixa de CEP](https://buscacepinter.correios.com.br/app/faixa_cep_uf_localidade/index.php)

### getStateByIbgeCode

Retorna o estado brasileiro cujo código IBGE de 2 dígitos (`cUF`, o Código da Unidade da Federação) corresponde ao valor informado.
Expand Down
27 changes: 27 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,33 @@ getStates();

Source: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades)

### getStateByCep

Get the Brazilian state a CEP belongs to, from the CEP ranges the Correios assign to each state (the "Faixa de CEP" of each UF).

- It runs offline: no CEP API is called, so the answer says which state owns the range, not whether the CEP is in use.
- Accepts what `isValidCep` accepts: 8 digits, as a string or a number, with spaces, dots and hyphens ignored. A CEP that starts with `0` has to be a string, and a negative or fractional number is rejected.
- Amazonas, Distrito Federal and Goiás have two ranges each, and no state range covers `00000-000` to `00999-999` nor `78900-000` to `78999-999`.
- A range is the block the state owns, not a promise that every CEP in it is in use: `10000-000` sits unused inside the São Paulo range and still answers São Paulo.
- Returns `null` for an invalid CEP or one outside every range. Exports the `State` type.

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

getStateByCep('01310-100');
// { code: 'SP', name: 'São Paulo', regionCode: 'SE', regionName: 'Sudeste', ibgeCode: 35 }

getStateByCep(20040020);
// { code: 'RJ', name: 'Rio de Janeiro', regionCode: 'SE', regionName: 'Sudeste', ibgeCode: 33 }

getStateByCep('69300-000')?.code; // 'RR'
getStateByCep('72800-000')?.code; // 'GO'
getStateByCep('00999-999'); // null
getStateByCep('12345'); // null
```

Source: [Correios, Busca Faixa de CEP](https://buscacepinter.correios.com.br/app/faixa_cep_uf_localidade/index.php)

### getStateByIbgeCode

Get the Brazilian state whose 2-digit IBGE code (`cUF`, the Código da Unidade da Federação) matches the given value.
Expand Down
1 change: 1 addition & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"./get-nfe-key-info": "./src/get-nfe-key-info/get-nfe-key-info.ts",
"./get-pix-key-info": "./src/get-pix-key-info/get-pix-key-info.ts",
"./get-pix-payload-info": "./src/get-pix-payload-info/get-pix-payload-info.ts",
"./get-state-by-cep": "./src/get-state-by-cep/get-state-by-cep.ts",
"./get-state-by-ibge-code": "./src/get-state-by-ibge-code/get-state-by-ibge-code.ts",
"./get-state-code-by-name": "./src/get-state-code-by-name/get-state-code-by-name.ts",
"./get-state-name-by-code": "./src/get-state-name-by-code/get-state-name-by-code.ts",
Expand Down
3 changes: 3 additions & 0 deletions reports/api/brazilian-utils.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@ export const getPixKeyInfo: (value: string) => PixKeyInfo | null;
// @public
export const getPixPayloadInfo: (value: string) => PixPayloadInfo | null;

// @public
export const getStateByCep: (value: string | number) => State | null;

// @public
export const getStateByIbgeCode: (code: string | number) => State | null;

Expand Down
4 changes: 2 additions & 2 deletions scripts/cities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ const main = async (): Promise<void> => {
}

await writeFile(
resolve(scriptsDir, "..", "./src/_internals/constants/cities.ts"),
`import type { StateCode } from "./states";
resolve(scriptsDir, "..", "./src/_internals/constants/municipalities.ts"),
`import { type StateCode } from "./states";

/**
* Brazilian municipalities by state, published by the IBGE. \`DATA\` holds, for each state, a
Expand Down
2 changes: 1 addition & 1 deletion scripts/data-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const DATASETS: Record<string, string> = {
"src/_internals/constants/banks.ts": "Banks (Banco Central, STR participants)",
"src/_internals/constants/cbo.ts": "CBO 2002 occupations (Ministério do Trabalho e Emprego)",
"src/_internals/constants/cfop.ts": "CFOP codes (CONFAZ, Convênio SINIEF s/nº 1970)",
"src/_internals/constants/cities.ts": "Municipalities (IBGE)",
"src/_internals/constants/cnae.ts": "CNAE subclasses (IBGE/CONCLA)",
"src/_internals/constants/municipalities.ts": "Municipalities (IBGE)",
"src/_internals/constants/states.ts": "States (IBGE)",
"src/is-valid-legal-nature/constants.ts": "Legal natures (IBGE/CONCLA)",
"src/is-valid-ncm/constants.ts": "NCM codes (Siscomex)",
Expand Down
2 changes: 1 addition & 1 deletion scripts/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ const generatedFiles = [
"./src/_internals/constants/banks.ts",
"./src/_internals/constants/cbo.ts",
"./src/_internals/constants/cfop.ts",
"./src/_internals/constants/cities.ts",
"./src/_internals/constants/cnae.ts",
"./src/_internals/constants/municipalities.ts",
"./src/_internals/constants/states.ts",
"./src/is-valid-legal-nature/constants.ts",
"./src/is-valid-ncm/constants.ts",
Expand Down
53 changes: 53 additions & 0 deletions src/_internals/find-cep-range/find-cep-range.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, test } from "../test/runtime";
import { findCepRange } from "./find-cep-range";

const RANGES = [
{ start: 1_000_000, end: 19_999_999, label: "SP" },
{ start: 20_000_000, end: 28_999_999, label: "RJ" },
];

describe("findCepRange", () => {
test("should return the range that contains a formatted CEP", () => {
expect(findCepRange("01310-100", RANGES)).toEqual(RANGES[0]);
});

test("should return the range that contains a CEP given as a number", () => {
expect(findCepRange(20_040_020, RANGES)).toEqual(RANGES[1]);
});

test("should return the range that owns the first and the last CEP of a range", () => {
expect(findCepRange("01000-000", RANGES)).toEqual(RANGES[0]);
expect(findCepRange("19999-999", RANGES)).toEqual(RANGES[0]);
});

test("should return null for a CEP outside every range", () => {
expect(findCepRange("29000-000", RANGES)).toBeNull();
});

test("should return null for an empty table", () => {
expect(findCepRange("01310-100", [])).toBeNull();
});

test("should return null for an invalid CEP", () => {
expect(findCepRange("12345", RANGES)).toBeNull();
expect(findCepRange("abc01310100", RANGES)).toBeNull();
expect(findCepRange(-20_040_020, RANGES)).toBeNull();
expect(findCepRange(2_004_002.5, RANGES)).toBeNull();
});

test("should return null for a value that is not a string or a number", () => {
// @ts-expect-error: intentionally invalid input
expect(findCepRange(null, RANGES)).toBeNull();
// @ts-expect-error: intentionally invalid input
expect(findCepRange(undefined, RANGES)).toBeNull();
});

test("should return the first match when ranges overlap", () => {
const overlapping = [
{ start: 1_000_000, end: 2_000_000, label: "first" },
{ start: 1_500_000, end: 2_500_000, label: "second" },
];

expect(findCepRange("01500-000", overlapping)).toEqual(overlapping[0]);
});
});
41 changes: 41 additions & 0 deletions src/_internals/find-cep-range/find-cep-range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { isValidCep } from "../../is-valid-cep/is-valid-cep";
import { parseCep } from "../../parse-cep/parse-cep";
import { isLookupCode } from "../is-lookup-code/is-lookup-code";

/** One entry of a table `findCepRange` searches: any range with a first and a last CEP. */
type CepRange = {
/** First CEP of the range, as a number. */
readonly start: number;
/** Last CEP of the range, as a number. */
readonly end: number;
};

/**
* Validates `value` as `isValidCep` does and finds the entry of `ranges` whose `start`/`end`
* bounds contain it, so the validation and the parsing of `getStateByCep` happen only once.
*
* `ranges` is searched in order and the first match wins, so a caller whose ranges may overlap
* controls the answer through their order; every range table this package ships is disjoint,
* so the order never matters in practice.
*
* @param {string|number} value - The CEP, with or without formatting.
* @param {readonly CepRange[]} ranges - The table to search, each entry at least a `CepRange`.
* @returns {T|null} The matching entry, or `null` when `value` is not a valid CEP or falls
* outside every range.
*
* @example
* ```typescript
* findCepRange("01310-100", [{ start: 1_000_000, end: 19_999_999 }]); // { start: 1_000_000, end: 19_999_999 }
* findCepRange("00999-999", [{ start: 1_000_000, end: 19_999_999 }]); // null
* ```
*/
export const findCepRange = <T extends CepRange>(
value: string | number,
ranges: readonly T[],
): T | null => {
if (!isLookupCode(value) || !isValidCep(value)) return null;

const cep = Number(parseCep(value));

return ranges.find((entry) => cep >= entry.start && cep <= entry.end) ?? null;
};
2 changes: 1 addition & 1 deletion src/get-cities/get-cities.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fc from "fast-check";

import { DATA } from "../_internals/constants/cities";
import { DATA } from "../_internals/constants/municipalities";
import { type StateCode } from "../_internals/constants/states";
import { anyGarbage, stateCodes } from "../_internals/test/arbitraries";
import { expectNeverThrows } from "../_internals/test/properties";
Expand Down
2 changes: 1 addition & 1 deletion src/get-cities/get-cities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DATA as CITIES_DATA } from "../_internals/constants/cities";
import { DATA as CITIES_DATA } from "../_internals/constants/municipalities";
import { type StateCode } from "../_internals/constants/states";

export type { StateCode } from "../_internals/constants/states";
Expand Down
2 changes: 1 addition & 1 deletion src/get-municipalities/get-municipalities.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fc from "fast-check";

import { DATA, type Municipality } from "../_internals/constants/cities";
import { DATA, type Municipality } from "../_internals/constants/municipalities";
import { type StateCode } from "../_internals/constants/states";
import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
import { getCities } from "../get-cities/get-cities";
Expand Down
4 changes: 2 additions & 2 deletions src/get-municipalities/get-municipalities.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/cities";
import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/municipalities";
import { DATA, type StateCode } from "../_internals/constants/states";

export type { Municipality } from "../_internals/constants/cities";
export type { Municipality } from "../_internals/constants/municipalities";
export type { StateCode } from "../_internals/constants/states";

const buildMunicipalities = (stateCode: StateCode): Municipality[] =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fc from "fast-check";

import { type Municipality } from "../_internals/constants/cities";
import { type Municipality } from "../_internals/constants/municipalities";
import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime";
import { getMunicipalities } from "../get-municipalities/get-municipalities";
import { getMunicipalityByCode } from "./get-municipality-by-code";
Expand Down
4 changes: 2 additions & 2 deletions src/get-municipality-by-code/get-municipality-by-code.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/cities";
import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/municipalities";
import { DATA } from "../_internals/constants/states";
import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits";

export type { Municipality } from "../_internals/constants/cities";
export type { Municipality } from "../_internals/constants/municipalities";

/**
* Looks up a Brazilian municipality by its 7 digit IBGE code, published by the IBGE.
Expand Down
2 changes: 1 addition & 1 deletion src/get-municipality/get-municipality.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DATA as CITIES_DATA } from "../_internals/constants/cities";
import { DATA as CITIES_DATA } from "../_internals/constants/municipalities";
import { type StateCode } from "../_internals/constants/states";
import { isLookupCode } from "../_internals/is-lookup-code/is-lookup-code";
import { isNullish } from "../_internals/is-nullish/is-nullish";
Expand Down
56 changes: 56 additions & 0 deletions src/get-state-by-cep/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { type StateCode } from "../_internals/constants/states";

/** One range of CEPs assigned by the Correios to a state. */
type CepRange = {
/** Two letter code of the state that owns the range. */
readonly state: StateCode;
/** First CEP of the range, as a number. */
readonly start: number;
/** Last CEP of the range, as a number. */
readonly end: number;
};

/**
* CEP ranges of each state ("Faixa de CEP" per UF), as answered by the Correios "Busca Faixa de
* CEP" search when only the UF is given, in ascending order. Amazonas, Distrito Federal and Goiás
* have two ranges each: Roraima sits inside the Amazonas block and the Goiás municipalities
* around Brasília sit inside the Distrito Federal block. No range covers `00000-000` to
* `00999-999` nor `78900-000` to `78999-999`. A range is the block the state owns, not a promise
* that every CEP in it is in use: `10000-000` to `10999-999` sits unused inside the São Paulo one.
*
* @see Official: https://buscacepinter.correios.com.br/app/faixa_cep_uf_localidade/index.php
* @see Based on: https://gist.github.com/tamnil/792a6a66f6df9fc028041587cfca0c3d
* Copy of the answers of the Correios search, the rows with an empty city are the state ranges.
*/
export const CEP_RANGES: readonly CepRange[] = [
{ state: "SP", start: 1_000_000, end: 19_999_999 },
{ state: "RJ", start: 20_000_000, end: 28_999_999 },
{ state: "ES", start: 29_000_000, end: 29_999_999 },
{ state: "MG", start: 30_000_000, end: 39_999_999 },
{ state: "BA", start: 40_000_000, end: 48_999_999 },
{ state: "SE", start: 49_000_000, end: 49_999_999 },
{ state: "PE", start: 50_000_000, end: 56_999_999 },
{ state: "AL", start: 57_000_000, end: 57_999_999 },
{ state: "PB", start: 58_000_000, end: 58_999_999 },
{ state: "RN", start: 59_000_000, end: 59_999_999 },
{ state: "CE", start: 60_000_000, end: 63_999_999 },
{ state: "PI", start: 64_000_000, end: 64_999_999 },
{ state: "MA", start: 65_000_000, end: 65_999_999 },
{ state: "PA", start: 66_000_000, end: 68_899_999 },
{ state: "AP", start: 68_900_000, end: 68_999_999 },
{ state: "AM", start: 69_000_000, end: 69_299_999 },
{ state: "RR", start: 69_300_000, end: 69_399_999 },
{ state: "AM", start: 69_400_000, end: 69_899_999 },
{ state: "AC", start: 69_900_000, end: 69_999_999 },
{ state: "DF", start: 70_000_000, end: 72_799_999 },
{ state: "GO", start: 72_800_000, end: 72_999_999 },
{ state: "DF", start: 73_000_000, end: 73_699_999 },
{ state: "GO", start: 73_700_000, end: 76_799_999 },
{ state: "RO", start: 76_800_000, end: 76_999_999 },
{ state: "TO", start: 77_000_000, end: 77_999_999 },
{ state: "MT", start: 78_000_000, end: 78_899_999 },
{ state: "MS", start: 79_000_000, end: 79_999_999 },
{ state: "PR", start: 80_000_000, end: 87_999_999 },
{ state: "SC", start: 88_000_000, end: 89_999_999 },
{ state: "RS", start: 90_000_000, end: 99_999_999 },
];
Loading
Loading