From 0b57240a63bc9c028986e95f2094f7cde0ebea8e Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:05:06 -0300 Subject: [PATCH 1/3] feat: implement some extra utilities --- docs/pt-br/utilities.md | 276 ++++++++++++++++++ docs/utilities.md | 276 ++++++++++++++++++ src/format-cnh/format-cnh.test.ts | 13 + src/format-cnh/format-cnh.ts | 11 + .../format-legal-nature.test.ts | 8 + .../format-legal-nature.ts | 8 + src/format-license-plate/constants.ts | 1 + .../format-license-plate.test.ts | 12 + .../format-license-plate.ts | 16 + src/format-voter-id/format-voter-id.test.ts | 8 + src/format-voter-id/format-voter-id.ts | 5 + src/generate-cep/generate-cep.test.ts | 8 + src/generate-cep/generate-cep.ts | 3 + src/generate-cnh/generate-cnh.test.ts | 11 + src/generate-cnh/generate-cnh.ts | 51 ++++ .../generate-legal-nature.test.ts | 11 + .../generate-legal-nature.ts | 6 + .../generate-license-plate.test.ts | 11 + .../generate-license-plate.ts | 14 + src/generate-phone/generate-phone.test.ts | 19 ++ src/generate-phone/generate-phone.ts | 21 ++ src/generate-pis/generate-pis.ts | 14 + .../generate-processo-juridico.ts | 36 +++ .../generate-voter-id.test.ts | 15 + src/generate-voter-id/generate-voter-id.ts | 55 ++++ .../get-cep-info-by-address.test.ts | 58 ++++ .../get-cep-info-by-address.ts | 81 +++++ src/get-format-license-plate/constants.ts | 3 + .../get-format-license-plate.test.ts | 10 + .../get-format-license-plate.ts | 14 + .../get-legal-natures.test.ts | 8 + src/get-legal-natures/get-legal-natures.ts | 3 + src/get-municipality/get-municipality.test.ts | 40 +++ src/get-municipality/get-municipality.ts | 83 ++++++ src/index.test.ts | 111 ++++--- src/index.ts | 49 +++- src/is-holiday/is-holiday.ts | 25 ++ .../is-valid-bank-account.ts | 22 +- src/is-valid-cnh/is-valid-cnh.test.ts | 14 + src/is-valid-cnh/is-valid-cnh.ts | 38 +++ src/is-valid-legal-nature/constants.ts | 62 ++++ .../is-valid-legal-nature.test.ts | 10 + .../is-valid-legal-nature.ts | 10 + src/is-valid-voter-id/constants.ts | 32 ++ .../is-valid-voter-id.test.ts | 13 + src/is-valid-voter-id/is-valid-voter-id.ts | 72 +++++ src/parse-cnh/parse-cnh.test.ts | 12 + src/parse-cnh/parse-cnh.ts | 3 + .../parse-legal-nature.test.ts | 8 + src/parse-legal-nature/parse-legal-nature.ts | 3 + .../parse-license-plate.test.ts | 9 + .../parse-license-plate.ts | 4 + src/parse-voter-id/parse-voter-id.test.ts | 8 + src/parse-voter-id/parse-voter-id.ts | 3 + 54 files changed, 1660 insertions(+), 57 deletions(-) create mode 100644 src/format-cnh/format-cnh.test.ts create mode 100644 src/format-cnh/format-cnh.ts create mode 100644 src/format-legal-nature/format-legal-nature.test.ts create mode 100644 src/format-legal-nature/format-legal-nature.ts create mode 100644 src/format-license-plate/constants.ts create mode 100644 src/format-license-plate/format-license-plate.test.ts create mode 100644 src/format-license-plate/format-license-plate.ts create mode 100644 src/format-voter-id/format-voter-id.test.ts create mode 100644 src/format-voter-id/format-voter-id.ts create mode 100644 src/generate-cep/generate-cep.test.ts create mode 100644 src/generate-cep/generate-cep.ts create mode 100644 src/generate-cnh/generate-cnh.test.ts create mode 100644 src/generate-cnh/generate-cnh.ts create mode 100644 src/generate-legal-nature/generate-legal-nature.test.ts create mode 100644 src/generate-legal-nature/generate-legal-nature.ts create mode 100644 src/generate-license-plate/generate-license-plate.test.ts create mode 100644 src/generate-license-plate/generate-license-plate.ts create mode 100644 src/generate-phone/generate-phone.test.ts create mode 100644 src/generate-phone/generate-phone.ts create mode 100644 src/generate-pis/generate-pis.ts create mode 100644 src/generate-processo-juridico/generate-processo-juridico.ts create mode 100644 src/generate-voter-id/generate-voter-id.test.ts create mode 100644 src/generate-voter-id/generate-voter-id.ts create mode 100644 src/get-cep-info-by-address/get-cep-info-by-address.test.ts create mode 100644 src/get-cep-info-by-address/get-cep-info-by-address.ts create mode 100644 src/get-format-license-plate/constants.ts create mode 100644 src/get-format-license-plate/get-format-license-plate.test.ts create mode 100644 src/get-format-license-plate/get-format-license-plate.ts create mode 100644 src/get-legal-natures/get-legal-natures.test.ts create mode 100644 src/get-legal-natures/get-legal-natures.ts create mode 100644 src/get-municipality/get-municipality.test.ts create mode 100644 src/get-municipality/get-municipality.ts create mode 100644 src/is-holiday/is-holiday.ts create mode 100644 src/is-valid-cnh/is-valid-cnh.test.ts create mode 100644 src/is-valid-cnh/is-valid-cnh.ts create mode 100644 src/is-valid-legal-nature/constants.ts create mode 100644 src/is-valid-legal-nature/is-valid-legal-nature.test.ts create mode 100644 src/is-valid-legal-nature/is-valid-legal-nature.ts create mode 100644 src/is-valid-voter-id/constants.ts create mode 100644 src/is-valid-voter-id/is-valid-voter-id.test.ts create mode 100644 src/is-valid-voter-id/is-valid-voter-id.ts create mode 100644 src/parse-cnh/parse-cnh.test.ts create mode 100644 src/parse-cnh/parse-cnh.ts create mode 100644 src/parse-legal-nature/parse-legal-nature.test.ts create mode 100644 src/parse-legal-nature/parse-legal-nature.ts create mode 100644 src/parse-license-plate/parse-license-plate.test.ts create mode 100644 src/parse-license-plate/parse-license-plate.ts create mode 100644 src/parse-voter-id/parse-voter-id.test.ts create mode 100644 src/parse-voter-id/parse-voter-id.ts diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 1b20da6f..a798663c 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -547,3 +547,279 @@ import { parsePassport } from '@brazilian-utils/brazilian-utils'; parsePassport('AB-123.456'); // 'AB123456' parsePassport(' AB 123 456 '); // 'AB123456' ``` + +## generateCep + +Gera um CEP aleatório. + +```javascript +import { generateCep } from '@brazilian-utils/brazilian-utils'; + +generateCep(); // '92500000' +``` + +## formatCnh + +Formata a CNH. + +```javascript +import { formatCnh } from '@brazilian-utils/brazilian-utils'; + +formatCnh('02650306461'); // 026503064-61 +formatCnh('2650306461', { pad: true }); // 026503064-61 +``` + +## isValidCnh + +Valida se a CNH é válida. + +```javascript +import { isValidCnh } from '@brazilian-utils/brazilian-utils'; + +isValidCnh('00000000119'); // true +``` + +## generateCnh + +Gera uma CNH válida aleatória. + +```javascript +import { generateCnh } from '@brazilian-utils/brazilian-utils'; + +generateCnh(); // '02650306461' +``` + +## parseCnh + +Remove a formatação da CNH e retorna apenas os dígitos. + +```javascript +import { parseCnh } from '@brazilian-utils/brazilian-utils'; + +parseCnh('026503064-61'); // '02650306461' +``` + +## getCepInfoByAddress + +Busca CEPs a partir de um endereço usando a ViaCEP. + +```javascript +import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; + +const ceps = await getCepInfoByAddress({ + federalUnit: 'SP', + city: 'Sao Paulo', + street: 'Avenida Paulista' +}); + +// [ +// { +// cep: '01310100', +// logradouro: 'Avenida Paulista', +// complemento: 'lado par', +// bairro: 'Bela Vista', +// localidade: 'São Paulo', +// uf: 'SP' +// } +// ] +``` + +## generateProcessoJuridico + +Gera um número de processo jurídico válido de acordo com a definição do [CNJ](https://www.conjur.com.br/dl/resolucao-65-cnj.pdf). + +```javascript +import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; + +generateProcessoJuridico(); // '00020802520125150049' +generateProcessoJuridico({ year: 2026, court: 5 }); // string | null +``` + +## formatLegalNature + +Formata um código de natureza jurídica. + +```javascript +import { formatLegalNature } from '@brazilian-utils/brazilian-utils'; + +formatLegalNature('2062'); // 206-2 +``` + +## isValidLegalNature + +Valida se um código de natureza jurídica existe na lista oficial. + +```javascript +import { isValidLegalNature } from '@brazilian-utils/brazilian-utils'; + +isValidLegalNature('2062'); // true +isValidLegalNature('9999'); // false +``` + +## generateLegalNature + +Gera um código de natureza jurídica válido aleatório. + +```javascript +import { generateLegalNature } from '@brazilian-utils/brazilian-utils'; + +generateLegalNature(); // '2062' +``` + +## parseLegalNature + +Remove a formatação da natureza jurídica e retorna apenas os dígitos. + +```javascript +import { parseLegalNature } from '@brazilian-utils/brazilian-utils'; + +parseLegalNature('206-2'); // '2062' +``` + +## getLegalNatures + +Retorna o mapa de naturezas jurídicas indexado pelo código. + +```javascript +import { getLegalNatures } from '@brazilian-utils/brazilian-utils'; + +const legalNatures = getLegalNatures(); + +legalNatures['2062']; // 'Sociedade Empresária Limitada' +``` + +## generatePhone + +Gera um telefone brasileiro aleatório. + +```javascript +import { generatePhone } from '@brazilian-utils/brazilian-utils'; + +generatePhone(); // '11912345678' ou '1131234567' +generatePhone('mobile'); // '11912345678' +generatePhone('landline'); // '1131234567' +``` + +## formatLicensePlate + +Formata uma placa. Placas antigas brasileiras são retornadas com hífen e placas Mercosul permanecem normalizadas. + +```javascript +import { formatLicensePlate } from '@brazilian-utils/brazilian-utils'; + +formatLicensePlate('abc1234'); // 'ABC-1234' +formatLicensePlate('abc1d23'); // 'ABC1D23' +``` + +## generateLicensePlate + +Gera uma placa aleatória no formato escolhido. + +```javascript +import { generateLicensePlate } from '@brazilian-utils/brazilian-utils'; + +generateLicensePlate(); // 'ABC1D23' +generateLicensePlate('LLLNNNN'); // 'ABC1234' +generateLicensePlate('LLLNNLN'); // 'ABC12D3' +``` + +## getFormatLicensePlate + +Detecta o formato normalizado de uma placa. + +```javascript +import { getFormatLicensePlate } from '@brazilian-utils/brazilian-utils'; + +getFormatLicensePlate('ABC-1234'); // 'LLLNNNN' +getFormatLicensePlate('ABC1D23'); // 'LLLNLNN' +getFormatLicensePlate('ABC12D3'); // 'LLLNNLN' +getFormatLicensePlate('INVALID'); // null +``` + +## parseLicensePlate + +Remove separadores de uma placa e normaliza para letras maiúsculas. + +```javascript +import { parseLicensePlate } from '@brazilian-utils/brazilian-utils'; + +parseLicensePlate('abc-1234'); // 'ABC1234' +``` + +## generatePis + +Gera um PIS válido aleatório. + +```javascript +import { generatePis } from '@brazilian-utils/brazilian-utils'; + +generatePis(); // '12345678901' +``` + +## getMunicipality + +Busca informações de município por código IBGE, ou obtém o código IBGE a partir do nome do município e UF. + +```javascript +import { getMunicipality } from '@brazilian-utils/brazilian-utils'; + +await getMunicipality({ code: '3550308' }); +// ['São Paulo', 'SP'] + +await getMunicipality({ municipalityName: 'São Paulo', uf: 'SP' }); +// '3550308' +``` + +## isHoliday + +Verifica se uma data específica é feriado brasileiro. + +```javascript +import { isHoliday } from '@brazilian-utils/brazilian-utils'; + +isHoliday({ targetDate: new Date('2024-01-01') }); // true +isHoliday({ targetDate: new Date('2024-07-09'), stateCode: 'SP' }); // true +``` + +## formatVoterId + +Formata um título de eleitor. + +```javascript +import { formatVoterId } from '@brazilian-utils/brazilian-utils'; + +formatVoterId('123456780175'); // '1234 5678 01 75' +``` + +## isValidVoterId + +Valida se um título de eleitor é válido. + +```javascript +import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-utils'; + +const voterId = generateVoterId('SP'); + +isValidVoterId(voterId); // true +``` + +## generateVoterId + +Gera um título de eleitor válido aleatório. Você pode opcionalmente informar a UF. + +```javascript +import { generateVoterId } from '@brazilian-utils/brazilian-utils'; + +generateVoterId(); // título de eleitor aleatório válido +generateVoterId('SP'); // título de eleitor aleatório válido de São Paulo +``` + +## parseVoterId + +Remove a formatação do título de eleitor e retorna apenas os dígitos. + +```javascript +import { parseVoterId } from '@brazilian-utils/brazilian-utils'; + +parseVoterId('1234 5678 01 75'); // '123456780175' +``` diff --git a/docs/utilities.md b/docs/utilities.md index 431ae804..1865df70 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -547,3 +547,279 @@ import { parsePassport } from '@brazilian-utils/brazilian-utils'; parsePassport('AB-123.456'); // 'AB123456' parsePassport(' AB 123 456 '); // 'AB123456' ``` + +## generateCep + +Generate a random CEP. + +```javascript +import { generateCep } from '@brazilian-utils/brazilian-utils'; + +generateCep(); // '92500000' +``` + +## formatCnh + +Format CNH. + +```javascript +import { formatCnh } from '@brazilian-utils/brazilian-utils'; + +formatCnh('02650306461'); // 026503064-61 +formatCnh('2650306461', { pad: true }); // 026503064-61 +``` + +## isValidCnh + +Check if CNH is valid. + +```javascript +import { isValidCnh } from '@brazilian-utils/brazilian-utils'; + +isValidCnh('00000000119'); // true +``` + +## generateCnh + +Generate a valid random CNH. + +```javascript +import { generateCnh } from '@brazilian-utils/brazilian-utils'; + +generateCnh(); // '02650306461' +``` + +## parseCnh + +Remove CNH formatting and return only digits. + +```javascript +import { parseCnh } from '@brazilian-utils/brazilian-utils'; + +parseCnh('026503064-61'); // '02650306461' +``` + +## getCepInfoByAddress + +Fetch CEPs from an address using ViaCEP. + +```javascript +import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; + +const ceps = await getCepInfoByAddress({ + federalUnit: 'SP', + city: 'Sao Paulo', + street: 'Avenida Paulista' +}); + +// [ +// { +// cep: '01310100', +// logradouro: 'Avenida Paulista', +// complemento: 'lado par', +// bairro: 'Bela Vista', +// localidade: 'São Paulo', +// uf: 'SP' +// } +// ] +``` + +## generateProcessoJuridico + +Generate a valid random processo jurídico number according to [CNJ's definition](https://www.conjur.com.br/dl/resolucao-65-cnj.pdf). + +```javascript +import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; + +generateProcessoJuridico(); // '00020802520125150049' +generateProcessoJuridico({ year: 2026, court: 5 }); // string | null +``` + +## formatLegalNature + +Format a legal nature code. + +```javascript +import { formatLegalNature } from '@brazilian-utils/brazilian-utils'; + +formatLegalNature('2062'); // 206-2 +``` + +## isValidLegalNature + +Check if a legal nature code exists in the official list. + +```javascript +import { isValidLegalNature } from '@brazilian-utils/brazilian-utils'; + +isValidLegalNature('2062'); // true +isValidLegalNature('9999'); // false +``` + +## generateLegalNature + +Generate a random valid legal nature code. + +```javascript +import { generateLegalNature } from '@brazilian-utils/brazilian-utils'; + +generateLegalNature(); // '2062' +``` + +## parseLegalNature + +Remove legal nature formatting and return only digits. + +```javascript +import { parseLegalNature } from '@brazilian-utils/brazilian-utils'; + +parseLegalNature('206-2'); // '2062' +``` + +## getLegalNatures + +Get the legal nature map keyed by code. + +```javascript +import { getLegalNatures } from '@brazilian-utils/brazilian-utils'; + +const legalNatures = getLegalNatures(); + +legalNatures['2062']; // 'Sociedade Empresária Limitada' +``` + +## generatePhone + +Generate a random Brazilian phone number. + +```javascript +import { generatePhone } from '@brazilian-utils/brazilian-utils'; + +generatePhone(); // '11912345678' or '1131234567' +generatePhone('mobile'); // '11912345678' +generatePhone('landline'); // '1131234567' +``` + +## formatLicensePlate + +Format a license plate. Old Brazilian plates are returned with a hyphen and Mercosul plates stay normalized. + +```javascript +import { formatLicensePlate } from '@brazilian-utils/brazilian-utils'; + +formatLicensePlate('abc1234'); // 'ABC-1234' +formatLicensePlate('abc1d23'); // 'ABC1D23' +``` + +## generateLicensePlate + +Generate a random license plate in the chosen format. + +```javascript +import { generateLicensePlate } from '@brazilian-utils/brazilian-utils'; + +generateLicensePlate(); // 'ABC1D23' +generateLicensePlate('LLLNNNN'); // 'ABC1234' +generateLicensePlate('LLLNNLN'); // 'ABC12D3' +``` + +## getFormatLicensePlate + +Detect the normalized format of a license plate. + +```javascript +import { getFormatLicensePlate } from '@brazilian-utils/brazilian-utils'; + +getFormatLicensePlate('ABC-1234'); // 'LLLNNNN' +getFormatLicensePlate('ABC1D23'); // 'LLLNLNN' +getFormatLicensePlate('ABC12D3'); // 'LLLNNLN' +getFormatLicensePlate('INVALID'); // null +``` + +## parseLicensePlate + +Remove separators from a license plate and normalize it to uppercase. + +```javascript +import { parseLicensePlate } from '@brazilian-utils/brazilian-utils'; + +parseLicensePlate('abc-1234'); // 'ABC1234' +``` + +## generatePis + +Generate a valid random PIS. + +```javascript +import { generatePis } from '@brazilian-utils/brazilian-utils'; + +generatePis(); // '12345678901' +``` + +## getMunicipality + +Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. + +```javascript +import { getMunicipality } from '@brazilian-utils/brazilian-utils'; + +await getMunicipality({ code: '3550308' }); +// ['São Paulo', 'SP'] + +await getMunicipality({ municipalityName: 'São Paulo', uf: 'SP' }); +// '3550308' +``` + +## isHoliday + +Check if a specific date is a Brazilian holiday. + +```javascript +import { isHoliday } from '@brazilian-utils/brazilian-utils'; + +isHoliday({ targetDate: new Date('2024-01-01') }); // true +isHoliday({ targetDate: new Date('2024-07-09'), stateCode: 'SP' }); // true +``` + +## formatVoterId + +Format a voter ID number. + +```javascript +import { formatVoterId } from '@brazilian-utils/brazilian-utils'; + +formatVoterId('123456780175'); // '1234 5678 01 75' +``` + +## isValidVoterId + +Check if a voter ID number is valid. + +```javascript +import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-utils'; + +const voterId = generateVoterId('SP'); + +isValidVoterId(voterId); // true +``` + +## generateVoterId + +Generate a valid random voter ID number. You can optionally provide a state code. + +```javascript +import { generateVoterId } from '@brazilian-utils/brazilian-utils'; + +generateVoterId(); // valid random voter ID +generateVoterId('SP'); // valid random voter ID for Sao Paulo +``` + +## parseVoterId + +Remove voter ID formatting and return only digits. + +```javascript +import { parseVoterId } from '@brazilian-utils/brazilian-utils'; + +parseVoterId('1234 5678 01 75'); // '123456780175' +``` diff --git a/src/format-cnh/format-cnh.test.ts b/src/format-cnh/format-cnh.test.ts new file mode 100644 index 00000000..9cca80e4 --- /dev/null +++ b/src/format-cnh/format-cnh.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatCnh } from "./format-cnh"; + +describe("formatCnh", () => { + it("should format CNH values", () => { + expect(formatCnh("00000000119")).toBe("000000001-19"); + expect(formatCnh("000000001")).toBe("000000001"); + }); + + it("should remove non numeric characters", () => { + expect(formatCnh("000.000.001-19")).toBe("000000001-19"); + }); +}); diff --git a/src/format-cnh/format-cnh.ts b/src/format-cnh/format-cnh.ts new file mode 100644 index 00000000..5b51bfd0 --- /dev/null +++ b/src/format-cnh/format-cnh.ts @@ -0,0 +1,11 @@ +import { type FormatParams, format } from "../_internals/format/format"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export type FormatCnhOptions = Pick; + +export const formatCnh = (value: string | number, options?: FormatCnhOptions): string => + format({ + pad: options?.pad, + value: sanitizeToDigits(value), + pattern: "000000000-00", + }); diff --git a/src/format-legal-nature/format-legal-nature.test.ts b/src/format-legal-nature/format-legal-nature.test.ts new file mode 100644 index 00000000..41530a8c --- /dev/null +++ b/src/format-legal-nature/format-legal-nature.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatLegalNature } from "./format-legal-nature"; + +describe("formatLegalNature", () => { + it("should format legal nature values", () => { + expect(formatLegalNature("2062")).toBe("206-2"); + }); +}); diff --git a/src/format-legal-nature/format-legal-nature.ts b/src/format-legal-nature/format-legal-nature.ts new file mode 100644 index 00000000..cf2c6568 --- /dev/null +++ b/src/format-legal-nature/format-legal-nature.ts @@ -0,0 +1,8 @@ +import { format } from "../_internals/format/format"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export const formatLegalNature = (value: string | number): string => + format({ + value: sanitizeToDigits(value), + pattern: "000-0", + }); diff --git a/src/format-license-plate/constants.ts b/src/format-license-plate/constants.ts new file mode 100644 index 00000000..2f082a72 --- /dev/null +++ b/src/format-license-plate/constants.ts @@ -0,0 +1 @@ +export const OLD_FORMAT_SEPARATOR_INDEX = 3; diff --git a/src/format-license-plate/format-license-plate.test.ts b/src/format-license-plate/format-license-plate.test.ts new file mode 100644 index 00000000..a625217d --- /dev/null +++ b/src/format-license-plate/format-license-plate.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatLicensePlate } from "./format-license-plate"; + +describe("formatLicensePlate", () => { + it("should format old pattern license plates", () => { + expect(formatLicensePlate("abc1234")).toBe("ABC-1234"); + }); + + it("should keep mercosul plates normalized", () => { + expect(formatLicensePlate("abc1d23")).toBe("ABC1D23"); + }); +}); diff --git a/src/format-license-plate/format-license-plate.ts b/src/format-license-plate/format-license-plate.ts new file mode 100644 index 00000000..ec60c809 --- /dev/null +++ b/src/format-license-plate/format-license-plate.ts @@ -0,0 +1,16 @@ +import { getFormatLicensePlate } from "../get-format-license-plate/get-format-license-plate"; +import { parseLicensePlate } from "../parse-license-plate/parse-license-plate"; +import { OLD_FORMAT_SEPARATOR_INDEX } from "./constants"; + +export const formatLicensePlate = (value: string): string => { + const parsed = parseLicensePlate(value); + const format = getFormatLicensePlate(parsed); + + if (!format) return ""; + + if (format === "LLLNNNN") { + return `${parsed.slice(0, OLD_FORMAT_SEPARATOR_INDEX)}-${parsed.slice(OLD_FORMAT_SEPARATOR_INDEX)}`; + } + + return parsed; +}; diff --git a/src/format-voter-id/format-voter-id.test.ts b/src/format-voter-id/format-voter-id.test.ts new file mode 100644 index 00000000..5449b676 --- /dev/null +++ b/src/format-voter-id/format-voter-id.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { formatVoterId } from "./format-voter-id"; + +describe("formatVoterId", () => { + it("should format voter ids", () => { + expect(formatVoterId("123456780124")).toBe("1234 5678 01 24"); + }); +}); diff --git a/src/format-voter-id/format-voter-id.ts b/src/format-voter-id/format-voter-id.ts new file mode 100644 index 00000000..df1a0f0e --- /dev/null +++ b/src/format-voter-id/format-voter-id.ts @@ -0,0 +1,5 @@ +import { format } from "../_internals/format/format"; +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export const formatVoterId = (value: string | number): string => + format({ value: sanitizeToDigits(value), pattern: "0000 0000 00 00" }); diff --git a/src/generate-cep/generate-cep.test.ts b/src/generate-cep/generate-cep.test.ts new file mode 100644 index 00000000..3355c927 --- /dev/null +++ b/src/generate-cep/generate-cep.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { generateCep } from "./generate-cep"; + +describe("generateCep", () => { + it("should generate a valid CEP", () => { + expect(generateCep()).toMatch(/^\d{8}$/); + }); +}); diff --git a/src/generate-cep/generate-cep.ts b/src/generate-cep/generate-cep.ts new file mode 100644 index 00000000..c2f5c9c8 --- /dev/null +++ b/src/generate-cep/generate-cep.ts @@ -0,0 +1,3 @@ +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; + +export const generateCep = (): string => generateRandomNumber(8); diff --git a/src/generate-cnh/generate-cnh.test.ts b/src/generate-cnh/generate-cnh.test.ts new file mode 100644 index 00000000..2eeeaaea --- /dev/null +++ b/src/generate-cnh/generate-cnh.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidCnh } from "../is-valid-cnh/is-valid-cnh"; +import { generateCnh } from "./generate-cnh"; + +describe("generateCnh", () => { + it("should generate valid CNH values", () => { + for (let i = 0; i < 50; i++) { + expect(isValidCnh(generateCnh())).toBe(true); + } + }); +}); diff --git a/src/generate-cnh/generate-cnh.ts b/src/generate-cnh/generate-cnh.ts new file mode 100644 index 00000000..db136016 --- /dev/null +++ b/src/generate-cnh/generate-cnh.ts @@ -0,0 +1,51 @@ +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; + +const calculateFirstVerifier = (base: string): { firstVerifier: number; decrement: number } => { + let sum = 0; + + for (let i = 0; i < 9; i++) { + sum += (base.charCodeAt(i) - 48) * (9 - i); + } + + const remainder = sum % 11; + + if (remainder >= 10) { + return { firstVerifier: 0, decrement: 2 }; + } + + return { firstVerifier: remainder, decrement: 0 }; +}; + +const calculateSecondVerifier = ({ + base, + decrement, +}: { + base: string; + decrement: number; +}): number => { + let sum = 0; + + for (let i = 0; i < 9; i++) { + sum += (base.charCodeAt(i) - 48) * (i + 1); + } + + let secondVerifier = (sum % 11) - decrement; + + if (secondVerifier < 0) secondVerifier += 11; + if (secondVerifier >= 10) secondVerifier = 0; + + return secondVerifier; +}; + +export const generateCnh = (): string => { + let base = generateRandomNumber(9); + + while (/^(\d)\1+$/.test(base)) { + base = generateRandomNumber(9); + } + + const { firstVerifier, decrement } = calculateFirstVerifier(base); + const secondVerifier = calculateSecondVerifier({ base, decrement }); + + return `${base}${firstVerifier}${secondVerifier}`; +}; diff --git a/src/generate-legal-nature/generate-legal-nature.test.ts b/src/generate-legal-nature/generate-legal-nature.test.ts new file mode 100644 index 00000000..08f75cff --- /dev/null +++ b/src/generate-legal-nature/generate-legal-nature.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidLegalNature } from "../is-valid-legal-nature/is-valid-legal-nature"; +import { generateLegalNature } from "./generate-legal-nature"; + +describe("generateLegalNature", () => { + it("should generate valid legal nature values", () => { + for (let i = 0; i < 50; i++) { + expect(isValidLegalNature(generateLegalNature())).toBe(true); + } + }); +}); diff --git a/src/generate-legal-nature/generate-legal-nature.ts b/src/generate-legal-nature/generate-legal-nature.ts new file mode 100644 index 00000000..70eff652 --- /dev/null +++ b/src/generate-legal-nature/generate-legal-nature.ts @@ -0,0 +1,6 @@ +import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; + +const LEGAL_NATURE_CODES = Object.keys(LEGAL_NATURE); + +export const generateLegalNature = (): string => + LEGAL_NATURE_CODES[Math.floor(Math.random() * LEGAL_NATURE_CODES.length)]; diff --git a/src/generate-license-plate/generate-license-plate.test.ts b/src/generate-license-plate/generate-license-plate.test.ts new file mode 100644 index 00000000..00dcf608 --- /dev/null +++ b/src/generate-license-plate/generate-license-plate.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidLicensePlate } from "../is-valid-license-plate/is-valid-license-plate"; +import { generateLicensePlate } from "./generate-license-plate"; + +describe("generateLicensePlate", () => { + it("should generate valid plates for all supported formats", () => { + expect(isValidLicensePlate(generateLicensePlate("LLLNNNN"))).toBe(true); + expect(isValidLicensePlate(generateLicensePlate("LLLNLNN"))).toBe(true); + expect(isValidLicensePlate(generateLicensePlate("LLLNNLN"))).toBe(true); + }); +}); diff --git a/src/generate-license-plate/generate-license-plate.ts b/src/generate-license-plate/generate-license-plate.ts new file mode 100644 index 00000000..d5597910 --- /dev/null +++ b/src/generate-license-plate/generate-license-plate.ts @@ -0,0 +1,14 @@ +const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +export type GenerateLicensePlateFormat = "LLLNNNN" | "LLLNLNN" | "LLLNNLN"; + +const randomLetter = (): string => LETTERS[Math.floor(Math.random() * LETTERS.length)]; + +const randomDigit = (): string => Math.floor(Math.random() * 10).toString(); + +export const generateLicensePlate = (format: GenerateLicensePlateFormat = "LLLNLNN"): string => { + return format + .split("") + .map((char) => (char === "L" ? randomLetter() : randomDigit())) + .join(""); +}; diff --git a/src/generate-phone/generate-phone.test.ts b/src/generate-phone/generate-phone.test.ts new file mode 100644 index 00000000..d9e20e4c --- /dev/null +++ b/src/generate-phone/generate-phone.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidLandlinePhone } from "../is-valid-landline-phone/is-valid-landline-phone"; +import { isValidMobilePhone } from "../is-valid-mobile-phone/is-valid-mobile-phone"; +import { isValidPhone } from "../is-valid-phone/is-valid-phone"; +import { generatePhone } from "./generate-phone"; + +describe("generatePhone", () => { + it("should generate a valid mobile phone", () => { + expect(isValidMobilePhone(generatePhone("mobile"), { version: 2 })).toBe(true); + }); + + it("should generate a valid landline phone", () => { + expect(isValidLandlinePhone(generatePhone("landline"))).toBe(true); + }); + + it("should generate a valid phone when type is omitted", () => { + expect(isValidPhone(generatePhone())).toBe(true); + }); +}); diff --git a/src/generate-phone/generate-phone.ts b/src/generate-phone/generate-phone.ts new file mode 100644 index 00000000..9fa2ee49 --- /dev/null +++ b/src/generate-phone/generate-phone.ts @@ -0,0 +1,21 @@ +import { VALID_AREA_CODES } from "../_internals/constants/area-codes"; +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; + +export type GeneratePhoneType = "mobile" | "landline"; + +const randomAreaCode = (): string => + VALID_AREA_CODES[Math.floor(Math.random() * VALID_AREA_CODES.length)].toString(); + +export const generatePhone = (type?: GeneratePhoneType): string => { + const areaCode = randomAreaCode(); + + if (type === "landline") { + return `${areaCode}${2 + Math.floor(Math.random() * 4)}${generateRandomNumber(7)}`; + } + + if (type === "mobile") { + return `${areaCode}9${generateRandomNumber(8)}`; + } + + return Math.random() >= 0.5 ? generatePhone("mobile") : generatePhone("landline"); +}; diff --git a/src/generate-pis/generate-pis.ts b/src/generate-pis/generate-pis.ts new file mode 100644 index 00000000..f5fbc033 --- /dev/null +++ b/src/generate-pis/generate-pis.ts @@ -0,0 +1,14 @@ +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; + +const WEIGHTS = [3, 2, 9, 8, 7, 6, 5, 4, 3, 2] as const; + +const calculateCheckDigit = (base: string): string => { + const sum = base.split("").reduce((acc, digit, index) => acc + Number(digit) * WEIGHTS[index], 0); + const digit = 11 - (sum % 11); + return digit >= 10 ? "0" : digit.toString(); +}; + +export const generatePis = (): string => { + const base = generateRandomNumber(10); + return `${base}${calculateCheckDigit(base)}`; +}; diff --git a/src/generate-processo-juridico/generate-processo-juridico.ts b/src/generate-processo-juridico/generate-processo-juridico.ts new file mode 100644 index 00000000..bd46f5cd --- /dev/null +++ b/src/generate-processo-juridico/generate-processo-juridico.ts @@ -0,0 +1,36 @@ +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; + +export type GenerateProcessoJuridicoOptions = { + year?: number; + court?: number; +}; + +const calculateCheckDigits = (base: string): string => { + const checksum = 98n - ((BigInt(base) * 100n) % 97n); + return checksum.toString().padStart(2, "0"); +}; + +export const generateProcessoJuridico = ( + options: GenerateProcessoJuridicoOptions = {}, +): string | null => { + const { year = new Date().getFullYear(), court = Math.floor(Math.random() * 9) + 1 } = options; + const currentYear = new Date().getFullYear(); + + if ( + !Number.isInteger(year) || + year < currentYear || + !Number.isInteger(court) || + court < 1 || + court > 9 + ) { + return null; + } + + const sequencial = generateRandomNumber(7); + const tribunal = generateRandomNumber(2); + const foro = generateRandomNumber(4); + const base = `${sequencial}${year}${court}${tribunal}${foro}`; + const checkDigits = calculateCheckDigits(base); + + return `${sequencial}${checkDigits}${year}${court}${tribunal}${foro}`; +}; diff --git a/src/generate-voter-id/generate-voter-id.test.ts b/src/generate-voter-id/generate-voter-id.test.ts new file mode 100644 index 00000000..e9825ccf --- /dev/null +++ b/src/generate-voter-id/generate-voter-id.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidVoterId } from "../is-valid-voter-id/is-valid-voter-id"; +import { generateVoterId } from "./generate-voter-id"; + +describe("generateVoterId", () => { + it("should generate valid voter ids", () => { + for (let i = 0; i < 50; i++) { + expect(isValidVoterId(generateVoterId())).toBe(true); + } + }); + + it("should generate voter id for a specific state", () => { + expect(generateVoterId("SP").slice(8, 10)).toBe("01"); + }); +}); diff --git a/src/generate-voter-id/generate-voter-id.ts b/src/generate-voter-id/generate-voter-id.ts new file mode 100644 index 00000000..dbe1199f --- /dev/null +++ b/src/generate-voter-id/generate-voter-id.ts @@ -0,0 +1,55 @@ +import type { StateCode } from "../_internals/constants/states"; +import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; +import { UF_TO_VOTER_ID_CODE } from "../is-valid-voter-id/constants"; + +const calculateFirstDigit = ({ + sequentialNumber, + federativeUnion, +}: { + sequentialNumber: string; + federativeUnion: string; +}): number => { + let sum = 0; + + for (let i = 0; i < 8; i++) { + sum += (sequentialNumber.charCodeAt(i) - 48) * (i + 2); + } + + const remainder = sum % 11; + + if (remainder === 0 && (federativeUnion === "01" || federativeUnion === "02")) { + return 1; + } + + return remainder === 10 ? 0 : remainder; +}; + +const calculateSecondDigit = ({ + federativeUnion, + firstDigit, +}: { + federativeUnion: string; + firstDigit: number; +}): number => { + const sum = + (federativeUnion.charCodeAt(0) - 48) * 7 + + (federativeUnion.charCodeAt(1) - 48) * 8 + + firstDigit * 9; + + const remainder = sum % 11; + + if ((federativeUnion === "01" || federativeUnion === "02") && remainder === 0) { + return 1; + } + + return remainder === 10 ? 0 : remainder; +}; + +export const generateVoterId = (state: StateCode | "ZZ" = "ZZ"): string => { + const federativeUnion = UF_TO_VOTER_ID_CODE[state]; + const sequentialNumber = generateRandomNumber(8); + const digit1 = calculateFirstDigit({ sequentialNumber, federativeUnion }); + const digit2 = calculateSecondDigit({ federativeUnion, firstDigit: digit1 }); + + return `${sequentialNumber}${federativeUnion}${digit1}${digit2}`; +}; diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.test.ts b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts new file mode 100644 index 00000000..3eb42663 --- /dev/null +++ b/src/get-cep-info-by-address/get-cep-info-by-address.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "../_internals/test/runtime"; +import { GetCepInfoByAddressValidationError, getCepInfoByAddress } from "./get-cep-info-by-address"; + +describe("getCepInfoByAddress", () => { + const fetchMock = vi.fn(); + const originalFetch = globalThis.fetch; + + beforeEach(() => { + globalThis.fetch = fetchMock as unknown as typeof fetch; + fetchMock.mockClear(); + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it("should validate UF before fetching", async () => { + await expect( + getCepInfoByAddress({ + federalUnit: "XX", + city: "São Paulo", + street: "Avenida Paulista", + }), + ).rejects.toThrow(GetCepInfoByAddressValidationError); + }); + + it("should return addresses from ViaCEP", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => [ + { + bairro: "Bela Vista", + cep: "01310-100", + localidade: "São Paulo", + logradouro: "Avenida Paulista", + uf: "SP", + }, + ], + }); + + await expect( + getCepInfoByAddress({ + federalUnit: "SP", + city: "São Paulo", + street: "Avenida Paulista", + }), + ).resolves.toEqual([ + { + bairro: "Bela Vista", + cep: "01310-100", + localidade: "São Paulo", + logradouro: "Avenida Paulista", + uf: "SP", + }, + ]); + }); +}); diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.ts b/src/get-cep-info-by-address/get-cep-info-by-address.ts new file mode 100644 index 00000000..61d527f2 --- /dev/null +++ b/src/get-cep-info-by-address/get-cep-info-by-address.ts @@ -0,0 +1,81 @@ +import { DATA as STATES } from "../_internals/constants/states"; + +export class GetCepInfoByAddressError extends Error { + constructor(message: string) { + super(message); + this.name = "GetCepInfoByAddressError"; + } +} + +export class GetCepInfoByAddressValidationError extends GetCepInfoByAddressError { + constructor(message: string) { + super(message); + this.name = "GetCepInfoByAddressValidationError"; + } +} + +export class GetCepInfoByAddressNotFoundError extends GetCepInfoByAddressError { + constructor(message: string) { + super(message); + this.name = "GetCepInfoByAddressNotFoundError"; + } +} + +export type CepAddressInfo = { + cep: string; + logradouro: string; + complemento: string; + bairro: string; + localidade: string; + uf: string; + ibge?: string; + gia?: string; + ddd?: string; + siafi?: string; +}; + +export type GetCepInfoByAddressOptions = { + federalUnit: string; + city: string; + street: string; +}; + +const VALID_STATE_CODES = new Set(STATES.map((state) => state.code)); + +const normalizeAddressPart = (value: string): string => + value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .trim(); + +export const getCepInfoByAddress = async ({ + federalUnit, + city, + street, +}: GetCepInfoByAddressOptions): Promise => { + const normalizedUf = federalUnit.trim().toUpperCase(); + + if (!VALID_STATE_CODES.has(normalizedUf)) { + throw new GetCepInfoByAddressValidationError(`Invalid UF: ${federalUnit}`); + } + + if (!city || !street) { + throw new GetCepInfoByAddressValidationError("City and street are required"); + } + + const response = await fetch( + `https://viacep.com.br/ws/${normalizedUf}/${encodeURIComponent(normalizeAddressPart(city))}/${encodeURIComponent(normalizeAddressPart(street))}/json/`, + ); + + if (!response.ok) { + throw new GetCepInfoByAddressError(`ViaCEP request failed with status ${response.status}`); + } + + const data = (await response.json()) as CepAddressInfo[]; + + if (!Array.isArray(data) || data.length === 0) { + throw new GetCepInfoByAddressNotFoundError(`${normalizedUf} - ${city} - ${street}`); + } + + return data; +}; diff --git a/src/get-format-license-plate/constants.ts b/src/get-format-license-plate/constants.ts new file mode 100644 index 00000000..1f155194 --- /dev/null +++ b/src/get-format-license-plate/constants.ts @@ -0,0 +1,3 @@ +export const OLD_FORMAT_REGEX = /^[A-Z]{3}[0-9]{4}$/; +export const MERCOSUL_CAR_REGEX = /^[A-Z]{3}[0-9][A-Z][0-9]{2}$/; +export const MERCOSUL_MOTORCYCLE_REGEX = /^[A-Z]{3}[0-9]{2}[A-Z][0-9]$/; diff --git a/src/get-format-license-plate/get-format-license-plate.test.ts b/src/get-format-license-plate/get-format-license-plate.test.ts new file mode 100644 index 00000000..1515c964 --- /dev/null +++ b/src/get-format-license-plate/get-format-license-plate.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getFormatLicensePlate } from "./get-format-license-plate"; + +describe("getFormatLicensePlate", () => { + it("should identify supported formats", () => { + expect(getFormatLicensePlate("ABC1234")).toBe("LLLNNNN"); + expect(getFormatLicensePlate("ABC1D23")).toBe("LLLNLNN"); + expect(getFormatLicensePlate("ABC12D3")).toBe("LLLNNLN"); + }); +}); diff --git a/src/get-format-license-plate/get-format-license-plate.ts b/src/get-format-license-plate/get-format-license-plate.ts new file mode 100644 index 00000000..0621d2c2 --- /dev/null +++ b/src/get-format-license-plate/get-format-license-plate.ts @@ -0,0 +1,14 @@ +import { parseLicensePlate } from "../parse-license-plate/parse-license-plate"; +import { MERCOSUL_CAR_REGEX, MERCOSUL_MOTORCYCLE_REGEX, OLD_FORMAT_REGEX } from "./constants"; + +export type LicensePlateFormat = "LLLNNNN" | "LLLNLNN" | "LLLNNLN"; + +export const getFormatLicensePlate = (value: string): LicensePlateFormat | null => { + const parsed = parseLicensePlate(value); + + if (OLD_FORMAT_REGEX.test(parsed)) return "LLLNNNN"; + if (MERCOSUL_CAR_REGEX.test(parsed)) return "LLLNLNN"; + if (MERCOSUL_MOTORCYCLE_REGEX.test(parsed)) return "LLLNNLN"; + + return null; +}; diff --git a/src/get-legal-natures/get-legal-natures.test.ts b/src/get-legal-natures/get-legal-natures.test.ts new file mode 100644 index 00000000..bbc2ea0c --- /dev/null +++ b/src/get-legal-natures/get-legal-natures.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { getLegalNatures } from "./get-legal-natures"; + +describe("getLegalNatures", () => { + it("should return legal nature entries", () => { + expect(getLegalNatures()["2062"]).toBe("Sociedade Empresaria Limitada"); + }); +}); diff --git a/src/get-legal-natures/get-legal-natures.ts b/src/get-legal-natures/get-legal-natures.ts new file mode 100644 index 00000000..d3511505 --- /dev/null +++ b/src/get-legal-natures/get-legal-natures.ts @@ -0,0 +1,3 @@ +import { LEGAL_NATURE } from "../is-valid-legal-nature/constants"; + +export const getLegalNatures = (): Record => ({ ...LEGAL_NATURE }); diff --git a/src/get-municipality/get-municipality.test.ts b/src/get-municipality/get-municipality.test.ts new file mode 100644 index 00000000..b78500dd --- /dev/null +++ b/src/get-municipality/get-municipality.test.ts @@ -0,0 +1,40 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "../_internals/test/runtime"; +import { getMunicipality } from "./get-municipality"; + +describe("getMunicipality", () => { + const fetchMock = vi.fn(); + const originalFetch = globalThis.fetch; + + beforeEach(() => { + globalThis.fetch = fetchMock as unknown as typeof fetch; + fetchMock.mockClear(); + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it("should get municipality code by name", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => [{ id: 3550308, nome: "São Paulo" }], + }); + + await expect(getMunicipality({ municipalityName: "Sao Paulo", uf: "sp" })).resolves.toBe( + "3550308", + ); + }); + + it("should get municipality name and UF by code", async () => { + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + microrregiao: { mesorregiao: { UF: { sigla: "SP" } } }, + nome: "São Paulo", + }), + }); + + await expect(getMunicipality({ code: "3550308" })).resolves.toEqual(["São Paulo", "SP"]); + }); +}); diff --git a/src/get-municipality/get-municipality.ts b/src/get-municipality/get-municipality.ts new file mode 100644 index 00000000..5c7b72db --- /dev/null +++ b/src/get-municipality/get-municipality.ts @@ -0,0 +1,83 @@ +type MunicipalityByCodeResponse = { + nome?: string; + microrregiao?: { + mesorregiao?: { + UF?: { + sigla?: string; + }; + }; + }; +}; + +type MunicipalityByNameResponse = { + id?: number; + nome?: string; +}; + +export type GetMunicipalityByCodeOptions = { + code: string; +}; + +export type GetMunicipalityByNameOptions = { + municipalityName: string; + uf: string; +}; + +export type GetMunicipalityOptions = GetMunicipalityByCodeOptions | GetMunicipalityByNameOptions; + +const normalizeText = (value: string): string => + value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/\s+/g, " ") + .trim() + .toUpperCase(); + +const getMunicipalityByCode = async (code: string): Promise<[string, string] | null> => { + const response = await fetch( + `https://servicodados.ibge.gov.br/api/v1/localidades/municipios/${code}`, + ); + + if (!response.ok) return null; + + const data = (await response.json()) as MunicipalityByCodeResponse; + const name = data.nome; + const uf = data.microrregiao?.mesorregiao?.UF?.sigla; + + if (!name || !uf) return null; + + return [name, uf]; +}; + +const getMunicipalityCodeByName = async ({ + municipalityName, + uf, +}: GetMunicipalityByNameOptions): Promise => { + if (!municipalityName || typeof municipalityName !== "string") return null; + + const normalizedUf = uf.trim().toUpperCase(); + + if (!/^[A-Z]{2}$/.test(normalizedUf)) return null; + + const response = await fetch( + `https://servicodados.ibge.gov.br/api/v1/localidades/estados/${normalizedUf}/municipios`, + ); + + if (!response.ok) return null; + + const data = (await response.json()) as MunicipalityByNameResponse[]; + const normalizedName = normalizeText(municipalityName); + const municipality = data.find((item) => normalizeText(item.nome ?? "") === normalizedName); + + return municipality?.id?.toString() ?? null; +}; + +export const getMunicipality = async ( + options: GetMunicipalityOptions, +): Promise<[string, string] | null | string> => { + if ("code" in options) { + return getMunicipalityByCode(options.code); + } + + return getMunicipalityCodeByName(options); +}; diff --git a/src/index.test.ts b/src/index.test.ts index 605cc73c..3ee79404 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -2,72 +2,91 @@ import { describe, expect, test } from "./_internals/test/runtime"; import * as brazilianUtils from "./index"; const PUBLIC = [ - "formatCep", - "formatCnpj", - "formatCpf", - "isValidRenavam", - "formatCurrency", - "parseCurrency", - "formatPis", - "isValidPis", - "getStates", - "getCities", - "isValidBankAccount", - "formatBoleto", "capitalize", "formatCep", + "formatBoleto", + "formatCnh", "formatCnpj", - "formatProcessoJuridico", "formatCpf", "formatCurrency", - "formatPhone", - "parseBoleto", - "parseCep", - "parseCnpj", - "parseCpf", - "parseCurrency", - "parsePhone", - "isValidPassport", - "generatePassport", - "parsePassport", + "formatLegalNature", + "formatLicensePlate", "formatPassport", + "formatPhone", "formatPis", - "parsePis", - "parseProcessoJuridico", - "getStates", - "getCities", - "getHolidays", + "formatProcessoJuridico", + "formatVoterId", + "generateBoleto", + "generateCep", + "generateCnh", + "generateCNPJ", + "generateCPF", + "generateCnpj", + "generateCpf", + "generateLegalNature", + "generateLicensePlate", + "generatePassport", + "generatePhone", + "generatePis", + "generateProcessoJuridico", + "generateVoterId", "getAddressInfoByCep", "GetAddressInfoByCepError", "GetAddressInfoByCepNotFoundError", "GetAddressInfoByCepServiceError", "GetAddressInfoByCepValidationError", - "isValidBoleto", - "generateBoleto", "getBoletoInfo", + "getCepInfoByAddress", + "GetCepInfoByAddressError", + "GetCepInfoByAddressNotFoundError", + "GetCepInfoByAddressValidationError", + "getCities", + "getFormatLicensePlate", + "getHolidays", + "getLegalNatures", + "getMunicipality", + "getStates", + "isHoliday", + "isValidBankAccount", + "isValidBoleto", + "isValidCEP", + "isValidCNPJ", + "isValidCPF", "isValidCep", - "isValidEmail", - "isValidProcessoJuridico", - "isValidPhone", - "isValidMobilePhone", - "isValidLandlinePhone", - "isValidLicensePlate", + "isValidCnh", "isValidCnpj", - "generateCnpj", "isValidCpf", - "generateCpf", + "isValidEmail", + "isValidIE", "isValidIe", - // Deprecated exports (will be removed in v3.0.0) + "isValidLandlinePhone", + "isValidLegalNature", + "isValidLicensePlate", + "isValidMobilePhone", + "isValidPIS", + "isValidPassport", + "isValidPhone", + "isValidPis", + "isValidProcessoJuridico", + "isValidRenavam", + "isValidVoterId", + "parseBoleto", + "parseCep", + "parseCnh", + "parseCnpj", + "parseCpf", + "formatCurrency", + "parseCurrency", + "parseLegalNature", + "parseLicensePlate", + "parsePassport", + "parsePhone", + "parsePis", + "parseProcessoJuridico", + "parseVoterId", "formatCEP", "formatCNPJ", "formatCPF", - "isValidCEP", - "isValidCNPJ", - "isValidCPF", - "isValidPIS", - "isValidIE", - "generateCNPJ", - "generateCPF", ]; describe("Public API", () => { diff --git a/src/index.ts b/src/index.ts index 05fe26f6..64d4fa10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ export { type CapitalizeOptions, capitalize } from "./capitalize/capitalize"; export { type FormatBoletoOptions, formatBoleto } from "./format-boleto/format-boleto"; export { type FormatCepOptions, formatCep } from "./format-cep/format-cep"; +export { type FormatCnhOptions, formatCnh } from "./format-cnh/format-cnh"; export { type FormatCnpjOptions, formatCnpj } from "./format-cnpj/format-cnpj"; export { type FormatCpfOptions, formatCpf } from "./format-cpf/format-cpf"; export { type FormatCurrencyOptions, formatCurrency } from "./format-currency/format-currency"; +export { formatLegalNature } from "./format-legal-nature/format-legal-nature"; +export { formatLicensePlate } from "./format-license-plate/format-license-plate"; export { formatPassport } from "./format-passport/format-passport"; export { type FormatPhoneOptions, formatPhone } from "./format-phone/format-phone"; export { type FormatPisOptions, formatPis } from "./format-pis/format-pis"; @@ -11,10 +14,22 @@ export { type FormatProcessoJuridicoOptions, formatProcessoJuridico, } from "./format-processo-juridico/format-processo-juridico"; +export { formatVoterId } from "./format-voter-id/format-voter-id"; export { generateBoleto } from "./generate-boleto/generate-boleto"; +export { generateCep } from "./generate-cep/generate-cep"; +export { generateCnh } from "./generate-cnh/generate-cnh"; export { generateCnpj } from "./generate-cnpj/generate-cnpj"; export { generateCpf } from "./generate-cpf/generate-cpf"; +export { generateLegalNature } from "./generate-legal-nature/generate-legal-nature"; +export { generateLicensePlate } from "./generate-license-plate/generate-license-plate"; export { generatePassport } from "./generate-passport/generate-passport"; +export { generatePhone, type GeneratePhoneType } from "./generate-phone/generate-phone"; +export { generatePis } from "./generate-pis/generate-pis"; +export { + type GenerateProcessoJuridicoOptions, + generateProcessoJuridico, +} from "./generate-processo-juridico/generate-processo-juridico"; +export { generateVoterId } from "./generate-voter-id/generate-voter-id"; export { GetAddressInfoByCepError, GetAddressInfoByCepNotFoundError, @@ -25,19 +40,41 @@ export { } from "./get-address-info-by-cep/get-address-info-by-cep"; export { getBoletoInfo } from "./get-boleto-info/get-boleto-info"; export { getCities } from "./get-cities/get-cities"; +export { + GetCepInfoByAddressError, + GetCepInfoByAddressNotFoundError, + type CepAddressInfo, + type GetCepInfoByAddressOptions, + GetCepInfoByAddressValidationError, + getCepInfoByAddress, +} from "./get-cep-info-by-address/get-cep-info-by-address"; +export { + getFormatLicensePlate, + type LicensePlateFormat, +} from "./get-format-license-plate/get-format-license-plate"; export { type GetHolidaysOptions, getHolidays } from "./get-holidays/get-holidays"; +export { getLegalNatures } from "./get-legal-natures/get-legal-natures"; +export { + type GetMunicipalityByCodeOptions, + type GetMunicipalityByNameOptions, + type GetMunicipalityOptions, + getMunicipality, +} from "./get-municipality/get-municipality"; export { getStates } from "./get-states/get-states"; +export { type IsHolidayOptions, isHoliday } from "./is-holiday/is-holiday"; export { - type IsValidBankAccountParams, + type IsValidBankAccountOptions, isValidBankAccount, } from "./is-valid-bank-account/is-valid-bank-account"; export { isValidBoleto } from "./is-valid-boleto/is-valid-boleto"; export { isValidCep } from "./is-valid-cep/is-valid-cep"; +export { isValidCnh } from "./is-valid-cnh/is-valid-cnh"; export { isValidCnpj } from "./is-valid-cnpj/is-valid-cnpj"; export { isValidCpf } from "./is-valid-cpf/is-valid-cpf"; export { isValidEmail } from "./is-valid-email/is-valid-email"; export { isValidIe } from "./is-valid-ie/is-valid-ie"; export { isValidLandlinePhone } from "./is-valid-landline-phone/is-valid-landline-phone"; +export { isValidLegalNature } from "./is-valid-legal-nature/is-valid-legal-nature"; export { isValidLicensePlate } from "./is-valid-license-plate/is-valid-license-plate"; export { type IsValidMobilePhoneOptions, @@ -48,20 +85,30 @@ export { type IsValidPhoneOptions, isValidPhone } from "./is-valid-phone/is-vali export { isValidPis } from "./is-valid-pis/is-valid-pis"; export { isValidProcessoJuridico } from "./is-valid-processo-juridico/is-valid-processo-juridico"; export { isValidRenavam } from "./is-valid-renavam/is-valid-renavam"; +export { isValidVoterId } from "./is-valid-voter-id/is-valid-voter-id"; export { parseBoleto } from "./parse-boleto/parse-boleto"; export { parseCep } from "./parse-cep/parse-cep"; +export { parseCnh } from "./parse-cnh/parse-cnh"; export { parseCnpj } from "./parse-cnpj/parse-cnpj"; export { parseCpf } from "./parse-cpf/parse-cpf"; export { parseCurrency } from "./parse-currency/parse-currency"; +export { parseLegalNature } from "./parse-legal-nature/parse-legal-nature"; +export { parseLicensePlate } from "./parse-license-plate/parse-license-plate"; export { parsePassport } from "./parse-passport/parse-passport"; export { parsePhone } from "./parse-phone/parse-phone"; export { parsePis } from "./parse-pis/parse-pis"; export { parseProcessoJuridico } from "./parse-processo-juridico/parse-processo-juridico"; +export { parseVoterId } from "./parse-voter-id/parse-voter-id"; // ============================================================================ // DEPRECATED EXPORTS - Will be removed in v3.0.0 // ============================================================================ +/** + * @deprecated Use `IsValidBankAccountOptions` instead. This alias will be removed in v3.0.0 + */ +export type { IsValidBankAccountParams } from "./is-valid-bank-account/is-valid-bank-account"; + /** * @deprecated Use `formatCep` instead. This alias will be removed in v3.0.0 */ diff --git a/src/is-holiday/is-holiday.ts b/src/is-holiday/is-holiday.ts new file mode 100644 index 00000000..4308d2dc --- /dev/null +++ b/src/is-holiday/is-holiday.ts @@ -0,0 +1,25 @@ +import type { StateCode } from "../_internals/constants/states"; +import { getHolidays } from "../get-holidays/get-holidays"; + +export type IsHolidayOptions = { + targetDate: Date; + stateCode?: StateCode; +}; + +export const isHoliday = ({ targetDate, stateCode }: IsHolidayOptions): boolean | null => { + if (!(targetDate instanceof Date) || Number.isNaN(targetDate.getTime())) { + return null; + } + + if (stateCode !== undefined && typeof stateCode !== "string") { + return null; + } + + return getHolidays({ year: targetDate.getFullYear(), stateCode }).some((holiday) => { + return ( + holiday.date.getFullYear() === targetDate.getFullYear() && + holiday.date.getMonth() === targetDate.getMonth() && + holiday.date.getDate() === targetDate.getDate() + ); + }); +}; diff --git a/src/is-valid-bank-account/is-valid-bank-account.ts b/src/is-valid-bank-account/is-valid-bank-account.ts index ab6f810c..1fa63d29 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.ts @@ -2,14 +2,16 @@ import { mod10 } from "../_internals/mod10/mod10"; import { mod11 } from "../_internals/mod11/mod11"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -export type IsValidBankAccountParams = { +export type IsValidBankAccountOptions = { bankCode: string; agency: string; account: string; digit: string; }; -const validateBancoDoBrasil = (params: IsValidBankAccountParams): boolean => { +export type IsValidBankAccountParams = IsValidBankAccountOptions; + +const validateBancoDoBrasil = (params: IsValidBankAccountOptions): boolean => { const { agency, account, digit } = params; if (agency.length < 4 || agency.length > 5) return false; @@ -34,7 +36,7 @@ const validateBancoDoBrasil = (params: IsValidBankAccountParams): boolean => { return digitChar === digit; }; -const validateItau = (params: IsValidBankAccountParams): boolean => { +const validateItau = (params: IsValidBankAccountOptions): boolean => { const { agency, account, digit } = params; if (agency.length !== 4) return false; @@ -60,7 +62,7 @@ const validateItau = (params: IsValidBankAccountParams): boolean => { return String(calculatedDigit) === digit; }; -const validateBradesco = (params: IsValidBankAccountParams): boolean => { +const validateBradesco = (params: IsValidBankAccountOptions): boolean => { const { agency, account, digit } = params; if (agency.length !== 4) return false; @@ -82,7 +84,7 @@ const validateBradesco = (params: IsValidBankAccountParams): boolean => { return String(calculatedDigit) === digit; }; -const validateSantander = (params: IsValidBankAccountParams): boolean => { +const validateSantander = (params: IsValidBankAccountOptions): boolean => { const { agency, account, digit } = params; if (agency.length !== 4) return false; @@ -95,7 +97,7 @@ const validateSantander = (params: IsValidBankAccountParams): boolean => { return String(digitValue) === digit; }; -const validateCaixa = (params: IsValidBankAccountParams): boolean => { +const validateCaixa = (params: IsValidBankAccountOptions): boolean => { const { agency, account, digit } = params; if (agency.length !== 4) return false; @@ -109,7 +111,7 @@ const validateCaixa = (params: IsValidBankAccountParams): boolean => { return String(digitValue) === digit; }; -const validateGeneric = (params: IsValidBankAccountParams): boolean => { +const validateGeneric = (params: IsValidBankAccountOptions): boolean => { const { account, digit } = params; if (digit.length < 1 || digit.length > 2) return false; @@ -127,7 +129,7 @@ const validateGeneric = (params: IsValidBankAccountParams): boolean => { return false; }; -const VALIDATORS: Record boolean> = { +const VALIDATORS: Record boolean> = { "001": validateBancoDoBrasil, "341": validateItau, "237": validateBradesco, @@ -146,7 +148,7 @@ const VALIDATORS: Record boolean> * * For other banks, uses generic mod10/mod11 validation. * - * @param {IsValidBankAccountParams} params - The bank account parameters. + * @param {IsValidBankAccountOptions} params - The bank account parameters. * @param {string} params.bankCode - The bank code (3 digits). * @param {string} params.agency - The agency number (1-5 digits). * @param {string} params.account - The account number (1-13 digits). @@ -170,7 +172,7 @@ const VALIDATORS: Record boolean> * }); // true (if valid Itaú account) * ``` */ -export const isValidBankAccount = (params: IsValidBankAccountParams): boolean => { +export const isValidBankAccount = (params: IsValidBankAccountOptions): boolean => { const { bankCode, agency, account, digit } = params; if ( diff --git a/src/is-valid-cnh/is-valid-cnh.test.ts b/src/is-valid-cnh/is-valid-cnh.test.ts new file mode 100644 index 00000000..05cb7522 --- /dev/null +++ b/src/is-valid-cnh/is-valid-cnh.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidCnh } from "./is-valid-cnh"; + +describe("isValidCnh", () => { + it("should return true for valid CNH", () => { + expect(isValidCnh("00000000119")).toBe(true); + expect(isValidCnh("000000001-19")).toBe(true); + }); + + it("should return false for invalid CNH", () => { + expect(isValidCnh("12345678901")).toBe(false); + expect(isValidCnh("11111111111")).toBe(false); + }); +}); diff --git a/src/is-valid-cnh/is-valid-cnh.ts b/src/is-valid-cnh/is-valid-cnh.ts new file mode 100644 index 00000000..f97b36d5 --- /dev/null +++ b/src/is-valid-cnh/is-valid-cnh.ts @@ -0,0 +1,38 @@ +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +const repeatedDigits = (value: string): boolean => value === value[0].repeat(value.length); + +export const isValidCnh = (value: string): boolean => { + if (!value || typeof value !== "string") return false; + + const digits = sanitizeToDigits(value); + + if (digits.length !== 11 || repeatedDigits(digits)) return false; + + let sum1 = 0; + for (let i = 0; i < 9; i++) { + sum1 += (digits.charCodeAt(i) - 48) * (9 - i); + } + + let digit1 = sum1 % 11; + let decrement = 0; + + if (digit1 >= 10) { + digit1 = 0; + decrement = 2; + } + + if (digit1 !== digits.charCodeAt(9) - 48) return false; + + let sum2 = 0; + for (let i = 0; i < 9; i++) { + sum2 += (digits.charCodeAt(i) - 48) * (i + 1); + } + + let digit2 = (sum2 % 11) - decrement; + + if (digit2 < 0) digit2 += 11; + if (digit2 >= 10) digit2 = 0; + + return digit2 === digits.charCodeAt(10) - 48; +}; diff --git a/src/is-valid-legal-nature/constants.ts b/src/is-valid-legal-nature/constants.ts new file mode 100644 index 00000000..884a236c --- /dev/null +++ b/src/is-valid-legal-nature/constants.ts @@ -0,0 +1,62 @@ +export const LEGAL_NATURE: Record = { + "1015": "Orgao Publico do Poder Executivo Federal", + "1023": "Orgao Publico do Poder Executivo Estadual ou do Distrito Federal", + "1031": "Orgao Publico do Poder Executivo Municipal", + "1040": "Orgao Publico do Poder Legislativo Federal", + "1058": "Orgao Publico do Poder Legislativo Estadual ou do Distrito Federal", + "1066": "Orgao Publico do Poder Legislativo Municipal", + "1074": "Orgao Publico do Poder Judiciario Federal", + "1082": "Orgao Publico do Poder Judiciario Estadual", + "1104": "Autarquia Federal", + "1112": "Autarquia Estadual ou do Distrito Federal", + "1120": "Autarquia Municipal", + "1139": "Fundacao Federal", + "1147": "Fundacao Estadual ou do Distrito Federal", + "1155": "Fundacao Municipal", + "1163": "Orgao Publico Autonomo da Uniao", + "1171": "Orgao Publico Autonomo Estadual ou do Distrito Federal", + "1180": "Orgao Publico Autonomo Municipal", + "2011": "Empresa Publica", + "2038": "Sociedade de Economia Mista", + "2046": "Sociedade Anonima Aberta", + "2054": "Sociedade Anonima Fechada", + "2062": "Sociedade Empresaria Limitada", + "2076": "Sociedade Empresaria em Nome Coletivo", + "2089": "Sociedade Empresaria em Comandita Simples", + "2097": "Sociedade Empresaria em Comandita por Acoes", + "2100": "Sociedade Mercantil de Capital e Industria (extinta pelo NCC/2002)", + "2127": "Sociedade Empresaria em Conta de Participacao", + "2135": "Empresario (Individual)", + "2143": "Cooperativa", + "2151": "Consorcio de Sociedades", + "2160": "Grupo de Sociedades", + "2178": "Estabelecimento, no Brasil, de Sociedade Estrangeira", + "2194": "Estabelecimento, no Brasil, de Empresa Binacional Argentino-Brasileira", + "2208": "Entidade Binacional Itaipu", + "2216": "Empresa Domiciliada no Exterior", + "2224": "Clube/Fundo de Investimento", + "2232": "Sociedade Simples Pura", + "2240": "Sociedade Simples Limitada", + "2259": "Sociedade em Nome Coletivo", + "2267": "Sociedade em Comandita Simples", + "2275": "Sociedade Simples em Conta de Participacao", + "2305": "Empresa Individual de Responsabilidade Limitada", + "3034": "Servico Notarial e Registral (Cartorio)", + "3042": "Organizacao Social", + "3050": "Organizacao da Sociedade Civil de Interesse Publico (Oscip)", + "3069": "Outras Formas de Fundacoes Mantidas com Recursos Privados", + "3077": "Servico Social Autonomo", + "3085": "Condominio Edilicios", + "3093": "Unidade Executora (Programa Dinheiro Direto na Escola)", + "3107": "Comissao de Conciliacao Previa", + "3115": "Entidade de Mediacao e Arbitragem", + "3123": "Partido Politico", + "3131": "Entidade Sindical", + "3204": "Estabelecimento, no Brasil, de Fundacao ou Associacao Estrangeiras", + "3212": "Fundacao ou Associacao Domiciliada no Exterior", + "3999": "Outras Formas de Associacao", + "4014": "Empresa Individual Imobiliaria", + "4022": "Segurado Especial", + "4081": "Contribuinte individual", + "5002": "Organizacao Internacional e Outras Instituicoes Extraterritoriais", +}; diff --git a/src/is-valid-legal-nature/is-valid-legal-nature.test.ts b/src/is-valid-legal-nature/is-valid-legal-nature.test.ts new file mode 100644 index 00000000..44abe67f --- /dev/null +++ b/src/is-valid-legal-nature/is-valid-legal-nature.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { isValidLegalNature } from "./is-valid-legal-nature"; + +describe("isValidLegalNature", () => { + it("should validate legal nature codes", () => { + expect(isValidLegalNature("2062")).toBe(true); + expect(isValidLegalNature("206-2")).toBe(true); + expect(isValidLegalNature("9999")).toBe(false); + }); +}); diff --git a/src/is-valid-legal-nature/is-valid-legal-nature.ts b/src/is-valid-legal-nature/is-valid-legal-nature.ts new file mode 100644 index 00000000..b652d004 --- /dev/null +++ b/src/is-valid-legal-nature/is-valid-legal-nature.ts @@ -0,0 +1,10 @@ +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LEGAL_NATURE } from "./constants"; + +export const isValidLegalNature = (code: string): boolean => { + if (!code || typeof code !== "string") return false; + + const normalized = sanitizeToDigits(code); + + return normalized.length === 4 && normalized in LEGAL_NATURE; +}; diff --git a/src/is-valid-voter-id/constants.ts b/src/is-valid-voter-id/constants.ts new file mode 100644 index 00000000..75c112a6 --- /dev/null +++ b/src/is-valid-voter-id/constants.ts @@ -0,0 +1,32 @@ +import type { StateCode } from "../_internals/constants/states"; + +export const UF_TO_VOTER_ID_CODE: Record = { + SP: "01", + MG: "02", + RJ: "03", + RS: "04", + BA: "05", + PR: "06", + CE: "07", + PE: "08", + SC: "09", + GO: "10", + MA: "11", + PB: "12", + PA: "13", + ES: "14", + PI: "15", + RN: "16", + AL: "17", + MT: "18", + MS: "19", + DF: "20", + SE: "21", + AM: "22", + RO: "23", + AC: "24", + AP: "25", + RR: "26", + TO: "27", + ZZ: "28", +}; diff --git a/src/is-valid-voter-id/is-valid-voter-id.test.ts b/src/is-valid-voter-id/is-valid-voter-id.test.ts new file mode 100644 index 00000000..b5dfa0d0 --- /dev/null +++ b/src/is-valid-voter-id/is-valid-voter-id.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { generateVoterId } from "../generate-voter-id/generate-voter-id"; +import { isValidVoterId } from "./is-valid-voter-id"; + +describe("isValidVoterId", () => { + it("should validate voter ids", () => { + const voterId = generateVoterId("SP"); + expect(isValidVoterId(voterId)).toBe(true); + expect(isValidVoterId(`${voterId.slice(0, -1)}${voterId.endsWith("0") ? "1" : "0"}`)).toBe( + false, + ); + }); +}); diff --git a/src/is-valid-voter-id/is-valid-voter-id.ts b/src/is-valid-voter-id/is-valid-voter-id.ts new file mode 100644 index 00000000..30227d0a --- /dev/null +++ b/src/is-valid-voter-id/is-valid-voter-id.ts @@ -0,0 +1,72 @@ +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +const isValidLength = (value: string): boolean => { + if (value.length === 12) return true; + + const federativeUnion = value.slice(-4, -2); + return value.length === 13 && (federativeUnion === "01" || federativeUnion === "02"); +}; + +const calculateFirstDigit = ({ + sequentialNumber, + federativeUnion, +}: { + sequentialNumber: string; + federativeUnion: string; +}): number => { + let sum = 0; + + for (let i = 0; i < 8; i++) { + sum += (sequentialNumber.charCodeAt(i) - 48) * (i + 2); + } + + const remainder = sum % 11; + + if (remainder === 0 && (federativeUnion === "01" || federativeUnion === "02")) { + return 1; + } + + return remainder === 10 ? 0 : remainder; +}; + +const calculateSecondDigit = ({ + federativeUnion, + firstDigit, +}: { + federativeUnion: string; + firstDigit: number; +}): number => { + const sum = + (federativeUnion.charCodeAt(0) - 48) * 7 + + (federativeUnion.charCodeAt(1) - 48) * 8 + + firstDigit * 9; + + const remainder = sum % 11; + + if ((federativeUnion === "01" || federativeUnion === "02") && remainder === 0) { + return 1; + } + + return remainder === 10 ? 0 : remainder; +}; + +export const isValidVoterId = (value: string): boolean => { + if (!value || typeof value !== "string") return false; + + const digits = sanitizeToDigits(value); + + if (!isValidLength(digits)) return false; + + const sequentialNumber = digits.slice(0, 8); + const federativeUnion = digits.slice(-4, -2); + const verifier = digits.slice(-2); + + const ufCode = Number(federativeUnion); + + if (!Number.isInteger(ufCode) || ufCode < 1 || ufCode > 28) return false; + + const digit1 = calculateFirstDigit({ sequentialNumber, federativeUnion }); + const digit2 = calculateSecondDigit({ federativeUnion, firstDigit: digit1 }); + + return verifier === `${digit1}${digit2}`; +}; diff --git a/src/parse-cnh/parse-cnh.test.ts b/src/parse-cnh/parse-cnh.test.ts new file mode 100644 index 00000000..a625c02f --- /dev/null +++ b/src/parse-cnh/parse-cnh.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { parseCnh } from "./parse-cnh"; + +describe("parseCnh", () => { + it("should remove CNH formatting", () => { + expect(parseCnh("000000001-19")).toBe("00000000119"); + }); + + it("should remove non numeric characters", () => { + expect(parseCnh("000.abc000001-19")).toBe("00000000119"); + }); +}); diff --git a/src/parse-cnh/parse-cnh.ts b/src/parse-cnh/parse-cnh.ts new file mode 100644 index 00000000..57bca7ad --- /dev/null +++ b/src/parse-cnh/parse-cnh.ts @@ -0,0 +1,3 @@ +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export const parseCnh = (value: string | number): string => sanitizeToDigits(value); diff --git a/src/parse-legal-nature/parse-legal-nature.test.ts b/src/parse-legal-nature/parse-legal-nature.test.ts new file mode 100644 index 00000000..0aac67b5 --- /dev/null +++ b/src/parse-legal-nature/parse-legal-nature.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { parseLegalNature } from "./parse-legal-nature"; + +describe("parseLegalNature", () => { + it("should remove legal nature formatting", () => { + expect(parseLegalNature("206-2")).toBe("2062"); + }); +}); diff --git a/src/parse-legal-nature/parse-legal-nature.ts b/src/parse-legal-nature/parse-legal-nature.ts new file mode 100644 index 00000000..76845b98 --- /dev/null +++ b/src/parse-legal-nature/parse-legal-nature.ts @@ -0,0 +1,3 @@ +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export const parseLegalNature = (value: string | number): string => sanitizeToDigits(value); diff --git a/src/parse-license-plate/parse-license-plate.test.ts b/src/parse-license-plate/parse-license-plate.test.ts new file mode 100644 index 00000000..5e2ab375 --- /dev/null +++ b/src/parse-license-plate/parse-license-plate.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { parseLicensePlate } from "./parse-license-plate"; + +describe("parseLicensePlate", () => { + it("should remove formatting and normalize casing", () => { + expect(parseLicensePlate("abc-1234")).toBe("ABC1234"); + expect(parseLicensePlate("abc1d23")).toBe("ABC1D23"); + }); +}); diff --git a/src/parse-license-plate/parse-license-plate.ts b/src/parse-license-plate/parse-license-plate.ts new file mode 100644 index 00000000..eef84652 --- /dev/null +++ b/src/parse-license-plate/parse-license-plate.ts @@ -0,0 +1,4 @@ +export const parseLicensePlate = (value: string): string => { + if (!value || typeof value !== "string") return ""; + return value.toUpperCase().replace(/[^A-Z0-9]/g, ""); +}; diff --git a/src/parse-voter-id/parse-voter-id.test.ts b/src/parse-voter-id/parse-voter-id.test.ts new file mode 100644 index 00000000..a3bdfee3 --- /dev/null +++ b/src/parse-voter-id/parse-voter-id.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "../_internals/test/runtime"; +import { parseVoterId } from "./parse-voter-id"; + +describe("parseVoterId", () => { + it("should remove voter id formatting", () => { + expect(parseVoterId("1234 5678 01 24")).toBe("123456780124"); + }); +}); diff --git a/src/parse-voter-id/parse-voter-id.ts b/src/parse-voter-id/parse-voter-id.ts new file mode 100644 index 00000000..15bf09d9 --- /dev/null +++ b/src/parse-voter-id/parse-voter-id.ts @@ -0,0 +1,3 @@ +import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; + +export const parseVoterId = (value: string | number): string => sanitizeToDigits(value); From 409780df19b40c093f1960121ab4cb524f2daaaf Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:27:34 -0300 Subject: [PATCH 2/3] fix: adjust apis --- docs/pt-br/utilities.md | 27 ++++++++++--------- docs/utilities.md | 27 ++++++++++--------- .../sanitize-to-alphanumeric.test.ts | 12 +++++++++ .../sanitize-to-alphanumeric.ts | 15 +++++++++++ src/format-cnh/format-cnh.test.ts | 12 ++++++++- src/format-cnpj/format-cnpj.ts | 4 +-- .../format-legal-nature.test.ts | 4 +++ .../format-license-plate.test.ts | 19 +++++++++++++ .../format-license-plate.ts | 25 ++++++++++++++--- src/format-passport/constants.ts | 1 + src/format-passport/format-passport.test.ts | 24 +++++++++++++++++ src/format-passport/format-passport.ts | 5 +++- src/format-voter-id/format-voter-id.test.ts | 12 +++++++++ .../get-cep-info-by-address.ts | 8 +++--- src/parse-boleto/parse-boleto.test.ts | 6 +++++ src/parse-boleto/parse-boleto.ts | 3 ++- src/parse-cep/parse-cep.test.ts | 4 +++ src/parse-cep/parse-cep.ts | 3 ++- src/parse-cnh/constants.ts | 1 + src/parse-cnh/parse-cnh.test.ts | 4 +++ src/parse-cnh/parse-cnh.ts | 3 ++- src/parse-cnpj/parse-cnpj.test.ts | 8 ++++++ src/parse-cnpj/parse-cnpj.ts | 9 +++---- src/parse-cpf/parse-cpf.test.ts | 4 +++ src/parse-cpf/parse-cpf.ts | 3 ++- src/parse-legal-nature/constants.ts | 1 + .../parse-legal-nature.test.ts | 4 +++ src/parse-legal-nature/parse-legal-nature.ts | 4 ++- src/parse-license-plate/constants.ts | 1 + .../parse-license-plate.test.ts | 5 ++++ .../parse-license-plate.ts | 5 +++- src/parse-passport/constants.ts | 1 + src/parse-passport/parse-passport.test.ts | 6 ++++- src/parse-passport/parse-passport.ts | 15 ++++++----- src/parse-phone/constants.ts | 1 + src/parse-phone/parse-phone.test.ts | 6 ++++- src/parse-phone/parse-phone.ts | 5 ++-- src/parse-pis/parse-pis.test.ts | 4 +++ src/parse-pis/parse-pis.ts | 3 ++- .../parse-processo-juridico.test.ts | 4 +++ .../parse-processo-juridico.ts | 4 ++- src/parse-voter-id/constants.ts | 1 + src/parse-voter-id/parse-voter-id.test.ts | 4 +++ src/parse-voter-id/parse-voter-id.ts | 3 ++- 44 files changed, 260 insertions(+), 60 deletions(-) create mode 100644 src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts create mode 100644 src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts create mode 100644 src/format-passport/constants.ts create mode 100644 src/parse-cnh/constants.ts create mode 100644 src/parse-legal-nature/constants.ts create mode 100644 src/parse-license-plate/constants.ts create mode 100644 src/parse-passport/constants.ts create mode 100644 src/parse-phone/constants.ts create mode 100644 src/parse-voter-id/constants.ts diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index a798663c..5571c808 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -25,7 +25,7 @@ formatCpf('746506880', { pad: true }); // 007.465.068-80 ## parseCpf -Remove a formatação do CPF e retorna apenas os dígitos. +Remove a formatação do CPF, mantém apenas os dígitos e limita o resultado a 11 dígitos. ```javascript import { parseCpf } from '@brazilian-utils/brazilian-utils'; @@ -67,7 +67,7 @@ formatCnpj('12OUT345000199', { version: 2 }); // 12.OUT.345/0001-99 ## parseCnpj -Remove a formatação do CNPJ e retorna um valor normalizado. +Remove a formatação do CNPJ, retorna um valor normalizado e limita o resultado a 14 caracteres. ```javascript import { parseCnpj } from '@brazilian-utils/brazilian-utils'; @@ -119,7 +119,7 @@ formatBoleto('1900000901149', { pad: true }); // 00000.00000 00000.000019 00000. ## parseBoleto -Remove a formatação do boleto e retorna apenas os dígitos. +Remove a formatação do boleto, mantém apenas os dígitos e limita o resultado a 47 dígitos. ```javascript import { parseBoleto } from '@brazilian-utils/brazilian-utils'; @@ -182,12 +182,13 @@ formatPhone('11900000000', { mask: 'auto' }); // Detecta automaticamente a másc ## parsePhone -Remove a formatação do telefone e retorna apenas os dígitos. +Remove a formatação do telefone, mantém apenas os dígitos e limita o resultado a 11 dígitos. ```javascript import { parsePhone } from '@brazilian-utils/brazilian-utils'; parsePhone('(11) 90000-0000'); // 11900000000 +parsePhone('+55 (11) 90000-0000'); // 55119000000 ``` ## isValidMobilePhone @@ -258,7 +259,7 @@ formatPis('123456789', { pad: true }); // 001.23456.78-9 ## parsePis -Remove a formatação do PIS e retorna apenas os dígitos. +Remove a formatação do PIS, mantém apenas os dígitos e limita o resultado a 11 dígitos. ```javascript import { parsePis } from '@brazilian-utils/brazilian-utils'; @@ -278,7 +279,7 @@ formatCep('92500000'); // 92500-000 ## parseCep -Remove a formatação do CEP e retorna apenas os dígitos. +Remove a formatação do CEP, mantém apenas os dígitos e limita o resultado a 8 dígitos. ```javascript import { parseCep } from '@brazilian-utils/brazilian-utils'; @@ -328,7 +329,7 @@ formatProcessoJuridico('00020802520125150049'); // 0002080-25.2012.515.0049 ## parseProcessoJuridico -Remove a formatação do processo jurídico e retorna apenas os dígitos. +Remove a formatação do processo jurídico, mantém apenas os dígitos e limita o resultado a 20 dígitos. ```javascript import { parseProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -518,7 +519,7 @@ isValidPassport('12345678'); // false ## formatPassport -Formata um número de passaporte brasileiro (maiúsculas, sem símbolos). +Formata um número de passaporte brasileiro (maiúsculas, sem símbolos, limitado a 8 caracteres). ```javascript import { formatPassport } from '@brazilian-utils/brazilian-utils'; @@ -539,7 +540,7 @@ generatePassport(); // 'RY393097' ## parsePassport -Remove todos os caracteres não alfanuméricos (incluindo '-', '.' e espaços) de um número de passaporte. +Remove todos os caracteres não alfanuméricos de um número de passaporte, converte para maiúsculas e limita o resultado a 8 caracteres. ```javascript import { parsePassport } from '@brazilian-utils/brazilian-utils'; @@ -591,7 +592,7 @@ generateCnh(); // '02650306461' ## parseCnh -Remove a formatação da CNH e retorna apenas os dígitos. +Remove a formatação da CNH, mantém apenas os dígitos e limita o resultado a 11 dígitos. ```javascript import { parseCnh } from '@brazilian-utils/brazilian-utils'; @@ -668,7 +669,7 @@ generateLegalNature(); // '2062' ## parseLegalNature -Remove a formatação da natureza jurídica e retorna apenas os dígitos. +Remove a formatação da natureza jurídica, mantém apenas os dígitos e limita o resultado a 4 dígitos. ```javascript import { parseLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -738,7 +739,7 @@ getFormatLicensePlate('INVALID'); // null ## parseLicensePlate -Remove separadores de uma placa e normaliza para letras maiúsculas. +Remove separadores de uma placa, normaliza para letras maiúsculas e limita o resultado a 7 caracteres. ```javascript import { parseLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -816,7 +817,7 @@ generateVoterId('SP'); // título de eleitor aleatório válido de São Paulo ## parseVoterId -Remove a formatação do título de eleitor e retorna apenas os dígitos. +Remove a formatação do título de eleitor, mantém apenas os dígitos e limita o resultado a 12 dígitos. ```javascript import { parseVoterId } from '@brazilian-utils/brazilian-utils'; diff --git a/docs/utilities.md b/docs/utilities.md index 1865df70..7769f437 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -25,7 +25,7 @@ formatCpf('746506880', { pad: true }); // 007.465.068-80 ## parseCpf -Remove CPF formatting and return only digits. +Remove CPF formatting, keep only digits, and cap the result to 11 digits. ```javascript import { parseCpf } from '@brazilian-utils/brazilian-utils'; @@ -67,7 +67,7 @@ formatCnpj('12OUT345000199', { version: 2 }); // 12.OUT.345/0001-99 ## parseCnpj -Remove CNPJ formatting and return a normalized value. +Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. ```javascript import { parseCnpj } from '@brazilian-utils/brazilian-utils'; @@ -119,7 +119,7 @@ formatBoleto('1900000901149', { pad: true }); // 00000.00000 00000.000019 00000. ## parseBoleto -Remove boleto formatting and return only digits. +Remove boleto formatting, keep only digits, and cap the result to 47 digits. ```javascript import { parseBoleto } from '@brazilian-utils/brazilian-utils'; @@ -182,12 +182,13 @@ formatPhone('11900000000', { mask: 'auto' }); // Automatically detects mask base ## parsePhone -Remove phone formatting and return only digits. +Remove phone formatting, keep only digits, and cap the result to 11 digits. ```javascript import { parsePhone } from '@brazilian-utils/brazilian-utils'; parsePhone('(11) 90000-0000'); // 11900000000 +parsePhone('+55 (11) 90000-0000'); // 55119000000 ``` ## isValidMobilePhone @@ -258,7 +259,7 @@ formatPis('123456789', { pad: true }); // 001.23456.78-9 ## parsePis -Remove PIS formatting and return only digits. +Remove PIS formatting, keep only digits, and cap the result to 11 digits. ```javascript import { parsePis } from '@brazilian-utils/brazilian-utils'; @@ -278,7 +279,7 @@ formatCep('92500000'); // 92500-000 ## parseCep -Remove CEP formatting and return only digits. +Remove CEP formatting, keep only digits, and cap the result to 8 digits. ```javascript import { parseCep } from '@brazilian-utils/brazilian-utils'; @@ -328,7 +329,7 @@ formatProcessoJuridico('00020802520125150049'); // 0002080-25.2012.515.0049 ## parseProcessoJuridico -Remove processo jurídico formatting and return only digits. +Remove processo jurídico formatting, keep only digits, and cap the result to 20 digits. ```javascript import { parseProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -518,7 +519,7 @@ isValidPassport('12345678'); // false ## formatPassport -Format a Brazilian passport number (uppercase, without symbols). +Format a Brazilian passport number (uppercase, without symbols, capped to 8 characters). ```javascript import { formatPassport } from '@brazilian-utils/brazilian-utils'; @@ -539,7 +540,7 @@ generatePassport(); // 'RY393097' ## parsePassport -Remove all non-alphanumeric characters (including '-', '.', and whitespaces) from a passport number. +Remove all non-alphanumeric characters from a passport number, uppercase the result, and cap it to 8 characters. ```javascript import { parsePassport } from '@brazilian-utils/brazilian-utils'; @@ -591,7 +592,7 @@ generateCnh(); // '02650306461' ## parseCnh -Remove CNH formatting and return only digits. +Remove CNH formatting, keep only digits, and cap the result to 11 digits. ```javascript import { parseCnh } from '@brazilian-utils/brazilian-utils'; @@ -668,7 +669,7 @@ generateLegalNature(); // '2062' ## parseLegalNature -Remove legal nature formatting and return only digits. +Remove legal nature formatting, keep only digits, and cap the result to 4 digits. ```javascript import { parseLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -738,7 +739,7 @@ getFormatLicensePlate('INVALID'); // null ## parseLicensePlate -Remove separators from a license plate and normalize it to uppercase. +Remove separators from a license plate, normalize it to uppercase, and cap it to 7 characters. ```javascript import { parseLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -816,7 +817,7 @@ generateVoterId('SP'); // valid random voter ID for Sao Paulo ## parseVoterId -Remove voter ID formatting and return only digits. +Remove voter ID formatting, keep only digits, and cap the result to 12 digits. ```javascript import { parseVoterId } from '@brazilian-utils/brazilian-utils'; diff --git a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts new file mode 100644 index 00000000..ade11fed --- /dev/null +++ b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "../test/runtime"; +import { sanitizeToAlphanumeric } from "./sanitize-to-alphanumeric"; + +describe("sanitizeToAlphanumeric", () => { + it("should remove non alphanumeric characters and uppercase the result", () => { + expect(sanitizeToAlphanumeric("ab-12.3")).toBe("AB123"); + }); + + it("should support number input", () => { + expect(sanitizeToAlphanumeric(12345)).toBe("12345"); + }); +}); diff --git a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts new file mode 100644 index 00000000..b2339ee0 --- /dev/null +++ b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts @@ -0,0 +1,15 @@ +/** + * Sanitizes the input value by removing all non-alphanumeric characters and uppercasing the result. + * + * @param {string|number} value - The input value to be sanitized. It can be a string or a number. + * @returns {string} A string containing only uppercase alphanumeric characters from the input value. + * + * @example + * ```typescript + * sanitizeToAlphanumeric("abc123") // "ABC123" + * sanitizeToAlphanumeric("Ab-12.3") // "AB123" + * sanitizeToAlphanumeric(12345) // "12345" + * ``` + */ +export const sanitizeToAlphanumeric = (value: string | number): string => + value.toString().replace(/[^A-Za-z0-9]/g, "").toUpperCase(); diff --git a/src/format-cnh/format-cnh.test.ts b/src/format-cnh/format-cnh.test.ts index 9cca80e4..45f36f9f 100644 --- a/src/format-cnh/format-cnh.test.ts +++ b/src/format-cnh/format-cnh.test.ts @@ -3,8 +3,18 @@ import { formatCnh } from "./format-cnh"; describe("formatCnh", () => { it("should format CNH values", () => { - expect(formatCnh("00000000119")).toBe("000000001-19"); + expect(formatCnh("")).toBe(""); + expect(formatCnh("0")).toBe("0"); + expect(formatCnh("00")).toBe("00"); + expect(formatCnh("000")).toBe("000"); + expect(formatCnh("0000")).toBe("0000"); + expect(formatCnh("00000")).toBe("00000"); + expect(formatCnh("000000")).toBe("000000"); + expect(formatCnh("0000000")).toBe("0000000"); + expect(formatCnh("00000000")).toBe("00000000"); expect(formatCnh("000000001")).toBe("000000001"); + expect(formatCnh("0000000011")).toBe("000000001-1"); + expect(formatCnh("00000000119")).toBe("000000001-19"); }); it("should remove non numeric characters", () => { diff --git a/src/format-cnpj/format-cnpj.ts b/src/format-cnpj/format-cnpj.ts index 3dbeec82..9c2cec4c 100644 --- a/src/format-cnpj/format-cnpj.ts +++ b/src/format-cnpj/format-cnpj.ts @@ -1,12 +1,12 @@ import { type FormatParams, format } from "../_internals/format/format"; +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; export type FormatCnpjOptions = Pick & { version?: 1 | 2 }; const sanitize = (value: string | number, version?: FormatCnpjOptions["version"]) => { if (version === 2) { - const enhancedValue = value.toString(); - return enhancedValue.replace(/[^A-Za-z0-9]/g, "").toUpperCase(); + return sanitizeToAlphanumeric(value); } return sanitizeToDigits(value); diff --git a/src/format-legal-nature/format-legal-nature.test.ts b/src/format-legal-nature/format-legal-nature.test.ts index 41530a8c..40b58254 100644 --- a/src/format-legal-nature/format-legal-nature.test.ts +++ b/src/format-legal-nature/format-legal-nature.test.ts @@ -3,6 +3,10 @@ import { formatLegalNature } from "./format-legal-nature"; describe("formatLegalNature", () => { it("should format legal nature values", () => { + expect(formatLegalNature("")).toBe(""); + expect(formatLegalNature("2")).toBe("2"); + expect(formatLegalNature("20")).toBe("20"); + expect(formatLegalNature("206")).toBe("206"); expect(formatLegalNature("2062")).toBe("206-2"); }); }); diff --git a/src/format-license-plate/format-license-plate.test.ts b/src/format-license-plate/format-license-plate.test.ts index a625217d..4fdf7de9 100644 --- a/src/format-license-plate/format-license-plate.test.ts +++ b/src/format-license-plate/format-license-plate.test.ts @@ -3,10 +3,29 @@ import { formatLicensePlate } from "./format-license-plate"; describe("formatLicensePlate", () => { it("should format old pattern license plates", () => { + expect(formatLicensePlate("")).toBe(""); + expect(formatLicensePlate("a")).toBe("A"); + expect(formatLicensePlate("ab")).toBe("AB"); + expect(formatLicensePlate("abc")).toBe("ABC"); + expect(formatLicensePlate("abc1")).toBe("ABC-1"); + expect(formatLicensePlate("abc12")).toBe("ABC-12"); + expect(formatLicensePlate("abc123")).toBe("ABC-123"); expect(formatLicensePlate("abc1234")).toBe("ABC-1234"); }); it("should keep mercosul plates normalized", () => { + expect(formatLicensePlate("")).toBe(""); + expect(formatLicensePlate("a")).toBe("A"); + expect(formatLicensePlate("ab")).toBe("AB"); + expect(formatLicensePlate("abc")).toBe("ABC"); + expect(formatLicensePlate("abc1")).toBe("ABC-1"); + expect(formatLicensePlate("abc1d")).toBe("ABC1D"); + expect(formatLicensePlate("abc1d2")).toBe("ABC1D2"); expect(formatLicensePlate("abc1d23")).toBe("ABC1D23"); }); + + it("should remove non alphanumeric characters", () => { + expect(formatLicensePlate("abc-1234")).toBe("ABC-1234"); + expect(formatLicensePlate("abc1-d23")).toBe("ABC1D23"); + }); }); diff --git a/src/format-license-plate/format-license-plate.ts b/src/format-license-plate/format-license-plate.ts index ec60c809..b040b814 100644 --- a/src/format-license-plate/format-license-plate.ts +++ b/src/format-license-plate/format-license-plate.ts @@ -4,13 +4,32 @@ import { OLD_FORMAT_SEPARATOR_INDEX } from "./constants"; export const formatLicensePlate = (value: string): string => { const parsed = parseLicensePlate(value); - const format = getFormatLicensePlate(parsed); - if (!format) return ""; + if (!parsed) return ""; + + const format = getFormatLicensePlate(parsed); if (format === "LLLNNNN") { return `${parsed.slice(0, OLD_FORMAT_SEPARATOR_INDEX)}-${parsed.slice(OLD_FORMAT_SEPARATOR_INDEX)}`; } - return parsed; + if (format) return parsed; + + if (!/^[A-Z]{1,3}$/.test(parsed.slice(0, Math.min(parsed.length, OLD_FORMAT_SEPARATOR_INDEX)))) { + return ""; + } + + if (parsed.length <= OLD_FORMAT_SEPARATOR_INDEX) return parsed; + + const tail = parsed.slice(OLD_FORMAT_SEPARATOR_INDEX); + + if (/^\d{1,4}$/.test(tail)) { + return `${parsed.slice(0, OLD_FORMAT_SEPARATOR_INDEX)}-${tail}`; + } + + if (/^\d[A-Z]\d{0,2}$/.test(tail) || /^\d{2}[A-Z]\d?$/.test(tail)) { + return parsed; + } + + return ""; }; diff --git a/src/format-passport/constants.ts b/src/format-passport/constants.ts new file mode 100644 index 00000000..3122517e --- /dev/null +++ b/src/format-passport/constants.ts @@ -0,0 +1 @@ +export const LENGTH = 8; diff --git a/src/format-passport/format-passport.test.ts b/src/format-passport/format-passport.test.ts index 08a73ae3..b9988c73 100644 --- a/src/format-passport/format-passport.test.ts +++ b/src/format-passport/format-passport.test.ts @@ -4,11 +4,35 @@ import { formatPassport } from "./format-passport"; describe("formatPassport", () => { describe("should return the formatted passport", () => { test("when passport is valid", () => { + expect(formatPassport("")).toBe(""); + expect(formatPassport("A")).toBe("A"); + expect(formatPassport("AB")).toBe("AB"); + expect(formatPassport("AB1")).toBe("AB1"); + expect(formatPassport("AB12")).toBe("AB12"); + expect(formatPassport("AB123")).toBe("AB123"); + expect(formatPassport("AB1234")).toBe("AB1234"); + expect(formatPassport("AB12345")).toBe("AB12345"); expect(formatPassport("AB123456")).toBe("AB123456"); }); test("when passport has lowercase letters", () => { + expect(formatPassport("")).toBe(""); + expect(formatPassport("a")).toBe("A"); + expect(formatPassport("ac")).toBe("AC"); + expect(formatPassport("acd")).toBe("ACD"); + expect(formatPassport("acd1")).toBe("ACD1"); + expect(formatPassport("acd12")).toBe("ACD12"); + expect(formatPassport("acd127")).toBe("ACD127"); + expect(formatPassport("acd1273")).toBe("ACD1273"); expect(formatPassport("acd12736")).toBe("ACD12736"); }); + + test("when passport contains symbols", () => { + expect(formatPassport("AB-123.456")).toBe("AB123456"); + }); + + test("when passport has extra characters", () => { + expect(formatPassport("AB123456789")).toBe("AB123456"); + }); }); }); diff --git a/src/format-passport/format-passport.ts b/src/format-passport/format-passport.ts index fb99ceac..343a27f5 100644 --- a/src/format-passport/format-passport.ts +++ b/src/format-passport/format-passport.ts @@ -1,3 +1,6 @@ +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; +import { LENGTH } from "./constants"; + /** * Formats a Brazilian passport number for display. * Converts to uppercase and removes all non-alphanumeric characters. @@ -12,5 +15,5 @@ */ export const formatPassport = (passport: string): string => { if (!passport || typeof passport !== "string") return ""; - return passport.toUpperCase().replace(/[^A-Z0-9]/g, ""); + return sanitizeToAlphanumeric(passport).slice(0, LENGTH); }; diff --git a/src/format-voter-id/format-voter-id.test.ts b/src/format-voter-id/format-voter-id.test.ts index 5449b676..4237b03c 100644 --- a/src/format-voter-id/format-voter-id.test.ts +++ b/src/format-voter-id/format-voter-id.test.ts @@ -3,6 +3,18 @@ import { formatVoterId } from "./format-voter-id"; describe("formatVoterId", () => { it("should format voter ids", () => { + expect(formatVoterId("")).toBe(""); + expect(formatVoterId("1")).toBe("1"); + expect(formatVoterId("12")).toBe("12"); + expect(formatVoterId("123")).toBe("123"); + expect(formatVoterId("1234")).toBe("1234"); + expect(formatVoterId("12345")).toBe("1234 5"); + expect(formatVoterId("123456")).toBe("1234 56"); + expect(formatVoterId("1234567")).toBe("1234 567"); + expect(formatVoterId("12345678")).toBe("1234 5678"); + expect(formatVoterId("123456780")).toBe("1234 5678 0"); + expect(formatVoterId("1234567801")).toBe("1234 5678 01"); + expect(formatVoterId("12345678012")).toBe("1234 5678 01 2"); expect(formatVoterId("123456780124")).toBe("1234 5678 01 24"); }); }); diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.ts b/src/get-cep-info-by-address/get-cep-info-by-address.ts index 61d527f2..fa507817 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.ts @@ -1,4 +1,4 @@ -import { DATA as STATES } from "../_internals/constants/states"; +import { DATA as STATES, type StateCode } from "../_internals/constants/states"; export class GetCepInfoByAddressError extends Error { constructor(message: string) { @@ -40,7 +40,9 @@ export type GetCepInfoByAddressOptions = { street: string; }; -const VALID_STATE_CODES = new Set(STATES.map((state) => state.code)); +const VALID_STATE_CODES = new Set(STATES.map((state) => state.code)); + +const isStateCode = (value: string): value is StateCode => VALID_STATE_CODES.has(value as StateCode); const normalizeAddressPart = (value: string): string => value @@ -55,7 +57,7 @@ export const getCepInfoByAddress = async ({ }: GetCepInfoByAddressOptions): Promise => { const normalizedUf = federalUnit.trim().toUpperCase(); - if (!VALID_STATE_CODES.has(normalizedUf)) { + if (!isStateCode(normalizedUf)) { throw new GetCepInfoByAddressValidationError(`Invalid UF: ${federalUnit}`); } diff --git a/src/parse-boleto/parse-boleto.test.ts b/src/parse-boleto/parse-boleto.test.ts index a43da3c9..abeea7d7 100644 --- a/src/parse-boleto/parse-boleto.test.ts +++ b/src/parse-boleto/parse-boleto.test.ts @@ -13,4 +13,10 @@ describe("parseBoleto", () => { "10491443385511900000200000000141325230000093423", ); }); + + it("should ignore digits after the boleto length", () => { + expect(parseBoleto("10491443385511900000200000000141325230000093423123")).toBe( + "10491443385511900000200000000141325230000093423", + ); + }); }); diff --git a/src/parse-boleto/parse-boleto.ts b/src/parse-boleto/parse-boleto.ts index 74341b05..018b32d3 100644 --- a/src/parse-boleto/parse-boleto.ts +++ b/src/parse-boleto/parse-boleto.ts @@ -1,4 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "../format-boleto/constants"; /** * Removes boleto formatting characters and returns only digits. @@ -6,4 +7,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @param {string|number} value - The boleto value to be parsed. * @returns {string} The boleto value without formatting. */ -export const parseBoleto = (value: string | number): string => sanitizeToDigits(value); +export const parseBoleto = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cep/parse-cep.test.ts b/src/parse-cep/parse-cep.test.ts index d438e841..a1dbc630 100644 --- a/src/parse-cep/parse-cep.test.ts +++ b/src/parse-cep/parse-cep.test.ts @@ -9,4 +9,8 @@ describe("parseCep", () => { it("should remove non numeric characters", () => { expect(parseCep("a0.10cr01?00#ab0")).toBe("01001000"); }); + + it("should ignore digits after the CEP length", () => { + expect(parseCep("01001000123")).toBe("01001000"); + }); }); diff --git a/src/parse-cep/parse-cep.ts b/src/parse-cep/parse-cep.ts index bea3165b..ce93b1cf 100644 --- a/src/parse-cep/parse-cep.ts +++ b/src/parse-cep/parse-cep.ts @@ -1,4 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "../format-cep/constants"; /** * Removes CEP formatting characters and returns only digits. @@ -6,4 +7,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @param {string|number} value - The CEP value to be parsed. * @returns {string} The CEP value without formatting. */ -export const parseCep = (value: string | number): string => sanitizeToDigits(value); +export const parseCep = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cnh/constants.ts b/src/parse-cnh/constants.ts new file mode 100644 index 00000000..5720885c --- /dev/null +++ b/src/parse-cnh/constants.ts @@ -0,0 +1 @@ +export const LENGTH = 11; diff --git a/src/parse-cnh/parse-cnh.test.ts b/src/parse-cnh/parse-cnh.test.ts index a625c02f..fe26f56f 100644 --- a/src/parse-cnh/parse-cnh.test.ts +++ b/src/parse-cnh/parse-cnh.test.ts @@ -9,4 +9,8 @@ describe("parseCnh", () => { it("should remove non numeric characters", () => { expect(parseCnh("000.abc000001-19")).toBe("00000000119"); }); + + it("should ignore digits after the CNH length", () => { + expect(parseCnh("00000000119123")).toBe("00000000119"); + }); }); diff --git a/src/parse-cnh/parse-cnh.ts b/src/parse-cnh/parse-cnh.ts index 57bca7ad..06bea092 100644 --- a/src/parse-cnh/parse-cnh.ts +++ b/src/parse-cnh/parse-cnh.ts @@ -1,3 +1,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; -export const parseCnh = (value: string | number): string => sanitizeToDigits(value); +export const parseCnh = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cnpj/parse-cnpj.test.ts b/src/parse-cnpj/parse-cnpj.test.ts index a782d9bc..23408699 100644 --- a/src/parse-cnpj/parse-cnpj.test.ts +++ b/src/parse-cnpj/parse-cnpj.test.ts @@ -13,4 +13,12 @@ describe("parseCnpj", () => { it("should keep alphanumeric characters for version 2", () => { expect(parseCnpj("Q0.SLF.MBD/7VX4-39", { version: 2 })).toBe("Q0SLFMBD7VX439"); }); + + it("should ignore digits after the CNPJ length", () => { + expect(parseCnpj("46843485000186123")).toBe("46843485000186"); + }); + + it("should ignore characters after the CNPJ length for version 2", () => { + expect(parseCnpj("Q0.SLF.MBD/7VX4-39ABC", { version: 2 })).toBe("Q0SLFMBD7VX439"); + }); }); diff --git a/src/parse-cnpj/parse-cnpj.ts b/src/parse-cnpj/parse-cnpj.ts index 4f4e9a17..3e00d08e 100644 --- a/src/parse-cnpj/parse-cnpj.ts +++ b/src/parse-cnpj/parse-cnpj.ts @@ -1,12 +1,11 @@ +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "../format-cnpj/constants"; import type { FormatCnpjOptions } from "../format-cnpj/format-cnpj"; const sanitize = (value: string | number, version?: FormatCnpjOptions["version"]): string => { if (version === 2) { - return value - .toString() - .replace(/[^A-Za-z0-9]/g, "") - .toUpperCase(); + return sanitizeToAlphanumeric(value); } return sanitizeToDigits(value); @@ -23,4 +22,4 @@ const sanitize = (value: string | number, version?: FormatCnpjOptions["version"] export const parseCnpj = ( value: string | number, options?: Pick, -): string => sanitize(value, options?.version); +): string => sanitize(value, options?.version).slice(0, LENGTH); diff --git a/src/parse-cpf/parse-cpf.test.ts b/src/parse-cpf/parse-cpf.test.ts index f833f7d9..edad3f79 100644 --- a/src/parse-cpf/parse-cpf.test.ts +++ b/src/parse-cpf/parse-cpf.test.ts @@ -9,4 +9,8 @@ describe("parseCpf", () => { it("should remove non numeric characters", () => { expect(parseCpf("943.?ABC895.751-04abc")).toBe("94389575104"); }); + + it("should ignore digits after the CPF length", () => { + expect(parseCpf("94389575104123")).toBe("94389575104"); + }); }); diff --git a/src/parse-cpf/parse-cpf.ts b/src/parse-cpf/parse-cpf.ts index a9e8f3f4..eb740eb0 100644 --- a/src/parse-cpf/parse-cpf.ts +++ b/src/parse-cpf/parse-cpf.ts @@ -1,4 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "../format-cpf/constants"; /** * Removes CPF formatting characters and returns only digits. @@ -6,4 +7,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @param {string|number} value - The CPF value to be parsed. * @returns {string} The CPF value without formatting. */ -export const parseCpf = (value: string | number): string => sanitizeToDigits(value); +export const parseCpf = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-legal-nature/constants.ts b/src/parse-legal-nature/constants.ts new file mode 100644 index 00000000..8a5178fe --- /dev/null +++ b/src/parse-legal-nature/constants.ts @@ -0,0 +1 @@ +export const LENGTH = 4; diff --git a/src/parse-legal-nature/parse-legal-nature.test.ts b/src/parse-legal-nature/parse-legal-nature.test.ts index 0aac67b5..da1f4ea7 100644 --- a/src/parse-legal-nature/parse-legal-nature.test.ts +++ b/src/parse-legal-nature/parse-legal-nature.test.ts @@ -5,4 +5,8 @@ describe("parseLegalNature", () => { it("should remove legal nature formatting", () => { expect(parseLegalNature("206-2")).toBe("2062"); }); + + it("should ignore digits after the legal nature length", () => { + expect(parseLegalNature("206299")).toBe("2062"); + }); }); diff --git a/src/parse-legal-nature/parse-legal-nature.ts b/src/parse-legal-nature/parse-legal-nature.ts index 76845b98..87fd603e 100644 --- a/src/parse-legal-nature/parse-legal-nature.ts +++ b/src/parse-legal-nature/parse-legal-nature.ts @@ -1,3 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; -export const parseLegalNature = (value: string | number): string => sanitizeToDigits(value); +export const parseLegalNature = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-license-plate/constants.ts b/src/parse-license-plate/constants.ts new file mode 100644 index 00000000..b66a2746 --- /dev/null +++ b/src/parse-license-plate/constants.ts @@ -0,0 +1 @@ +export const LENGTH = 7; diff --git a/src/parse-license-plate/parse-license-plate.test.ts b/src/parse-license-plate/parse-license-plate.test.ts index 5e2ab375..24fd552b 100644 --- a/src/parse-license-plate/parse-license-plate.test.ts +++ b/src/parse-license-plate/parse-license-plate.test.ts @@ -6,4 +6,9 @@ describe("parseLicensePlate", () => { expect(parseLicensePlate("abc-1234")).toBe("ABC1234"); expect(parseLicensePlate("abc1d23")).toBe("ABC1D23"); }); + + it("should ignore characters after the license plate length", () => { + expect(parseLicensePlate("abc123456")).toBe("ABC1234"); + expect(parseLicensePlate("abc1d23xyz")).toBe("ABC1D23"); + }); }); diff --git a/src/parse-license-plate/parse-license-plate.ts b/src/parse-license-plate/parse-license-plate.ts index eef84652..3d79f4e6 100644 --- a/src/parse-license-plate/parse-license-plate.ts +++ b/src/parse-license-plate/parse-license-plate.ts @@ -1,4 +1,7 @@ +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; +import { LENGTH } from "./constants"; + export const parseLicensePlate = (value: string): string => { if (!value || typeof value !== "string") return ""; - return value.toUpperCase().replace(/[^A-Z0-9]/g, ""); + return sanitizeToAlphanumeric(value).slice(0, LENGTH); }; diff --git a/src/parse-passport/constants.ts b/src/parse-passport/constants.ts new file mode 100644 index 00000000..3122517e --- /dev/null +++ b/src/parse-passport/constants.ts @@ -0,0 +1 @@ +export const LENGTH = 8; diff --git a/src/parse-passport/parse-passport.test.ts b/src/parse-passport/parse-passport.test.ts index c7e0f0d2..59d45803 100644 --- a/src/parse-passport/parse-passport.test.ts +++ b/src/parse-passport/parse-passport.test.ts @@ -4,7 +4,7 @@ import { parsePassport } from "./parse-passport"; describe("parsePassport", () => { describe("should return the string without symbols", () => { test("when there are no symbols, returns the same string", () => { - expect(parsePassport("Ab123456")).toBe("Ab123456"); + expect(parsePassport("Ab123456")).toBe("AB123456"); }); test("when there are spaces", () => { @@ -22,5 +22,9 @@ describe("parsePassport", () => { test("when there are multiple symbols", () => { expect(parsePassport(".A B.1.2-3.45 -. 6.")).toBe("AB123456"); }); + + test("when there are extra characters after the passport length", () => { + expect(parsePassport("AB123456789")).toBe("AB123456"); + }); }); }); diff --git a/src/parse-passport/parse-passport.ts b/src/parse-passport/parse-passport.ts index 0b588076..042afe22 100644 --- a/src/parse-passport/parse-passport.ts +++ b/src/parse-passport/parse-passport.ts @@ -1,15 +1,18 @@ +import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; +import { LENGTH } from "./constants"; + /** - * Removes symbols ('-', '.', and whitespaces) from a passport number. + * Removes non-alphanumeric characters from a passport number, uppercases it, and caps it to 8 characters. * * @param passport - The string containing a passport number. - * @returns The passport number with dashes, dots, and whitespaces removed. + * @returns The normalized passport number. * * @example - * parsePassport("Ab123456") // "Ab123456" - * parsePassport("Ab-123456") // "Ab123456" - * parsePassport("Ab -. 123456") // "Ab123456" + * parsePassport("Ab123456") // "AB123456" + * parsePassport("Ab-123456") // "AB123456" + * parsePassport("Ab -. 123456") // "AB123456" */ export const parsePassport = (passport: string): string => { if (!passport || typeof passport !== "string") return ""; - return passport.replace(/[^a-zA-Z0-9]/g, ""); + return sanitizeToAlphanumeric(passport).slice(0, LENGTH); }; diff --git a/src/parse-phone/constants.ts b/src/parse-phone/constants.ts new file mode 100644 index 00000000..5720885c --- /dev/null +++ b/src/parse-phone/constants.ts @@ -0,0 +1 @@ +export const LENGTH = 11; diff --git a/src/parse-phone/parse-phone.test.ts b/src/parse-phone/parse-phone.test.ts index 59d8774a..167ab9fe 100644 --- a/src/parse-phone/parse-phone.test.ts +++ b/src/parse-phone/parse-phone.test.ts @@ -8,6 +8,10 @@ describe("parsePhone", () => { }); it("should remove non numeric characters", () => { - expect(parsePhone("+55 (11) 98888-7777")).toBe("5511988887777"); + expect(parsePhone("+55 (11) 98888-7777")).toBe("55119888877"); + }); + + it("should ignore digits after the phone length", () => { + expect(parsePhone("11988887777123")).toBe("11988887777"); }); }); diff --git a/src/parse-phone/parse-phone.ts b/src/parse-phone/parse-phone.ts index e70f729f..09335268 100644 --- a/src/parse-phone/parse-phone.ts +++ b/src/parse-phone/parse-phone.ts @@ -1,9 +1,10 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; /** - * Removes phone formatting characters and returns only digits. + * Removes phone formatting characters, returns only digits, and caps the result to 11 digits. * * @param {string|number} value - The phone value to be parsed. * @returns {string} The phone value without formatting. */ -export const parsePhone = (value: string | number): string => sanitizeToDigits(value); +export const parsePhone = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-pis/parse-pis.test.ts b/src/parse-pis/parse-pis.test.ts index 282f5ea0..24f39616 100644 --- a/src/parse-pis/parse-pis.test.ts +++ b/src/parse-pis/parse-pis.test.ts @@ -9,4 +9,8 @@ describe("parsePis", () => { it("should remove non numeric characters", () => { expect(parsePis("123#Error*&@#45678#Char!90-1")).toBe("12345678901"); }); + + it("should ignore digits after the PIS length", () => { + expect(parsePis("12345678901123")).toBe("12345678901"); + }); }); diff --git a/src/parse-pis/parse-pis.ts b/src/parse-pis/parse-pis.ts index a87f9f7d..3b19e697 100644 --- a/src/parse-pis/parse-pis.ts +++ b/src/parse-pis/parse-pis.ts @@ -1,4 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "../format-pis/constants"; /** * Removes PIS formatting characters and returns only digits. @@ -6,4 +7,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @param {string|number} value - The PIS value to be parsed. * @returns {string} The PIS value without formatting. */ -export const parsePis = (value: string | number): string => sanitizeToDigits(value); +export const parsePis = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-processo-juridico/parse-processo-juridico.test.ts b/src/parse-processo-juridico/parse-processo-juridico.test.ts index d04073fd..9b9035bf 100644 --- a/src/parse-processo-juridico/parse-processo-juridico.test.ts +++ b/src/parse-processo-juridico/parse-processo-juridico.test.ts @@ -9,4 +9,8 @@ describe("parseProcessoJuridico", () => { it("should remove non numeric characters", () => { expect(parseProcessoJuridico("0002080@$25201%!@2515.%0049")).toBe("00020802520125150049"); }); + + it("should ignore digits after the processo juridico length", () => { + expect(parseProcessoJuridico("00020802520125150049123")).toBe("00020802520125150049"); + }); }); diff --git a/src/parse-processo-juridico/parse-processo-juridico.ts b/src/parse-processo-juridico/parse-processo-juridico.ts index 7997145c..0adbc3be 100644 --- a/src/parse-processo-juridico/parse-processo-juridico.ts +++ b/src/parse-processo-juridico/parse-processo-juridico.ts @@ -1,4 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "../format-processo-juridico/constants"; /** * Removes legal process formatting characters and returns only digits. @@ -6,4 +7,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @param {string|number} value - The legal process value to be parsed. * @returns {string} The legal process value without formatting. */ -export const parseProcessoJuridico = (value: string | number): string => sanitizeToDigits(value); +export const parseProcessoJuridico = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-voter-id/constants.ts b/src/parse-voter-id/constants.ts new file mode 100644 index 00000000..9a6bb7ff --- /dev/null +++ b/src/parse-voter-id/constants.ts @@ -0,0 +1 @@ +export const LENGTH = 12; diff --git a/src/parse-voter-id/parse-voter-id.test.ts b/src/parse-voter-id/parse-voter-id.test.ts index a3bdfee3..b20883d7 100644 --- a/src/parse-voter-id/parse-voter-id.test.ts +++ b/src/parse-voter-id/parse-voter-id.test.ts @@ -5,4 +5,8 @@ describe("parseVoterId", () => { it("should remove voter id formatting", () => { expect(parseVoterId("1234 5678 01 24")).toBe("123456780124"); }); + + it("should ignore digits after the voter id length", () => { + expect(parseVoterId("12345678012499")).toBe("123456780124"); + }); }); diff --git a/src/parse-voter-id/parse-voter-id.ts b/src/parse-voter-id/parse-voter-id.ts index 15bf09d9..35508577 100644 --- a/src/parse-voter-id/parse-voter-id.ts +++ b/src/parse-voter-id/parse-voter-id.ts @@ -1,3 +1,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { LENGTH } from "./constants"; -export const parseVoterId = (value: string | number): string => sanitizeToDigits(value); +export const parseVoterId = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); From ea10397132862e41b54b855cb87d906e0bb0cf98 Mon Sep 17 00:00:00 2001 From: Hyan Mandian <5044101+hyanmandian@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:40:08 -0300 Subject: [PATCH 3/3] fix: lint and tests --- .../sanitize-to-alphanumeric.ts | 5 ++- src/_internals/test/runtime-deno.ts | 45 ++++++++++++++++++- .../get-cep-info-by-address.ts | 3 +- src/parse-boleto/parse-boleto.ts | 3 +- src/parse-cep/parse-cep.ts | 3 +- src/parse-cnh/parse-cnh.ts | 3 +- src/parse-cpf/parse-cpf.ts | 3 +- src/parse-phone/parse-phone.ts | 3 +- src/parse-pis/parse-pis.ts | 3 +- src/parse-voter-id/parse-voter-id.ts | 3 +- 10 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts index b2339ee0..3c88196e 100644 --- a/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts +++ b/src/_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric.ts @@ -12,4 +12,7 @@ * ``` */ export const sanitizeToAlphanumeric = (value: string | number): string => - value.toString().replace(/[^A-Za-z0-9]/g, "").toUpperCase(); + value + .toString() + .replace(/[^A-Za-z0-9]/g, "") + .toUpperCase(); diff --git a/src/_internals/test/runtime-deno.ts b/src/_internals/test/runtime-deno.ts index 406c5941..6eff7879 100644 --- a/src/_internals/test/runtime-deno.ts +++ b/src/_internals/test/runtime-deno.ts @@ -126,7 +126,7 @@ function createMock( return mockFn; } -function createExpect(actual: unknown) { +function createMatchers(actual: unknown) { return { toBe(expected: unknown) { if (!Object.is(actual, expected)) { @@ -156,6 +156,23 @@ function createExpect(actual: unknown) { throw createAssertionError(`Expected value to contain ${String(expected)}`); } }, + toMatch(expected: RegExp | string) { + if (typeof actual !== "string") { + throw createAssertionError("Expected value to be a string"); + } + + if (expected instanceof RegExp) { + if (!expected.test(actual)) { + throw createAssertionError(`Expected ${actual} to match ${String(expected)}`); + } + + return; + } + + if (!actual.includes(expected)) { + throw createAssertionError(`Expected ${actual} to contain ${expected}`); + } + }, toContainEqual(expected: unknown) { if (!Array.isArray(actual)) { throw createAssertionError("Expected value to be an array"); @@ -225,6 +242,32 @@ function createExpect(actual: unknown) { throw createAssertionError("Expected object to match"); } }, + }; +} + +function createExpect(actual: unknown) { + const matchers = createMatchers(actual); + + return { + ...matchers, + get resolves() { + const promise = Promise.resolve(actual); + + return Object.fromEntries( + Object.entries(createMatchers(undefined)).map(([name]) => [ + name, + async (...args: unknown[]) => { + const resolved = await promise; + const resolvedMatchers = createMatchers(resolved) as Record< + string, + (...args: unknown[]) => unknown + >; + + return resolvedMatchers[name](...args); + }, + ]), + ); + }, get rejects() { return { async toThrow(expected?: new (...args: any[]) => unknown) { diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.ts b/src/get-cep-info-by-address/get-cep-info-by-address.ts index fa507817..780d241b 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.ts @@ -42,7 +42,8 @@ export type GetCepInfoByAddressOptions = { const VALID_STATE_CODES = new Set(STATES.map((state) => state.code)); -const isStateCode = (value: string): value is StateCode => VALID_STATE_CODES.has(value as StateCode); +const isStateCode = (value: string): value is StateCode => + VALID_STATE_CODES.has(value as StateCode); const normalizeAddressPart = (value: string): string => value diff --git a/src/parse-boleto/parse-boleto.ts b/src/parse-boleto/parse-boleto.ts index 018b32d3..48542c27 100644 --- a/src/parse-boleto/parse-boleto.ts +++ b/src/parse-boleto/parse-boleto.ts @@ -7,4 +7,5 @@ import { LENGTH } from "../format-boleto/constants"; * @param {string|number} value - The boleto value to be parsed. * @returns {string} The boleto value without formatting. */ -export const parseBoleto = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); +export const parseBoleto = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cep/parse-cep.ts b/src/parse-cep/parse-cep.ts index ce93b1cf..d6f00d0a 100644 --- a/src/parse-cep/parse-cep.ts +++ b/src/parse-cep/parse-cep.ts @@ -7,4 +7,5 @@ import { LENGTH } from "../format-cep/constants"; * @param {string|number} value - The CEP value to be parsed. * @returns {string} The CEP value without formatting. */ -export const parseCep = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); +export const parseCep = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cnh/parse-cnh.ts b/src/parse-cnh/parse-cnh.ts index 06bea092..9454fffd 100644 --- a/src/parse-cnh/parse-cnh.ts +++ b/src/parse-cnh/parse-cnh.ts @@ -1,4 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { LENGTH } from "./constants"; -export const parseCnh = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); +export const parseCnh = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-cpf/parse-cpf.ts b/src/parse-cpf/parse-cpf.ts index eb740eb0..eb8a05ef 100644 --- a/src/parse-cpf/parse-cpf.ts +++ b/src/parse-cpf/parse-cpf.ts @@ -7,4 +7,5 @@ import { LENGTH } from "../format-cpf/constants"; * @param {string|number} value - The CPF value to be parsed. * @returns {string} The CPF value without formatting. */ -export const parseCpf = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); +export const parseCpf = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-phone/parse-phone.ts b/src/parse-phone/parse-phone.ts index 09335268..f042a8c9 100644 --- a/src/parse-phone/parse-phone.ts +++ b/src/parse-phone/parse-phone.ts @@ -7,4 +7,5 @@ import { LENGTH } from "./constants"; * @param {string|number} value - The phone value to be parsed. * @returns {string} The phone value without formatting. */ -export const parsePhone = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); +export const parsePhone = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-pis/parse-pis.ts b/src/parse-pis/parse-pis.ts index 3b19e697..ac5d439b 100644 --- a/src/parse-pis/parse-pis.ts +++ b/src/parse-pis/parse-pis.ts @@ -7,4 +7,5 @@ import { LENGTH } from "../format-pis/constants"; * @param {string|number} value - The PIS value to be parsed. * @returns {string} The PIS value without formatting. */ -export const parsePis = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); +export const parsePis = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH); diff --git a/src/parse-voter-id/parse-voter-id.ts b/src/parse-voter-id/parse-voter-id.ts index 35508577..928901d1 100644 --- a/src/parse-voter-id/parse-voter-id.ts +++ b/src/parse-voter-id/parse-voter-id.ts @@ -1,4 +1,5 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { LENGTH } from "./constants"; -export const parseVoterId = (value: string | number): string => sanitizeToDigits(value).slice(0, LENGTH); +export const parseVoterId = (value: string | number): string => + sanitizeToDigits(value).slice(0, LENGTH);