diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a6b2b227b..94d22bd58 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -56,9 +56,13 @@ jobs: - name: Check llms.txt/llms-full.txt are up to date run: npm run build:llms && git diff --exit-code -- docs/llms.txt docs/llms-full.txt - - name: Check the docs site shells and sitemap are up to date - # --intent-to-add makes a shell written for a new page show up in the diff as well. - run: npm run build:site && git add --intent-to-add docs && git diff --exit-code -- docs/sitemap.xml 'docs/*.html' 'docs/pt-br/*.html' + - name: Check the JSR exports are up to date + run: npm run build:jsr && git diff --exit-code -- jsr.json + + - name: Build the documentation site + # The site is generated at deploy time (the Docs workflow), so nothing here can be stale; + # this only has to fail when a generator does. + run: npm run build:docs - name: Check the VEX statements against the suppressed advisories run: npm run check:vex diff --git a/.github/workflows/datasets.yml b/.github/workflows/datasets.yml index 3c3530574..f93a1466f 100644 --- a/.github/workflows/datasets.yml +++ b/.github/workflows/datasets.yml @@ -11,7 +11,7 @@ permissions: jobs: update-datasets: - name: Update IBGE/CONCLA datasets + name: Update the embedded datasets runs-on: ubuntu-latest # Only this job pushes the dataset branch and opens the pull request. permissions: @@ -43,14 +43,17 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi + # Written outside the workspace, so the pull request does not pick the file up as a change. + - name: Summarize what changed + if: steps.diff.outputs.changed == 'true' + run: node scripts/data-summary.ts "$RUNNER_TEMP/dataset-summary.md" + - name: Open pull request if: steps.diff.outputs.changed == 'true' uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: - commit-message: "chore(data): update IBGE/CONCLA datasets" + commit-message: "chore(data): update the embedded datasets" branch: chore/update-datasets base: ${{ github.event.repository.default_branch }} - title: "chore(data): update IBGE/CONCLA datasets" - body: | - Automated weekly refresh of the cities, states and legal-natures - datasets sourced from IBGE/CONCLA. + title: "chore(data): update the embedded datasets" + body-path: ${{ runner.temp }}/dataset-summary.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..6c05f68cb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,63 @@ +# Builds the documentation site and publishes it to GitHub Pages. Everything the site needs that a +# script can write - the per-page shells, the sitemap, llms.txt, llms-full.txt and the examples of +# the document field page - is written here rather than kept in the repository, so a page and what +# it is generated from can never disagree. +# +# This needs Pages set to "GitHub Actions" (Settings -> Pages -> Build and deployment -> Source); +# serving the branch directly would publish `docs/` without these files. +name: Docs + +on: + push: + branches: [main] + paths: + - docs/** + - scripts/** + - package.json + - .github/workflows/docs.yml + workflow_dispatch: + +concurrency: + group: pages + cancel-in-progress: false + +permissions: + contents: read + +jobs: + deploy: + name: Build and deploy + runs-on: ubuntu-latest + timeout-minutes: 15 + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + + - name: Build the site + run: npm run build:docs + + - name: Configure Pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + + - name: Upload the site + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: docs + + - name: Deploy to Pages + id: deploy + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 000000000..81f843e54 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,38 @@ +# Publishes every pull request's build to pkg.pr.new, so a change can be installed and tried before +# it is merged: `npm i https://pkg.pr.new/@brazilian-utils/brazilian-utils@`. The +# pkg.pr.new GitHub App (https://github.com/apps/pkg-pr-new), installed on this repository, posts +# the install command as a comment; the job itself needs no token and no write permission, which is +# why it can run for pull requests from forks. +name: Preview + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + preview: + name: Publish a preview to pkg.pr.new + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + + - name: Run build + run: vp run build + + - name: Publish the preview + run: npx --yes pkg-pr-new@0.0.88 publish --compact --comment=update diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11bfe1d5a..ad49c2808 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ # published yet. A maintainer reviews and merges that PR: that merge is confirmation #1. # 2. Merging the release PR makes release-please tag the release commit and create a GitHub # Release, which triggers this same workflow again. This time `release_created` is `true`, -# so the `publish` job builds, validates and STAGES the package on npm +# so the `publish-npm` job builds, validates and STAGES the package on npm # (`npm stage publish --provenance`). A staged version is not installable until a # maintainer approves it with 2FA, on npmjs.com (package -> Staged versions) or with # `npm stage approve `. That approval is confirmation #2 (npm's @@ -26,6 +26,12 @@ # 4. Save. No NPM_TOKEN secret is needed: npm exchanges the workflow's OIDC token for a # short-lived token automatically. # +# One-time JSR setup (done by a package maintainer, not by CI): +# 1. Sign in at jsr.io with GitHub and create the scope `brazilian-utils` and the package +# `brazilian-utils` in it. +# 2. Package Settings -> GitHub Actions -> link `brazilian-utils/javascript`. JSR then trusts +# this repository's OIDC token; no JSR token is stored anywhere. +# # One-time GitHub Environment setup (done by a package maintainer, not by CI): # Repository Settings -> Environments -> New environment -> name it `npm`. No protection rules # are needed: the environment only exists so the npm trusted publisher can be bound to it. @@ -64,7 +70,7 @@ jobs: config-file: release-please-config.json manifest-file: .release-please-manifest.json - publish: + publish-npm: name: Publish to npm needs: release-please if: ${{ needs.release-please.outputs.release_created == 'true' }} @@ -120,6 +126,37 @@ jobs: - name: Stage on npm run: npm stage publish --provenance --access public + publish-jsr: + name: Publish to JSR + needs: release-please + # JSR publishes through OIDC as well: no token is stored. It needs the one-time JSR setup + # described at the top of this file; until that is done this job fails, and the npm job, which + # does not depend on it, still publishes. + if: ${{ needs.release-please.outputs.release_created == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + + steps: + - name: Checkout the release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup + + - name: Setup Deno + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 + with: + deno-version: v2.x + + - name: Publish to JSR + run: deno publish + sbom: name: Record the SBOM of the release needs: release-please diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 09f28837d..867cfa08b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -97,6 +97,11 @@ jobs: - name: Run tests run: deno test --unstable-sloppy-imports --allow-net --allow-env src + # What JSR would reject at release time fails here instead: slow types in the public API, + # an export of jsr.json that does not resolve, a file outside the publish list. + - name: Dry-run the JSR publication + run: deno publish --dry-run + test-browsers: name: Test on Browsers (${{ matrix.browser }}) runs-on: ubuntu-latest diff --git a/.github/zizmor.yml b/.github/zizmor.yml index dc83a972c..27697cb21 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -7,8 +7,10 @@ rules: ignore: - build.yml - check.yml + - docs.yml - datasets.yml - live-tests.yml - mutation.yml + - preview.yml - release.yml - tests.yml diff --git a/.gitignore b/.gitignore index 7a784b1d1..29922d40c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,23 @@ reports/* !reports/api/ reports/api/* !reports/api/brazilian-utils.api.md +# Deno writes a lockfile next to jsr.json on every `deno test`/`deno publish`; the npm lockfile is +# the one that pins this project's dependencies. +deno.lock + +# The documentation site is generated by `npm run build:docs` and published by the Docs workflow: +# only what a person writes is kept here. +docs/*.html +!docs/index.html +docs/pt-br/*.html +docs/guides/*.html +docs/pt-br/guides/*.html +docs/sitemap.xml +docs/llms.txt +docs/llms-full.txt +docs/snippets/document-field/generated/ +docs/snippets/address-form/*/cep-field.* +docs/snippets/address-form/*/field.* +docs/snippets/address-form/react/use-mask.ts +docs/snippets/address-form/vue/mask.ts +docs/snippets/address-form/angular/mask.directive.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb98fd889..0d178c695 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thank you for your interest in contributing to Brazilian Utils! This project exists thanks to [everyone who contributes](README.md#contributors), and we'd love your help solving the little -day-to-day problems of building software for Brazilian businesses. +day-to-day problems of building software for Brazil. By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). @@ -42,8 +42,11 @@ and is invoked through the `npm` scripts below, so you don't need to install any | `npm run test:chrome-browser`, `npm run test:firefox-browser`, `npm run test:edge-browser`, `npm run test:safari-browser` | Runs the test suite in real browsers via `vp test --browser.enabled`. | | `npm run build` | Builds the library for publishing with `vp pack` (also runs attw and publint over the built output). | | `npm run build:data` | Regenerates the datasets under `src/_internals/constants` from the IBGE/CONCLA sources (`scripts/data.ts`); run by the scheduled `Update datasets` workflow. | -| `npm run build:llms` | Regenerates `docs/llms.txt` and `docs/llms-full.txt` from the docs (`scripts/llms.ts`); CI fails if they're out of date. | -| `npm run build:site` | Regenerates the per-page copies of `docs/index.html`, `docs/404.html` and `docs/sitemap.xml` from the sidebars (`scripts/site.ts`); CI fails if they're out of date. | +| `npm run build:llms` | Regenerates `docs/llms.txt` and `docs/llms-full.txt` from the docs (`scripts/llms.ts`). Generated at deploy time, not kept in the repository. | +| `npm run build:site` | Regenerates the per-page copies of `docs/index.html`, `docs/404.html` and `docs/sitemap.xml` from the sidebars (`scripts/site.ts`). Generated at deploy time, not kept in the repository. | +| `npm run build:jsr` | Regenerates the `exports` of `jsr.json`, one per utility folder (`scripts/jsr.ts`); CI fails if they're out of date. | +| `npm run build:docs` | Runs the three generators of the site above, which is what the Docs workflow deploys and what a Vercel preview builds. | +| `npm run build:examples` | Regenerates the examples of the document field page from their templates (`scripts/examples.ts`). Generated at deploy time, not kept in the repository. | | `npm run check:dependencies` | Fails if `package.json` declares any runtime `dependencies` (this package ships zero by design). | | `npm run check:tree-shaking` | Builds nothing; measures the single-import size of every export against `dist` (`scripts/tree-shaking.ts`). Run it after `npm run build` when you change a dataset, and update the bundle-size table in `docs/getting-started.md` / `docs/pt-br/getting-started.md`. | | `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. | @@ -89,6 +92,28 @@ from forks), the maintainers (review, merge, release approval) and the automatio builds, tests and publishes, Dependabot and the `Update datasets` workflow open update pull requests, and release-please turns merged commits into releases. +## Datasets + +The tables under `src/_internals/constants/` fall in two groups, and only the first refreshes +itself: + +- **Generated from an official source** by a script in `scripts/` (`npm run build:data`, run + every Monday by the `Update datasets` workflow): banks (Banco Central, `banks.ts`), CBO + (`cbo.ts`), CFOP (CONFAZ, `cfop.ts`), municipalities and states (IBGE, `cities.ts`, + `states.ts`), CNAE (`cnae.ts`), legal natures (CONCLA, `legal-natures.ts`) and NCM (Siscomex, + `ncm.ts`). When a run changes a file, the workflow opens a pull request whose description, written + by `scripts/data-summary.ts`, lists per table how many entries were added and removed, with a + sample of each. Never edit these files by hand. +- **Maintained by hand**, because the source is a law or a regulation with no machine-readable + form: area codes and their states (Anatel, `area-codes.ts`), service phone prefixes (Anatel, + `service-phone.ts`), national and state holidays (`holidays.ts`), the órgãos and tribunals of the + processo number (Resolução CNJ nº 65/2008, `processo-juridico.ts`), IBAN lengths per country + (`iban.ts`), IBGE state codes (`ibge-uf-codes.ts`), legal nature categories, the CST and CSOSN + tables (`src/is-valid-cst`, `src/is-valid-csosn`), the professional councils + (`src/is-valid-registro-profissional`), the região fiscal digit of each state + (`src/generate-cpf`) and the voter ID state codes (`src/is-valid-voter-id`). A change to one of + these cites the act that changed it (`@see Official:`), like any rule. + ## Adding a new utility Brazilian Utils follows a consistent folder convention for every utility. To add a new one (for @@ -125,7 +150,11 @@ example `formatSomething`): never values computed by the code under test. Close the file with a `describe("properties")` block of [fast-check](https://fast-check.dev) properties that hold by specification (a generated value is valid, format/parse round-trip, masks never change the verdict, arbitrary - input never throws) and a `describe(" types")` block that pins the public signature with + input never throws); a property that needs a valid document draws it with `fc.gen()` from the + arbitraries of `src/_internals/test/arbitraries.ts` (`const cpf = g(cpfs)`), never by calling a + `generate*` utility inside the property: those use + `Math.random()`, which the seed fast-check reports does not control, so a failure could be + neither replayed nor shrunk. Then a `describe(" types")` block that pins the public signature with `expectTypeOf` (parameters, options and return type; `vp check` fails on a wrong assertion). A hot path may also get a `describe(" benchmarks")` block of `bench` cases: they are todo entries in a normal run and execute with `npx vp test bench --run`. `describe`, `test`, @@ -139,9 +168,14 @@ example `formatSomething`): - `docs/utilities.md` (English) - `docs/pt-br/utilities.md` (Portuguese translation) - Follow the existing format: a `##` heading with the function name, a short description, and a - `javascript` code block showing example input/output. Place the new section next to the other - utilities in the same domain, keeping both files in the same order. + Follow the existing format: a `###` heading with the function name under the `##` family it + belongs to, one sentence saying what it does (`llms.txt` indexes that sentence), a few short + bullets for the options and the return rules, a `javascript` code block showing example + input/output, and a one-line `Source:` (`Fonte:` in Portuguese) with the official reference when + there is one. Do not repeat what the Conventions section at the top of the file already says + (nothing throws, masked input is accepted, generators use `Math.random()`); the JSDoc is the place + for every edge case, the reference is the place for what a caller needs. Keep both files in the + same order. After editing `docs/getting-started.md` or `docs/utilities.md`, run `npm run build:llms` to regenerate `docs/llms.txt` and `docs/llms-full.txt` (see [llms.txt](https://llmstxt.org/)) and @@ -328,25 +362,45 @@ JavaScript/TypeScript features. Pages with [docsify](https://docsify.js.org): `docs/index.html` renders the Markdown in the browser, with `_sidebar.md`, `_navbar.md` and `_coverpage.md` as its navigation. +- Only what a person writes lives in `docs/`: the `Docs` workflow runs `npm run build:docs` and + publishes the result to GitHub Pages, so the page shells, `sitemap.xml`, `llms.txt`, + `llms-full.txt` and the generated examples are never committed (and never stale). Run + `npm run build:docs` to see the site as it is published; a pull request that touches `docs/` or + `scripts/` gets the same build as a Vercel preview. - docsify runs in history mode, so every page is a real URL (`/getting-started`, `/pt-br/utilities`) that search engines index on its own. GitHub Pages serves each one from a copy of `index.html` next to the page (`getting-started.html`) that carries the page's own title, description, canonical URL and hreflang pair, and `npm run build:site` (`scripts/site.ts`) writes those copies, `404.html` and `sitemap.xml` from the sidebars and the - pages' front matter. The Check workflow fails when they are stale, so run it after editing - `index.html`, a sidebar or a page's front matter. Links from the hash-router era - (`/#/getting-started?id=usage`) are rewritten on load, so nothing out there breaks. + pages' front matter. Links from the hash-router era (`/#/getting-started?id=usage`) are + rewritten on load, so nothing out there breaks. - Every page starts with a front matter block with a quoted `title` and `description` (and `keywords`), and has no `#` heading of its own: the plugin in `docs/index.html` turns the title into the page's heading and the block feeds the page's metadata (a small wrapper there hands the search plugin the same view, so the block never shows up in search results). Scripts read the block through `scripts/front-matter.ts`. +- The site's own CSS is `docs/styles.css`, linked by every shell: styles go there, not in a + ` - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 6d6fc24b2..1aa2343f8 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -1,6 +1,6 @@ Brazilian Utils -> Utils library for Brazilian-specific businesses. +> Utilities for Brazilian data: CPF, CNPJ, CEP, boleto, Pix, holidays and more. - Zero runtime dependencies - Tree-shakeable, one import per util diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 478e19202..c3411f02e 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,3 +1,8 @@ * [Getting Started](getting-started.md) * [Utilities](utilities.md) +* Guides + * [Document field](guides/document-field.md) + * [Address from a CEP](guides/address-form.md) + * [State and city](guides/state-city.md) + * [Schema libraries](guides/schema.md) * [Migration v1 to v2](migration-v1-to-v2.md) diff --git a/docs/getting-started.html b/docs/getting-started.html deleted file mode 100644 index aad70064a..000000000 --- a/docs/getting-started.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - Getting Started · Brazilian Utils - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/getting-started.md b/docs/getting-started.md index b88032c2d..3dbe79b89 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,49 +1,27 @@ --- title: "Getting Started" -description: "Install Brazilian Utils, the zero-dependency utils library for Brazilian businesses, and learn how to import a util, which runtimes are supported and how the bundle size behaves." +description: "Install Brazilian Utils, the zero-dependency library of utilities for Brazilian data, import a util, check the supported runtimes and keep your bundle small." keywords: ["Brazilian Utils", "install", "npm", "tree-shaking", "bundle size", "subpath imports", "Node.js", "Bun", "Deno", "browser", "AI assistants", "Context7"] --- -Brazilian Utils is a library focused on solving problems that we face daily in the development of applications for the Brazilian business. +Brazilian Utils is a zero-dependency library of small utilities for the day-to-day problems of building software for Brazil: validating, formatting, parsing and generating CPF, CNPJ, CEP, boleto, Pix, phone numbers, holidays and more. ## Why Brazilian Utils - **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle. -- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.4 KB minified (0.8 KB gzipped); every util is also its own subpath entry (`@brazilian-utils/brazilian-utils/get-cities`) for the heavy ones. -- **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, tested in CI on every one of them. -- **Written in TypeScript.** Types ship with the package; the public API is tracked by an API report so nothing changes silently. -- **Validated against the official rules.** Every validator cites the specification, law or dataset it implements (`@see` in the docs), and the test suite is mutation-tested, not just covered. +- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.4 KB minified (0.8 KB gzipped). Every util is also its own subpath entry, so the heavy ones can be lazy-loaded. +- **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, all tested in CI. +- **Written in TypeScript.** Types ship with the package, and an API report tracks the public API so nothing changes silently. +- **Validated against the official rules.** Every validator cites the specification, law or dataset it implements, and the test suite is mutation-tested, not just covered. - **Documented in English and Portuguese**, with an `llms.txt` for AI assistants. ## Installation -You can install **Brazilian Utils** in a few ways: - -as npm package: - -```bash -npm install --save @brazilian-utils/brazilian-utils -``` - -with yarn package manager: - ```bash -yarn add @brazilian-utils/brazilian-utils +npm install @brazilian-utils/brazilian-utils ``` -with pnpm: - -```bash -pnpm add @brazilian-utils/brazilian-utils -``` - -with bun: - -```bash -bun add @brazilian-utils/brazilian-utils -``` - -or ` @@ -51,11 +29,16 @@ or ` + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/migration-v1-to-v2.md b/docs/migration-v1-to-v2.md index 6222f8c12..524b20b51 100644 --- a/docs/migration-v1-to-v2.md +++ b/docs/migration-v1-to-v2.md @@ -4,149 +4,35 @@ description: "How to move a project from Brazilian Utils v1.x to v2: the renamed keywords: ["migration", "v1", "v2", "deprecated", "renamed exports", "upgrade"] --- -This guide will help you migrate from Brazilian Utils v1.x to v2.0.0. +This guide covers moving a project from Brazilian Utils v1.x to v2. -## TL;DR - Quick Migration +## Summary -**Good news!** v2.x maintains backward compatibility for most breaking changes: +v2 renames every function to camelCase (`formatCPF` is now `formatCpf`) but keeps the v1 names as deprecated aliases, so most projects upgrade without changing code. TypeScript and your editor flag the old names. The aliases are removed in v3.0.0. -✅ **You can upgrade to v2.x without changing your code** - old function names like `formatCPF`, `isValidCNPJ`, etc. still work -⚠️ **You'll receive deprecation warnings** - encouraging you to migrate to the new names -🗑️ **Old names will be removed in v3.0.0** - so migrate gradually +Four v1 helpers were internal and have no alias. Replace them before upgrading: -**However**, you must remove usage of these helper functions before upgrading: -- `onlyNumbers` → use `string.replace(/\D/g, '')` -- `isLastChar` → use `index === input.length - 1` -- `generateChecksum` → now internal only -- `generateRandomNumber` → now internal only - -## Improvements in v2.0.0 - -Version 2.0.0 brings significant improvements in architecture, tooling, and developer experience: - -### 🎯 Better Tree Shaking - -The library now uses modern ES module exports with proper `exports` field in `package.json`, enabling better tree shaking in modern bundlers. You can import only what you need: - -```javascript -// Only the functions you import will be included in your bundle -import { isValidCpf, formatCpf } from '@brazilian-utils/brazilian-utils'; -``` - -Since 2.4.0 every util is also its own subpath entry, so a bundler that does not tree-shake (or a -plain `require`) still loads a single module, and the few heavy ones (`getCities`, -`getMunicipalities`, `isValidNcm`, `isValidCbo`, `isValidCnae`, `getBanks`) can be lazy-loaded: - -```javascript -import { isValidCpf } from '@brazilian-utils/brazilian-utils/is-valid-cpf'; // ~1.4 KB, 0.8 KB gzipped -const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities'); // only when needed -``` - -See [Bundle size](getting-started.md#bundle-size) for the sizes of every entry. - -### 📁 Simpler Structure - -The codebase has been reorganized for better maintainability: -- **v1**: Complex structure with separate `utilities/` and `helpers/` directories -- **v2**: Flat structure with internal utilities in `_internals/` directory -- Each utility is self-contained in its own directory -- Cleaner import paths and better code organization - -### 🔧 Modern Tooling - -Updated to modern, faster tooling: -- **Build**: Migrated from `tsdx` to a **Vite+** toolchain for faster builds and scripts -- **Testing**: Migrated from `jest` to **Vitest** (faster, Jest-compatible, ESM-native) -- **Linting/Formatting**: Migrated from `prettier` + `eslint` to the Vite+ toolchain (`vp fmt` and `vp check`, backed by Oxc) -- **TypeScript**: Modern configuration optimized for bundlers - -### 🌐 Browser Testing - -Now includes cross-browser testing support: -- Tests run in real browsers (Chrome, Firefox, Safari, Edge) -- Ensures compatibility across different browser environments -- Better confidence in cross-platform functionality - -Run browser tests with: -```bash -npm run test:chrome-browser -npm run test:firefox-browser -npm run test:safari-browser -npm run test:edge-browser -``` - -### 📦 Fewer Dependencies - -Reduced development dependencies while maintaining zero runtime dependencies: -- **v1**: Multiple tools (tsdx, jest, prettier, eslint, husky, lint-staged, etc.) -- **v2**: One toolchain (Vite+ for build, lint, format and tests, with Vitest browser support through webdriverio) plus the quality gates listed in CONTRIBUTING.md (Stryker, knip, jscpd, API Extractor, commitlint) -- Simpler maintenance and faster CI/CD pipelines -- Zero runtime dependencies (maintained) - -### ✨ New Functions & Features - -Added new useful utilities: -- `getHolidays` - Get Brazilian holidays (national and state-specific) -- `getBoletoInfo` - Extract information from boleto (amount, expiration, bank code) -- `formatPhone` - Format phone numbers with Brazilian patterns -- `formatBoleto` - Format boleto numbers -- `generateBoleto` - Generate valid random boleto numbers -- `formatPis` - Format PIS numbers -- `isValidRenavam` - Validate RENAVAM (vehicle registration number) -- `isValidBankAccount` - Validate Brazilian bank accounts with specific algorithms for major banks - -2.4.0 added many more families on top of these, all listed in the [utilities documentation](utilities.md): -Pix (`isValidPixKey`, `generatePixPayload`, `getPixPayloadInfo`), NF-e/DF-e keys, CNS, certidão, -CEI/CNO/CAEPF, IBAN, card numbers, VIN, professional registrations, bank lookups (`getBanks`, -`getBankByCode`, `getBankByIspb`), CBO/CNAE/NCM/CFOP/CST/CSOSN codes, business days -(`isBusinessDay`, `addBusinessDays`, `differenceInBusinessDays`), legal nature categories, offline -municipalities (`getMunicipalities`, `getMunicipalityByCode`), DDD and time zone lookups, numbers in -words, and a `capitalize` that knows the Brazilian company designations. - -#### Alphanumeric CNPJ Support (Version 2) - -v2.0.0 adds support for the new alphanumeric CNPJ format introduced by the Brazilian Federal Revenue. Both `isValidCnpj` and `generateCnpj` now support version 2 (alphanumeric) CNPJs: - -```javascript -import { isValidCnpj, generateCnpj } from '@brazilian-utils/brazilian-utils'; - -// Generate alphanumeric CNPJ -const alphaCnpj = generateCnpj(2); // e.g., "Q0SLFMBD7VX439" - -// Validate alphanumeric CNPJ (requires version option) -isValidCnpj("Q0.SLF.MBD/7VX4-39", { version: 2 }); // true -isValidCnpj("Q0SLFMBD7VX439", { version: 2 }); // true - -// Version 1 (numeric) is the default -isValidCnpj("12.345.678/0001-95"); // true (validates numeric only) -isValidCnpj("12.345.678/0001-95", { version: 1 }); // true (explicit) -``` - -**Important**: By default, `isValidCnpj()` validates only numeric (version 1) CNPJs. To validate alphanumeric CNPJs, you must explicitly pass `{ version: 2 }`. - -### 📈 Better TypeScript Support - -- Modern TypeScript configuration optimized for bundlers -- Better type inference and exports -- Improved developer experience with better autocomplete - -## Breaking Changes - -### Function Names Changed (PascalCase → camelCase) - -All function names have been changed from PascalCase to camelCase to follow JavaScript naming conventions. - -**⚠️ Important: Backward Compatibility** +| v1 | Replacement | +|---|---| +| `onlyNumbers(value)` | `value.replace(/\D/g, '')` | +| `isLastChar(index, input)` | `index === input.length - 1` | +| `generateChecksum` | Not exported anymore. Inline the check-digit calculation you need. | +| `generateRandomNumber(length)` | Your own loop over `Math.floor(Math.random() * 10)`. | -To make the migration easier, **v2.x still exports the old PascalCase names as deprecated aliases**. This means: +## What changed -- ✅ Your existing code using `formatCPF`, `isValidCNPJ`, etc. will continue to work in v2.x -- ⚠️ You'll receive deprecation warnings in your IDE/TypeScript -- 🗑️ The old names will be **removed in v3.0.0** +- **Names are camelCase.** See [Renamed functions](#renamed-functions). +- **Tree-shaking works down to the function**, and every util is also its own subpath (`@brazilian-utils/brazilian-utils/is-valid-cpf`), so the heavy ones can be lazy-loaded. See [Bundle size](getting-started.md#bundle-size). +- **Alphanumeric CNPJ.** `isValidCnpj` and `generateCnpj` accept the new alphanumeric format with `{ version: 2 }`. Numeric (version 1) stays the default. See [`generateCnpj` and the version](#generatecnpj-and-the-version). +- **`getAddressInfoByCep`** accepts a `providers` option, pads a numeric CEP, and throws typed errors: `GetAddressInfoByCepValidationError`, `GetAddressInfoByCepNotFoundError` and `GetAddressInfoByCepServiceError`. Calls without options work as in v1. +- **`getCities`** returns the list sorted alphabetically. Since 2.4.0 it is deprecated: `getMunicipalities('SP')` returns the same municipalities with their IBGE codes, and `getMunicipalityByCode('3550308')` looks one up offline. +- **`isValidIe`** takes one object since 2.4.0, `isValidIe({ value, stateCode })`. The positional form is deprecated. +- **Many new utilities** since v2: holidays and business days, Pix, NF-e keys, boleto parsing, phone formatting, bank accounts and lookups, classification codes (CBO, CNAE, NCM, CFOP), municipalities offline, numbers in words and more. They are all in the [utilities reference](utilities.md). +- **Tooling** moved to Vite+ and Vitest, with browser tests in CI. This only matters if you contribute; see [CONTRIBUTING.md](https://github.com/brazilian-utils/javascript/blob/main/CONTRIBUTING.md). -**Recommendation:** While you can upgrade to v2.x without changing your code immediately, we recommend migrating to the new camelCase names as soon as possible to prepare for v3.0.0. +## Renamed functions -#### Validation Functions +Every other export keeps its v1 name. | v1 | v2 | |---|---| @@ -154,63 +40,15 @@ To make the migration easier, **v2.x still exports the old PascalCase names as d | `isValidCNPJ` | `isValidCnpj` | | `isValidCEP` | `isValidCep` | | `isValidPIS` | `isValidPis` | -| `isValidIE` | `isValidIe` (since 2.4.0 prefer the object form, `isValidIe({ value, stateCode })`; the positional form is deprecated) | -| `isValidProcessoJuridico` | `isValidProcessoJuridico` (unchanged) | -| `isValidBoleto` | `isValidBoleto` (unchanged) | -| `isValidEmail` | `isValidEmail` (unchanged) | -| `isValidPhone` | `isValidPhone` (unchanged) | -| `isValidMobilePhone` | `isValidMobilePhone` (unchanged) | -| `isValidLandlinePhone` | `isValidLandlinePhone` (unchanged) | -| `isValidLicensePlate` | `isValidLicensePlate` (unchanged) | -| `isValidRenavam` | `isValidRenavam` (new) | - -#### Format Functions - -| v1 | v2 | -|---|---| +| `isValidIE` | `isValidIe` | | `formatCPF` | `formatCpf` | | `formatCNPJ` | `formatCnpj` | | `formatCEP` | `formatCep` | -| `formatProcessoJuridico` | `formatProcessoJuridico` (unchanged) | -| `formatBoleto` | `formatBoleto` (unchanged) | -| `formatCurrency` | `formatCurrency` (unchanged) | -| `formatPhone` | `formatPhone` (new) | - -#### Generation Functions - -| v1 | v2 | -|---|---| | `generateCPF` | `generateCpf` | | `generateCNPJ` | `generateCnpj` | -| `generateBoleto` | `generateBoleto` (unchanged) | - -**⚠️ Note on `generateCnpj` behavior:** -In v2.x, `generateCnpj()` without arguments defaults to version 1 (numeric CNPJ). In v3.0.0, this behavior will change to randomly select between version 1 (numeric) and version 2 (alphanumeric) CNPJs for better randomness. If you need a specific version, always pass the version parameter explicitly: +Before (v1): -```javascript -// Recommended: Always specify the version -generateCnpj(1); // Always generates numeric CNPJ -generateCnpj(2); // Always generates alphanumeric CNPJ - -// Not recommended: Relying on default behavior -generateCnpj(); // Currently generates numeric (v1), but will be random in v3.0.0 -``` - -#### Other Functions - -| v1 | v2 | -|---|---| -| `parseCurrency` | `parseCurrency` (unchanged) | -| `capitalize` | `capitalize` (unchanged) | -| `getStates` | `getStates` (unchanged) | -| `getCities` | `getCities` (unchanged; deprecated in 2.4.0 in favour of `getMunicipalities`) | -| `getMunicipality` | `getMunicipality` (deprecated in 2.4.0 in favour of `getMunicipalityByCode`, which is synchronous and offline) | -| `getAddressInfoByCep` | `getAddressInfoByCep` (API changed, see below) | - -### Migration Example - -**Before (v1):** ```javascript import { isValidCPF, formatCPF, generateCNPJ } from '@brazilian-utils/brazilian-utils'; @@ -219,7 +57,8 @@ const formatted = formatCPF('12345678909'); const cnpj = generateCNPJ(); ``` -**After (v2):** +After (v2): + ```javascript import { isValidCpf, formatCpf, generateCnpj } from '@brazilian-utils/brazilian-utils'; @@ -228,176 +67,25 @@ const formatted = formatCpf('12345678909'); const cnpj = generateCnpj(); ``` -### Removed Helper Functions - -The following helper functions are no longer exported in the public API. These were internal utilities that should not have been exposed. - -**⚠️ Note:** Unlike the renamed functions above, these helpers do **NOT** have backward compatibility aliases. You must migrate away from them before upgrading to v2.x. +### `generateCnpj` and the version -#### `onlyNumbers` -This function has been removed from the public API. It's now an internal utility called `sanitizeToDigits`. +`generateCnpj()` without arguments generates a numeric CNPJ in v2.x. In v3.0.0 it will pick numeric or alphanumeric at random, so pass the version when you need a specific one: -**Migration:** ```javascript -// v1 - Don't use this anymore -import { onlyNumbers } from '@brazilian-utils/brazilian-utils'; -const digits = onlyNumbers('123-456'); - -// v2 - Use a simple replacement -const digits = '123-456'.replace(/\D/g, ''); +generateCnpj(1); // always numeric +generateCnpj(2); // always alphanumeric, e.g. "Q0SLFMBD7VX439" +generateCnpj(); // numeric today, random in v3.0.0 ``` -#### `isLastChar` -This function has been removed. Use a simple inline comparison instead. +`isValidCnpj` validates numeric CNPJs by default. To validate alphanumeric ones, pass `{ version: 2 }`: -**Migration:** ```javascript -// v1 - Don't use this anymore -import { isLastChar } from '@brazilian-utils/brazilian-utils'; -if (isLastChar(index, input)) { /* ... */ } - -// v2 - Use inline comparison -if (index === input.length - 1) { /* ... */ } +isValidCnpj('12.345.678/0001-95'); // true +isValidCnpj('Q0.SLF.MBD/7VX4-39', { version: 2 }); // true +isValidCnpj('Q0.SLF.MBD/7VX4-39'); // false (numeric only without the option) ``` -#### `generateChecksum` -This function is now internal and no longer exported in the public API. The package exports no internals: `dist/_internals` is not published and there is no subpath for it, so there is no supported way to import this function in v2. Inline the check digit calculation you need instead. - -**Migration:** -```javascript -// v1 - Don't use this anymore -import { generateChecksum } from '@brazilian-utils/brazilian-utils'; -``` - -#### `generateRandomNumber` -This function is now internal and no longer exported in the public API. - -**Migration:** -```javascript -// v1 - Don't use this anymore -import { generateRandomNumber } from '@brazilian-utils/brazilian-utils'; - -// v2 - Use your own implementation -function generateRandomNumber(length) { - let result = ''; - for (let i = 0; i < length; i++) { - result += Math.floor(Math.random() * 10).toString(); - } - return result; -} -``` - -## New Functions - -The following functions are new in v2.0.0: - -### `getHolidays` - -Get Brazilian holidays for a given year. Supports national and state-specific holidays. - -```javascript -import { getHolidays } from '@brazilian-utils/brazilian-utils'; - -// Get all national holidays -const holidays = getHolidays(2024); - -// Get holidays for a specific state -const spHolidays = getHolidays({ year: 2024, stateCode: 'SP' }); -``` - -### `getBoletoInfo` - -Extract information from a boleto (amount, expiration date, bank code). - -```javascript -import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; - -const info = getBoletoInfo('00190000090114971860168524522114675860000102656'); -// { amount: 102656, expirationDate: Date, bankCode: '001' } -``` - -### `formatPhone` - -Format phone numbers according to Brazilian patterns. - -```javascript -import { formatPhone } from '@brazilian-utils/brazilian-utils'; - -formatPhone('11900000000'); // 11900-0000 (BEWARE: default "sn" truncates a DDD-prefixed number) -formatPhone('11900000000', { mask: 'nanp' }); // (11) 90000-0000 -formatPhone('11900000000', { mask: 'auto' }); // (11) 90000-0000 -``` - -### `isValidRenavam` - -Validate RENAVAM (Registro Nacional de Veículos Automotores). Supports both old format (9 digits) and new format (11 digits). - -```javascript -import { isValidRenavam } from '@brazilian-utils/brazilian-utils'; - -isValidRenavam('639884962'); // true (9 digits, old format) -isValidRenavam('00639884962'); // true (11 digits, new format) -isValidRenavam('12345678901'); // false (invalid checksum) -``` - -### `isValidBankAccount` - -Validate Brazilian bank accounts. Supports specific validation algorithms for major banks (Banco do Brasil, Itaú, Bradesco, Santander, Caixa Econômica Federal) and generic mod10/mod11 validation for other banks. - -```javascript -import { isValidBankAccount } from '@brazilian-utils/brazilian-utils'; - -// Banco do Brasil -isValidBankAccount({ - bankCode: '001', - agency: '1584', - account: '00210169', - digit: '6' -}); // true - -// Itaú -isValidBankAccount({ - bankCode: '341', - agency: '2545', - account: '02366', - digit: '1' -}); // true - -// Other banks use generic validation -isValidBankAccount({ - bankCode: '246', - agency: '1234', - account: '123456', - digit: '6' -}); // true (the digit matches mod10) -``` - -## API Changes - -### `getAddressInfoByCep` - -The `getAddressInfoByCep` function now supports additional options and improved error handling. - -**Before (v1):** -```javascript -const address = await getAddressInfoByCep('01310100'); -``` - -**After (v2):** -```javascript -// Still works the same way -const address = await getAddressInfoByCep('01310100'); - -// But now supports options -const address = await getAddressInfoByCep('01310-100', { - providers: ['viacep', 'brasilapi'] -}); - -// Also accepts numbers (will be padded automatically) -const address = await getAddressInfoByCep(1310100); -``` - -The function now exports error classes for better error handling: +### `getAddressInfoByCep` errors ```javascript import { @@ -411,58 +99,28 @@ try { const address = await getAddressInfoByCep('01310100'); } catch (error) { if (error instanceof GetAddressInfoByCepValidationError) { - // Handle validation error + // invalid CEP } else if (error instanceof GetAddressInfoByCepNotFoundError) { - // Handle not found error + // no address for this CEP } else if (error instanceof GetAddressInfoByCepServiceError) { - // Handle service error + // the providers failed } } ``` -### `getCities` - -The `getCities` function now returns sorted results alphabetically. - -**Before (v1):** -```javascript -getCities(); // Returned unsorted array -getCities('SP'); // Returned unsorted array -``` - -**After (v2):** -```javascript -getCities(); // Returns sorted alphabetically -getCities('SP'); // Returns sorted alphabetically -``` - -**Since 2.4.0:** `getCities` is deprecated. `getMunicipalities('SP')` returns the same municipalities -with their IBGE codes (`{ code, name, stateCode }`), and `getMunicipalityByCode('3550308')` looks one -up without a network call. - -## Migration Checklist - -### Required (before upgrading to v2.x) -- [ ] Remove usage of helper functions (`onlyNumbers`, `isLastChar`, `generateChecksum`, `generateRandomNumber`) +## Checklist -### Optional (recommended before v3.0.0) -- [ ] Update all imports to use camelCase function names -- [ ] Replace all function calls with camelCase names -- [ ] Replace `getCities` with `getMunicipalities` and `getMunicipality` with `getMunicipalityByCode` (deprecated in 2.4.0) -- [ ] Call `isValidIe({ value, stateCode })` instead of `isValidIe(stateCode, ie)` (deprecated in 2.4.0) -- [ ] Import the `*Params` type names instead of the `*Options` aliases kept for the single-object-argument functions (deprecated in 2.4.0) -- [ ] Drop `'widenet'` from the `providers` of `getAddressInfoByCep` (the service is gone; deprecated in 2.4.0) +Required before upgrading: -### Review if applicable -- [ ] Update error handling for `getAddressInfoByCep` if needed -- [ ] Review usage of `getCities` if sorting was important -- [ ] Test all validation and formatting functions -- [ ] Update any TypeScript type imports if applicable +- [ ] Replace `onlyNumbers`, `isLastChar`, `generateChecksum` and `generateRandomNumber`. -## Getting Help +Recommended before v3.0.0: -If you encounter any issues during migration, please: +- [ ] Rename the imports and calls in the table above to camelCase. +- [ ] Replace `getCities` with `getMunicipalities` and `getMunicipality` with `getMunicipalityByCode`. +- [ ] Call `isValidIe({ value, stateCode })` instead of `isValidIe(stateCode, ie)`. +- [ ] Import the `*Params` type names instead of the `*Options` aliases of the single-object-argument functions. +- [ ] Drop `'widenet'` from the `providers` of `getAddressInfoByCep` (the service is gone). +- [ ] Pass the version to `generateCnpj` when you need a specific one. -1. Check the [utilities documentation](utilities.md) for the correct function signatures -2. Review the examples in this migration guide -3. Open an issue on the [GitHub repository](https://github.com/brazilian-utils/javascript) if you find a bug +Found a bug during the migration? [Open an issue](https://github.com/brazilian-utils/javascript/issues). diff --git a/docs/pt-br/_coverpage.md b/docs/pt-br/_coverpage.md index 68c4d6fa1..c7291e5f3 100644 --- a/docs/pt-br/_coverpage.md +++ b/docs/pt-br/_coverpage.md @@ -1,6 +1,6 @@ Brazilian Utils -> Biblioteca de utilitários para o negócio brasileiro. +> Utilitários para dados brasileiros: CPF, CNPJ, CEP, boleto, Pix, feriados e mais. - Zero dependências de runtime - Tree-shakeable, um import por utilitário diff --git a/docs/pt-br/_sidebar.md b/docs/pt-br/_sidebar.md index 58c08e931..8e22ee7cd 100644 --- a/docs/pt-br/_sidebar.md +++ b/docs/pt-br/_sidebar.md @@ -1,3 +1,8 @@ * [Introdução](pt-br/getting-started.md) * [Utilitários](pt-br/utilities.md) +* Guias + * [Campo de documento](pt-br/guides/document-field.md) + * [Endereço pelo CEP](pt-br/guides/address-form.md) + * [Estado e cidade](pt-br/guides/state-city.md) + * [Bibliotecas de schema](pt-br/guides/schema.md) * [Migração v1 para v2](pt-br/migration-v1-to-v2.md) diff --git a/docs/pt-br/getting-started.html b/docs/pt-br/getting-started.html deleted file mode 100644 index dc71c6c4c..000000000 --- a/docs/pt-br/getting-started.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - Introdução · Brazilian Utils - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index 51c7bbb5d..86e5f018f 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -1,61 +1,44 @@ --- title: "Introdução" -description: "Instale o Brazilian Utils, a biblioteca de utilitários sem dependências para o business brasileiro, e veja como importar um utilitário, quais runtimes são suportados e como o tamanho do bundle se comporta." +description: "Instale o Brazilian Utils, a biblioteca sem dependências de utilitários para dados brasileiros, importe um utilitário, veja os runtimes suportados e mantenha o bundle pequeno." keywords: ["Brazilian Utils", "instalação", "npm", "tree-shaking", "tamanho do bundle", "subpath", "Node.js", "Bun", "Deno", "navegador", "assistentes de IA", "Context7"] --- -Brazilian Utils é uma biblioteca com foco na resolução de problemas que enfrentamos diariamente no desenvolvimento de aplicações para o business brasileiro. +Brazilian Utils é uma biblioteca de utilitários, sem dependências, para os problemas do dia a dia de quem desenvolve software para o Brasil: validar, formatar, interpretar e gerar CPF, CNPJ, CEP, boleto, Pix, telefone, feriados e mais. ## Por que Brazilian Utils -- **Zero dependências de runtime.** Nada além da lib entra no seu `node_modules` ou no seu bundle. -- **Tree-shakeable até a função.** `import { isValidCpf }` custa cerca de 1,4 KB minificado (0,8 KB com gzip); cada utilitário também é um subpath próprio (`@brazilian-utils/brazilian-utils/get-cities`) para os mais pesados. -- **Roda em qualquer lugar.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno e navegadores modernos, testados no CI em todos eles. -- **Escrita em TypeScript.** Os tipos vêm no pacote; a API pública é acompanhada por um relatório de API, então nada muda em silêncio. -- **Validada contra as regras oficiais.** Cada validador cita a especificação, lei ou base de dados que implementa (`@see` na documentação), e a suíte de testes passa por mutation testing, não só por cobertura. +- **Zero dependências de runtime.** Nada além da biblioteca entra no seu `node_modules` ou no seu bundle. +- **Tree-shakeable até a função.** `import { isValidCpf }` custa cerca de 1,4 KB minificado (0,8 KB com gzip). Cada utilitário também é um subpath próprio, então os pesados podem ser carregados sob demanda. +- **Roda em qualquer lugar.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno e navegadores modernos, todos testados no CI. +- **Escrita em TypeScript.** Os tipos vêm no pacote, e um relatório de API acompanha a API pública para que nada mude em silêncio. +- **Validada contra as regras oficiais.** Cada validador cita a especificação, lei ou base de dados que implementa, e a suíte de testes passa por mutation testing, não só por cobertura. - **Documentada em inglês e português**, com um `llms.txt` para assistentes de IA. ## Instalação -Você pode instalar o **Brazilian Utils** de algumas formas: - -como um pacote npm: - -```bash -npm install --save @brazilian-utils/brazilian-utils -``` - -com gerenciador de pacotes yarn: - ```bash -yarn add @brazilian-utils/brazilian-utils +npm install @brazilian-utils/brazilian-utils ``` -com pnpm: - -```bash -pnpm add @brazilian-utils/brazilian-utils -``` - -com bun: - -```bash -bun add @brazilian-utils/brazilian-utils -``` - -ou ` ``` -### Suporte a runtimes +### Runtimes suportados -Node `^20.19.0 || >=22.12.0`, Bun, Deno e navegadores modernos. +| Runtime | Suportado | Testado no CI | +| ----------- | ------------------------- | ----------------------------- | +| Node.js | `^20.19.0 \|\| >=22.12.0` | 20, 22, 24, 26 | +| Bun | mais recente | mais recente | +| Deno | 2.x | 2.x | +| Navegadores | modernos | Chrome, Firefox, Edge, Safari | ## Como usar -Para usar um de nossos utilitários, basta importar a função necessária, como no exemplo abaixo: +Importe a função que precisar: ```javascript import { isValidCpf } from '@brazilian-utils/brazilian-utils'; @@ -63,7 +46,7 @@ import { isValidCpf } from '@brazilian-utils/brazilian-utils'; isValidCpf('1232454233345'); // false ``` -Você pode conferir a lista de utilitários [clicando aqui](pt-br/utilities.md). +A [referência de utilitários](pt-br/utilities.md) lista todas as funções, agrupadas por família, com opções e exemplos. Os [guias](pt-br/guides/document-field.md) mostram um campo de CPF em React, Angular, Vue e JavaScript puro. ## Assistentes de IA @@ -73,17 +56,17 @@ A documentação está indexada no Context7 como [`/brazilian-utils/javascript`] Valide um CNPJ com o Brazilian Utils. use library /brazilian-utils/javascript ``` -Para não repetir isso a cada prompt, coloque a regra no arquivo de instruções do agente (`CLAUDE.md`, regras do Cursor ou equivalente): "Para utilitários de documentos brasileiros, use a biblioteca /brazilian-utils/javascript do Context7". +Para não repetir isso a cada prompt, coloque uma regra no arquivo de instruções do agente (`CLAUDE.md`, regras do Cursor ou equivalente): "Para utilitários de documentos brasileiros, use a biblioteca /brazilian-utils/javascript do Context7". -Sem o Context7, aponte o assistente para o [llms.txt](https://brazilian-utils.com.br/llms.txt), que lista todos os utilitários com uma descrição de uma linha e o link para a seção de cada um, ou para o [llms-full.txt](https://brazilian-utils.com.br/llms-full.txt), a documentação completa em inglês em um único arquivo Markdown. +Sem o Context7, aponte o assistente para o [llms.txt](https://brazilian-utils.com.br/llms.txt), que lista todos os utilitários com uma descrição de uma linha, ou para o [llms-full.txt](https://brazilian-utils.com.br/llms-full.txt), a documentação completa em inglês em um único arquivo Markdown. ## Tamanho do bundle -O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário, não o resto da biblioteca. `isValidCpf`, por exemplo, adiciona cerca de 1,4 KB minificado (0,8 KB com gzip) ao seu bundle. Um bundler com suporte a tree-shaking (webpack, Rollup, esbuild, Vite, etc.) descarta todos os outros utilitários. +O pacote é tree-shakeable: importar um utilitário da raiz traz apenas o código daquele utilitário. `isValidCpf`, por exemplo, adiciona cerca de 1,4 KB minificado (0,8 KB com gzip) ao seu bundle. -Alguns utilitários são a exceção: cada um embute um dataset oficial e pesa muito mais que todos os outros utilitários somados. Estes são os tamanhos de um import isolado, minificado e com gzip: +Alguns utilitários embutem uma base de dados oficial e pesam muito mais que todos os outros somados: -| Utilitário | Dataset | Minificado | Gzip | +| Utilitário | Base de dados | Minificado | Gzip | | --- | --- | --- | --- | | `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 154,9 - 156,5 KB | 50,3 - 50,4 KB | | `getCities` | nomes dos 5571 municípios do IBGE | 154,2 KB | 49,8 KB | @@ -93,9 +76,7 @@ Alguns utilitários são a exceção: cada um embute um dataset oficial e pesa m | `isValidCfop` · `getCfop` | descrições das operações do CFOP | 68,9 KB | 6,9 KB | | `getBanks` · `getBankByCode` · `getBankByIspb` | participantes do STR do Banco Central (COMPE + ISPB) | 38,3 - 38,6 KB | 9,5 - 9,7 KB | -Importar qualquer um deles da raiz, mesmo ao lado de um único utilitário pequeno, traz todo esse dataset para o seu bundle principal, porque este pacote é publicado como um único módulo ESM: um `import()` dinâmico da raiz (`await import('@brazilian-utils/brazilian-utils')`) ainda resolve para esse mesmo arquivo único, então não há como separá-lo sozinho. Um bundler que faz code-splitting precisa de um módulo separado para separar. - -Esses módulos separados são os subpaths por utilitário. Carregue um utilitário pesado sob demanda, apenas onde você realmente precisar dos dados dele: +A raiz do pacote é um único módulo ESM, então o bundler não consegue separar uma dessas bases de dados dele: importar um utilitário pesado da raiz coloca a base inteira no seu bundle principal, e um `import()` dinâmico da raiz não ajuda. Para carregar sob demanda, importe do subpath próprio: ```javascript const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities'); @@ -111,6 +92,6 @@ const { getMunicipalityByCode } = await import( getMunicipalityByCode('3550308'); ``` -Todos os utilitários estão disponíveis dessa forma, como `@brazilian-utils/brazilian-utils/` (kebab-case, seguindo o nome da função: `isValidCpf` → `is-valid-cpf`), pelo mesmo motivo de lazy-loading/code-splitting. +Todo utilitário tem um subpath, `@brazilian-utils/brazilian-utils/` em kebab-case (`isValidCpf` vira `is-valid-cpf`). -Escolha um estilo por utilitário em cada aplicação: um bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` tanto da raiz quanto de `/get-cities` na mesma aplicação inclui a tabela de 154,2 KB de cidades duas vezes, uma em cada módulo. +Escolha um estilo por utilitário em cada aplicação. O bundler trata o import da raiz e o import do subpath como dois módulos independentes, então importar `getCities` dos dois inclui a tabela de municípios duas vezes. diff --git a/docs/pt-br/guides/address-form.md b/docs/pt-br/guides/address-form.md new file mode 100644 index 000000000..8c07048cd --- /dev/null +++ b/docs/pt-br/guides/address-form.md @@ -0,0 +1,78 @@ +--- +title: "Endereço pelo CEP" +description: "Um formulário que consulta o CEP e preenche rua, bairro, cidade e estado, com Brazilian Utils em React, Angular, Vue e JavaScript puro." +keywords: ["consulta de CEP", "endereço pelo CEP", "preencher endereço", "getAddressInfoByCep", "CEP React", "CEP Angular", "CEP Vue"] +--- + +Digite um CEP e o resto do endereço se preenche. Escolha o framework: cada exemplo roda o código logo abaixo dele, que pode ser copiado do jeito que está. + +O `getAddressInfoByCep` pergunta aos provedores de CEP e devolve rua, bairro, cidade e estado, ou lança quando ninguém tem aquele CEP. Ele só é chamado quando o `isValidCep` diz que o CEP está completo, então não sai uma requisição a cada tecla, e o que volta continua editável: a consulta preenche o formulário, não toma conta dele. O [guia do campo de documento](pt-br/guides/document-field.md) tem a máscara que mantém o cursor no lugar. + + +
+ +Um hook recebe o CEP e devolve o que se sabe sobre ele; o formulário desenha isso. O campo de CEP é o que o [guia do campo de documento](pt-br/guides/document-field.md) constrói: + +
+ +[address-form.tsx](../../snippets/address-form/react/address-form.tsx ':include :type=code tsx') + +
+ +
+ +[use-get-address-by-cep.ts](../../snippets/address-form/react/use-get-address-by-cep.ts ':include :type=code ts') + +
+ +
+ +
+ +Um `resource` recebe o CEP e devolve o que se sabe sobre ele, recarregando quando ele muda. O campo de CEP é o que o [guia do campo de documento](pt-br/guides/document-field.md) constrói: + +
+ +[address-form.ts](../../snippets/address-form/angular/address-form.ts ':include :type=code ts') + +
+ +
+ +[address-by-cep.ts](../../snippets/address-form/angular/address-by-cep.ts ':include :type=code ts') + +
+ +
+ +
+ +Um composable recebe o CEP e devolve o que se sabe sobre ele; o formulário desenha isso. O campo de CEP é o que o [guia do campo de documento](pt-br/guides/document-field.md) constrói: + +
+ +[address-form.vue](../../snippets/address-form/vue/address-form.vue ':include :type=code vue') + +
+ +
+ +[use-get-address-by-cep.ts](../../snippets/address-form/vue/use-get-address-by-cep.ts ':include :type=code ts') + +
+ +
+ +
+ +Sem build: salve como um arquivo `.html` e abra. Ele importa o pacote de um CDN e descarta a resposta de um CEP que já não é o do campo. + +
+ +[address-form.html](../../snippets/address-form/vanilla/address-form.html ':include :type=code html') + +
+ +
+ +A [referência de utilitários](pt-br/utilities.md) documenta o `getAddressInfoByCep`, os provedores e o que ele lança. diff --git a/docs/pt-br/guides/document-field.md b/docs/pt-br/guides/document-field.md new file mode 100644 index 000000000..e20282c21 --- /dev/null +++ b/docs/pt-br/guides/document-field.md @@ -0,0 +1,411 @@ +--- +title: "Campo de documento" +description: "Um campo que aplica máscara e valida CPF, CNPJ, CEP ou telefone enquanto você digita, com Brazilian Utils em React, Angular, Vue e JavaScript puro." +keywords: ["máscara de CPF", "máscara de CNPJ", "máscara de CEP", "máscara de telefone", "CPF React", "CPF Angular", "CPF Vue", "validar CPF"] +--- + +Um campo que formata enquanto você digita, dentro de um formulário que valida. Cada exemplo roda o código logo abaixo dele, que pode ser copiado do jeito que está. + +O campo só aplica a máscara e entrega ao formulário o valor sem ela, com `parse*`, então o formulário guarda `52998224725` e é isso que o envio manda. Validar é trabalho do formulário, o que também deixa uma mensagem de erro por campo em vez de duas. A máscara é a mesma em todos: um formatter aceita o que já foi digitado, e formatar o que vem antes do cursor diz para onde ele vai, então editar no meio funciona. + +
+ +O hook é dono do input: ele recebe o formatter e o valor que o formulário guarda, aplica a máscara no que é digitado e avisa pelo próprio `onChange`. O React nunca escreve o valor do input, que é o que desfaria a máscara. O campo aceita as props do próprio input, então o `field` do react-hook-form entra inteiro: + +
+ +
+ +[cpf-field.tsx](../../snippets/document-field/generated/cpf/react/cpf-field.tsx ':include :type=code tsx') + +
+ +
+ +[field.tsx](../../snippets/document-field/generated/cpf/react/field.tsx ':include :type=code tsx') + +
+ +
+ +[use-mask.ts](../../snippets/document-field/generated/cpf/react/use-mask.ts ':include :type=code ts') + +
+ +
+ +[cpf-form.tsx](../../snippets/document-field/generated/cpf/react/cpf-form.tsx ':include :type=code tsx') + +
+ +
+ +
+ +
+ +[cnpj-field.tsx](../../snippets/document-field/generated/cnpj/react/cnpj-field.tsx ':include :type=code tsx') + +
+ +
+ +[field.tsx](../../snippets/document-field/generated/cnpj/react/field.tsx ':include :type=code tsx') + +
+ +
+ +[use-mask.ts](../../snippets/document-field/generated/cnpj/react/use-mask.ts ':include :type=code ts') + +
+ +
+ +[cnpj-form.tsx](../../snippets/document-field/generated/cnpj/react/cnpj-form.tsx ':include :type=code tsx') + +
+ +
+ +
+ +
+ +[cep-field.tsx](../../snippets/document-field/generated/cep/react/cep-field.tsx ':include :type=code tsx') + +
+ +
+ +[field.tsx](../../snippets/document-field/generated/cep/react/field.tsx ':include :type=code tsx') + +
+ +
+ +[use-mask.ts](../../snippets/document-field/generated/cep/react/use-mask.ts ':include :type=code ts') + +
+ +
+ +[cep-form.tsx](../../snippets/document-field/generated/cep/react/cep-form.tsx ':include :type=code tsx') + +
+ +
+ +
+ +
+ +[phone-field.tsx](../../snippets/document-field/generated/phone/react/phone-field.tsx ':include :type=code tsx') + +
+ +
+ +[field.tsx](../../snippets/document-field/generated/phone/react/field.tsx ':include :type=code tsx') + +
+ +
+ +[use-mask.ts](../../snippets/document-field/generated/phone/react/use-mask.ts ':include :type=code ts') + +
+ +
+ +[phone-form.tsx](../../snippets/document-field/generated/phone/react/phone-form.tsx ':include :type=code tsx') + +
+ +
+ +
+ +
+ +Um `ControlValueAccessor`, então aceita `formControlName` (ou `formControl`, ou `ngModel`) como um input nativo, fica touched no blur e desabilita junto com o seu controle. O validador é um `ValidatorFn` no controle: + +
+ +
+ +[cpf-field.ts](../../snippets/document-field/generated/cpf/angular/cpf-field.ts ':include :type=code ts') + +
+ +
+ +[field.ts](../../snippets/document-field/generated/cpf/angular/field.ts ':include :type=code ts') + +
+ +
+ +[mask.directive.ts](../../snippets/document-field/generated/cpf/angular/mask.directive.ts ':include :type=code ts') + +
+ +
+ +[cpf-form.ts](../../snippets/document-field/generated/cpf/angular/cpf-form.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cnpj-field.ts](../../snippets/document-field/generated/cnpj/angular/cnpj-field.ts ':include :type=code ts') + +
+ +
+ +[field.ts](../../snippets/document-field/generated/cnpj/angular/field.ts ':include :type=code ts') + +
+ +
+ +[mask.directive.ts](../../snippets/document-field/generated/cnpj/angular/mask.directive.ts ':include :type=code ts') + +
+ +
+ +[cnpj-form.ts](../../snippets/document-field/generated/cnpj/angular/cnpj-form.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cep-field.ts](../../snippets/document-field/generated/cep/angular/cep-field.ts ':include :type=code ts') + +
+ +
+ +[field.ts](../../snippets/document-field/generated/cep/angular/field.ts ':include :type=code ts') + +
+ +
+ +[mask.directive.ts](../../snippets/document-field/generated/cep/angular/mask.directive.ts ':include :type=code ts') + +
+ +
+ +[cep-form.ts](../../snippets/document-field/generated/cep/angular/cep-form.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[phone-field.ts](../../snippets/document-field/generated/phone/angular/phone-field.ts ':include :type=code ts') + +
+ +
+ +[field.ts](../../snippets/document-field/generated/phone/angular/field.ts ':include :type=code ts') + +
+ +
+ +[mask.directive.ts](../../snippets/document-field/generated/phone/angular/mask.directive.ts ':include :type=code ts') + +
+ +
+ +[phone-form.ts](../../snippets/document-field/generated/phone/angular/phone-form.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +O valor é o `v-model` do componente (`defineModel`), que é onde o `defineField` do VeeValidate se liga, e a regra fica no schema do formulário: + +
+ +
+ +[cpf-field.vue](../../snippets/document-field/generated/cpf/vue/cpf-field.vue ':include :type=code vue') + +
+ +
+ +[field.vue](../../snippets/document-field/generated/cpf/vue/field.vue ':include :type=code vue') + +
+ +
+ +[mask.ts](../../snippets/document-field/generated/cpf/vue/mask.ts ':include :type=code ts') + +
+ +
+ +[cpf-form.vue](../../snippets/document-field/generated/cpf/vue/cpf-form.vue ':include :type=code vue') + +
+ +
+ +
+ +
+ +[cnpj-field.vue](../../snippets/document-field/generated/cnpj/vue/cnpj-field.vue ':include :type=code vue') + +
+ +
+ +[field.vue](../../snippets/document-field/generated/cnpj/vue/field.vue ':include :type=code vue') + +
+ +
+ +[mask.ts](../../snippets/document-field/generated/cnpj/vue/mask.ts ':include :type=code ts') + +
+ +
+ +[cnpj-form.vue](../../snippets/document-field/generated/cnpj/vue/cnpj-form.vue ':include :type=code vue') + +
+ +
+ +
+ +
+ +[cep-field.vue](../../snippets/document-field/generated/cep/vue/cep-field.vue ':include :type=code vue') + +
+ +
+ +[field.vue](../../snippets/document-field/generated/cep/vue/field.vue ':include :type=code vue') + +
+ +
+ +[mask.ts](../../snippets/document-field/generated/cep/vue/mask.ts ':include :type=code ts') + +
+ +
+ +[cep-form.vue](../../snippets/document-field/generated/cep/vue/cep-form.vue ':include :type=code vue') + +
+ +
+ +
+ +
+ +[phone-field.vue](../../snippets/document-field/generated/phone/vue/phone-field.vue ':include :type=code vue') + +
+ +
+ +[field.vue](../../snippets/document-field/generated/phone/vue/field.vue ':include :type=code vue') + +
+ +
+ +[mask.ts](../../snippets/document-field/generated/phone/vue/mask.ts ':include :type=code ts') + +
+ +
+ +[phone-form.vue](../../snippets/document-field/generated/phone/vue/phone-form.vue ':include :type=code vue') + +
+ +
+ +
+ +
+ +Sem build: salve como um arquivo `.html` e abra. Ele importa o pacote de um CDN, aplica a máscara no `input` e valida no `submit`, levando o foco ao campo que recusou. + +
+ +
+ +[cpf-field.html](../../snippets/document-field/generated/cpf/vanilla/cpf-field.html ':include :type=code html') + +
+ +
+ +
+ +
+ +[cnpj-field.html](../../snippets/document-field/generated/cnpj/vanilla/cnpj-field.html ':include :type=code html') + +
+ +
+ +
+ +
+ +[cep-field.html](../../snippets/document-field/generated/cep/vanilla/cep-field.html ':include :type=code html') + +
+ +
+ +
+ +
+ +[phone-field.html](../../snippets/document-field/generated/phone/vanilla/phone-field.html ':include :type=code html') + +
+ +
+ +
+ +A [referência de utilitários](pt-br/utilities.md) lista todas as funções. diff --git a/docs/pt-br/guides/schema.md b/docs/pt-br/guides/schema.md new file mode 100644 index 000000000..b282731eb --- /dev/null +++ b/docs/pt-br/guides/schema.md @@ -0,0 +1,196 @@ +--- +title: "Bibliotecas de schema" +description: "Os validadores do Brazilian Utils dentro de um schema do Zod, do Valibot ou do ArkType, ou como um Standard Schema próprio." +keywords: ["zod CPF", "valibot CPF", "arktype CPF", "Standard Schema", "toStandardSchema", "validar CNPJ schema"] +--- + +Um validador entra direto num schema: escolha o documento e a biblioteca. Aqui não roda nada, um schema é o mesmo código em qualquer lugar. + +Cada um deles monta o documento sozinho primeiro, para ser reaproveitado onde um schema precisar dele, e depois compõe num formulário. Todos falam [Standard Schema](https://standardschema.dev), que é como um schema chega a uma biblioteca de formulário, e o `toStandardSchema` dá essa mesma interface a um validador sem nenhuma biblioteca de schema. + + +
+ +O `refine` recebe o validador como ele é: + +
+ +
+ +[cpf-zod.ts](../../snippets/document-field/generated/cpf/schema/cpf-zod.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cnpj-zod.ts](../../snippets/document-field/generated/cnpj/schema/cnpj-zod.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cep-zod.ts](../../snippets/document-field/generated/cep/schema/cep-zod.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[phone-zod.ts](../../snippets/document-field/generated/phone/schema/phone-zod.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +O `check` recebe o validador dentro de um pipe: + +
+ +
+ +[cpf-valibot.ts](../../snippets/document-field/generated/cpf/schema/cpf-valibot.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cnpj-valibot.ts](../../snippets/document-field/generated/cnpj/schema/cnpj-valibot.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cep-valibot.ts](../../snippets/document-field/generated/cep/schema/cep-valibot.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[phone-valibot.ts](../../snippets/document-field/generated/phone/schema/phone-valibot.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +O `narrow` recebe ele, e diz o que o valor tem que ser quando recusa: + +
+ +
+ +[cpf-arktype.ts](../../snippets/document-field/generated/cpf/schema/cpf-arktype.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cnpj-arktype.ts](../../snippets/document-field/generated/cnpj/schema/cnpj-arktype.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cep-arktype.ts](../../snippets/document-field/generated/cep/schema/cep-arktype.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[phone-arktype.ts](../../snippets/document-field/generated/phone/schema/phone-arktype.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +Sem biblioteca de schema nenhuma: o `toStandardSchema` dá ao validador a interface que toda biblioteca de formulário fala. + +
+ +
+ +[cpf-standard.ts](../../snippets/document-field/generated/cpf/schema/cpf-standard.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cnpj-standard.ts](../../snippets/document-field/generated/cnpj/schema/cnpj-standard.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[cep-standard.ts](../../snippets/document-field/generated/cep/schema/cep-standard.ts ':include :type=code ts') + +
+ +
+ +
+ +
+ +[phone-standard.ts](../../snippets/document-field/generated/phone/schema/phone-standard.ts ':include :type=code ts') + +
+ +
+ +
+ +A [referência de utilitários](pt-br/utilities.md#tostandardschema) documenta o `toStandardSchema` e os tipos da especificação. diff --git a/docs/pt-br/guides/state-city.md b/docs/pt-br/guides/state-city.md new file mode 100644 index 000000000..69354f627 --- /dev/null +++ b/docs/pt-br/guides/state-city.md @@ -0,0 +1,96 @@ +--- +title: "Estado e cidade" +description: "Escolha um estado e as cidades dele carregam sob demanda, com Brazilian Utils em React, Angular, Vue e JavaScript puro." +keywords: ["select de estado e cidade", "cidades do IBGE", "getCities", "import lazy", "code splitting", "municípios de um estado"] +--- + +Escolha um estado e as cidades dele preenchem o segundo select. Escolha o framework: cada exemplo roda o código logo abaixo dele, que pode ser copiado do jeito que está. + +O ponto aqui é quando cada tabela é carregada, e a resposta é: quando alguém abre o select que a mostra. Nem com a página, nem ao escolher um estado — num formulário em que a cidade vem preenchida de outro lugar, ou é deixada em branco, os 154 KB de cidades nunca são buscados. Os estados são 27 linhas, 2,5 KB; as cidades são 5.571, 154 KB. Cada utilitário é um subpath, então `await import("@brazilian-utils/brazilian-utils/get-cities")` busca essa tabela e nada mais. O bundler transforma isso num chunk separado, e o browser busca uma vez e guarda, então só a primeira abertura espera. + + +
+ +Um hook por lista, cada um buscando sua tabela quando o `onFocus` avisa que o select foi aberto. As cidades são de um estado, então o que foi carregado só vale como as cidades da tela enquanto aquele estado for o escolhido: + +
+ +[state-city.tsx](../../snippets/state-city/react/state-city.tsx ':include :type=code tsx') + +
+ +
+ +[use-states.ts](../../snippets/state-city/react/use-states.ts ':include :type=code ts') + +
+ +
+ +[use-cities-of-state.ts](../../snippets/state-city/react/use-cities-of-state.ts ':include :type=code ts') + +
+ +
+ +
+ +Um `resource` sem nada para perguntar espera, que é onde os dois começam; abrir o select dá a ele o que perguntar. As cidades são do estado para o qual foram pedidas, então escolher outro estado devolve esse resource à espera: + +
+ +[state-city.ts](../../snippets/state-city/angular/state-city.ts ':include :type=code ts') + +
+ +
+ +[states.ts](../../snippets/state-city/angular/states.ts ':include :type=code ts') + +
+ +
+ +[cities-of-state.ts](../../snippets/state-city/angular/cities-of-state.ts ':include :type=code ts') + +
+ +
+ +
+ +Um composable por lista, cada um buscando sua tabela quando o `@focus` avisa que o select foi aberto. As cidades são de um estado, então o que foi carregado só vale como as cidades da tela enquanto aquele estado for o escolhido: + +
+ +[state-city.vue](../../snippets/state-city/vue/state-city.vue ':include :type=code vue') + +
+ +
+ +[use-states.ts](../../snippets/state-city/vue/use-states.ts ':include :type=code ts') + +
+ +
+ +[use-cities-of-state.ts](../../snippets/state-city/vue/use-cities-of-state.ts ':include :type=code ts') + +
+ +
+ +
+ +Sem build: salve como um arquivo `.html` e abra. Cada subpath é um módulo próprio no CDN, e o `import()` dentro de um listener de `focus` busca esse módulo na primeira vez que o select é aberto. + +
+ +[state-city.html](../../snippets/state-city/vanilla/state-city.html ':include :type=code html') + +
+ +
+ +A [introdução](pt-br/getting-started.md#bundle-size) lista todos os utilitários que embutem uma tabela e valem um subpath próprio. diff --git a/docs/pt-br/index.html b/docs/pt-br/index.html deleted file mode 100644 index dc71c6c4c..000000000 --- a/docs/pt-br/index.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - Introdução · Brazilian Utils - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/pt-br/migration-v1-to-v2.html b/docs/pt-br/migration-v1-to-v2.html deleted file mode 100644 index 4871841de..000000000 --- a/docs/pt-br/migration-v1-to-v2.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - Guia de Migração: v1 para v2 · Brazilian Utils - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/pt-br/migration-v1-to-v2.md b/docs/pt-br/migration-v1-to-v2.md index 21edcf90d..28c89f703 100644 --- a/docs/pt-br/migration-v1-to-v2.md +++ b/docs/pt-br/migration-v1-to-v2.md @@ -1,153 +1,38 @@ --- -title: "Guia de Migração: v1 para v2" -description: "Como migrar um projeto do Brazilian Utils v1.x para a v2: os exports renomeados, os aliases deprecados que ainda funcionam e um checklist para seguir." -keywords: ["migração", "v1", "v2", "deprecado", "exports renomeados", "atualização"] +title: "Guia de migração: v1 para v2" +description: "Como migrar um projeto do Brazilian Utils v1.x para a v2: os exports renomeados, os aliases descontinuados que ainda funcionam e um checklist para seguir." +keywords: ["migração", "v1", "v2", "descontinuado", "exports renomeados", "atualização"] --- -Este guia irá ajudá-lo a migrar do Brazilian Utils v1.x para v2.0.0. +Este guia mostra como migrar um projeto do Brazilian Utils v1.x para a v2. -## TL;DR - Migração Rápida +## Resumo -**Boas notícias!** A v2.x mantém compatibilidade para a maioria das mudanças quebradoras: +A v2 renomeia todas as funções para camelCase (`formatCPF` agora é `formatCpf`), mas mantém os nomes da v1 como aliases descontinuados, então a maioria dos projetos atualiza sem mudar código. O TypeScript e o editor marcam os nomes antigos. Os aliases são removidos na v3.0.0. -**Você pode atualizar para v2.x sem alterar seu código** - nomes antigos de funções como `formatCPF`, `isValidCNPJ`, etc. ainda funcionam -**Você receberá avisos de deprecação** - encorajando você a migrar para os novos nomes -**Nomes antigos serão removidos na v3.0.0** - então migre gradualmente +Quatro helpers da v1 eram internos e não têm alias. Substitua-os antes de atualizar: -**Porém**, você deve remover o uso dessas funções helper antes de atualizar: -- `onlyNumbers` → use `string.replace(/\D/g, '')` -- `isLastChar` → use `index === input.length - 1` -- `generateChecksum` → agora apenas interno -- `generateRandomNumber` → agora apenas interno - -## Melhorias na v2.0.0 - -A versão 2.0.0 traz melhorias significativas em arquitetura, ferramentas e experiência do desenvolvedor: - -### Melhor Tree Shaking - -A biblioteca agora usa exports de módulos ES modernos com o campo `exports` adequado no `package.json`, permitindo melhor tree shaking em bundlers modernos. Você pode importar apenas o que precisa: - -```javascript -// Apenas as funções que você importar serão incluídas no seu bundle -import { isValidCpf, formatCpf } from '@brazilian-utils/brazilian-utils'; -``` - -Desde a 2.4.0 cada utilitário também é um subpath próprio, então um bundler que não faz tree -shaking (ou um `require` simples) ainda carrega um único módulo, e os poucos pesados (`getCities`, -`getMunicipalities`, `isValidNcm`, `isValidCbo`, `isValidCnae`, `getBanks`) podem ser carregados sob -demanda: - -```javascript -import { isValidCpf } from '@brazilian-utils/brazilian-utils/is-valid-cpf'; // ~1,4 KB, 0,8 KB com gzip -const { getCities } = await import('@brazilian-utils/brazilian-utils/get-cities'); // só quando precisar -``` - -Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle) para o tamanho de cada entrada. - -### Estrutura Mais Simples - -O código foi reorganizado para melhor manutenibilidade: -- **v1**: Estrutura complexa com diretórios separados `utilities/` e `helpers/` -- **v2**: Estrutura plana com utilitários internos no diretório `_internals/` -- Cada utilitário é autocontido em seu próprio diretório -- Caminhos de importação mais limpos e melhor organização do código - -### Ferramentas Modernas - -Atualizado para ferramentas modernas e mais rápidas: -- **Build**: Migrado de `tsdx` para uma stack com **Vite+** para builds e scripts mais rápidos -- **Testes**: Migrado de `jest` para **Vitest** (mais rápido, compatível com Jest, nativo ESM) -- **Linting/Formatação**: Migrado de `prettier` + `eslint` para a toolchain do Vite+ (`vp fmt` e `vp check`, sobre o Oxc) -- **TypeScript**: Configuração moderna otimizada para bundlers - -### Testes em Browsers - -Agora inclui suporte para testes cross-browser: -- Testes rodam em browsers reais (Chrome, Firefox, Safari, Edge) -- Garante compatibilidade entre diferentes ambientes de browser -- Melhor confiança na funcionalidade cross-platform - -Execute testes em browsers com: -```bash -npm run test:chrome-browser -npm run test:firefox-browser -npm run test:safari-browser -npm run test:edge-browser -``` - -### Menos Dependências - -Redução de dependências de desenvolvimento mantendo zero dependências de runtime: -- **v1**: Múltiplas ferramentas (tsdx, jest, prettier, eslint, husky, lint-staged, etc.) -- **v2**: Uma toolchain (Vite+ para build, lint, formatação e testes, com suporte de browser do Vitest via webdriverio) mais os gates de qualidade listados no CONTRIBUTING.md (Stryker, knip, jscpd, API Extractor, commitlint) -- Manutenção mais simples e pipelines CI/CD mais rápidos -- Zero dependências de runtime (mantido) - -### Novas Funções e Recursos - -Adicionadas novas utilitários úteis: -- `getHolidays` - Obtém feriados brasileiros (nacionais e estaduais) -- `getBoletoInfo` - Extrai informações de boleto (valor, vencimento, código do banco) -- `formatPhone` - Formata números de telefone com padrões brasileiros -- `formatBoleto` - Formata números de boleto -- `generateBoleto` - Gera números de boleto válidos aleatórios -- `formatPis` - Formata números de PIS -- `isValidRenavam` - Valida RENAVAM (número de registro de veículos) -- `isValidBankAccount` - Valida contas bancárias brasileiras com algoritmos específicos para principais bancos - -A 2.4.0 acrescentou muitas outras famílias a essas, todas listadas na [documentação de utilitários](pt-br/utilities.md): -Pix (`isValidPixKey`, `generatePixPayload`, `getPixPayloadInfo`), chave de NF-e/DF-e, CNS, certidão, -CEI/CNO/CAEPF, IBAN, número de cartão, VIN, registro profissional, consulta de bancos (`getBanks`, -`getBankByCode`, `getBankByIspb`), códigos CBO/CNAE/NCM/CFOP/CST/CSOSN, dias úteis (`isBusinessDay`, -`addBusinessDays`, `differenceInBusinessDays`), categorias de natureza jurídica, municípios offline -(`getMunicipalities`, `getMunicipalityByCode`), DDD e fuso horário, número por extenso e um -`capitalize` que conhece as designações societárias brasileiras. - -#### Suporte a CNPJ Alfanumérico (Versão 2) - -A v2.0.0 adiciona suporte ao novo formato alfanumérico de CNPJ introduzido pela Receita Federal. Tanto `isValidCnpj` quanto `generateCnpj` agora suportam CNPJs versão 2 (alfanuméricos): - -```javascript -import { isValidCnpj, generateCnpj } from '@brazilian-utils/brazilian-utils'; - -// Gerar CNPJ alfanumérico -const alphaCnpj = generateCnpj(2); // ex: "Q0SLFMBD7VX439" - -// Validar CNPJ alfanumérico (requer opção de versão) -isValidCnpj("Q0.SLF.MBD/7VX4-39", { version: 2 }); // true -isValidCnpj("Q0SLFMBD7VX439", { version: 2 }); // true - -// Versão 1 (numérico) é o padrão -isValidCnpj("12.345.678/0001-95"); // true (valida apenas numérico) -isValidCnpj("12.345.678/0001-95", { version: 1 }); // true (explícito) -``` - -**Importante**: Por padrão, `isValidCnpj()` valida apenas CNPJs numéricos (versão 1). Para validar CNPJs alfanuméricos, você deve passar explicitamente `{ version: 2 }`. - -### Melhor Suporte TypeScript - -- Configuração TypeScript moderna otimizada para bundlers -- Melhor inferência de tipos e exports -- Experiência do desenvolvedor melhorada com melhor autocomplete - -## Mudanças Quebradoras - -### Nomes de Funções Alterados (PascalCase → camelCase) - -Todos os nomes de funções foram alterados de PascalCase para camelCase para seguir as convenções de nomenclatura JavaScript. - -**Importante: Compatibilidade com Versões Anteriores** +| v1 | Substituto | +|---|---| +| `onlyNumbers(value)` | `value.replace(/\D/g, '')` | +| `isLastChar(index, input)` | `index === input.length - 1` | +| `generateChecksum` | Não é mais exportada. Escreva o cálculo do dígito verificador que precisar. | +| `generateRandomNumber(length)` | Um laço próprio sobre `Math.floor(Math.random() * 10)`. | -Para facilitar a migração, **a v2.x ainda exporta os nomes antigos em PascalCase como aliases deprecated**. Isso significa: +## O que mudou -- Seu código existente usando `formatCPF`, `isValidCNPJ`, etc. continuará funcionando na v2.x -- Você receberá avisos de deprecação no seu IDE/TypeScript -- Os nomes antigos serão **removidos na v3.0.0** +- **Os nomes são camelCase.** Veja [Funções renomeadas](#funções-renomeadas). +- **O tree-shaking funciona até a função**, e cada utilitário também é um subpath próprio (`@brazilian-utils/brazilian-utils/is-valid-cpf`), então os pesados podem ser carregados sob demanda. Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle). +- **CNPJ alfanumérico.** `isValidCnpj` e `generateCnpj` aceitam o novo formato alfanumérico com `{ version: 2 }`. O numérico (versão 1) continua sendo o padrão. Veja [`generateCnpj` e a versão](#generatecnpj-e-a-versão). +- **`getAddressInfoByCep`** aceita a opção `providers`, completa com zeros um CEP numérico e lança erros tipados: `GetAddressInfoByCepValidationError`, `GetAddressInfoByCepNotFoundError` e `GetAddressInfoByCepServiceError`. Chamadas sem opções funcionam como na v1. +- **`getCities`** retorna a lista em ordem alfabética. Desde a 2.4.0 está descontinuada: `getMunicipalities('SP')` retorna os mesmos municípios com seus códigos IBGE, e `getMunicipalityByCode('3550308')` busca um deles offline. +- **`isValidIe`** recebe um único objeto desde a 2.4.0, `isValidIe({ value, stateCode })`. A forma posicional está descontinuada. +- **Muitos utilitários novos** desde a v2: feriados e dias úteis, Pix, chave de NF-e, leitura de boleto, formatação de telefone, contas bancárias e consulta de bancos, códigos de classificação (CBO, CNAE, NCM, CFOP), municípios offline, números por extenso e mais. Todos estão na [referência de utilitários](pt-br/utilities.md). +- **As ferramentas** mudaram para Vite+ e Vitest, com testes em navegador no CI. Isso só importa para quem contribui; veja o [CONTRIBUTING.md](https://github.com/brazilian-utils/javascript/blob/main/CONTRIBUTING.md). -**Recomendação:** Embora você possa atualizar para v2.x sem alterar seu código imediatamente, recomendamos migrar para os novos nomes em camelCase o quanto antes para se preparar para a v3.0.0. +## Funções renomeadas -#### Funções de Validação +Todos os outros exports mantêm o nome da v1. | v1 | v2 | |---|---| @@ -155,63 +40,15 @@ Para facilitar a migração, **a v2.x ainda exporta os nomes antigos em PascalCa | `isValidCNPJ` | `isValidCnpj` | | `isValidCEP` | `isValidCep` | | `isValidPIS` | `isValidPis` | -| `isValidIE` | `isValidIe` (desde a 2.4.0 prefira a forma objeto, `isValidIe({ value, stateCode })`; a forma posicional está descontinuada) | -| `isValidProcessoJuridico` | `isValidProcessoJuridico` (inalterado) | -| `isValidBoleto` | `isValidBoleto` (inalterado) | -| `isValidEmail` | `isValidEmail` (inalterado) | -| `isValidPhone` | `isValidPhone` (inalterado) | -| `isValidMobilePhone` | `isValidMobilePhone` (inalterado) | -| `isValidLandlinePhone` | `isValidLandlinePhone` (inalterado) | -| `isValidLicensePlate` | `isValidLicensePlate` (inalterado) | -| `isValidRenavam` | `isValidRenavam` (novo) | - -#### Funções de Formatação - -| v1 | v2 | -|---|---| +| `isValidIE` | `isValidIe` | | `formatCPF` | `formatCpf` | | `formatCNPJ` | `formatCnpj` | | `formatCEP` | `formatCep` | -| `formatProcessoJuridico` | `formatProcessoJuridico` (inalterado) | -| `formatBoleto` | `formatBoleto` (inalterado) | -| `formatCurrency` | `formatCurrency` (inalterado) | -| `formatPhone` | `formatPhone` (novo) | - -#### Funções de Geração - -| v1 | v2 | -|---|---| | `generateCPF` | `generateCpf` | | `generateCNPJ` | `generateCnpj` | -| `generateBoleto` | `generateBoleto` (inalterado) | - -**Nota sobre o comportamento do `generateCnpj`:** -Na v2.x, `generateCnpj()` sem argumentos retorna por padrão a versão 1 (CNPJ numérico). Na v3.0.0, este comportamento mudará para selecionar aleatoriamente entre versão 1 (numérico) e versão 2 (alfanumérico) para melhor aleatoriedade. Se você precisa de uma versão específica, sempre passe o parâmetro de versão explicitamente: +Antes (v1): -```javascript -// Recomendado: Sempre especifique a versão -generateCnpj(1); // Sempre gera CNPJ numérico -generateCnpj(2); // Sempre gera CNPJ alfanumérico - -// Não recomendado: Depender do comportamento padrão -generateCnpj(); // Atualmente gera numérico (v1), mas será aleatório na v3.0.0 -``` - -#### Outras Funções - -| v1 | v2 | -|---|---| -| `parseCurrency` | `parseCurrency` (inalterado) | -| `capitalize` | `capitalize` (inalterado) | -| `getStates` | `getStates` (inalterado) | -| `getCities` | `getCities` (inalterado; descontinuado na 2.4.0 em favor de `getMunicipalities`) | -| `getMunicipality` | `getMunicipality` (descontinuado na 2.4.0 em favor de `getMunicipalityByCode`, que é síncrono e offline) | -| `getAddressInfoByCep` | `getAddressInfoByCep` (API alterada, veja abaixo) | - -### Exemplo de Migração - -**Antes (v1):** ```javascript import { isValidCPF, formatCPF, generateCNPJ } from '@brazilian-utils/brazilian-utils'; @@ -220,7 +57,8 @@ const formatted = formatCPF('12345678909'); const cnpj = generateCNPJ(); ``` -**Depois (v2):** +Depois (v2): + ```javascript import { isValidCpf, formatCpf, generateCnpj } from '@brazilian-utils/brazilian-utils'; @@ -229,176 +67,25 @@ const formatted = formatCpf('12345678909'); const cnpj = generateCnpj(); ``` -### Funções Helper Removidas - -As seguintes funções helper não são mais exportadas na API pública. Estas eram utilitários internos que não deveriam ter sido expostos. - -**Nota:** Diferentemente das funções renomeadas acima, esses helpers **NÃO** possuem aliases de compatibilidade. Você deve migrar para longe deles antes de atualizar para a v2.x. +### `generateCnpj` e a versão -#### `onlyNumbers` -Esta função foi removida da API pública. Agora é um utilitário interno chamado `sanitizeToDigits`. +`generateCnpj()` sem argumentos gera um CNPJ numérico na v2.x. Na v3.0.0 vai sortear entre numérico e alfanumérico, então passe a versão quando precisar de uma específica: -**Migração:** ```javascript -// v1 - Não use mais isso -import { onlyNumbers } from '@brazilian-utils/brazilian-utils'; -const digits = onlyNumbers('123-456'); - -// v2 - Use uma substituição simples -const digits = '123-456'.replace(/\D/g, ''); +generateCnpj(1); // sempre numérico +generateCnpj(2); // sempre alfanumérico, ex.: "Q0SLFMBD7VX439" +generateCnpj(); // numérico hoje, aleatório na v3.0.0 ``` -#### `isLastChar` -Esta função foi removida. Use uma comparação inline simples. +`isValidCnpj` valida CNPJs numéricos por padrão. Para validar alfanuméricos, passe `{ version: 2 }`: -**Migração:** ```javascript -// v1 - Não use mais isso -import { isLastChar } from '@brazilian-utils/brazilian-utils'; -if (isLastChar(index, input)) { /* ... */ } - -// v2 - Use comparação inline -if (index === input.length - 1) { /* ... */ } +isValidCnpj('12.345.678/0001-95'); // true +isValidCnpj('Q0.SLF.MBD/7VX4-39', { version: 2 }); // true +isValidCnpj('Q0.SLF.MBD/7VX4-39'); // false (só numérico sem a opção) ``` -#### `generateChecksum` -Esta função agora é interna e não é mais exportada na API pública. O pacote não exporta internals: `dist/_internals` não é publicado e não existe subpath para ele, então não há forma suportada de importar essa função na v2. Calcule o dígito verificador que você precisa no seu próprio código. - -**Migração:** -```javascript -// v1 - Não use mais isso -import { generateChecksum } from '@brazilian-utils/brazilian-utils'; -``` - -#### `generateRandomNumber` -Esta função agora é interna e não é mais exportada na API pública. - -**Migração:** -```javascript -// v1 - Não use mais isso -import { generateRandomNumber } from '@brazilian-utils/brazilian-utils'; - -// v2 - Use sua própria implementação -function generateRandomNumber(length) { - let result = ''; - for (let i = 0; i < length; i++) { - result += Math.floor(Math.random() * 10).toString(); - } - return result; -} -``` - -## Novas Funções - -As seguintes funções são novas na v2.0.0: - -### `getHolidays` - -Obtém feriados brasileiros para um determinado ano. Suporta feriados nacionais e estaduais. - -```javascript -import { getHolidays } from '@brazilian-utils/brazilian-utils'; - -// Obtém todos os feriados nacionais -const holidays = getHolidays(2024); - -// Obtém feriados para um estado específico -const spHolidays = getHolidays({ year: 2024, stateCode: 'SP' }); -``` - -### `getBoletoInfo` - -Extrai informações de um boleto (valor, data de vencimento, código do banco). - -```javascript -import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; - -const info = getBoletoInfo('00190000090114971860168524522114675860000102656'); -// { amount: 102656, expirationDate: Date, bankCode: '001' } -``` - -### `formatPhone` - -Formata números de telefone de acordo com padrões brasileiros. - -```javascript -import { formatPhone } from '@brazilian-utils/brazilian-utils'; - -formatPhone('11900000000'); // 11900-0000 (CUIDADO: a máscara padrão "sn" trunca um número com DDD) -formatPhone('11900000000', { mask: 'nanp' }); // (11) 90000-0000 -formatPhone('11900000000', { mask: 'auto' }); // (11) 90000-0000 -``` - -### `isValidRenavam` - -Valida RENAVAM (Registro Nacional de Veículos Automotores). Suporta tanto o formato antigo (9 dígitos) quanto o novo formato (11 dígitos). - -```javascript -import { isValidRenavam } from '@brazilian-utils/brazilian-utils'; - -isValidRenavam('639884962'); // true (9 dígitos, formato antigo) -isValidRenavam('00639884962'); // true (11 dígitos, formato novo) -isValidRenavam('12345678901'); // false (checksum inválido) -``` - -### `isValidBankAccount` - -Valida contas bancárias brasileiras. Suporta algoritmos de validação específicos para os principais bancos (Banco do Brasil, Itaú, Bradesco, Santander, Caixa Econômica Federal) e validação genérica mod10/mod11 para outros bancos. - -```javascript -import { isValidBankAccount } from '@brazilian-utils/brazilian-utils'; - -// Banco do Brasil -isValidBankAccount({ - bankCode: '001', - agency: '1584', - account: '00210169', - digit: '6' -}); // true - -// Itaú -isValidBankAccount({ - bankCode: '341', - agency: '2545', - account: '02366', - digit: '1' -}); // true - -// Outros bancos usam validação genérica -isValidBankAccount({ - bankCode: '246', - agency: '1234', - account: '123456', - digit: '6' -}); // true (o dígito corresponde ao mod10) -``` - -## Mudanças na API - -### `getAddressInfoByCep` - -A função `getAddressInfoByCep` agora suporta opções adicionais e melhor tratamento de erros. - -**Antes (v1):** -```javascript -const address = await getAddressInfoByCep('01310100'); -``` - -**Depois (v2):** -```javascript -// Ainda funciona da mesma forma -const address = await getAddressInfoByCep('01310100'); - -// Mas agora suporta opções -const address = await getAddressInfoByCep('01310-100', { - providers: ['viacep', 'brasilapi'] -}); - -// Também aceita números (será preenchido automaticamente com zeros à esquerda) -const address = await getAddressInfoByCep(1310100); -``` - -A função agora exporta classes de erro para melhor tratamento de erros: +### Erros de `getAddressInfoByCep` ```javascript import { @@ -412,58 +99,28 @@ try { const address = await getAddressInfoByCep('01310100'); } catch (error) { if (error instanceof GetAddressInfoByCepValidationError) { - // Tratar erro de validação + // CEP inválido } else if (error instanceof GetAddressInfoByCepNotFoundError) { - // Tratar erro de não encontrado + // nenhum endereço para este CEP } else if (error instanceof GetAddressInfoByCepServiceError) { - // Tratar erro de serviço + // os provedores falharam } } ``` -### `getCities` - -A função `getCities` agora retorna resultados ordenados alfabeticamente. - -**Antes (v1):** -```javascript -getCities(); // Retornava array não ordenado -getCities('SP'); // Retornava array não ordenado -``` - -**Depois (v2):** -```javascript -getCities(); // Retorna ordenado alfabeticamente -getCities('SP'); // Retorna ordenado alfabeticamente -``` - -**Desde a 2.4.0:** `getCities` está descontinuado. `getMunicipalities('SP')` retorna os mesmos municípios -com o código do IBGE (`{ code, name, stateCode }`), e `getMunicipalityByCode('3550308')` busca um deles -sem chamada de rede. - -## Checklist de Migração - -### Obrigatório (antes de atualizar para v2.x) -- [ ] Remover uso de funções helper (`onlyNumbers`, `isLastChar`, `generateChecksum`, `generateRandomNumber`) +## Checklist -### Opcional (recomendado antes da v3.0.0) -- [ ] Atualizar todas as importações para usar nomes de funções em camelCase -- [ ] Substituir todas as chamadas de funções com nomes em camelCase -- [ ] Trocar `getCities` por `getMunicipalities` e `getMunicipality` por `getMunicipalityByCode` (descontinuados na 2.4.0) -- [ ] Chamar `isValidIe({ value, stateCode })` em vez de `isValidIe(stateCode, ie)` (descontinuado na 2.4.0) -- [ ] Importar os tipos `*Params` em vez dos aliases `*Options` mantidos para as funções de um único argumento objeto (descontinuados na 2.4.0) -- [ ] Tirar `'widenet'` dos `providers` do `getAddressInfoByCep` (o serviço acabou; descontinuado na 2.4.0) +Obrigatório antes de atualizar: -### Revisar se aplicável -- [ ] Atualizar tratamento de erros para `getAddressInfoByCep` se necessário -- [ ] Revisar uso de `getCities` se a ordenação era importante -- [ ] Testar todas as funções de validação e formatação -- [ ] Atualizar importações de tipos TypeScript se aplicável +- [ ] Substituir `onlyNumbers`, `isLastChar`, `generateChecksum` e `generateRandomNumber`. -## Obter Ajuda +Recomendado antes da v3.0.0: -Se você encontrar problemas durante a migração, por favor: +- [ ] Renomear os imports e as chamadas da tabela acima para camelCase. +- [ ] Trocar `getCities` por `getMunicipalities` e `getMunicipality` por `getMunicipalityByCode`. +- [ ] Chamar `isValidIe({ value, stateCode })` em vez de `isValidIe(stateCode, ie)`. +- [ ] Importar os tipos `*Params` em vez dos aliases `*Options` das funções que recebem um único objeto. +- [ ] Tirar `'widenet'` dos `providers` de `getAddressInfoByCep` (o serviço não existe mais). +- [ ] Passar a versão para `generateCnpj` quando precisar de uma específica. -1. Verifique a [documentação de utilitários](/pt-br/utilities.md) para as assinaturas corretas das funções -2. Revise os exemplos neste guia de migração -3. Abra uma issue no [repositório GitHub](https://github.com/brazilian-utils/javascript) se encontrar um bug +Encontrou um bug na migração? [Abra uma issue](https://github.com/brazilian-utils/javascript/issues). diff --git a/docs/pt-br/utilities.html b/docs/pt-br/utilities.html deleted file mode 100644 index 383835487..000000000 --- a/docs/pt-br/utilities.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - Utilitários · Brazilian Utils - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 22503866e..a4adb4a72 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -4,13 +4,27 @@ description: "Todos os utilitários do Brazilian Utils, agrupados por família ( keywords: ["CPF", "CNPJ", "CEP", "boleto", "Pix", "NF-e", "telefone", "placa", "RENAVAM", "PIS", "CNH", "IBAN", "feriados", "dias úteis", "CBO", "CNAE", "NCM", "CFOP", "validador", "formatador", "parser", "gerador"] --- -Aqui você encontrará todos os utilitários disponíveis para uso. +Todas as funções do pacote, agrupadas por família. Cada seção diz o que a função faz, quais são as opções, o que ela retorna com entrada inválida e mostra um exemplo. + +## Convenções + +Estas regras valem para todas as funções, a não ser que a seção diga o contrário. + +- **Nada lança erro com entrada inválida** (`null`, `undefined`, tipo errado): `isValid*` retornam `false`, `format*` e `parse*` retornam `''`, `get*` de um item retornam `null`, `get*` de lista retornam `[]`. As únicas exceções são as assíncronas `getAddressInfoByCep` e `getCepInfoByAddress`, que rejeitam com erros tipados. +- **Validadores aceitam o valor com ou sem máscara**: os caracteres de máscara usuais (`.`, `-`, `/`) e espaços entre ou ao redor dos grupos são ignorados, então não é preciso limpar a formatação antes. +- **Formatadores aplicam a máscara até onde o valor vai**, então também servem como máscara de digitação. As funções `parse*` fazem o inverso e mantêm só os caracteres que importam. +- **Geradores usam `Math.random()`**, então servem para testes e dados de exemplo e nunca para nada relacionado a segurança. +- **Getters retornam um array ou objeto novo a cada chamada**, então alterar um resultado nunca afeta a chamada seguinte. +- **Todas as funções são síncronas**, exceto `getAddressInfoByCep`, `getCepInfoByAddress` e a descontinuada `getMunicipality`. + ## CPF ### isValidCpf -Valida se o CPF é válido. Aceita os caracteres de máscara usuais e espaços em branco entre/ao redor dos grupos. +Valida um CPF. + +- Retorna `false` para um número reservado (todos os dígitos iguais, como `00000000000`) e para um dígito verificador errado. ```javascript import { isValidCpf } from '@brazilian-utils/brazilian-utils'; @@ -21,7 +35,10 @@ isValidCpf('111 444 777 35'); // true (máscara com espaços) ### formatCpf -Formata o CPF. `options.pad` (parte de `FormatCpfOptions`) preenche o valor com zeros à esquerda até as 11 posições do padrão antes de aplicar a máscara (padrão `false`). `options.obfuscate` (do mesmo tipo) esconde os 3 primeiros dígitos e os 2 dígitos verificadores (`***.456.789-**`), a convenção de exibição do gov.br / Receita Federal, aplicada após o `pad`. É lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. +Formata um CPF. + +- **Opções** (`FormatCpfOptions`): `pad` preenche o valor com zeros à esquerda até 11 dígitos antes de aplicar a máscara (padrão `false`); `obfuscate` esconde os 3 primeiros dígitos e os 2 dígitos verificadores. +- `obfuscate` é aplicada após o `pad`. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -43,7 +60,10 @@ parseCpf('746.506.880-00'); // 74650688000 ### generateCpf -Gera um CPF válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. O argumento opcional `state` (tipado como `StateCode`, os códigos de duas letras dos 27 estados brasileiros, ex. `"SP"`, `"MG"`) vincula o CPF a um estado fixando o dígito da região fiscal na 9ª posição ao código desse estado. Omitido, uma região aleatória é usada. Um código desconhecido sorteia um dígito de região fiscal aleatório em vez de lançar erro, então o resultado continua sendo um CPF válido. +Gera um CPF válido aleatório. + +- O argumento opcional `state` (`StateCode`, ex. `"SP"`) fixa o dígito da região fiscal (o 9º) no código desse estado. +- Sem `state`, ou com um código desconhecido, um dígito de região fiscal aleatório é sorteado. ```javascript import { generateCpf } from '@brazilian-utils/brazilian-utils' @@ -53,11 +73,16 @@ generateCpf('SP'); // o 9º dígito é 8, o código da região fiscal de SP generateCpf('MG'); // o 9º dígito é 6, o código da região fiscal de MG ``` +Fonte: [Receita Federal, "Cadastros: CPF e CNPJ"](https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf). + ## CNPJ ### isValidCnpj -Valida se o CNPJ é válido. `options.version` (parte de `IsValidCnpjOptions`) escolhe qual formato é aceito: `1` (padrão) apenas o formato numérico, `2` tanto o numérico quanto o alfanumérico; qualquer outro valor é lido como `1`, do mesmo jeito que `formatCnpj` e `parseCnpj` o leem. Os caracteres de máscara usuais e espaços em branco são aceitos nas duas versões. A versão `2` não tem lista de valores reservados, porque o manual da Receita Federal não define nenhuma para o formato alfanumérico: uma base alfanumérica de caracteres repetidos (todos `A`, por exemplo) que passe no dígito verificador é aceita, enquanto os números reservados numéricos são rejeitados na versão `1`. +Valida um CNPJ. + +- **Opções** (`IsValidCnpjOptions`): `version` escolhe o formato aceito: `1` (padrão) apenas numérico, `2` numérico e alfanumérico. Qualquer outro valor é lido como `1`. +- Um número reservado (todos os dígitos iguais) é rejeitado nas duas versões; a versão `2` não tem lista de reservados para letras. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -66,9 +91,15 @@ isValidCnpj('15515147234255'); // false isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (alfanumérico minúsculo) ``` +Fonte: [Receita Federal, Manual do DV do CNPJ](https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj/manual-dv-cnpj.pdf), [CNPJ alfanumérico](https://www.gov.br/receitafederal/pt-br/acesso-a-informacao/acoes-e-programas/programas-e-atividades/cnpj-alfanumerico). + ### formatCnpj -Formata o CNPJ. `options.pad` (parte de `FormatCnpjOptions`) preenche o valor com zeros à esquerda até as 14 posições do padrão antes de aplicar a máscara (padrão `false`). `options.version` (do mesmo tipo) escolhe qual formato de CNPJ é lido: `1` (padrão) apenas numérico, `2` alfanumérico. `options.obfuscate` esconde os 2 primeiros dígitos e os 2 dígitos verificadores (`**.345.678/0001-**`), a convenção de exibição do gov.br / Receita Federal. Vale para as duas versões, é aplicada após o `pad` e é lida por veracidade (truthiness), do mesmo jeito que o `pad`, então qualquer valor verdadeiro esconde os dígitos. +Formata um CNPJ. + +- **Opções** (`FormatCnpjOptions`): `pad` preenche o valor com zeros à esquerda até 14 caracteres antes de aplicar a máscara (padrão `false`); `version` escolhe o formato, `1` (padrão) apenas numérico, `2` alfanumérico; `obfuscate` esconde os 2 primeiros dígitos e os 2 dígitos verificadores. +- A versão `2` mantém letras (em maiúsculas) e dígitos; a versão `1` mantém apenas dígitos. +- `obfuscate` vale para as duas versões e é aplicada após o `pad`. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -81,7 +112,9 @@ formatCnpj('12345678000195', { obfuscate: true }); // **.345.678/0001-** ### parseCnpj -Remove a formatação do CNPJ, retorna um valor normalizado e limita o resultado a 14 caracteres. `options.version` (parte de `ParseCnpjOptions`) escolhe qual formato de CNPJ é normalizado: `1` (padrão) mantém apenas dígitos, `2` mantém letras e dígitos, de modo que um CNPJ alfanumérico sobrevive à ida e volta. +Remove a formatação do CNPJ, retorna um valor normalizado e limita o resultado a 14 caracteres. + +- **Opções** (`ParseCnpjOptions`): `version` escolhe o formato: `1` (padrão) mantém apenas dígitos, `2` mantém letras e dígitos, em maiúsculas. ```javascript import { parseCnpj } from '@brazilian-utils/brazilian-utils'; @@ -92,7 +125,10 @@ parseCnpj('12.OUT.345/0001-99', { version: 2 }); // 12OUT345000199 ### generateCnpj -Gera um CNPJ válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. O primeiro argumento é a versão, como antes, ou um objeto `GenerateCnpjParams` com a mesma `version` mais `branch`, o bloco do "número de ordem" (filial) nas posições 9 a 12: um inteiro de 1 a 9999 escrito com zeros à esquerda em quatro caracteres, aleatório por padrão. Um `branch` inválido é ignorado e um bloco aleatório é usado, e o bloco continua numérico na versão alfanumérica. +Gera um CNPJ válido aleatório. + +- O primeiro argumento é a versão, `1` (padrão) numérico ou `2` alfanumérico, ou um objeto `GenerateCnpjParams` com `version` mais `branch`. +- `branch` é o bloco do "número de ordem" (filial), um inteiro de 1 a 9999 (aleatório por padrão). Um `branch` inválido é ignorado. O bloco continua numérico nas duas versões. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' @@ -107,7 +143,10 @@ generateCnpj({ version: 2, branch: 1 }); // CNPJ alfanumérico cujo bloco de ord ### isValidCep -Valida se o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) é válido. Aceita entrada como `string` ou `number`, mas um CEP que começa com `0` precisa ser passado como string, já que um número não preserva o zero à esquerda (`isValidCep(1310100)` é `false`, `isValidCep('01310100')` é `true`); espaços, pontos e hífens ao redor/entre os 8 dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida um CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). + +- Aceita `string` ou `number`. Um CEP que começa com `0` precisa ser string, já que um número não preserva o zero à esquerda. +- Espaços, pontos e hífens são ignorados. Qualquer outro caractere invalida o valor. ```javascript import { isValidCep } from '@brazilian-utils/brazilian-utils'; @@ -123,7 +162,10 @@ isValidCep('12345'); // false (tamanho inválido) ### formatCep -Formata o CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (parte de `FormatCepOptions`) completa o valor com zeros à esquerda até os 8 dígitos antes de aplicar a máscara (padrão `false`); um CEP que começa com `0` passado como número perde esse zero, então passe-o como string ou use `pad`. +Formata um CEP ([código de endereçamento postal](https://pt.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). + +- **Opções** (`FormatCepOptions`): `pad` preenche o valor com zeros à esquerda até 8 dígitos antes de aplicar a máscara (padrão `false`). +- Um CEP que começa com `0` passado como número perde esse zero: passe uma string ou use `pad`. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; @@ -144,7 +186,7 @@ parseCep('92500-000'); // 92500000 ### generateCep -Gera um CEP aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um CEP aleatório. Um CEP não tem dígito verificador, então toda string de 8 dígitos é estruturalmente válida. ```javascript import { generateCep } from '@brazilian-utils/brazilian-utils'; @@ -154,7 +196,13 @@ generateCep(); // '92500000' ### getAddressInfoByCep -Busca informações de endereço para um CEP usando múltiplos provedores. O padrão é `['viacep', 'brasilapi']`. O provedor `'widenet'` está descontinuado (seu endpoint não responde mais) e foi excluído da lista padrão, mas ainda pode ser solicitado explicitamente via `options.providers` (tipado como `CepProvider[]`). O endereço retornado é tipado como `AddressInfo`. Uma falha transitória de rede é repetida duas vezes por provedor, com backoff linear de 250 ms (250 ms e depois 500 ms), então um provedor que continua falhando é tentado até 3 vezes e acrescenta cerca de 750 ms antes de a sua própria falha se concretizar; um status de erro HTTP ou uma falha não recuperável não é repetida. Os provedores são disparados juntos e disputados com `Promise.any`, não consultados um após o outro, então essas tentativas não atrasam nada para os demais provedores, apenas o momento em que uma rejeição por falha de todos pode aparecer. Um `options.providers` que não nomeia nenhum provedor conhecido rejeita com `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): um array vazio, um array de nomes desconhecidos e um valor que não é um array, incluindo `null`. Com `providers: ['brasilapi']`, um CEP que a BrasilAPI não conhece rejeita com `GetAddressInfoByCepNotFoundError`, já que a BrasilAPI sinaliza a ausência com HTTP 404; qualquer outro status de erro continua sendo um `GetAddressInfoByCepServiceError`. Os três estendem `GetAddressInfoByCepError`, a classe base de todos os erros com que este utilitário rejeita, então um único `catch` nela cobre todos. +Busca o endereço de um CEP em vários provedores ao mesmo tempo e resolve com a primeira resposta bem-sucedida. O resultado é um `AddressInfo`: `cep`, `state`, `city`, `neighborhood` e `street`. + +- **Opções** (`GetAddressInfoByCepOptions`): `providers` (`CepProvider[]`) lista os provedores a disputar (padrão `['viacep', 'brasilapi']`). `'widenet'` está descontinuado e fica fora da lista padrão. +- Aceita string ou número. Um número é preenchido com zeros à esquerda até 8 dígitos. +- Repete falhas transitórias de rede por provedor. +- Rejeita com `GetAddressInfoByCepValidationError` quando o CEP é inválido ou `providers` não nomeia nenhum provedor conhecido, com `GetAddressInfoByCepNotFoundError` quando todos os provedores falharam e pelo menos um informou que o CEP é desconhecido, e com `GetAddressInfoByCepServiceError` quando todos os provedores falharam por outro motivo. +- Os três estendem `GetAddressInfoByCepError`, então um único `catch` cobre todos. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -174,7 +222,12 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ### getCepInfoByAddress -Busca CEPs a partir de um endereço usando a ViaCEP. Lança `GetCepInfoByAddressValidationError` quando a UF, a cidade ou a rua estão ausentes/inválidas — inclusive quando o argumento não é um objeto (omitido, `null`, uma string) e quando `federalUnit` não é uma string, casos em que nenhum `TypeError` cru escapa — `GetCepInfoByAddressNotFoundError` quando nenhum endereço corresponde à busca, e `GetCepInfoByAddressError` quando a própria ViaCEP responde com um status de erro HTTP. Uma requisição que não pode ser realizada (falha de transporte) rejeita com o erro original do `fetch`. Cada item é tipado como `CepAddressInfo` e traz a resposta da ViaCEP sem alterações, com os nomes de campo da própria ViaCEP: `cep`, `logradouro`, `complemento`, `unidade`, `bairro`, `localidade`, `uf`, `estado`, `regiao`, `ibge`, `gia`, `ddd` e `siafi`. Um nome de rua abrangente corresponde a muitos CEPs, então busque de forma tão específica quanto o endereço permitir. +Busca os CEPs de um endereço na ViaCEP. Resolve com um array de `CepAddressInfo`. + +- O argumento (`GetCepInfoByAddressParams`) traz `federalUnit`, `city` e `street`. `federalUnit` pode estar em minúsculas; `city` e `street` têm os espaços nas pontas removidos e os acentos retirados antes da consulta. +- Rejeita com `GetCepInfoByAddressValidationError` quando a UF, a cidade ou a rua está ausente ou inválida, com `GetCepInfoByAddressNotFoundError` quando nenhum endereço corresponde à busca, e com `GetCepInfoByAddressError` quando a ViaCEP responde com um status de erro HTTP. +- Repete falhas transitórias de rede, como `getAddressInfoByCep`. +- Cada item traz a resposta da ViaCEP sem alterações, com os nomes de campo da própria ViaCEP. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; @@ -208,7 +261,10 @@ const ceps = await getCepInfoByAddress({ ### isValidBoleto -Valida se o boleto ([meio de pagamento brasileiro](https://pt.wikipedia.org/wiki/Boleto_banc%C3%A1rio)) é válido. Suporta tanto o boleto de "cobrança bancária" de 47 dígitos quanto o "boleto de arrecadação" (convênio/tributos): seja a linha digitável de 48 dígitos, seja o código de barras de 44 dígitos, ambos iniciados com `8`. Uma tolerância é mantida desde a 2.3.0: o código de moeda na posição 4 do código de barras da cobrança bancária não é verificado, embora a Carta-Circular BCB nº 2.926/2000 o fixe em `9` (real), então um boleto com qualquer outro dígito de moeda continua válido. +Valida um boleto ([meio de pagamento brasileiro](https://pt.wikipedia.org/wiki/Boleto_banc%C3%A1rio)). + +- Aceita a linha digitável de 47 dígitos da "cobrança bancária" e, do "boleto de arrecadação", seja a linha digitável de 48 dígitos, seja o código de barras de 44 dígitos. +- O código de moeda (posição 4 do código de barras da cobrança bancária) não é verificado. ```javascript import { isValidBoleto } from '@brazilian-utils/brazilian-utils'; @@ -217,9 +273,14 @@ isValidBoleto('00190000090114971860168524522114675860000102656'); // true isValidBoleto('846100000005246100291102005460339004695895061080'); // true (boleto de arrecadação) ``` +Fonte: [Carta-Circular BCB nº 2.926/2000](https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf), [FEBRABAN, Layout Padrão de Arrecadação](https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf). + ### formatBoleto -Formata um número de boleto. `options.pad` (parte de `FormatBoletoOptions`) preenche o valor com zeros à esquerda até o número de posições do padrão antes de aplicar a máscara (padrão `false`). A máscara de arrecadação (convênio/tributos) só se aplica à linha digitável de 48 dígitos que começa com `8`; o código de barras de arrecadação de 44 dígitos não tem agrupamento de exibição definido pela FEBRABAN e mantém a máscara de "cobrança bancária". +Formata um número de boleto. + +- **Opções** (`FormatBoletoOptions`): `pad` preenche o valor com zeros à esquerda até o tamanho do padrão antes de aplicar a máscara (padrão `false`). +- Uma linha digitável de 48 dígitos que começa com `8` recebe a máscara de arrecadação: quatro blocos de 11 dígitos, cada um seguido do seu dígito verificador. O código de barras de arrecadação de 44 dígitos mantém a máscara de "cobrança bancária". ```javascript import { formatBoleto } from '@brazilian-utils/brazilian-utils'; @@ -230,6 +291,8 @@ formatBoleto('846100000005246100291102005460339004695895061080'); // 84610000000 formatBoleto('84610000000246100291100054603390069589506108'); // 84610.00000 02461.002911 00054.603390 0 69589506108 (código de barras de arrecadação de 44 dígitos mantém a máscara bancária) ``` +Fonte: [FEBRABAN, Layout Padrão de Arrecadação](https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf). + ### parseBoleto Remove a formatação do boleto, mantém apenas os dígitos e limita o resultado a 47 dígitos (48 para boleto de arrecadação). @@ -242,7 +305,9 @@ parseBoleto('00190.00009 01149.718601 68524.522114 6 75860000102656'); // 001900 ### generateBoleto -Gera um boleto válido aleatório. Informe `{ type: "arrecadacao" }` (tipado como `GenerateBoletoParams`) para gerar um boleto de arrecadação em vez do tipo padrão "bancario" (cobrança bancária). Um boleto de arrecadação sorteia o segmento entre 1 e 7 (o segmento 9 é de uso dos próprios bancos) e o identificador de valor entre os quatro valores possíveis, `6` e `8` para valor efetivo e `7` e `9` para quantidade de referência, de modo que os dois ramos de `hasEffectiveValue` do `getBoletoInfo` sejam alcançáveis. +Gera um boleto válido aleatório. + +- Informe `{ type: 'arrecadacao' }` (`GenerateBoletoParams`) para um boleto de arrecadação de 48 dígitos em vez do tipo padrão `'bancario'` (cobrança bancária, 47 dígitos). ```javascript import { generateBoleto } from '@brazilian-utils/brazilian-utils'; @@ -253,7 +318,12 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ### getBoletoInfo -Extrai informações de um boleto (valor, data de vencimento, código do banco). Retorna `null` quando `value` não é um boleto válido — o `isValidBoleto` é verificado antes —, então o resultado precisa ser estreitado antes de ser lido. A 2.3.0 retornava `undefined` aqui; agora todo getter do pacote responde com `null` a uma busca que não resolve, então só uma comparação estrita `=== undefined` é afetada. Aceita opcionalmente `{ referenceDate }` (tipado como `GetBoletoInfoOptions`) para resolver o ciclo do "fator de vencimento" a partir de uma data específica em vez de agora (o ciclo de data-base do fator reiniciou em 22/02/2025, segundo a FEBRABAN). Nem a FEBRABAN nem o Banco Central publicam uma forma de distinguir um fator do ciclo antigo de um do ciclo novo, então todo fator resolve para uma de duas datas separadas por 9000 dias e o `referenceDate` escolhe entre elas por meio das janelas de segurança da própria biblioteca: o mesmo boleto pode passar a resolver para a outra candidata com o tempo, então informe `referenceDate` explicitamente sempre que a resposta precisar ser estável. A busca de ciclo nunca desce abaixo do primeiro ciclo, então um `referenceDate` anterior ao próprio esquema ainda resolve um fator para a data mais antiga que aquele fator consegue representar, em vez de uma anterior à data-base de 07/10/1997. Para um boleto de arrecadação, o resultado, tipado como `BoletoInfo`, continua trazendo as duas chaves, porém vazias, `bankCode: ''` e `expirationDate: null`, já que o boleto não tem código de banco nem fator de vencimento, e acrescenta `type: "arrecadacao"`, `segment`, `value` e `hasEffectiveValue`. +Extrai informações de um boleto (valor, data de vencimento, código do banco). Retorna `null` quando o valor não é um boleto válido. + +- **Opções** (`GetBoletoInfoOptions`): `referenceDate` resolve o ciclo do "fator de vencimento" a partir dessa data em vez de agora. +- Retorna um `BoletoInfo`: `amount` em centavos, `expirationDate` e o `bankCode` de três dígitos. `expirationDate` é `null` quando o boleto não traz fator de vencimento (um fator abaixo de `1000`). +- O ciclo do fator de vencimento reiniciou em 22/02/2025, então um fator pode significar uma de duas datas separadas por 9000 dias. `referenceDate` escolhe entre elas; informe-a sempre que a resposta precisar ser estável. +- Um boleto de arrecadação tem `bankCode: ''` e `expirationDate: null`, mais `type: 'arrecadacao'`, `segment`, `value` (o valor em reais) e `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -264,7 +334,7 @@ getBoletoInfo('00190000090114971860168524522114675860000102656'); getBoletoInfo('00190000090114971860168524522114675860000102656', { referenceDate: new Date(2018, 6, 1) }); -// Resolve o ciclo do fator de vencimento a partir de 2018-07-01 +// Resolve o ciclo do fator de vencimento a partir de 01/07/2018 getBoletoInfo('846100000005246100291102005460339004695895061080'); // { amount: 2461, expirationDate: null, bankCode: '', type: 'arrecadacao', segment: 4, value: 24.61, hasEffectiveValue: true } @@ -272,11 +342,16 @@ getBoletoInfo('846100000005246100291102005460339004695895061080'); getBoletoInfo('invalid'); // null ``` +Fonte: [Carta-Circular BCB nº 2.926/2000](https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf), [FEBRABAN, Layout Padrão de Arrecadação](https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf). + ## Pix ### isValidPixKey -Valida se uma chave Pix é válida: um CPF, um CNPJ, um e-mail, um telefone celular brasileiro ou uma chave aleatória (EVP), conforme os formatos de chave do DICT. O manual registra um "número de telefone celular", então um telefone fixo não é uma chave Pix válida. `options.accept` (tipado como `IsValidPixKeyOptions`) restringe quais tipos de chave são aceitos; o padrão é aceitar todos, e `[]` rejeita todos. Exporta o tipo `PixKeyType`. +Valida uma chave Pix: um CPF, um CNPJ, um e-mail, um telefone celular brasileiro ou uma chave aleatória EVP, conforme os formatos de chave do DICT. + +- **Opções** (`IsValidPixKeyOptions`): `accept` (`PixKeyType[]`, padrão todos) lista os tipos de chave aceitos; `[]` rejeita todos. +- Mesmas regras de reconhecimento de `getPixKeyInfo`. ```javascript import { isValidPixKey } from '@brazilian-utils/brazilian-utils'; @@ -290,9 +365,16 @@ isValidPixKey('123.456.789-09', { accept: ['email', 'evp'] }); // false isValidPixKey('not a key'); // false ``` +Fonte: [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf), [API do DICT](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html), [pix-api](https://github.com/bacen/pix-api). + ### getPixKeyInfo -Identifica uma chave Pix e a normaliza para a forma canônica que o DICT espera dentro do BR Code: CPF com 11 dígitos, CNPJ com 14 caracteres, e-mail em minúsculas, telefone celular em E.164 (um telefone fixo não é chave Pix) ou UUID em minúsculas (EVP). Um valor de 11 dígitos válido tanto como CPF quanto como celular é lido como CPF, a menos que tenha sido escrito como telefone (prefixo `+55`/`0055` ou DDD entre parênteses). O CPF e o telefone são reconhecidos pela forma como são escritos, não apenas pelos dígitos que carregam, então texto ao redor não é descartado e `'abc123.456.789-09'` não é uma chave CPF. Uma chave de e-mail é trimada e passada para minúsculas, e uma maior que os 77 caracteres que o DICT permite é rejeitada. Um valor cujos dígitos carregam um dígito verificador de CNPJ válido é lido como CNPJ mesmo quando começa com `0055`, já que uma chave de telefone dentro do BR Code sempre carrega o prefixo `+55`. Retorna `null` quando o valor não é uma chave Pix válida. O resultado é tipado como `PixKeyInfo`. +Identifica uma chave Pix e a normaliza para a forma canônica que o DICT espera dentro de um BR Code. Retorna `null` quando o valor não é uma chave Pix válida. + +- Retorna um `PixKeyInfo` com o `type` (`PixKeyType`) e o `value`. +- O `value` canônico é só dígitos para CPF ou CNPJ (letras maiúsculas), e-mail minúsculo, celular em E.164 ou UUID minúsculo. +- Um valor de 11 dígitos válido como CPF e celular é lido como CPF, salvo se escrito como telefone (prefixo `+55` ou DDD entre parênteses). +- Um e-mail com mais de 77 caracteres é rejeitado. ```javascript import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; @@ -307,9 +389,17 @@ getPixKeyInfo('51998259765'); // { type: 'cpf', value: '51998259765' } (também getPixKeyInfo('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ``` +Fonte: [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf), [API do DICT](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html). + ### isValidPixPayload -Valida se um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola") é válido: estrutura TLV bem formada, objetos obrigatórios presentes, um dos templates "Merchant Account Information" carregando o GUI `br.gov.bcb.pix` junto com uma chave ou uma URL, e um CRC-16 que confere. O objeto "Point of Initiation Method" (`01`) é informativo: o Manual do BR Code o marca como opcional e só atribui significado ao valor `"12"` ("só pode ser utilizado uma vez"), então ele pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` torna o payload inválido. Quando um payload construído em torno de uma chave traz um valor (`54`), esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque, ou seja, a menos que traga o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`) como prescreve o §2.6 do manual do Pix; rejeitar `"0"`/`"0.00"` sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP torna o payload inválido: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. A chave em si não é validada contra os formatos do DICT, use `isValidPixKey` para isso. Os Unreserved Templates (IDs 80 a 99) são ignorados: o "QR Code composto" do Pix Automático (Pix recorrente) grava em um deles a localização de recorrência e, quando esse payload também traz uma localização de pagamento em 26-25, como no exemplo composto do manual do Pix, ele é aceito e lido como um payload dinâmico comum, com a localização de recorrência descartada. Só um payload sem nenhum template Pix nos IDs 26 a 51 é considerado inválido. +Valida um payload de BR Code Pix (a string por trás de um QR Code Pix e do "Pix copia e cola"). A chave em si não é conferida; use `isValidPixKey`. + +- A estrutura TLV, o CRC-16 e os objetos obrigatórios (format indicator, category code, moeda, país, nome e cidade do recebedor) são verificados. +- Um template "Merchant Account Information" (IDs 26 a 51) precisa trazer o GUI `br.gov.bcb.pix` com uma chave (estático) ou a URL do PSP (dinâmico), nunca os dois. +- Os objetos `01` (Point of Initiation Method) e `62` (Additional Data Field) são opcionais; `01` precisa ser `11` ou `12` quando presente. +- Um valor (`54`) precisa ser maior que zero, exceto num BR Code de Pix Saque (`fss` de 8 dígitos no subobjeto 26-03). +- Os Unreserved Templates (IDs 80 a 99) são ignorados. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -322,9 +412,16 @@ isValidPixPayload( isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (CRC quebrado) ``` +Fonte: [Manual do BR Code](https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf), [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf). + ### getPixPayloadInfo -Interpreta um payload de BR Code Pix e retorna seus campos. O payload é validado pelo `isValidPixPayload` primeiro, então uma estrutura malformada, um CRC quebrado ou um objeto obrigatório ausente retornam `null` em vez de um resultado parcial. Um payload estático vem com `key`, um dinâmico com `url`. A chave Pix em si não é validada, já que o manual permite um QR Code estático construído com uma chave que não existe mais no DICT; a titularidade da chave só é resolvida no momento do pagamento. O "Additional Data Field Template" (ID 62) é obrigatório na tabela do BR Code mas opcional na especificação EMV® a que ela se refere, então é aceito quando ausente. Os tamanhos que o manual reserva para o nome do recebedor (25), a cidade do recebedor (15), o `txid` (25) e o campo 26-01 da chave Pix (77) são limites do lado do gerador, aplicados por `generatePixPayload` e não verificados aqui, já que payloads reais os ultrapassam com frequência. O resultado é tipado como `PixPayloadInfo`; `pointOfInitiation` está sempre presente e é tipado como `PixPointOfInitiation`, `"dynamic"` quando o payload traz uma localização de PSP ou quando o objeto "Point of Initiation Method" (`01`) é `"12"`, e `"static"` nos demais casos. As informações da conta do recebedor devem trazer exatamente um entre uma chave e uma `url` (verificada com a mesma regra de localização de PSP do `generatePixPayload`); o próprio `01` é informativo, então pode estar ausente em qualquer um dos formatos e apenas um valor fora de `{"11", "12"}` retorna `null`. Quando um payload construído em torno de uma chave traz um valor, esse valor precisa ser maior que zero, a menos que o payload seja um BR Code de Pix Saque: o §2.6 do manual do Pix coloca o ISPB do facilitador de serviço de saque no subobjeto 26-03 (`fss`), devolvido como `withdrawalFacilitator`, e `54` igual a `"0"` ou `"0.00"` é aceito junto dele. Rejeitar um valor zero sem o `fss` é uma restrição deliberada desta biblioteca, não uma regra do manual. Um `fss` escrito ao lado de uma localização de PSP retorna `null`: o §2.7 do Manual de Padrões para Iniciação do Pix mapeia o QR Code dinâmico para exatamente dois subobjetos, `00` (GUI) e `25` (URL), e o `fss` pertence ao template estático do §2.6. Quando o payload traz uma localização de PSP, o valor e o `txid` são ignorados, como o manual determina. Os Unreserved Templates (IDs 80 a 99) são ignorados: um "QR Code composto" do Pix Automático que também traga uma localização de pagamento em 26-25 é interpretado como um payload dinâmico comum e sua localização de recorrência é descartada, então quem precisa distinguir os dois não pode se apoiar neste parser. Só um payload sem nenhum template Pix nos IDs 26 a 51 retorna `null`. +Interpreta um payload de BR Code Pix e retorna seus campos. Aceita o que `isValidPixPayload` aceita; para o resto retorna `null`, nunca um resultado parcial. + +- Retorna um `PixPayloadInfo`: `merchantName`, `merchantCity`, `pointOfInitiation` e `key` (estático) ou `url` (dinâmico). +- `amount`, `txid`, `description` e `withdrawalFacilitator` (o `fss` do Pix Saque) só aparecem quando o payload os traz; `txid` fica ausente para o marcador `***`. +- `pointOfInitiation` (`PixPointOfInitiation`) é `"dynamic"` quando o payload traz uma localização de PSP ou o objeto `01` é `"12"`; senão, `"static"`. +- Com localização de PSP, `amount` e `txid` são ignorados, como manda o manual. ```javascript import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; @@ -341,11 +438,18 @@ getPixPayloadInfo( // } ``` +Fonte: [Manual do BR Code](https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf), [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf). + ### generatePixPayload -Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado (parte de `GeneratePixPayloadParams`); `null` é retornado quando ambos ou nenhum são informados. `url` deve ser uma localização de PSP como o manual do Bacen define: um host com caminho, sem esquema (`pix.example.com/qr/v2/1234`); um payload dinâmico não pode carregar `amount` nem `txid`, que pertencem à localização do PSP. O valor é escrito com as duas casas decimais que o BR Code aceita, então tanto um que arredonda para `0.00` quanto um que não sobrevive a esse round-trip (`0.005`, `123.456`) são rejeitados, em vez de escritos como uma quantia diferente. O BR Code de Pix Saque, que anuncia o `fss` do subobjeto 26-03, é interpretado pelo `getPixPayloadInfo`, mas não é gerado aqui. +Gera o payload de um BR Code Pix. Exatamente um entre `params.key` e `params.url` deve ser informado; `null` é retornado quando ambos ou nenhum são informados. -Quando `params.key` é informado, ela é normalizada para a forma canônica do DICT pelo `getPixKeyInfo` e o payload é estático. Quando `params.url` é informado no lugar (a localização do PSP, sem o esquema da URL, ex.: `"pix.example.com/qr/v2/1234"`), o payload é dinâmico conforme o Manual de Padrões para Iniciação do Pix: a URL ocupa o lugar da chave no template "Merchant Account Information" e o objeto "Point of Initiation Method" é definido como dinâmico (`12`); `params.url` pode ter no máximo 77 caracteres. `merchantName`, `merchantCity` e `description` são convertidos para ASCII imprimível (acentos removidos) e truncados ao que o BR Code permite. O `getPixPayloadInfo` já interpreta os dois formatos, então `getPixPayloadInfo(generatePixPayload({ url, ... }))` forma um round-trip. +- **Parâmetros** (`GeneratePixPayloadParams`): `key` ou `url`, `merchantName`, `merchantCity` e os opcionais `amount`, `txid` e `description`. +- Com `key` o payload é estático e a chave é normalizada por `getPixKeyInfo`. Com `url` é dinâmico (objeto `01` definido como `12`) e não pode carregar `amount` nem `txid`. +- `url` é uma localização de PSP: host e caminho, sem esquema (`pix.example.com/qr/v2/1234`), com no máximo 77 caracteres. +- `amount` recebe duas casas decimais; `0.005`, `123.456` ou um valor que arredonda para `0.00` é rejeitado. +- `txid` tem de 1 a 25 caracteres de `[A-Za-z0-9]` (padrão `***`). +- `merchantName`, `merchantCity` e `description` perdem os acentos e são truncados a 25, 15 e o que sobra do template. ```javascript import { generatePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -368,13 +472,28 @@ generatePixPayload({ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // null (nem key nem url) ``` +Fonte: [Manual do BR Code](https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf), [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf). + ## Chave de NF-e ### isValidNfeKey -Valida se uma chave de acesso de DF-e (Documento Fiscal eletrônico) é válida. Cobre todos os documentos cuja chave de acesso é a mesma string de 44 dígitos: NF-e (modelo 55), NFC-e (65), CT-e (57, o Conhecimento de Transporte Eletrônico instituído pela cláusula primeira do [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, o Conhecimento de Transporte Eletrônico para Outros Serviços instituído pela cláusula primeira do [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, o CT-e Guia de Transporte de Valores instituído pela cláusula primeira do [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) e NFCom (62). O CF-e-SAT (59) fica de fora: sua "chave de consulta" de 44 posições é composta de outro jeito. Os 44 dígitos podem ser separados nos grupos impressos de 4 por espaço em branco, `.`, `-` ou `/`, inclusive uma sequência deles entre dois grupos, a mesma máscara intercambiável que `isValidCpf` e `isValidCnpj` aceitam; um separador dentro de um grupo de 4, ou qualquer outro caractere, é rejeitado em vez de removido. Os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` encontrados no atributo `Id` do XML do documento são removidos antes dessa verificação, junto com qualquer espaço em branco entre o prefixo e o primeiro grupo. +Valida uma chave de acesso de DF-e. Cobre todo DF-e com chave de acesso de 44 dígitos; o CF-e-SAT (59) fica de fora. -O tipo de emissão (`tpEmis`) é conferido contra os códigos que o MOC daquele modelo atribui, então o conjunto aceito muda com o modelo: de 1 a 7 e 9 para NF-e e NFC-e, `{1, 3, 4, 5, 7, 8}` para o CT-e, `{1, 5, 7, 8}` para o CT-e OS, `{1, 2, 7, 8}` para a GTV-e, `{1, 2, 3}` para o MDF-e e `{1, 2}` para o BP-e, a NF3e e a NFCom. O código 8, a autorização pela SVC-SP, é atribuído somente pelo [MOC do CT-e 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos), nunca pelo da NF-e; os domínios do [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), da [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) e da [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) vêm dos manuais deles. Para NF-e e NFC-e o código numérico também é conferido contra a regra B03-10 do MOC da NF-e, que proíbe os vinte valores repetidos e sequenciais de `cNF` que ela lista e um `cNF` igual ao número do documento. Já um número de documento todo zerado é recusado em todos os modelos seguindo o leiaute, não por escolha desta biblioteca: o `tiposBasico_v4.00.xsd` do [pacote de schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) tipa o `nNF` como `TNF`, cujo pattern é `[1-9]{1}[0-9]{0,8}`, e o Anexo I de cada um dos outros modelos repete o mesmo regex no seu próprio campo de número. +- Modelos: NF-e (55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) e NFCom (62). +- Os 44 dígitos podem ser agrupados de 4 em 4 por espaço, `.`, `-` ou `/`. Os prefixos `Id` do XML (`NFe`, `CTe`, `MDFe`, `BPe`, `NF3e`, `NFCom`) são removidos antes. +- `tpEmis` precisa ser um dos que o MOC do modelo atribui (tabela abaixo). +- Para NF-e e NFC-e o `cNF` precisa passar na regra B03-10 do MOC (sem valores repetidos ou sequenciais, diferente do número do documento). +- Um número de documento todo zerado é rejeitado. O dígito verificador é um módulo 11 sobre os 43 primeiros dígitos. + +| Modelo | `tpEmis` aceitos | +| --- | --- | +| NF-e (55), NFC-e (65) | 1 a 7 e 9 | +| CT-e (57) | 1, 3, 4, 5, 7, 8 | +| CT-e OS (67) | 1, 5, 7, 8 | +| GTV-e (64) | 1, 2, 7, 8 | +| MDF-e (58) | 1, 2, 3 | +| BP-e (63), NF3e (66), NFCom (62) | 1, 2 | ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -386,13 +505,20 @@ isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true isValidNfeKey('3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458'); // true (qualquer um dos caracteres de máscara) isValidNfeKey('351 70458716523000119550010000000121000123458'); // false (separador dentro de um grupo de 4) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (cUF inválido) +isValidNfeKey('35170458716523000119010010000000121000123450'); // false (modelo inválido) isValidNfeKey('35170458716523000119550010000000128000123455'); // false (o MOC da NF-e não atribui tpEmis 8) isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, regra B03-10) ``` +Fonte: [MOC da NF-e](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf), [schemas da NF-e](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) e os MOCs citados em `src/is-valid-nfe-key/is-valid-nfe-key.ts`. + ### formatNfeKey -Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço, a forma em que todo documento auxiliar a imprime: o DANFE da NF-e e da NFC-e, o DACTE do CT-e, do CT-e OS e da GTV-e, o DAMDFE do MDF-e, o DABPE do BP-e, o DANF3E da NF3e e o DANFE-COM da NFCom. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e agrupado até onde eles vão, então uma chave com máscara ou parcial, ainda sendo digitada, é agrupada progressivamente, e qualquer coisa sem dígito (um objeto, `true`, um objeto criado com `Object.create(null)`) devolve `''` em vez de lançar. Use `isValidNfeKey` para verificar uma chave. O `options.pad` (parte de `FormatNfeKeyOptions`) preenche o valor com zeros à esquerda até os 44 dígitos de uma chave de acesso completa (padrão `false`). O parâmetro é tipado como string porque 44 dígitos são mais do que um número JavaScript comporta com exatidão; em tempo de execução um número é lido como a string dos seus dígitos, como em todo formatador deste pacote. +Formata uma chave de acesso de DF-e (Documento Fiscal eletrônico) em grupos de 4 dígitos separados por espaço. É a forma em que o DANFE, o DACTE, o DAMDFE, o DABPE, o DANF3E e o DANFE-COM a imprimem. + +- **Opções** (`FormatNfeKeyOptions`): `pad` preenche o valor com zeros à esquerda até os 44 dígitos de uma chave de acesso completa (padrão `false`). +- Uma chave com máscara ou parcial é agrupada até onde os dígitos vão. +- Use `isValidNfeKey` para verificar uma chave. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -408,7 +534,9 @@ formatNfeKey('12345', { pad: true }); ### parseNfeKey -Remove a formatação de uma chave de acesso de DF-e, mantém apenas os dígitos e limita o resultado a 44 dígitos. Os prefixos `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` e `NFCom` que o atributo `Id` do XML do documento coloca antes da chave são retirados primeiro, já que o `NF3e` carrega um dígito próprio; use `isValidNfeKey` para verificar a chave e `getNfeKeyInfo` para ler os campos dela. +Remove a formatação de uma chave de acesso de DF-e (chave de acesso), mantém apenas os dígitos e limita o resultado a 44 dígitos. + +- Os prefixos `Id` do XML (`NFe`, `CTe`, `MDFe`, `BPe`, `NF3e`, `NFCom`) são removidos antes. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -422,7 +550,10 @@ parseNfeKey('NFe35170458716523000119550010000000121000123458'); ### getNfeKeyInfo -Interpreta uma chave de acesso de DF-e e retorna seus campos (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Aceita as mesmas formas de entrada do `isValidNfeKey` e retorna `null` quando a chave não é válida. O resultado é tipado como `NfeKeyInfo`, cujo `model` é um `NfeKeyModel`. A NFCom (`'62'`) e a NF3e (`'66'`) gastam a posição 36 da chave com o `nSiteAutoriz`, o site do autorizador que recebeu o documento, então para esses dois modelos o resultado também traz `authorizationSite` e o `code` tem 7 dígitos em vez de 8. +Interpreta uma chave de acesso de DF-e e retorna seus campos. Aceita as mesmas formas de entrada de `isValidNfeKey` e retorna `null` quando a chave não é válida. + +- Retorna um `NfeKeyInfo`: `stateCode`, `year`, `month`, `taxId`, `model` (`NfeKeyModel`), `series`, `number`, `emissionType`, `code` e `checkDigit`. +- Para NFCom e NF3e (modelos `'62'` e `'66'`) o resultado também traz `authorizationSite` e o `code` tem 7 dígitos em vez de 8. ```javascript import { getNfeKeyInfo } from '@brazilian-utils/brazilian-utils'; @@ -442,7 +573,9 @@ getNfeKeyInfo('invalid'); // null ### isValidPhone -Valida se o número de telefone (celular ou fixo) é válido. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes da validação, seguindo a regra documentada em `parsePhone`. `options.accept` (tipado como `PhoneType[]`, parte de `IsValidPhoneOptions`) define quais tipos de número são aceitos e tem como padrão `['mobile', 'landline']`; adicione `'service'` para também aceitar os números não geográficos reconhecidos por `isValidServicePhone`, ou informe `[]` para não aceitar nenhum. `options.version` (tipado como `PhoneVersion`, parte do mesmo tipo) é repassado ao `isValidMobilePhone` e escolhe qual regra de numeração celular é aplicada: `1` (padrão) o formato antigo, cujo primeiro dígito do número pode ser 6, 7, 8 ou 9, e `2` o atual, da Resolução Anatel 749/2022, art. 12, I, "a", que aceita 7, 8 ou 9 e rejeita o prefixo `700`. Vale apenas para celulares; números fixos e de serviço não são afetados. +Valida um número de telefone (celular ou fixo). Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes, como em `parsePhone`. + +- **Opções** (`IsValidPhoneOptions`): `accept` (`PhoneType[]`, padrão `['mobile', 'landline']`) define quais tipos de número são aceitos; inclua `'service'` para os números que `isValidServicePhone` reconhece. `version` (`PhoneVersion`, padrão `1`) é repassado a `isValidMobilePhone`. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; @@ -456,9 +589,17 @@ isValidPhone('08001234567', { accept: ['service'] }); // true isValidPhone('11900000000', { accept: [] }); // false ``` +Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749). + ### formatPhone -Formata número de telefone de acordo com padrões brasileiros. `options.mask` (tipado como `PhoneMask`) aceita `"sn"` (padrão, apenas o número assinante, 9 dígitos, sem DDD), `"nanp"` (DDD + número assinante, `"(00) 00000-0000"` para os 11 dígitos de um celular e `"(00) 0000-0000"` para os 10 dígitos de um fixo, mantendo o agrupamento de 11 dígitos em qualquer outro tamanho), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, a forma como um número brasileiro é exibido para quem liga do exterior), `"service"` (`"0800 123 4567"` ou `"4004-1234"`, os agrupamentos convencionais para números de serviço) ou `"auto"`. O `"auto"` usa `"international"` quando `value` traz um código de país brasileiro (`+55`, `0055` ou um `55` seguido de 10 ou 11 dígitos), `"service"` quando `value` é um número de serviço e, nos demais casos, decide pela quantidade de dígitos: `"nanp"` quando `value` tem mais dígitos que um número assinante isolado, `"sn"` quando não tem. `"e164"` e `"international"` removem antes o código de país (regra documentada em `parsePhone`) e recaem para a apresentação `"service"` no caso de um número de serviço, já que esses não têm forma E.164. Se `value` incluir o DDD, informe `{ mask: 'auto' }` (ou `'nanp'`) explicitamente, já que a máscara padrão `"sn"` assume que não há DDD e trunca silenciosamente um DDD presente. Uma `mask` fora da união recai para o padrão `"sn"` em vez de lançar erro. +Formata um número de telefone de acordo com os padrões brasileiros. Se `value` incluir o DDD, informe `{ mask: 'auto' }` ou `'nanp'`: a máscara padrão `"sn"` assume que não há DDD e o trunca. + +- **Opções** (`FormatPhoneOptions`): `mask` (`PhoneMask`, padrão `"sn"`) escolhe um dos padrões abaixo. Uma `mask` desconhecida recai para `"sn"`. +- `"sn"`: apenas o número assinante, 9 dígitos. `"nanp"`: DDD mais número assinante, 11 dígitos para celular e 10 para fixo; outros tamanhos mantêm o agrupamento de 11 dígitos. +- `"e164"` e `"international"` removem antes o código de país, como `parsePhone`, e recaem para `"service"` para um número de serviço. +- `"service"`: os Códigos Não Geográficos (`0800 123 4567`) e os números abreviados `300X`/`400X` (`4004-1234`). +- `"auto"`: `"service"` para um número de serviço, `"international"` quando `value` traz código de país, senão `"nanp"` para mais de 9 dígitos, ou `"sn"`. ```javascript import { formatPhone } from '@brazilian-utils/brazilian-utils'; @@ -473,12 +614,17 @@ formatPhone('+5511987654321', { mask: 'international' }); // +55 11 98765-4321 formatPhone('08001234567', { mask: 'service' }); // 0800 123 4567 formatPhone('40041234', { mask: 'service' }); // 4004-1234 formatPhone('+5511987654321', { mask: 'auto' }); // +55 11 98765-4321 ("auto" detecta o prefixo +55 e escolhe "international") +formatPhone('5508001234567', { mask: 'auto' }); // 0800 123 4567 ("auto" lê o número 0800, não um +55 08) formatPhone('11900000000'); // 11900-0000 (CUIDADO: a máscara padrão "sn" trunca um número com DDD) ``` +Fonte: [ITU-T E.164](https://www.itu.int/rec/T-REC-E.164), [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749). + ### parsePhone -Remove a formatação do telefone, mantém apenas os dígitos e limita o resultado a 11 dígitos. Um código de país brasileiro é removido antes, mas somente quando os dígitos restantes tiverem exatamente 10 ou 11 dígitos, ou seja, um número nacional plausível. A regra é baseada no tamanho, não no sinal, então um número da área 55 não é confundido com o código de país. +Remove a formatação do telefone, mantém apenas os dígitos e limita o resultado a 11 dígitos. + +- Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é removido antes, mas só quando sobram 10 ou 11 dígitos (DDD mais número assinante), então o DDD 55 não é confundido com ele. ```javascript import { parsePhone } from '@brazilian-utils/brazilian-utils'; @@ -491,7 +637,9 @@ parsePhone('55987654321'); // 55987654321 (DDD 55, não confundido com o código ### generatePhone -Gera um telefone brasileiro aleatório. Aceita `'mobile'`, `'landline'` ou `'service'` (tipado como `GeneratePhoneType`); um número de serviço não tem DDD. Se omitido, gera aleatoriamente um celular ou um fixo, nunca um número de serviço. Um celular gerado sempre começa com 9, então passa nas duas regras de numeração do `isValidMobilePhone`. +Gera um telefone brasileiro aleatório. Aceita `'mobile'`, `'landline'` ou `'service'` (`GeneratePhoneType`); sem o tipo, gera um celular ou um fixo ao acaso, nunca um número de serviço. + +- Um celular começa com 9 depois do DDD (válido nas duas versões de `isValidMobilePhone`); um fixo tem 8 dígitos depois do DDD, começando com 2 a 6; um número de serviço não tem DDD. ```javascript import { generatePhone } from '@brazilian-utils/brazilian-utils'; @@ -504,7 +652,9 @@ generatePhone('service'); // '08001234567' ou '40041234' ### isValidMobilePhone -Valida se o número de telefone celular é válido. `options.version` (tipado como `PhoneVersion`) controla qual regra de numeração celular é aplicada: `1` (padrão) é o formato anterior à Resolução Anatel 749/2022, mantido por compatibilidade com a 2.3.0, cujo primeiro dígito do número (após o DDD) pode ser 6, 7, 8 ou 9; `2` aplica o art. 12, I, "a" da resolução, que coloca 7, 8 e 9 no Serviço Móvel Pessoal (SMP), então um 6 inicial é Reserva Técnica e é rejeitado. A versão `2` também exclui o prefixo `700`, que o art. 12, II reserva ao Serviço Móvel Global por Satélite e não ao SMP, então `isValidMobilePhone('11700123456', { version: 2 })` é `false`; a versão `1` não o exclui e o aceita. +Valida um número de telefone celular. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes, como em `parsePhone`. + +- **Opções** (`IsValidMobilePhoneOptions`): `version` (`PhoneVersion`, padrão `1`) escolhe a regra de numeração: `1` aceita 6, 7, 8 ou 9 como primeiro dígito; `2` segue a Resolução Anatel 749/2022, aceita só 7, 8 ou 9 e rejeita a série `700`. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; @@ -516,19 +666,26 @@ isValidMobilePhone('11612345678', { version: 2 }); // false (6 é Reserva Técni isValidMobilePhone('11700123456', { version: 2 }); // false (a série 700 é de satélite) ``` +Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749). + ### isValidLandlinePhone -Valida se o número de telefone fixo é válido. +Valida um número de telefone fixo. Um código de país brasileiro (`+55`, `0055` ou um `55` isolado) é aceito e removido antes, como em `parsePhone`. ```javascript import { isValidLandlinePhone } from '@brazilian-utils/brazilian-utils'; isValidLandlinePhone('1130000000'); // true +isValidLandlinePhone('+55 11 3000-0000'); // true (código de país aceito) ``` ### isValidServicePhone -Valida se um número de telefone é um número de serviço brasileiro válido, discado sem DDD: os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` (11 dígitos no total, então a forma curta e extinta de `0800` + 6 dígitos é rejeitada), os números abreviados `300X`/`400X` (8 dígitos), e os códigos de 3 dígitos dos Códigos de Acesso a Serviços de Utilidade Pública designados pela Anatel (ex.: `190`, `192`), cuja tabela consolidada é o Anexo do [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). O `112` e o `911` são rejeitados: a Anatel não designa nenhum dos dois, e o `911` sequer está dentro da faixa `1N₂N₁` que o art. 13 da [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destina aos serviços de utilidade pública, então o encaminhamento deles nos aparelhos é uma convenção GSM, não uma designação de numeração. Apenas a estrutura é verificada: o número não precisa estar atribuído a ninguém, e a regra do `0500` que codifica o valor da doação nos dois últimos dígitos não é aplicada. A Anatel retirou os códigos de 4 dígitos em vez de alocá-los (o art. 43 I da [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) e o art. 2º II do Ato acima mandaram liberá-los), então apenas as raízes convencionais `300X` e `400X` são reconhecidas: outros prefixos de "Número Único" usados no mercado, como `4020` e `4062`, estão fora de escopo e são rejeitados. +Valida um número de serviço brasileiro, discado sem DDD. Apenas a estrutura é verificada: o número não precisa estar atribuído a ninguém. + +- Os Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` e `0900` seguidos de 7 dígitos (11 no total). +- Os números abreviados `300X`/`400X`, com 8 dígitos. Outros prefixos de operadora, como `4020` e `4062`, são rejeitados. +- Os códigos de utilidade pública de 3 dígitos designados pela Anatel (ex.: `190`, `192`). O `112` e o `911` não estão entre eles e são rejeitados. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -539,11 +696,14 @@ isValidServicePhone('190'); // true isValidServicePhone('11987654321'); // false (número geográfico) ``` +Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151), [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86). + ### getAreaCodeInfo -Retorna o estado (e a região) a que um DDD brasileiro pertence, dentre os 67 DDDs em uso no Plano Geral de Numeração da Anatel. Aceita string ou número inteiro não negativo, removendo caracteres não numéricos antes de comparar. Exporta o tipo `AreaCodeInfo`. +Retorna o estado e a região a que um DDD brasileiro (código de área) pertence, dentre os 67 DDDs em uso no Plano Geral de Numeração da Anatel. Aceita string ou número inteiro não negativo. -`stateCode` é sempre um único estado: a sede do DDD, o estado da cidade em torno da qual o código foi alocado, que não é necessariamente o estado que concentra a maioria dos seus municípios. Quatro DDDs cruzam a divisa de um estado, e para esses o `stateCodes` lista também os demais. O DDD 61 é o mais amplo deles: atende o Distrito Federal e os doze municípios goianos do Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás e Vila Boa), então seu `stateCode` é `'DF'` mesmo o Distrito Federal tendo apenas um dos seus treze municípios, Brasília. Os outros três são o 42, compartilhado entre o Paraná e Porto União (SC), o 47, entre Santa Catarina e Rio Negro (PR), e o 49, entre Santa Catarina e Barracão (PR), e neles a sede realmente concentra todos os municípios menos o citado. +- Retorna um `AreaCodeInfo`: `areaCode`, `stateCode`, `stateName`, `regionCode`, `regionName` e `stateCodes`. Retorna `null` quando o DDD não está em uso. +- `stateCode` é o estado sede do DDD. Para os quatro DDDs que cruzam uma divisa (61, 42, 47 e 49) `stateCodes` lista também o outro estado, a sede primeiro. ```javascript import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; @@ -562,11 +722,14 @@ getAreaCodeInfo(-11); // null getAreaCodeInfo(1.1); // null ``` +Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais). + ### getAreaCodesByState -Retorna todos os DDDs (códigos de área) que atendem um determinado estado brasileiro, dentro do Plano Geral de Numeração da Anatel. A comparação não diferencia maiúsculas de minúsculas e o resultado vem ordenado de forma crescente. +Retorna todos os DDDs (códigos de área) que atendem um estado brasileiro, dentro do Plano Geral de Numeração da Anatel. A comparação não diferencia maiúsculas de minúsculas e o resultado vem em ordem crescente. -Um DDD que cruza a divisa de um estado aparece em todos os estados que atende, então o DDD 61 volta tanto para `'DF'` quanto para `'GO'`: ele atende o Distrito Federal e os doze municípios goianos do Entorno do Distrito Federal. Os outros três são o 42, compartilhado entre o Paraná e Porto União (SC), o 47, entre Santa Catarina e Rio Negro (PR), e o 49, entre Santa Catarina e Barracão (PR). +- Retorna `[]` quando `stateCode` não é um estado brasileiro. +- Um DDD de divisa (os mesmos quatro de `getAreaCodeInfo`) é listado em cada estado que atende. ```javascript import { getAreaCodesByState } from '@brazilian-utils/brazilian-utils'; @@ -579,11 +742,13 @@ getAreaCodesByState('SC'); // [42, 47, 48, 49] getAreaCodesByState('XX'); // [] ``` +Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais). + ## Placa de veículo ### isValidLicensePlate -Valida se a placa de carro ou moto é válida. Suporta o formato antigo brasileiro (ABC-1234) e o formato Mercosul (ABC1D23), a sequência única que a Resolução CONTRAN nº 969/2022 define para todo veículo, motos incluídas. +Valida uma placa de veículo. Aceita o formato antigo brasileiro (`ABC-1234`) e o formato Mercosul (`ABC1D23`), com ou sem hífen ou espaço, em maiúsculas ou minúsculas. ```javascript import { isValidLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -596,9 +761,13 @@ isValidLicensePlate('ABC12D3'); // false (não é uma sequência Mercosul) isValidLicensePlate('ABC1234EXTRA'); // false (caracteres em excesso) ``` +Fonte: [Resolução CONTRAN nº 969/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf), [Anexos](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf). + ### formatLicensePlate -Formata uma placa. Placas antigas brasileiras (`LLLNNNN`) são retornadas com hífen e placas Mercosul (`LLLNLNN`) permanecem normalizadas. Valores parciais são formatados até onde os caracteres informados alcançarem, então também pode ser usada como máscara de digitação, e um valor que não pode iniciar uma placa válida retorna `''`. +Formata uma placa. Placas antigas brasileiras (`LLLNNNN`) recebem hífen; placas Mercosul (`LLLNLNN`) são retornadas sem separador. + +- Retorna `''` quando o valor não pode iniciar uma placa válida. ```javascript import { formatLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -619,7 +788,9 @@ parseLicensePlate('abc-1234'); // 'ABC1234' ### generateLicensePlate -Gera uma placa aleatória no formato escolhido. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera uma placa válida aleatória no formato escolhido. + +- `format` (`GenerateLicensePlateFormat`): `'LLLNLNN'` (Mercosul, o padrão) ou `'LLLNNNN'` (o formato antigo brasileiro). Qualquer outro valor cai no padrão. ```javascript import { generateLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -629,11 +800,14 @@ generateLicensePlate('LLLNNNN'); // 'ABC1234' generateLicensePlate('LLLNNLN'); // 'ABC1D23' (um formato fora dos dois em circulação cai no padrão) ``` -Um `format` fora dos dois literais suportados recai no padrão Mercosul, como todo gerador deste pacote faz com uma opção que não conhece, então o resultado é sempre uma placa que `isValidLicensePlate` aceita. Essa sequência padrão é `LLLNLNN`, da Resolução CONTRAN nº 969/2022, Anexo I item 1.2, a única sequência que a resolução define para todo veículo, motocicletas incluídas. (A versão 2.3.0 usava uma string desconhecida literalmente, então `generateLicensePlate('LLLNNLN')` produzia a sequência de motocicleta que foi retirada e `generateLicensePlate('bogus')`, cinco dígitos; nenhuma das duas é uma placa.) +Fonte: [Resolução CONTRAN nº 969/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf). ### getFormatLicensePlate -Detecta o formato normalizado de uma placa. +Detecta o formato normalizado de uma placa: `'LLLNNNN'` para o formato antigo brasileiro, `'LLLNLNN'` para o Mercosul. + +- Retorna `null` quando o valor, sem os separadores, não tem 7 letras e dígitos em um dos dois formatos. +- Exporta o tipo `LicensePlateFormat`, que `generateLicensePlate` reexporta como `GenerateLicensePlateFormat`. ```javascript import { getFormatLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -645,11 +819,11 @@ getFormatLicensePlate('INVALID'); // null getFormatLicensePlate('ABC1234EXTRA'); // null (caracteres em excesso) ``` -`getFormatLicensePlate` exporta o tipo `LicensePlateFormat` (`"LLLNNNN" | "LLLNLNN"`); `generateLicensePlate` reexporta como `GenerateLicensePlateFormat`. - ### convertLicensePlateToMercosul -Converte uma placa brasileira no formato antigo (`LLLNNNN`) para o formato Mercosul (`LLLNLNN`), seguindo a tabela oficial de conversão: o dígito na 5ª posição vira uma letra (`0` a `9` mapeados para `A` a `J`). Retorna `""` quando o valor não é uma placa válida no formato antigo. +Converte uma placa brasileira no formato antigo (`LLLNNNN`) para o formato Mercosul (`LLLNLNN`). O 5º dígito vira uma letra, `0` a `9` mapeados para `A` a `J`. + +- Retorna `""` quando o valor não é uma placa válida no formato antigo. ```javascript import { convertLicensePlateToMercosul } from '@brazilian-utils/brazilian-utils'; @@ -659,11 +833,15 @@ convertLicensePlateToMercosul('abc-1234'); // 'ABC1C34' convertLicensePlateToMercosul('ABC1D23'); // '' (já está no formato Mercosul) ``` +Fonte: [Resolução CONTRAN nº 969/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf), [Anexo II](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf). + ## RENAVAM ### isValidRenavam -Valida se o RENAVAM (Registro Nacional de Veículos Automotores) é válido. Suporta tanto o formato antigo (9 dígitos) quanto o novo formato (11 dígitos). Espaços, pontos e hífens ao redor/entre os dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. Um registro com todos os dígitos iguais também é rejeitado. +Valida um RENAVAM (Registro Nacional de Veículos Automotores). Aceita o formato antigo (9 dígitos) e o formato novo (11 dígitos). + +- Espaços, pontos e hífens são ignorados; qualquer outro caractere invalida o valor. ```javascript import { isValidRenavam } from '@brazilian-utils/brazilian-utils'; @@ -678,7 +856,7 @@ isValidRenavam('ab00639884962'); // false (letras são rejeitadas) ### generateRenavam -Gera um RENAVAM válido aleatório: o formato de 11 dígitos, dez dígitos de base mais o dígito verificador. Uma base com todos os dígitos iguais é sorteada de novo, já que `isValidRenavam` rejeita essas. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um RENAVAM válido aleatório na forma de 11 dígitos: dez dígitos de base mais o dígito verificador. ```javascript import { generateRenavam } from '@brazilian-utils/brazilian-utils'; @@ -690,17 +868,22 @@ generateRenavam(); // '12345678900' ### isValidPis -Valida se o PIS é válido. Aceita os caracteres de máscara usuais (`.`, `-`, `/`, `(`, `)`, `,`, `*`) e espaços em branco. +Valida um PIS. Aceita o valor com ou sem máscara. + +- Um valor com todos os dígitos iguais é rejeitado. ```javascript import { isValidPis } from '@brazilian-utils/brazilian-utils'; +isValidPis('12056412847'); // true isValidPis('12056412547'); // false ``` ### formatPis -Formata número de PIS. `options.pad` (parte de `FormatPisOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara (padrão `false`). +Formata um PIS. + +- **Opções** (`FormatPisOptions`): `pad` completa o valor com zeros à esquerda até 11 dígitos antes de aplicar a máscara (padrão `false`). ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -721,7 +904,7 @@ parsePis('123.45678.90-1'); // 12345678901 ### generatePis -Gera um PIS válido aleatório. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um PIS válido aleatório. ```javascript import { generatePis } from '@brazilian-utils/brazilian-utils'; @@ -733,7 +916,10 @@ generatePis(); // '91077906857' ### isValidProcessoJuridico -Valida o número do processo jurídico de acordo com definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119): o layout `NNNNNNN-DD.AAAA.J.TR.OOOO`, os dígitos verificadores `DD` e o par `J`/`TR`, que precisa identificar um órgão e um tribunal existentes nas listas fechadas definidas pela Resolução CNJ nº 65/2008, de modo que um número com dígito verificador correto mas com um tribunal inexistente é rejeitado. As listas fechadas vêm do art. 1º, § 4º e § 5º da resolução, o § 5º, III na redação que a Resolução CNJ nº 477/2022 lhe deu para acomodar o TRF da 6ª Região. A unidade de origem (`OOOO`) é lida apenas como quatro dígitos, já que o art. 1º, § 6º deixa a codificação dela a cargo de cada tribunal e não publica lista central. Os separadores da máscara do CNJ (espaços, `.` e `-`) são aceitos entre os campos, e espaços em branco ao redor do valor são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida um número de processo jurídico, conforme a Resolução CNJ nº 65/2008. Três coisas são verificadas: o layout `NNNNNNN-DD.AAAA.J.TR.OOOO`, os dígitos verificadores `DD` (ISO 7064 MOD 97-10) e o par `J`/`TR`. + +- `J` e `TR` precisam nomear um órgão e um tribunal que existem. +- A unidade de origem (`OOOO`) é verificada apenas como quatro dígitos. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -745,9 +931,13 @@ isValidProcessoJuridico('0000100-23.2008.8.28.0000'); // false (não existe 28º isValidProcessoJuridico('ab00020802520125150049'); // false (letras são rejeitadas) ``` +Fonte: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ### formatProcessoJuridico -Formata um número no formato definido pelo [CNJ](https://atos.cnj.jus.br/atos/detalhar/119) (máscara `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (parte de `FormatProcessoJuridicoOptions`) completa o valor com zeros à esquerda até os 20 dígitos antes de aplicar a máscara (padrão `false`). +Formata um número de processo jurídico na máscara do CNJ `NNNNNNN-DD.AAAA.J.TR.OOOO`. + +- **Opções** (`FormatProcessoJuridicoOptions`): `pad` completa o valor com zeros à esquerda até 20 dígitos antes de aplicar a máscara (padrão `false`). ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -756,9 +946,11 @@ formatProcessoJuridico('00020802520125150049'); // 0002080-25.2012.5.15.0049 formatProcessoJuridico('20802520125150049', { pad: true }); // 0002080-25.2012.5.15.0049 ``` +Fonte: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ### parseProcessoJuridico -Remove a formatação do processo jurídico, mantém apenas os dígitos e limita o resultado a 20 dígitos. Tanto a máscara atual do CNJ (`NNNNNNN-DD.AAAA.J.TR.OOOO`) quanto a máscara antiga são aceitas, já que apenas os dígitos são mantidos. +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'; @@ -768,7 +960,11 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ### generateProcessoJuridico -Gera um número de processo jurídico válido de acordo com a definição do [CNJ](https://atos.cnj.jus.br/atos/detalhar/119). `year` deve estar entre o ano atual e 9999, `court` entre 1 e 9; valores fora do intervalo retornam `null`. O órgão (`J`) e o tribunal (`TR`) são sorteados das listas fechadas do art. 1º, § 4º e § 5º, então o par sempre nomeia um tribunal que existe: `court` escolhe o órgão e o `TR` é sorteado entre os tribunais que aquele órgão tem. A unidade de origem (`OOOO`) é sorteada livremente, já que a resolução não publica lista central para ela. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um número de processo jurídico válido aleatório no layout da Resolução CNJ nº 65/2008. + +- **Opções** (`GenerateProcessoJuridicoParams`): `year` define o campo `AAAA`, um inteiro entre o ano atual e 9999 (padrão: o ano atual); `court` define o órgão `J`, de 1 a 9 (padrão: aleatório). +- `TR` é sorteado entre os tribunais do órgão escolhido, então o par sempre nomeia um tribunal que existe. +- Retorna `null` quando `year` ou `court` está fora do intervalo. ```javascript import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -779,11 +975,16 @@ generateProcessoJuridico({ year: 10000 }); // null (ano fora do intervalo) generateProcessoJuridico({ court: 10 }); // null (órgão inexistente) ``` +Fonte: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ## Contas bancárias e bancos ### isValidBankAccount -Verifica se uma conta bancária brasileira é válida. O `bankCode` precisa estar na lista de participantes do STR publicada pelo Banco Central do Brasil (o mesmo dataset usado por `getBankByCode`), então um código não atribuído como `'999'` é sempre inválido. A partir daí o banco é validado de uma de três formas: pelo algoritmo de dígito verificador publicado, apenas pela estrutura (o banco existe e a agência/conta respeitam a quantidade de dígitos documentada, para bancos que não publicam regra de dígito) ou pela verificação genérica mod10/mod11, que continua sendo o fallback para os demais bancos da lista. +Valida uma conta bancária brasileira. O `bankCode` precisa ser um participante do STR do Banco Central (a lista que `getBankByCode` usa). + +- **Parâmetros** (`IsValidBankAccountParams`, todos strings): `bankCode` (3 dígitos), `agency` (1-5 dígitos), `account` (1-13 dígitos) e `digit` (1-2 caracteres, ou `X` para o Banco do Brasil e `P` para o Bradesco). +- Um banco da lista é validado de uma de três formas: pelo algoritmo de dígito verificador publicado, apenas pela estrutura ou por um fallback genérico mod10/mod11. Bancos validados pelo algoritmo de dígito verificador publicado: @@ -799,7 +1000,7 @@ Bancos validados pelo algoritmo de dígito verificador publicado: | HSBC / Kirton Bank | `399` | 4 dígitos | 6 dígitos | pesos `8,9,2,3,4,5,6,7,8,9` sobre agência + conta; resto 10 gera `0` | | Citibank | `745` | 4 dígitos | 10 dígitos | pesos `11..2` sobre a conta; resto 0 ou 1 gera `0` | -Bancos validados apenas pela estrutura, por não publicarem regra de dígito verificador. A agência (1-5 dígitos), a conta (1-13 dígitos) e um único `digit` numérico já tornam a conta válida: +Bancos validados apenas pela estrutura (um único `digit` numérico basta): | Banco | Código | | Banco | Código | | --- | --- | --- | --- | --- | @@ -813,9 +1014,7 @@ Bancos validados apenas pela estrutura, por não publicarem regra de dígito ver | PagBank | `290` | | Sicredi | `748` | | BMG | `318` | | Sicoob | `756` | -Quando `digit` tem 2 caracteres, o fallback genérico encadeia mod10 seguido de mod11 sobre a conta, do mesmo jeito que os dígitos de CPF/CNPJ são encadeados. - -Fontes: o compêndio "Regras de Validação de dígito verificador de agência e conta corrente", conferido contra `banktools-br` (Ruby), `luizalabs/heimdall` (Python) e `Xerpa/bran_checker` (Elixir). Cada algoritmo publicado aqui tem pelo menos duas fontes independentes concordantes. +- Todo outro banco da lista usa o fallback genérico: `digit` precisa bater com mod10 ou mod11 sobre a conta. Um `digit` de 2 caracteres encadeia mod10 e depois mod11. ```javascript import { isValidBankAccount } from '@brazilian-utils/brazilian-utils'; @@ -884,9 +1083,13 @@ isValidBankAccount({ }); // true (Banco ABC Brasil, fallback genérico mod10) ``` +Fonte: [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv), [Regras de Validação de dígito verificador](https://github.com/eduardokum/laravel-boleto/blob/master/manuais/Regras%20Validacao%20Conta%20Corrente%20VI_EPS.pdf). + ### getBanks -Obtém todos os bancos brasileiros com código de compensação (COMPE), publicados pelo Banco Central do Brasil na [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Cada banco (tipado como `Bank`) tem um `code` (COMPE, 3 dígitos), um `ispb` (Identificador do Sistema de Pagamentos Brasileiro, 8 dígitos) e um `name`. Cada chamada retorna um novo array com novos objetos, então alterar o resultado nunca afeta chamadas seguintes. +Obtém todos os bancos brasileiros com código de compensação (COMPE), a partir da lista de participantes do STR do Banco Central do Brasil. + +- Cada banco (`Bank`) tem um `code` (COMPE, 3 dígitos), um `ispb` (8 dígitos) e um `name`. ```javascript import { getBanks } from '@brazilian-utils/brazilian-utils'; @@ -900,9 +1103,13 @@ getBanks(); // ] ``` +Fonte: [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). + ### getBankByCode -Busca um banco brasileiro pelo seu código de compensação (COMPE), publicado pelo Banco Central do Brasil na [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Aceita tanto `string` quanto `number`, com ou sem zeros à esquerda. Retorna uma nova cópia (tipada como `Bank`) do banco correspondente, ou `null` quando nenhum banco tem esse código. +Busca um banco brasileiro pelo seu código de compensação (COMPE), a partir da lista de participantes do STR do Banco Central do Brasil. Aceita `string` ou `number`. + +- Retorna o `Bank` correspondente, ou `null` quando nenhum banco tem esse código. ```javascript import { getBankByCode } from '@brazilian-utils/brazilian-utils'; @@ -912,9 +1119,13 @@ getBankByCode(1); // { code: '001', ispb: '00000000', name: 'Banco do Brasil S.A getBankByCode('999'); // null ``` +Fonte: [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). + ### getBankByIspb -Busca um banco brasileiro pelo seu ISPB (Identificador do Sistema de Pagamentos Brasileiro), o código de 8 dígitos publicado pelo Banco Central do Brasil na [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Todo participante do SPB tem um ISPB, mas este conjunto de dados só traz as instituições que também têm código COMPE, então um ISPB cuja instituição não tem código COMPE próprio retorna `null`. Aceita tanto `string` quanto `number`, com ou sem zeros à esquerda, então `getBankByIspb(0)` encontra o mesmo banco que `getBankByIspb('00000000')`. O conjunto de dados é gerado a partir desse CSV, recorrendo à [BrasilAPI](https://brasilapi.com.br/api/banks/v1) quando a requisição ao Bacen falha. Retorna uma nova cópia (tipada como `Bank`) do banco correspondente, ou `null` quando nenhum banco tem esse ISPB. +Busca um banco brasileiro pelo seu ISPB (Identificador do Sistema de Pagamentos Brasileiro), o código de 8 dígitos de todo participante do SPB. Aceita `string` ou `number`, com ou sem zeros à esquerda. + +- Retorna o `Bank` correspondente, ou `null` quando nenhum banco tem esse ISPB. A base só traz as instituições que também têm código COMPE. ```javascript import { getBankByIspb } from '@brazilian-utils/brazilian-utils'; @@ -924,11 +1135,17 @@ getBankByIspb('60701190'); // { code: '341', ispb: '60701190', name: 'ITAÚ UNIB getBankByIspb('99999999'); // null ``` +Fonte: [lista de participantes do STR](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv), [BrasilAPI](https://brasilapi.com.br/api/banks/v1). + ## IBAN ### isValidIban -Valida se um IBAN (International Bank Account Number) brasileiro é válido, conforme as [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) do Bacen (Circular BCB nº 3.625/2013): `BR` + 2 dígitos verificadores ISO 7064 MOD 97-10 + 8 dígitos de ISPB + 5 dígitos de agência + 10 dígitos de conta + 1 letra de tipo de conta (qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 indicador de titularidade (`1` para o primeiro ou único titular até `9` para o nono, depois `A` a `Z` a partir do décimo, então `0` é rejeitado), totalizando 29 caracteres. Somente IBANs brasileiros (código de país `BR`) são reconhecidos; qualquer outro país retorna `false`, já que este pacote não conhece o layout de campos dos outros mais de 90 países da ISO 13616. Não diferencia maiúsculas de minúsculas e aceita as duas formas em que um IBAN é escrito: compacta (`'BR1500000000000010932840814P2'`) ou no formato impresso da ISO 13616, letras e dígitos em grupos de 4 (o último menor), em ambos os casos com espaços em branco opcionais no início e no fim. Os grupos podem ser separados por espaço em branco, `.`, `-` ou `/`, os caracteres de máscara intercambiáveis que `isValidCpf` e `isValidCnpj` aceitam. Apenas um separador fora do limite de um grupo, uma sequência de separadores (a ISO 13616 imprime um único) ou um caractere fora de letras e dígitos faz do valor algo que não é um IBAN, então ele é rejeitado em vez de removido. +Valida um IBAN (International Bank Account Number) brasileiro. Somente IBANs brasileiros (código de país `BR`) são reconhecidos; qualquer outro país retorna `false`. + +- Layout, 29 caracteres: `BR`, 2 dígitos verificadores (ISO 7064 MOD 97-10), ISPB de 8 dígitos, agência de 5, conta de 10, 1 letra de tipo de conta, 1 indicador de titularidade. +- Tipo de conta: qualquer letra, normalmente `C` ou `P`. Titularidade: `1` a `9`, depois `A` a `Z`. +- Aceita a forma compacta ou grupos de 4 separados por um espaço, `.`, `-` ou `/`, em maiúsculas ou minúsculas. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; @@ -941,9 +1158,13 @@ isValidIban('BR15 000 00000 0000 1093 2840 814P 2'); // false (separador dentro isValidIban('DE89370400440532013000'); // false (IBAN não brasileiro) ``` +Fonte: [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf), [Circular BCB nº 3.625/2013](https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf), [ISO 13616-1:2020](https://www.iso.org/standard/81090.html). + ### formatIban -Formata um IBAN no agrupamento impresso da ISO 13616, blocos de 4 caracteres, a apresentação usada em extratos e formulários bancários. Não valida os dígitos verificadores nem o layout dos campos; formata o que for passado, até o limite de 29 caracteres de um IBAN brasileiro, até onde for possível, então a função também pode ser usada como máscara de digitação, e um IBAN de outro país é agrupado do mesmo jeito até esse limite. Use `isValidIban` para verificar a validade. O valor pode ser compacto (`'BR1500000000000010932840814P2'`), já estar no formato impresso da ISO 13616 ou ser um valor parcial ainda sendo digitado (`'BR15'`); como todo formatador deste pacote, ele é lido pelas suas letras e dígitos e agrupado até onde eles vão, qualquer outro caractere (hífen, ponto, espaço a mais) é descartado e as letras viram maiúsculas. Só um valor que não seja string resulta em uma string vazia. +Formata um IBAN no agrupamento impresso da ISO 13616: blocos de 4 caracteres, a apresentação usada em extratos e formulários bancários. Não valida; para isso, use `isValidIban`. + +- Limita o resultado a 29 caracteres, o tamanho de um IBAN brasileiro. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -956,7 +1177,7 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ### parseIban -Remove a formatação do IBAN, mantém as letras e os dígitos, coloca o resultado em maiúsculas e o limita aos 29 caracteres de um IBAN brasileiro. Um IBAN carrega letras além de dígitos, então o valor é lido como o `parsePassport` lê um número de passaporte; use `isValidIban` para verificar os dígitos verificadores e `getIbanInfo` para ler os campos. +Remove a formatação do IBAN, mantém as letras e os dígitos, coloca o resultado em maiúsculas e o limita aos 29 caracteres de um IBAN brasileiro. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -967,7 +1188,10 @@ parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR15000000000000109328408 ### getIbanInfo -Interpreta um IBAN brasileiro em seus campos: 2 (código do país, sempre `BR`) + 2 (dígitos verificadores ISO 7064 MOD 97-10) + 8 (ISPB) + 5 (agência) + 10 (conta) + 1 (tipo de conta, qualquer letra, normalmente `C` para conta corrente ou `P` para conta poupança) + 1 (indicador do titular, `1` a `9` e depois `A` a `Z`). Apenas IBANs brasileiros são suportados: o layout de campos dos demais países da ISO 13616 está fora de escopo, então um IBAN bem formado que não seja `BR` também retorna `null`. Aceita as mesmas formas de entrada que `isValidIban`, compacta ou no formato impresso da ISO 13616 (grupos de 4 separados por um único espaço em branco, `.`, `-` ou `/`), em ambos os casos com espaços em branco opcionais no início e no fim e sem diferenciar maiúsculas de minúsculas, e retorna `null` sempre que `isValidIban` retornaria `false`, inclusive quando o valor carrega um separador fora do limite de um grupo, uma sequência de separadores ou qualquer caractere além de letras e dígitos. O resultado é tipado como `IbanInfo`, cujo `accountType` é uma `string`. +Interpreta um IBAN brasileiro em seus campos. Retorna um objeto `IbanInfo`, ou `null` sempre que `isValidIban` retornaria `false`. + +- Campos, todos strings: `countryCode`, `checkDigits`, `bankIspb`, `branch`, `account`, `accountType` (normalmente `C` ou `P`) e `owner` (`1` a `9`, depois `A` a `Z`). +- Mesmas regras de entrada de `isValidIban`. ```javascript import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; @@ -987,11 +1211,17 @@ getIbanInfo('DE89370400440532013000'); // null (IBAN não brasileiro) getIbanInfo('BR15 000 00000 0000 1093 2840 814P 2'); // null (separador dentro de um grupo) ``` +Fonte: [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf), [Circular BCB nº 3.625/2013](https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf), [ISO 13616-1:2020](https://www.iso.org/standard/81090.html). + ## Moeda, números e datas por extenso ### formatCurrency -Formata um número inteiro ou float para uma string no padrão BRL. Um `number` é formatado como está (sinal e decimais preservados). Uma entrada em `string` é lida pela mesma regra do `parseCurrency`, com a diferença de que um valor escrito sem nenhum separador permanece em unidades inteiras: o último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal, todo outro `,` ou `.` é separador de milhar, e um `-` escrito antes do primeiro dígito é preservado. Assim `'1.234,56'` vira `1.234,56`, `'-10.5'` vira `-10,50` e `'1234'` vira `1.234,00`. `precision` é limitado ao intervalo `0..20` (o limite do pacote, o que o Node 20 ainda impõe ao `Intl.NumberFormat`), o padrão é 2 e volta a 2 quando não é um número finito. Um valor que não seja um número finito (`NaN`, `Infinity`, `-Infinity`) vira string vazia, e um valor que não pode ser convertido em número (um symbol, um objeto simples, um objeto sem protótipo) também; `null`, arrays e booleanos passam por `Number()` como no 2.3.0. `options.symbol` prefixa o resultado com o símbolo monetário `R$` (padrão `false`). As opções são tipadas como `FormatCurrencyOptions`. +Formata um número ou uma string numérica no padrão BRL (`1.234,56`). Um `number` é formatado como está, com sinal e decimais preservados. + +- **Opções** (`FormatCurrencyOptions`): `symbol` (padrão `false`) prefixa o resultado com `R$`; `precision` (padrão 2) define as casas decimais, limitada de 0 a 20. +- Uma `string` é lida como `parseCurrency` a lê, com uma diferença: um valor sem nenhum separador permanece em unidades inteiras, então `'1234'` vira `1.234,00`. +- Retorna `''` para um valor não finito ou que não pode ser convertido em número. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1009,7 +1239,11 @@ formatCurrency(Number.NaN); // "" (números não finitos viram string vazia) ### parseCurrency -Transforma uma string para o formato de inteiro ou float. O último `,` ou `.` seguido de 1 ou 2 dígitos (ou de até `precision` dígitos, quando esse valor for maior) é o separador decimal; todo outro `,` ou `.` é separador de milhar. Assim `'R$ 1.234,56'` vira `1234.56`, `'R$ 1.234'` vira `1234`, `'1,5'` vira `1.5` e `'12.34'` vira `12.34`. Um valor escrito sem nenhum separador mantém a convenção de centavos e é dividido por `10 ** precision`, então `'1234'` vira `12.34`. Um `-` escrito antes do primeiro dígito é preservado, então `'-R$ 1,00'` vira `-1`. `precision` (padrão 2, limitado a `0..20`, e voltando a 2 quando não é um número finito) controla quantos dígitos são tratados como subunidades monetárias. As opções são tipadas como `ParseCurrencyOptions`. +Converte uma string de moeda no padrão BRL em número. + +- **Opções** (`ParseCurrencyOptions`): `precision` (padrão 2) é a quantidade de dígitos lidos como subunidades monetárias, limitada de 0 a 20. +- O último `,` ou `.` seguido de 1 a 2 dígitos (até `precision`, quando maior) é o separador decimal; todo outro `,` ou `.` é separador de milhar. +- Um valor sem nenhum separador é lido como centavos e dividido por `10 ** precision`. ```javascript import { parseCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1027,7 +1261,11 @@ parseCurrency(''); // 0 ### convertNumberToWords -Formata um número inteiro por extenso em português do Brasil, ex.: `1235` vira `"mil duzentos e trinta e cinco"`. Só são suportados inteiros de `-999999999999999` a `999999999999999` (999 trilhões em valor absoluto); fora desse intervalo, `NaN` ou um valor não finito retornam `""`. Um `value` não inteiro é truncado em direção a zero antes da conversão. `options.gender` (parte de `ConvertNumberToWordsOptions`) concorda "um/dois" e a centena ("duzentos/duzentas" etc.) com o substantivo que o número qualifica, com padrão `"masculine"`. Um valor inválido de `gender` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. +Escreve um número inteiro por extenso em português do Brasil: `1235` vira `"mil duzentos e trinta e cinco"`. + +- **Opções** (`ConvertNumberToWordsOptions`): `gender` (padrão `"masculine"`) concorda "um/dois" e a centena ("duzentos/duzentas") com o substantivo que o número qualifica. +- Aceita inteiros de `-999999999999999` a `999999999999999` (999 trilhões). Um valor não inteiro é truncado em direção a zero. +- Retorna `""` para um valor fora desse intervalo ou não finito. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -1043,7 +1281,10 @@ convertNumberToWords(NaN); // "" ### convertCurrencyToWords -Formata um valor monetário em Reais por extenso, no estilo usado para escrever o valor à mão em cheques e contratos, ex.: `1523.45` vira `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. O `value` é truncado (não arredondado) para 2 casas decimais. O substantivo no singular é usado para exatamente 1 ("um real", "um centavo") e "de" é inserido antes de "reais" quando o valor é um milhão, bilhão ou trilhão de reais redondo. Um valor que trunca para nada vira `"zero reais"`, sem o prefixo "menos"; qualquer outro valor negativo recebe o prefixo "menos", e uma entrada inválida retorna `""`. Acima de `Number.MAX_SAFE_INTEGER / 100` reais (cerca de 90 trilhões) um double não consegue carregar centavos, então o valor é lido como um número inteiro de reais. Não recebe opções: o resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. +Escreve um valor em reais por extenso, como em cheques e contratos: `1523.45` vira `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. Não recebe opções. + +- O `value` é truncado (não arredondado) para 2 casas decimais. +- Retorna `""` para uma entrada inválida ou um valor acima de 999 trilhões de reais. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; @@ -1059,7 +1300,10 @@ convertCurrencyToWords(-0.001); // "zero reais" (trunca para nada) ### convertDateToWords -Formata uma data por extenso em português do Brasil, ex.: `"01/01/2024"` vira `"primeiro de janeiro de dois mil e vinte e quatro"`. Aceita um `Date` (lido pela sua data de calendário local, a mesma convenção usada por `isHoliday`) ou uma string no formato `"dd/mm/yyyy"` ou ISO `"yyyy-mm-dd"`. Com o `options.style` padrão `"full"`, o dia 1 é escrito como "primeiro" e os demais dias usam o número cardinal; com `"month"`, só o nome do mês é escrito por extenso e o dia/ano ficam em dígitos (o dia 1 como `"1º"`, ex.: `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Os nomes dos meses ficam em minúsculo. No estilo `"full"` o ano é escrito por extenso sem a vírgula de milhar que `convertNumberToWords`/`convertCurrencyToWords` usam (`1999` vira `"mil novecentos e noventa e nove"`, não `"mil novecentos e noventa e nove"`), do jeito que uma data é lida em voz alta. `options.weekday` (padrão `false`) prefixa o nome do dia da semana em pt-BR minúsculo seguido de vírgula (`"sábado, dois de março de dois mil e vinte e quatro"`), calculado a partir da data de calendário resolvida. Um valor inválido de `style` é ignorado e o padrão é usado. O resultado sai sempre em minúsculas; aplique qualquer outra caixa por conta própria. O dia 29 de fevereiro é aceito nos anos bissextos do calendário gregoriano proléptico (divisíveis por 4, exceto séculos não divisíveis por 400). Retorna `""` para um `Date` inválido, uma string malformada, um dia/mês que não existe ou uma data anterior ao ano 1. +Escreve uma data por extenso em português do Brasil: `"01/01/2024"` vira `"primeiro de janeiro de dois mil e vinte e quatro"`. Aceita um `Date`, lido pela sua data de calendário local, ou uma string no formato `"dd/mm/yyyy"` ou ISO `"yyyy-mm-dd"`. + +- **Opções** (`ConvertDateToWordsOptions`): `style` (padrão `"full"`) escreve dia, mês e ano por extenso; `"month"` escreve só o mês e deixa dia e ano em dígitos, o dia 1 como `"1º"`. `weekday` (padrão `false`) prefixa o nome do dia da semana em minúsculas e uma vírgula. +- Retorna `""` para um `Date` inválido, uma string malformada, um dia ou mês que não existe ou uma data anterior ao ano 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; @@ -1080,7 +1324,10 @@ convertDateToWords('29/02/1900'); // "" (1900 não é bissexto) ### getStates -Retorna todos os estados brasileiros, cada um com sigla, nome, código da região, nome da região e código IBGE de 2 dígitos da Unidade da Federação (`cUF`). A lista é ordenada por nome com `localeCompare` no locale "pt-BR", então nomes acentuados caem onde um leitor brasileiro espera: Pará, Paraíba, Paraná e Rio de Janeiro, Rio Grande do Norte, Rio Grande do Sul. Cada chamada retorna um array novo com objetos novos, então alterar o resultado nunca afeta chamadas seguintes. Exporta os tipos `State`, `StateCode` e `StateName`. `State` é uma união discriminada com um membro por estado, então os campos de um estado ficam amarrados entre si: estreitar um `State` pelo `code` também estreita `name`, `regionCode`, `regionName` e `ibgeCode` (`Extract['name']` é `'São Paulo'`), e uma combinação impossível como `{ code: 'SP', name: 'Acre' }` não é um `State`. +Retorna todos os estados brasileiros, cada um com sigla, nome, código da região, nome da região e código IBGE de 2 dígitos (`cUF`). + +- Ordenados por nome no locale "pt-BR". +- Exporta os tipos `State`, `StateCode` e `StateName`. `State` é uma união discriminada: estreitá-lo pelo `code` também estreita os demais campos. ```javascript import { getStates } from '@brazilian-utils/brazilian-utils'; @@ -1117,9 +1364,15 @@ getStates(); // ] ``` +Fonte: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) + ### getStateByIbgeCode -Retorna o estado brasileiro cujo código IBGE de 2 dígitos ("cUF", Código da Unidade da Federação) corresponde ao valor informado. É o mesmo código de UF de 2 dígitos presente no primeiro campo de toda chave de acesso de DF-e de qualquer um dos modelos que o `isValidNfeKey` cobre: NF-e (55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) e NFCom (62). Aceita string ou número inteiro não negativo, removendo caracteres não numéricos antes de comparar. Exporta o tipo `State`. +Retorna o estado brasileiro cujo código IBGE de 2 dígitos (`cUF`, o Código da Unidade da Federação) corresponde ao valor informado. + +- É o código de UF do primeiro campo de uma chave de acesso de DF-e, a que `isValidNfeKey` cobre. +- Aceita string ou número inteiro não negativo. +- Retorna `null` quando o código não corresponde a nenhum estado. Exporta o tipo `State`. ```javascript import { getStateByIbgeCode } from '@brazilian-utils/brazilian-utils'; @@ -1135,9 +1388,14 @@ getStateByIbgeCode(-35); // null getStateByIbgeCode(3.5); // null ``` +Fonte: [IBGE Localidades](https://servicodados.ibge.gov.br/api/v1/localidades/estados), [Manual de Orientação do Contribuinte](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf) + ### getStateCodeByName -Retorna a sigla de um estado brasileiro a partir do nome completo. A comparação ignora acentos, maiúsculas/minúsculas e espaços nas pontas, então `'sao paulo'`, `'SÃO PAULO'` e `' São Paulo '` resolvem para `'SP'`. Toda sequência de espaços internos também vira um único espaço, então `'Rio de Janeiro'` resolve para `'RJ'`, enquanto um nome escrito sem o espaço não corresponde a nada (`'saopaulo'` não é `'São Paulo'`). Exporta o tipo `StateCode`. +Retorna a sigla de um estado brasileiro a partir do nome completo. + +- A comparação ignora acentos, não diferencia maiúsculas de minúsculas e remove os espaços nas pontas; espaços internos repetidos viram um só. +- Retorna `null` quando nenhum estado corresponde. Exporta o tipo `StateCode`. ```javascript import { getStateCodeByName } from '@brazilian-utils/brazilian-utils'; @@ -1150,7 +1408,10 @@ getStateCodeByName('Neverland'); // null ### getStateNameByCode -Retorna o nome completo de um estado brasileiro a partir da sigla. A comparação ignora maiúsculas/minúsculas e espaços nas pontas, então `'sp'`, `'SP'` e `' Sp '` resolvem para `'São Paulo'`. Exporta o tipo `StateName`. +Retorna o nome completo de um estado brasileiro a partir da sigla. + +- A comparação não diferencia maiúsculas de minúsculas e remove os espaços nas pontas. +- Retorna `null` quando nenhum estado corresponde. Exporta o tipo `StateName`. ```javascript import { getStateNameByCode } from '@brazilian-utils/brazilian-utils'; @@ -1163,7 +1424,10 @@ getStateNameByCode('ZZ'); // null ### getTimezoneByState -Retorna o nome do fuso horário do banco de dados IANA (tzdata) para um estado brasileiro, escolhido como o fuso da capital do estado. A comparação ignora maiúsculas/minúsculas e espaços nas pontas. Alguns fusos do tzdata cobrem mais de um estado: `America/Sao_Paulo` também cobre DF, GO, MG, ES, RJ, PR, SC e RS além de SP, e `America/Fortaleza` também cobre MA, PI, RN e PB além do CE. Pernambuco resolve para `America/Recife`, não `America/Noronha`: Fernando de Noronha é um distrito arquipélago de PE, não um estado próprio. +Retorna o nome do fuso horário IANA (zona do tzdata) de um estado brasileiro: o fuso da sua capital. + +- A comparação não diferencia maiúsculas de minúsculas e remove os espaços nas pontas. +- Retorna `null` quando nenhum estado corresponde. ```javascript import { getTimezoneByState } from '@brazilian-utils/brazilian-utils'; @@ -1175,9 +1439,16 @@ getTimezoneByState('PE'); // 'America/Recife' getTimezoneByState('ZZ'); // null ``` +Fonte: [IANA Time Zone Database](https://www.iana.org/time-zones) + ### getMunicipalities -Retorna os municípios brasileiros publicados pelo IBGE. Retorna todos os municípios se nenhum estado for fornecido, ou os municípios de um estado específico. Cada município é retornado como `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos do município. Os resultados são ordenados por nome com `localeCompare` no locale "pt-BR". Cada chamada retorna um array novo com objetos novos, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido retorna um array vazio em vez de lançar erro. Só um `stateCode` omitido (ou `undefined`) pede a lista completa: `getMunicipalities(null)` e `getMunicipalities('')` retornam `[]`, enquanto os mais permissivos `getCities(null)` e `getCities('')` retornam todas as cidades. O código do estado é comparado exatamente, inclusive na caixa: `getMunicipalities('sp')` retorna `[]` enquanto `getMunicipalities('SP')` retorna os 645 municípios paulistas. `getMunicipalities` e `getCities` são as únicas buscas por estado sensíveis à caixa; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` e `getMunicipality` ignoram a caixa. +Retorna os municípios brasileiros publicados pelo IBGE: todos os municípios, ou só os de um estado quando `stateCode` é informado. + +- Cada município (`Municipality`) é `{ code, name, stateCode }`, onde `code` é o código IBGE de 7 dígitos. Ordenados por nome no locale "pt-BR". +- Só um `stateCode` omitido (ou `undefined`) pede a lista completa: `null` e `''` retornam `[]`. +- `stateCode` diferencia maiúsculas de minúsculas: `'sp'`, como um código desconhecido, retorna `[]`. +- Embute todos os 5571 municípios. Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle) para carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-municipalities`. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1207,11 +1478,14 @@ getMunicipalities('SP'); getMunicipalities('ZZ'); // [] ``` -`getMunicipalities` embute todos os 5571 municípios do IBGE e seus códigos, então carrega o mesmo custo de tamanho de pacote que `getCities`. Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-municipalities` em vez do import da raiz. +Fonte: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) ### getMunicipalityByCode -Busca um município brasileiro pelo código IBGE de 7 dígitos. Aceita o código como string ou número, removendo qualquer caractere não numérico antes de comparar; um código informado como número precisa ser um inteiro não negativo, então `-3550308` e `355030.8` retornam `null` em vez de serem lidos como `3550308`. Retorna `{ code, name, stateCode }`, um objeto novo, ou `null` quando o código não tem 7 dígitos ou não corresponde a nenhum município conhecido. +Busca um município brasileiro pelo código IBGE de 7 dígitos. + +- Aceita o código como string ou número inteiro não negativo. +- Retorna `{ code, name, stateCode }` (`Municipality`), ou `null` quando o código não tem 7 dígitos ou não corresponde a nenhum município. ```javascript import { getMunicipalityByCode } from '@brazilian-utils/brazilian-utils'; @@ -1226,9 +1500,16 @@ getMunicipalityByCode('0000000'); // null (código desconhecido) getMunicipalityByCode('123'); // null (não tem 7 dígitos) ``` +Fonte: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) + ### getCities -Retorna as cidades brasileiras. **Obsoleta:** use `getMunicipalities` no lugar. Retorna todas as cidades se nenhum estado for fornecido, ou cidades de um estado específico. Cada chamada retorna um array novo, então alterar o resultado nunca afeta chamadas seguintes. Um código de estado desconhecido (ou um valor que não seja `StateCode`) retorna um array vazio em vez de lançar erro, exceto quando é um valor falsy: `getCities(null)` e `getCities('')` são lidos como "nenhum estado informado" e retornam todas as cidades, enquanto o mais estrito `getMunicipalities` retorna `[]` para eles. O código do estado é comparado exatamente, inclusive na caixa: `getCities('sp')` retorna `[]` enquanto `getCities('SP')` retorna as 645 cidades paulistas. `getCities` e `getMunicipalities` são as únicas buscas por estado sensíveis à caixa; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` e `getMunicipality` ignoram a caixa. +Retorna os nomes das cidades brasileiras: todas as cidades, ou só as de um estado. **Descontinuada:** use `getMunicipalities` no lugar. + +- Ordenadas no locale "pt-BR". +- Qualquer `state` falsy pede a lista completa, enquanto `getMunicipalities` retorna `[]`. +- `state` diferencia maiúsculas de minúsculas: `'sp'`, como um código desconhecido, retorna `[]`. +- Embute os 5571 nomes (~154,2 KB minificado, ~49,8 KB com gzip). Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle) para carregá-la sob demanda via `@brazilian-utils/brazilian-utils/get-cities`. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1266,11 +1547,16 @@ getCities('SP'); // ] ``` -`getCities` embute os nomes dos 5571 municípios do IBGE (~154,2 KB minificado, ~49,8 KB com gzip) e é uma das poucas exceções pesadas neste pacote, que é tree-shakeable no restante. Veja [Tamanho do bundle](pt-br/getting-started.md#tamanho-do-bundle) para saber como carregá-lo sob demanda via `@brazilian-utils/brazilian-utils/get-cities` em vez do import da raiz. +Fonte: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) ### 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. **Obsoleta:** use `getMunicipalityByCode` no lugar, que é síncrona e offline; casar um município pelo nome fica a cargo da aplicação, sobre `getMunicipalities`. Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. `code` aceita tanto `string` quanto `number` e deve ter exatamente 7 dígitos, caso contrário a função resolve para `null`. Um `code` informado como número precisa ser um inteiro não negativo: sinal e ponto decimal não são dígitos, então `-3550308` e `355030.8` resolvem para `null` em vez de serem lidos como `3550308`. A resolução é totalmente offline, a partir de um dataset do IBGE embutido na biblioteca: nenhuma requisição de rede é feita. A comparação do nome do município ignora acentos e diferenças entre maiúsculas/minúsculas, e toda sequência de espaços vira um único espaço, então `'sao paulo'` corresponde a `'São Paulo'`, enquanto um nome escrito sem o espaço não; a caixa é convertida para maiúsculas, a direção em que o Unicode expande `'ß'` para `'SS'`, então `'Paßos'` corresponde a `'Passos'`. Um município desconhecido, uma UF desconhecida ou uma entrada inválida resolvem para `null`. O par `[name, uf]` é um array novo a cada chamada, então alterar o resultado nunca afeta as buscas seguintes. +Busca informações de município por código IBGE, ou um código IBGE a partir do nome do município e UF. **Descontinuada:** use `getMunicipalityByCode` no lugar, que é síncrona e offline; casar um município pelo nome fica a cargo da aplicação, sobre `getMunicipalities`. + +- Uma única função cobre as duas direções, dependendo se `options` tem `code` ou `municipalityName`/`uf`. A busca é offline: nenhuma requisição de rede é feita. +- A comparação do nome ignora acentos, não diferencia maiúsculas de minúsculas e reduz espaços repetidos a um só. +- Resolve para `null` para um município desconhecido, uma UF desconhecida ou uma entrada inválida. +- `GetMunicipalityOptions`, `GetMunicipalityByCodeOptions` e `GetMunicipalityByNameOptions` são aliases descontinuados dos tipos abaixo. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1291,8 +1577,6 @@ await getMunicipality({ code: '123' }); // null (não tem 7 dígitos) ``` -Em TypeScript o tipo de retorno acompanha a direção da busca: uma consulta `{ code }` resolve para `[string, string] | null`, uma consulta `{ municipalityName, uf }` resolve para `string | null`, e uma consulta cuja direção só é conhecida em tempo de execução (uma variável tipada como `GetMunicipalityParams`) resolve para a união das duas. Os nomes da 2.3.0 `GetMunicipalityOptions`, `GetMunicipalityByCodeOptions` e `GetMunicipalityByNameOptions` continuam exportados como aliases deprecados destes. - ```typescript import { getMunicipality, @@ -1314,22 +1598,19 @@ const lookUp = (options: GetMunicipalityParams) => getMunicipality(options); // (options: GetMunicipalityParams) => Promise<[string, string] | string | null> ``` +Fonte: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) + ## Feriados e dias úteis ### getHolidays -Retorna feriados brasileiros para um determinado ano. Retorna feriados nacionais e opcionalmente feriados estaduais. Cada feriado (tipado como `Holiday`) tem um campo `type` (`HolidayType`: `"national"`, `"state"`, `"optional"` ou `"religious"`). O "Dia da Consciência Negra" (20 de novembro) é feriado nacional a partir de 2024 (Lei nº 14.759/2023). Antes disso, vários estados ainda trazem um feriado estadual próprio na mesma data, com o mesmo nome `"Dia da Consciência Negra"` em MT, RJ, AM e SP, e com `"Dia Estadual da Consciência Negra"` no AP, o nome que a lei daquele estado usa. Datas comemorativas que nenhuma lei transforma em feriado não entram na lista: o "Dia do Rio Grande do Norte" do RN (7 de agosto, Lei RN nº 7.831/2000) é uma delas, e o "Dia dos Evangélicos" de RO (18 de junho) também não entra, porque o STF derrubou a lei que o criou na ADI 3940. Os resultados são memoizados por `year`/`stateCode`, mas cada chamada ainda retorna uma cópia nova. Um `stateCode` desconhecido/inválido é ignorado, retornando apenas os feriados nacionais; a busca lê apenas propriedades próprias, então `"__proto__"`, `"constructor"` e afins são códigos desconhecidos como qualquer outro, e não uma exceção. Só os anos de 1900 a 2099 são suportados, o intervalo que os utilitários de dias úteis herdam; um ano fora dele retorna `[]`. +Retorna os feriados brasileiros de um ano: os nacionais e, com um `stateCode`, também os daquele estado. Aceita um ano ou `{ year, stateCode }` (`GetHolidaysParams`). -Apenas um feriado estadual por UF é feriado civil pela [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, que autoriza "a data magna do Estado fixada em lei estadual", no singular; as demais entradas se apoiam em leis estaduais ordinárias e são reportadas por serem observadas na prática. Regras notáveis por estado: - -- **SC** — a [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) transfere os dois feriados estaduais, "Dia do Estado de Santa Catarina" (11/08) e "Dia de Santa Catarina de Alexandria" (25/11), para o domingo subsequente sempre que caem de segunda a sexta, então a segunda-feira 11/08/2025 é dia útil em SC e o feriado cai no domingo 17/08. As duas datas não passaram a ser transferidas juntas. O 11/08 é transferido a partir de 2005, ano em que a [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) estendeu a cláusula a ele (publicada e em vigor em 15/07/2005), e antes disso fica em 11/08. O 25/11 é transferido a partir de 1999, ano em que a [Lei SC nº 11.213/1999](http://leis.alesc.sc.gov.br/html/1999/11213_1999_lei.html) introduziu a cláusula (publicada e em vigor em 12/11/1999, treze dias antes do 25/11 daquele ano), com um intervalo de um ano: o art. 3º da [Lei SC nº 12.906/2004](http://leis.alesc.sc.gov.br/html/2004/12906_2004_lei.html) revogou aquela lei sem repetir a cláusula, então só o 25/11/2004 fica na data estatutária, até a Lei SC nº 13.408/2005 reinstituir a transferência. Assim, o 25/11/1999 (uma quinta-feira) cai no domingo 28/11, o 25/11/2002 (uma segunda-feira) no domingo 01/12, o 25/11/2004 (uma quinta-feira) não se move, e o 25/11/2005 (uma sexta-feira) cai no domingo 27/11. -- **DF** — a [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declara Corpus Christi feriado. Com `stateCode: 'DF'` a única entrada de Corpus Christi volta tipada como `"state"` em vez de `"optional"`; ela é substituída, não duplicada. -- **GO** — a [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lista três feriados estaduais: 26/07 (Fundação da Cidade de Goiás), 24/10 (Lançamento da Pedra Fundamental de Goiânia) e 28/10 (Dia do Servidor Público). -- **AL** — 16/09 é feriado estadual a partir de 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) e apenas ponto facultativo (`"optional"`) antes disso. -- **PB** — 26/07 ("Morte de João Pessoa") é emitido apenas até 2015: a [Lei PB nº 10.601/2015](https://sapl.al.pb.leg.br/norma/11988), art. 2º, revogou a sua base. -- **TO** — 18/03 ("Autonomia do Estado do Tocantins") é emitido apenas até 2008: a [Lei TO nº 2.013/2009](https://www.al.to.leg.br/arquivo/15724) transformou em meramente comemorativo o dispositivo que declarava o feriado. - -A data retornada é a legal. O deslocamento de SC acima é o único modelado; o de Acre (feriados de terça a quinta transferidos para a sexta) e os decretos goianos que podem mover 26/07 e 28/10 não são. +- Cada feriado é um `Holiday` cujo `type` (`HolidayType`) é `"national"`, `"state"`, `"optional"` ou `"religious"`. Os feriados vêm ordenados por data. +- O "Dia da Consciência Negra", 20/11, é nacional a partir de 2024. +- As regras por estado (o deslocamento para domingo em SC, o Corpus Christi no DF, datas que deixaram de ser feriado) seguem a lei de cada estado; veja a fonte para a lista. +- Um `stateCode` desconhecido ou que não é string é ignorado e só os feriados nacionais são retornados. +- Retorna `[]` quando o ano não é um inteiro de 1900 a 2099, ou quando o argumento não é nem número nem objeto. ```javascript import { getHolidays } from '@brazilian-utils/brazilian-utils'; @@ -1350,9 +1631,15 @@ getHolidays({ year: 2024, stateCode: 'SP' }); // Inclui feriados nacionais mais feriados estaduais (ex: "Revolução Constitucionalista") ``` +Fonte: `src/get-holidays/constants.ts`, [Lei nº 662/1949](https://www.planalto.gov.br/ccivil_03/leis/l0662.htm), [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm). + ### isHoliday -Verifica se uma data específica é feriado brasileiro. A verificação compara a data local do `targetDate` (ano/mês/dia lidos localmente), não seu instante UTC subjacente. Retorna `false` quando `targetDate` está ausente ou não é um `Date` válido. Um `stateCode` inválido é tratado de duas formas diferentes: uma string que não é um código de estado conhecido é ignorada e só os feriados nacionais são considerados, igual ao `getHolidays`, enquanto um `stateCode` presente que não é uma string (um número, `null`, um objeto) é rejeitado e faz a chamada retornar `false` mesmo em um feriado nacional. +Verifica se uma data é feriado brasileiro. Aceita `{ targetDate, stateCode? }` (`IsHolidayParams`). + +- A verificação usa a data de calendário local de `targetDate`, não o seu instante UTC. +- `stateCode` também considera os feriados daquele estado. Um código desconhecido é ignorado, como em `getHolidays`. +- Retorna `false` quando `targetDate` está ausente ou não é um `Date` válido, ou quando `stateCode` está presente e não é string. ```javascript import { isHoliday } from '@brazilian-utils/brazilian-utils'; @@ -1364,7 +1651,10 @@ isHoliday(); // false ### isBusinessDay -Verifica se uma data é um dia útil no Brasil. Retorna `false` para sábados, domingos e feriados brasileiros retornados por `getHolidays` para a data local de `value` (ano/mês/dia lidos localmente), a mesma convenção usada por `isHoliday`. `options.includeOptional` (parte de `BusinessDayOptions`, o tipo de opções que todos os utilitários de dias úteis compartilham) tem valor padrão `true`, então feriados do tipo opcional (`Holiday.type === "optional"`, ou seja, Carnaval e Corpus Christi) também contam como dias não úteis; passe `false` para considerar apenas os feriados estatutários. `options.stateCode` também considera os feriados daquele estado; uma string que não é um código de estado conhecido é ignorada, considerando apenas os feriados nacionais, enquanto um `stateCode` presente que não é uma string (um número, `null`, um objeto) é rejeitado e faz a chamada retornar `false` mesmo em um dia de semana comum — a mesma distinção que `isHoliday` faz, e o valor que `addBusinessDays`, `subBusinessDays` e `differenceInBusinessDays` rejeitam com `null`. Um `value` que não é um `Date` válido retorna `false`. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `false`. +Verifica se uma data é dia útil no Brasil: não é sábado, domingo nem um feriado que `getHolidays` lista para o seu dia de calendário local. + +- **Opções** (`BusinessDayOptions`, as mesmas de todos os utilitários de dias úteis): `includeOptional` (padrão `true`) também conta os feriados `"optional"`, Carnaval e Corpus Christi, como dias não úteis; `stateCode` também conta os feriados daquele estado. +- Retorna `false` quando `value` não é um `Date` válido ou o seu ano está fora de 1900 a 2099, ou quando `stateCode` está presente e não é string. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1372,7 +1662,7 @@ import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; isBusinessDay(new Date(2024, 0, 2)); // true (terça-feira, não é feriado) isBusinessDay(new Date(2024, 0, 1)); // false (Ano novo) isBusinessDay(new Date(2024, 0, 6)); // false (sábado) -isBusinessDay(new Date(2024, 1, 13)); // false (Carnaval, feriado opcional, conta por padrão) +isBusinessDay(new Date(2024, 1, 13)); // false (Carnaval, feriado facultativo, conta por padrão) isBusinessDay(new Date(2024, 1, 13), { includeOptional: false }); // true isBusinessDay(new Date(2024, 6, 9), { stateCode: 'SP' }); // false (Revolução Constitucionalista) isBusinessDay(new Date(2024, 6, 9)); // true (feriado estadual ignorado sem stateCode) @@ -1381,7 +1671,12 @@ isBusinessDay(new Date('not a date')); // false ### addBusinessDays -Adiciona um número de dias úteis brasileiros a uma data, pulando sábados, domingos e feriados brasileiros exatamente como `isBusinessDay` os define (as mesmas `BusinessDayOptions`: `options.includeOptional`, padrão `true`, e `options.stateCode` funcionam exatamente como lá). A assinatura é a do date-fns: `addBusinessDays(date, amount, options?)`. Retorna um novo `Date`; a `date` de entrada nunca é alterada, e seu horário é preservado no resultado. Um `amount` igual a `0` retorna um novo `Date` igual a `date`, sem alterações, mesmo quando `date` cai em um fim de semana ou feriado, isso reflete o comportamento verificado de [`addBusinessDays(date, 0)` do date-fns](https://date-fns.org/docs/addBusinessDays), que também não avança a entrada para o próximo dia útil. Um `amount` negativo anda para trás, um dia útil por vez, também como no date-fns. Retorna `null` em caso de entrada inválida: uma `date` que não é um `Date` válido, um `amount` que não é um número inteiro finito, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado, exatamente como o `isBusinessDay` o ignora. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele, ou um percurso que sai dele, retorna `null`. +Soma dias úteis a uma data, pulando sábados, domingos e os feriados que `isBusinessDay` considera. Assinatura: `addBusinessDays(date, amount, options?)`, a mesma do date-fns. + +- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `stateCode` também pula os feriados daquele estado. +- Retorna um novo `Date`, com o horário preservado; `date` não é alterado. +- `amount` igual a `0` retorna a mesma data, mesmo em fim de semana ou feriado. Um `amount` negativo anda para trás. +- Retorna `null` quando `date` é inválido, `amount` não é um inteiro finito, `stateCode` não é string ou o resultado sai dos anos de 1900 a 2099. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1397,7 +1692,9 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) ### subBusinessDays -Subtrai um número de dias úteis brasileiros de uma data: `subBusinessDays(date, amount, options?)` é `addBusinessDays(date, -amount, options)`, e é exatamente assim que a função é implementada, então tudo o que vale acima vale aqui (o horário preservado, a entrada intacta, um `amount` igual a `0` devolvendo a data sem alterações, o intervalo de 1900 a 2099 e os casos de `null`), inclusive o `options.stateCode` e o `options.includeOptional`. Um `amount` negativo anda para frente. +Subtrai dias úteis de uma data. `subBusinessDays(date, amount, options?)` é `addBusinessDays(date, -amount, options)`. + +- As mesmas regras de `addBusinessDays`, `BusinessDayOptions` incluídas. Um `amount` negativo anda para frente. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1414,7 +1711,12 @@ subBusinessDays(new Date(2024, 0, 2), 1.5); // null (não é um número inteiro) ### differenceInBusinessDays -Conta o número de dias úteis brasileiros entre duas datas, refletindo a semântica de [`differenceInBusinessDays` do date-fns](https://date-fns.org/docs/differenceInBusinessDays) (verificada em seu código-fonte), inclusive a ordem dos argumentos: `differenceInBusinessDays(laterDate, earlierDate, options?)`. O percurso começa em `earlierDate` e para logo antes de `laterDate`, então `earlierDate` é contado quando ele próprio é um dia útil, `laterDate` nunca é contado, e cada dia útil estritamente entre os dois é contado uma vez. Só a data de calendário de cada `Date` importa, o horário é ignorado. Os dias úteis são determinados exatamente como em `isBusinessDay` (as mesmas `BusinessDayOptions`), inclusive o `options.includeOptional` (padrão `true`) e o `options.stateCode`. O resultado é positivo quando `laterDate` é posterior a `earlierDate` e negativo quando é anterior; duas datas no mesmo dia de calendário retornam `0`. Retorna `null` em caso de entrada inválida: uma data que não é um `Date` válido, ou um `stateCode` que não é uma string; um `options` que não é um objeto é ignorado. Só os anos de 1900 a 2099 são suportados, o intervalo que `getHolidays` calcula; uma data fora dele retorna `null`. +Conta os dias úteis entre duas datas. Assinatura: `differenceInBusinessDays(laterDate, earlierDate, options?)`, a mesma do date-fns. + +- **Opções** (`BusinessDayOptions`, as mesmas de `isBusinessDay`): `includeOptional` (padrão `true`) também pula Carnaval e Corpus Christi; `stateCode` também pula os feriados daquele estado. +- Conta `earlierDate` quando é dia útil e cada dia útil estritamente entre as duas datas; `laterDate` nunca é contado. O horário é ignorado. +- O resultado é negativo quando `laterDate` é anterior a `earlierDate`, e `0` no mesmo dia de calendário. +- Retorna `null` quando uma das datas não é um `Date` válido ou está fora dos anos de 1900 a 2099, ou quando `stateCode` não é string. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1431,20 +1733,24 @@ differenceInBusinessDays(new Date(), new Date('not a date')); // null ### isValidPassport -Verifica se um número de passaporte brasileiro é válido (2 letras seguidas de 6 dígitos). Aceita tanto `string` quanto `number`; a entrada é case-insensitive e caracteres não alfanuméricos (espaços, pontos, hífens) são ignorados. Um `number` é aceito por simetria com `formatPassport`/`parsePassport`, mas nunca é válido: a forma decimal de um número nunca começa com as duas letras que um número de passaporte exige. +Valida um número de passaporte brasileiro: 2 letras seguidas de 6 dígitos. + +- Não há dígito verificador, então um número bem formado não é necessariamente um passaporte real. ```javascript import { isValidPassport } from '@brazilian-utils/brazilian-utils'; isValidPassport('AB123456'); // true -isValidPassport('ab123456'); // true (case-insensitive) +isValidPassport('ab123456'); // true (não diferencia maiúsculas de minúsculas) isValidPassport('AB-123.456'); // true (símbolos são ignorados) isValidPassport('12345678'); // false ``` +Fonte: [Polícia Federal](https://www.gov.br/pf/pt-br/assuntos/passaporte) e seu [FAQ](https://www.gov.br/pf/pt-br/assuntos/passaporte/ajuda/duvidas_/caderneta/caderneta-numero-onde-fica-e). + ### formatPassport -Formata um número de passaporte brasileiro (maiúsculas, sem símbolos, limitado a 8 caracteres). Uma entrada que não seja `string` retorna uma string vazia. +Formata um número de passaporte brasileiro: maiúsculas, sem símbolos, limitado a 8 caracteres. É a mesma operação de `parsePassport`, da qual é um alias. ```javascript import { formatPassport } from '@brazilian-utils/brazilian-utils'; @@ -1455,7 +1761,7 @@ formatPassport('AB-123.456'); // 'AB123456' ### parsePassport -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. Uma entrada que não seja `string` retorna uma string vazia. +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'; @@ -1466,7 +1772,7 @@ parsePassport(' AB 123 456 '); // 'AB123456' ### generatePassport -Gera um número de passaporte brasileiro válido aleatoriamente. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um número de passaporte brasileiro válido aleatório. ```javascript import { generatePassport } from '@brazilian-utils/brazilian-utils'; @@ -1478,7 +1784,10 @@ generatePassport(); // 'RY393097' ### isValidCnh -Valida se a CNH é válida. Espaços, pontos e hífens ao redor/entre os dígitos são ignorados, mas qualquer outro caractere, uma letra em especial, invalida o valor. Um valor cujos 11 dígitos são todos iguais é rejeitado antes do cálculo dos dígitos verificadores, então `'11111111111'` é inválido. +Valida uma CNH. Espaços, pontos e hífens são ignorados; qualquer outro caractere invalida o valor. + +- Um valor cujos 11 dígitos são todos iguais é rejeitado, então `'11111111111'` é inválido. +- O primeiro dígito verificador mantém o resto 1 como `1`, como nos números reais de registro. A Resolução CONTRAN nº 886/2021 diz `0`. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; @@ -1488,9 +1797,13 @@ isValidCnh('000000001-19'); // true (hífen antes dos dígitos verificadores) isValidCnh('ab00000000119'); // false (letras são rejeitadas) ``` +Fonte: [Resolução CONTRAN nº 886/2021, art. 4º](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/Resolucao8862021F.pdf); pesos conforme o [siga0984](https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-cnh/). + ### formatCnh -Formata a CNH. `options.pad` (parte de `FormatCnhOptions`) completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara (padrão `false`). +Formata uma CNH. + +- **Opções** (`FormatCnhOptions`): `pad` completa o valor com zeros à esquerda até os 11 dígitos antes de aplicar a máscara (padrão `false`). ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1501,7 +1814,7 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ### parseCnh -Remove a formatação da CNH, mantém apenas os dígitos e limita o resultado a 11 dígitos. Retorna `''` quando não há nenhum dígito. +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'; @@ -1511,7 +1824,7 @@ parseCnh('026503064-61'); // '02650306461' ### generateCnh -Gera uma CNH válida aleatória. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera uma CNH válida aleatória. ```javascript import { generateCnh } from '@brazilian-utils/brazilian-utils'; @@ -1523,7 +1836,9 @@ generateCnh(); // '02650306461' ### isValidLegalNature -Valida se um código de natureza jurídica existe na lista oficial. A tabela segue a "Natureza Jurídica 2021" do IBGE/CONCLA: os 92 códigos em vigor mais os 8 que uma revisão anterior da tabela extinguiu, mantidos porque continuam aparecendo em registros feitos enquanto valiam. Use `getLegalNature` para distinguir os dois: um código extinto volta com `legacy: true` e o `currentCode` a que corresponde hoje. Somente os caracteres de máscara usuais (hífens, pontos, espaços) são tolerados ao redor dos 4 dígitos, então `'2062a'` é rejeitado em vez de ser lido como `'2062'`. +Valida se um código de natureza jurídica existe na lista oficial, a tabela "Natureza Jurídica 2021" do IBGE/CONCLA. Somente hífens, pontos e espaços são tolerados ao redor dos 4 dígitos. + +- Os 92 códigos em vigor são aceitos, mais os 8 que uma revisão anterior extinguiu. `getLegalNature` distingue os dois (`legacy: true`). ```javascript import { isValidLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1533,9 +1848,13 @@ isValidLegalNature('2208'); // true (extinto por uma revisão anterior, ainda ac isValidLegalNature('9999'); // false ``` +Fonte: [CONCLA, Natureza Jurídica 2021](https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021) e seu [PDF de estrutura detalhada](https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf). + ### formatLegalNature -Formata um código de natureza jurídica. `options.pad` (parte de `FormatLegalNatureOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai; com `true` o valor é primeiro completado com zeros à esquerda até os 4 dígitos de um código completo. Use `isValidLegalNature` para verificar um código. +Formata um código de natureza jurídica. Use `isValidLegalNature` para verificar um código. + +- **Opções** (`FormatLegalNatureOptions`): `pad` primeiro completa o valor com zeros à esquerda até os 4 dígitos de um código completo (padrão `false`). ```javascript import { formatLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1558,7 +1877,7 @@ parseLegalNature('206-2'); // '2062' ### generateLegalNature -Gera um código de natureza jurídica válido aleatório. Apenas os 92 códigos em vigor são sorteados, nunca um dos 8 que uma revisão anterior extinguiu. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um código de natureza jurídica válido aleatório. Apenas os 92 códigos em vigor são sorteados, nunca um extinto. ```javascript import { generateLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1568,9 +1887,10 @@ generateLegalNature(); // '2062' ### getLegalNature -Busca um código de natureza jurídica na tabela oficial do IBGE/CONCLA. A entrada também traz a categoria do CONCLA em que o código está listado, dada pelo seu primeiro dígito. Nenhum código de natureza jurídica começa com zero, esse primeiro dígito é a categoria (1 a 5), então aqui nada é completado: um número e a string dos mesmos dígitos são lidos de forma idêntica. +Busca um código de natureza jurídica na tabela oficial do IBGE/CONCLA. Retorna `null` para um código desconhecido. -Um código que uma revisão anterior da tabela extinguiu continua sendo encontrado, porque segue aparecendo em registros feitos enquanto valia, e volta com `legacy: true` e o `currentCode` a que corresponde hoje, conforme as planilhas de correspondência do CONCLA. Os 92 códigos em vigor têm `legacy: false` e nenhum `currentCode`. +- A entrada (`LegalNature`) também traz a categoria do CONCLA do código, dada pelo seu primeiro dígito. +- Um código que uma revisão anterior extinguiu retorna com `legacy: true` e o `currentCode` a que corresponde hoje, ou `currentCode: null` quando não há sucessor. Os códigos em vigor têm `legacy: false` e nenhum `currentCode`. | Código extinto | Descrição | Corresponde a | | --- | --- | --- | @@ -1607,9 +1927,13 @@ getLegalNature(206.2)?.category.description; // 'Entidades Empresariais' getLegalNature('0000'); // null ``` +Fonte: [CONCLA, Natureza Jurídica 2021](https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021). + ### getLegalNatures -Retorna o mapa de naturezas jurídicas indexado pelo código. Por padrão apenas os 92 códigos da tabela CONCLA 2021, os em vigor, são listados; passe `{ includeLegacy: true }` (`GetLegalNaturesParams`) para somar os 8 que uma revisão anterior da tabela extinguiu. +Retorna o mapa de naturezas jurídicas indexado pelo código. Por padrão apenas os 92 códigos em vigor são listados. + +- **Opções** (`GetLegalNaturesParams`): `includeLegacy` (padrão `false`) soma os 8 códigos extintos. ```javascript import { getLegalNatures } from '@brazilian-utils/brazilian-utils'; @@ -1624,7 +1948,11 @@ getLegalNatures({ includeLegacy: true })['2208']; // 'Entidade Binacional Itaipu ### getLegalNaturesByCategory -Retorna todas as naturezas jurídicas de uma categoria do CONCLA, o grupo dado pelo primeiro dígito do código: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas e `5` Organizações Internacionais e Outras Instituições Extraterritoriais. A categoria é aceita como string ou como número, as entradas voltam ordenadas por código e uma categoria desconhecida devolve `[]`. Por padrão apenas os códigos em vigor são listados; passe `{ includeLegacy: true }` (`GetLegalNaturesByCategoryOptions`) para somar os códigos extintos da categoria, na ordem dos códigos. +Retorna todas as naturezas jurídicas de uma categoria do CONCLA, o grupo dado pelo primeiro dígito do código. A categoria é aceita como string ou como número. + +- Categorias: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas e `5` Organizações Internacionais e Outras Instituições Extraterritoriais. +- **Opções** (`GetLegalNaturesByCategoryOptions`): `includeLegacy` (padrão `false`) soma os códigos extintos da categoria. +- As entradas retornam ordenadas por código. Uma categoria desconhecida retorna `[]`. ```javascript import { getLegalNaturesByCategory } from '@brazilian-utils/brazilian-utils'; @@ -1646,7 +1974,10 @@ getLegalNaturesByCategory('9'); // [] ### isValidVoterId -Valida se um título de eleitor é válido. Aceita tanto o título padrão de 12 dígitos quanto o título de 13 dígitos emitido por São Paulo (UF `01`) e Minas Gerais (UF `02`). Espaços e pontos são aceitos ao redor e entre os grupos `0000 0000 00 00`, mas qualquer outro caractere, uma letra em especial, invalida o valor. +Valida um título de eleitor. Aceita o título padrão de 12 dígitos e o título de 13 dígitos expedido por São Paulo (UF `01`) e Minas Gerais (UF `02`). + +- Um título é um número sequencial de 8 dígitos, um código de unidade federativa de 2 dígitos (`01` a `28`) e 2 dígitos verificadores. +- Espaços e pontos são aceitos ao redor e entre os grupos. Qualquer outro caractere, inclusive um hífen, invalida o valor. ```javascript import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1654,11 +1985,19 @@ import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-util const voterId = generateVoterId('SP'); isValidVoterId(voterId); // true +isValidVoterId('102385010671'); // true (12 dígitos) +isValidVoterId('1234567880191'); // true (13 dígitos, São Paulo) +isValidVoterId('123456780124'); // false (dígitos verificadores inválidos) ``` +Fonte: [Resolução TSE nº 23.659/2021, art. 36](https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021), [brutils](https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py) e [siga0984](https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-titulo-de-eleitor/). + ### formatVoterId -Formata um título de eleitor. Usa por padrão o agrupamento de 12 dígitos `0000 0000 00 00`; o agrupamento de 13 dígitos `0000 0000 0 00 00` só é usado quando o valor sanitizado tem mais de 12 dígitos **e** o código de unidade federativa (o 10º e o 11º dígitos) é `01` (São Paulo) ou `02` (Minas Gerais), os dois estados cujos títulos podem ter um número sequencial de 9 dígitos. +Formata um título de eleitor com o agrupamento de 12 dígitos `0000 0000 00 00`. + +- O agrupamento de 13 dígitos `0000 0000 0 00 00` só é usado quando o valor tem mais de 12 dígitos e o código da UF (o 10º e o 11º dígitos) é `01` ou `02`. +- Os dígitos além da última posição do padrão são descartados. ```javascript import { formatVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1680,7 +2019,10 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (título de 13 dígitos SP ### generateVoterId -Gera um título de eleitor válido aleatório. Você pode opcionalmente informar a UF; uma UF desconhecida usa `"ZZ"` (título emitido no exterior) em vez de lançar erro. Usa `Math.random()` internamente, então não é criptograficamente seguro. +Gera um título de eleitor válido aleatório. O argumento opcional `state` (`StateCode`, ou `"ZZ"` para um título expedido no exterior) define o código de unidade federativa. + +- Uma UF desconhecida, ou um valor que não seja string, usa `"ZZ"` (UF `28`). +- O resultado sempre tem 12 dígitos, nunca a forma de 13 dígitos de São Paulo ou Minas Gerais. ```javascript import { generateVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1694,23 +2036,30 @@ generateVoterId('XX'); // usa "ZZ" em vez de lançar erro ### isValidCns -Verifica se um número de CNS (Cartão Nacional de Saúde) é válido, o identificador único do usuário do SUS (Sistema Único de Saúde). Cartões definitivos (iniciados em 1 ou 2) são validados sobre uma base embutida de 11 dígitos derivada do PIS/PASEP/NIS, ponderada de 15 até 5; quando o dígito bruto resulta em 10, o DATASUS soma 2 à soma ponderada, recalcula o dígito e marca o cartão com o sufixo `001` em vez de `000`. Cartões provisórios (iniciados em 7, 8 ou 9) são validados por uma soma ponderada única (pesos de 15 a 1) que deve ser múltipla de 11. O valor precisa vir escrito como os 15 dígitos, opcionalmente separados nos grupos impressos de 3-4-4-4 por espaço em branco, `.`, `-` ou `/`, os caracteres de máscara intercambiáveis que `isValidCpf` e `isValidCnpj` aceitam, inclusive uma sequência deles entre dois grupos; letras no meio dos dígitos, ou um separador dentro de um grupo, são rejeitadas em vez de ignoradas. +Valida um número de CNS (Cartão Nacional de Saúde), o identificador do SUS (Sistema Único de Saúde) de um usuário, profissional ou estabelecimento de saúde. O valor precisa ser os 15 dígitos, opcionalmente separados nos grupos impressos de 3-4-4-4 por espaço, `.`, `-` ou `/`. -As duas rotinas vêm da [página de validação de CNS da ANVISA](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), que fica atrás de um filtro de bots e responde HTTP 403 a clientes que não sejam navegadores. A [página do e-SUS APS](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documenta o mesmo algoritmo e é acessível sem navegador, mas aplica a rotina de provisórios a números iniciados em 5, 7, 8 ou 9; esta implementação segue a ANVISA e rejeita um número iniciado em 5 mesmo quando a soma ponderada fecha. +- Cartões definitivos começam com 1 ou 2, provisórios com 7, 8 ou 9; cada um tem sua própria regra de módulo 11. +- Um número iniciado em 5 é rejeitado, seguindo a ANVISA. ```javascript import { isValidCns } from '@brazilian-utils/brazilian-utils'; isValidCns('123456789010000'); // true (definitivo) +isValidCns('100000000060018'); // true (definitivo, dígito bruto 10, sufixo 001) isValidCns('700000000000005'); // true (provisório) isValidCns('123.4567-8901/0000'); // true (qualquer um dos caracteres de máscara) +isValidCns('123456789010001'); // false (dígito verificador inválido) isValidCns('12345678901'); // false (tamanho inválido) isValidCns('abc123456789010000'); // false (não escrito como um CNS) ``` +Fonte: [página de validação de CNS da ANVISA](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/) e a [página do e-SUS APS](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html). + ### formatCns -Formata um número de CNS (Cartão Nacional de Saúde) nos grupos de exibição usuais de 3-4-4-4 dígitos separados por espaço. `options.pad` (parte de `FormatCnsOptions`) preenche o valor com zeros à esquerda até as 15 posições do padrão antes de aplicar a máscara (padrão `false`). +Formata um número de CNS (Cartão Nacional de Saúde) nos grupos de exibição usuais de 3-4-4-4 dígitos separados por espaço. + +- **Opções** (`FormatCnsOptions`): `pad` completa o valor com zeros à esquerda até as 15 posições do padrão antes de aplicar a máscara (padrão `false`). ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; @@ -1722,7 +2071,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ### parseCns -Remove a formatação do CNS (Cartão Nacional de Saúde), mantém apenas os dígitos e limita o resultado a 15 dígitos. Um valor parcial passa adiante até onde vai, então também dá para tirar a máscara de um campo ainda sendo digitado; use `isValidCns` para verificar o número em si. +Remove a formatação do CNS (Cartão Nacional de Saúde), mantém apenas os dígitos e limita o resultado a 15 dígitos. ```javascript import { parseCns } from '@brazilian-utils/brazilian-utils'; @@ -1734,9 +2083,25 @@ parseCns('123 4567 8901 0000'); // '123456789010000' ### isValidCertidao -Verifica se a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos mantidos por uma serventia de registro civil das pessoas naturais) é válida. A matrícula tem 32 dígitos distribuídos em 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), e os dois dígitos verificadores usam módulo 11 com os pesos ciclando de 2 a 10 e voltando por 0: o primeiro cálculo começa em 2 sobre os 30 dígitos da base, o segundo em 1 sobre os 31 dígitos que incluem o primeiro dígito verificador, e nos dois um resto 10 é lido como 1. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos. O layout é o publicado atualmente no [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023), com o inciso II e os §§ 1º e 3º a 5º na redação do Provimento CN nº 237/2026 e o restante do artigo, inclusive o § 2º, na do Provimento CN nº 182/2024; a própria matrícula foi instituída pelo já revogado [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) e ganhou sua estrutura de dígitos no também revogado [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). Os dígitos verificadores estão detalhados em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e implementado pelo [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) e pelo [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Valida a matrícula de uma certidão de registro civil (nascimento, casamento, óbito e os demais atos de um registro civil das pessoas naturais). Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. -Os dígitos do serviço são fixos em `55`, o código que o [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) atribui ao registro civil das pessoas naturais, então uma matrícula com qualquer outro par na nona e décima posições é rejeitada por mais que os dígitos verificadores confiram. O dígito do tipo de livro sempre precisa nomear um dos nove tipos de livro (o mesmo `CertidaoType` retornado por `getCertidaoInfo`), então uma matrícula cujo dígito é `0` é rejeitada por mais que os dígitos verificadores confiram, do mesmo jeito que `getCertidaoInfo` devolve `null` para ela. `options.accept` (parte de `IsValidCertidaoOptions`) restringe ainda mais aos tipos listados; o padrão é aceitar todos os tipos, e um valor que não seja um array volta para esse padrão. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +A matrícula tem 32 dígitos, impressos como `000000 00 00 0000 0 00000 000 0000000 00`: + +| Dígitos | Campo | +| --- | --- | +| 6 | CNS da serventia | +| 2 | acervo | +| 2 | serviço, sempre `55` | +| 4 | ano | +| 1 | tipo do livro | +| 5 | livro | +| 3 | folha | +| 7 | termo | +| 2 | dígitos verificadores | + +- **Opções** (`IsValidCertidaoOptions`): `accept` restringe os tipos de livro válidos (`CertidaoType`) aos listados (padrão: todos os tipos). +- O serviço precisa ser `55`, e o dígito do tipo de livro precisa ser um dos nove livros (`0` é rejeitado). +- Aceita o valor com ou sem máscara, com espaços entre e ao redor dos grupos. ```javascript import { isValidCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1750,9 +2115,14 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['birth'] isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] }); // false ``` +Fonte: [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243); dígitos verificadores conforme o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts). + ### formatCertidao -Formata a matrícula de uma certidão de registro civil na máscara impressa do Provimento, os 32 dígitos agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. `options.pad` (parte de `FormatCertidaoOptions`) preenche o valor com zeros à esquerda até 32 dígitos (padrão `false`). A máscara é a do [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). Um número é aceito e lido como a string dos seus dígitos, como no `formatCpf`, mas uma matrícula completa de 32 dígitos precisa ser uma string: essa quantidade de dígitos é mais do que um número JavaScript comporta com exatidão. Em tempo de execução o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão, como em todo formatador deste pacote, então uma matrícula parcial ainda sendo digitada é mascarada progressivamente. +Formata a matrícula de uma certidão de registro civil na máscara impressa do art. 473. Os 32 dígitos são agrupados em 6 2 2 4 1 5 3 7 2 e separados por espaços. + +- **Opções** (`FormatCertidaoOptions`): `pad` completa o valor com zeros à esquerda até 32 dígitos (padrão `false`). +- Um número é aceito, mas uma matrícula completa de 32 dígitos precisa ser uma string. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1763,9 +2133,11 @@ formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 formatCertidao(104539015520); // 104539 01 55 20 (um número é lido como a string dos seus dígitos) ``` +Fonte: [art. 473 do Código Nacional de Normas](https://atos.cnj.jus.br/atos/detalhar/5243). + ### parseCertidao -Remove a formatação da matrícula de uma certidão de registro civil, mantém apenas os dígitos e limita o resultado a 32 dígitos. Isso só tira a máscara: use `isValidCertidao` para verificar a matrícula e `getCertidaoInfo` para ler os campos dela. +Remove a formatação da matrícula de uma certidão de registro civil, mantém apenas os dígitos e limita o resultado a 32 dígitos. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1776,7 +2148,25 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); ### getCertidaoInfo -Extrai os campos da matrícula de uma certidão de registro civil, retornando `null` quando a matrícula é inválida, o que inclui um código de livro que não é um dos nove livros. Um serviço diferente do `55` que o art. 473, III fixa para o registro civil das pessoas naturais também resulta em `null`. O [art. 473, V do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lista os códigos de 1 a 7; nenhum texto primário do CNJ acessível hoje publica os outros dois, inclusive o Anexo IV do revogado Provimento CNJ nº 63/2017, que lista os mesmos sete. Os códigos 8 (emancipação) e 9 (interdição) vêm das referências em que a regra do dígito verificador se apoia: o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) publicam a lista dos nove livros. Eles são mantidos porque matrículas com eles circulam. Só uma string é aceita: os 32 dígitos de uma matrícula são mais do que um número JavaScript comporta. +Extrai os campos da matrícula de uma certidão de registro civil. Aceita as mesmas formas de entrada de `isValidCertidao` e retorna `null` quando a matrícula é inválida. + +- Retorna `null` também para um serviço diferente de `55` e para um código de livro fora de 1 a 9. +- O art. 473, V lista apenas os códigos de livro de 1 a 7. Os códigos 8 (emancipação) e 9 (interdição) também são aceitos. + +O resultado `CertidaoInfo` traz: + +| Chave | Descrição | +| --- | --- | +| `registryCns` | O CNS (Código Nacional de Serventia) de 6 dígitos da serventia que lavrou o ato. | +| `acervo` | Acervo a que o livro pertence: `"01"` acervo próprio, `"02"` em diante um por acervo incorporado. O art. 473, §§ 3º a 5º separa os incorporados pela data em que a serventia de origem foi extinta ou desativada. Até 31/12/2009: o CNS da unidade incorporadora e um código de acervo a partir de `"02"`, um por incorporação. A partir de 01/01/2010: o CNS da própria unidade incorporada e o código `"01"`, considerado acervo próprio dessa unidade. Um acervo fracionado entre duas ou mais serventias sucessoras leva o CNS próprio de cada sucessora com o código `"02"`. | +| `service` | Serviço prestado pela serventia, sempre `"55"`, o registro civil das pessoas naturais. | +| `year` | Ano do registro, com 4 dígitos. | +| `type` | Livro a que o ato pertence: `"birth"`, `"marriage"`, `"religious-marriage"`, `"death"`, `"stillbirth"`, `"banns"`, `"other"`, `"emancipation"` ou `"interdiction"`. | +| `typeCode` | Código bruto do livro, de 1 a 9, como impresso na décima quinta posição da matrícula. | +| `book` | Número do livro, com 5 dígitos e zeros à esquerda. | +| `page` | Número da folha, com 3 dígitos e zeros à esquerda. | +| `term` | Número do termo, com 7 dígitos e zeros à esquerda. | +| `checkDigits` | Os 2 dígitos verificadores módulo 11 da matrícula. | ```javascript import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; @@ -1798,26 +2188,15 @@ getCertidaoInfo('104539 01 55 2013 1 00012 021 0000123 21'); getCertidaoInfo('invalid'); // null ``` -O resultado `CertidaoInfo` traz: - -| Chave | Descrição | -| --- | --- | -| `registryCns` | O CNS (Código Nacional de Serventia) de 6 dígitos da serventia que lavrou o ato. | -| `acervo` | Acervo a que o livro pertence: `"01"` acervo próprio, `"02"` em diante um por acervo incorporado. O [art. 473, §§ 3º a 5º](https://atos.cnj.jus.br/atos/detalhar/5243) separa os incorporados pela data em que a serventia de origem foi extinta ou desativada: até 31/12/2009 a matrícula leva o CNS da unidade incorporadora e um código de acervo a partir de `"02"`, um por incorporação; a partir de 1º/01/2010 leva o CNS da própria unidade incorporada e o código `"01"`, considerado acervo próprio dessa unidade; e um acervo fracionado entre duas ou mais serventias sucessoras leva o CNS próprio de cada sucessora com o código `"02"`. | -| `service` | Serviço prestado pela serventia, sempre `"55"`, o registro civil das pessoas naturais. | -| `year` | Ano do registro, com 4 dígitos. | -| `type` | Livro a que o ato pertence: `"birth"`, `"marriage"`, `"religious-marriage"`, `"death"`, `"stillbirth"`, `"banns"`, `"other"`, `"emancipation"` ou `"interdiction"`. | -| `typeCode` | Código bruto do livro, de 1 a 9, como impresso na décima quinta posição da matrícula. | -| `book` | Número do livro, com 5 dígitos e zeros à esquerda. | -| `page` | Número da folha, com 3 dígitos e zeros à esquerda. | -| `term` | Número do termo, com 7 dígitos e zeros à esquerda. | -| `checkDigits` | Os 2 dígitos verificadores módulo 11 da matrícula. | +Fonte: [art. 473 do Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243); códigos de livro 8 e 9 conforme o [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e o [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts). ## CEI, CNO e CAEPF ### isValidCei -Verifica se um número de CEI (Cadastro Específico do INSS) é válido. O CEI identifica o empregador sem CNPJ, como uma obra ou um produtor rural: 12 dígitos impressos como `00.000.00000/00`, sendo o último um dígito verificador calculado sobre os 11 dígitos da base com os pesos 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 e 4. Aceita os caracteres de máscara usuais e espaços entre e ao redor dos grupos, inclusive uma sequência deles entre dois grupos. A Receita Federal não publica essa regra de dígito verificador, então ela segue as implementações de referência do [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) e do [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), conferida contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal. +Valida um número de CEI (Cadastro Específico do INSS). O CEI identifica o empregador sem CNPJ, como uma obra ou um produtor rural. + +- Layout: 12 dígitos impressos como `00.000.00000/00`, 11 dígitos de base e um dígito verificador. ```javascript import { isValidCei } from '@brazilian-utils/brazilian-utils'; @@ -1829,9 +2208,13 @@ isValidCei('24.985.96743/68'); // false (dígito verificador inválido) isValidCei('000000000000'); // false (dígitos repetidos) ``` +Fonte: [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php), [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs) e a [base de dados aberta do CNO](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno). + ### formatCei -Formata um número de CEI (Cadastro Específico do INSS) na máscara usual `00.000.00000/00`, a mesma em que as implementações de referência do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCeiOptions`) preenche à esquerda com zeros até 12 dígitos (padrão `false`). +Formata um número de CEI (Cadastro Específico do INSS) na máscara usual `00.000.00000/00`. + +- **Opções** (`FormatCeiOptions`): `pad` completa o valor com zeros à esquerda até 12 dígitos (padrão `false`). ```javascript import { formatCei } from '@brazilian-utils/brazilian-utils'; @@ -1843,7 +2226,7 @@ formatCei('249', { pad: true }); // 00.000.00002/49 ### parseCei -Remove a formatação do CEI (Cadastro Específico do INSS), mantém apenas os dígitos e limita o resultado a 12 dígitos. Um valor parcial passa adiante até onde vai; use `isValidCei` para verificar o número em si. +Remove a formatação do CEI (Cadastro Específico do INSS), mantém apenas os dígitos e limita o resultado a 12 dígitos. ```javascript import { parseCei } from '@brazilian-utils/brazilian-utils'; @@ -1853,7 +2236,9 @@ parseCei('27.729.71181/87'); // '277297118187' ### isValidCno -Verifica se um número de CNO (Cadastro Nacional de Obras) é válido. O CNO substituiu o CEI para obras e manteve a mesma numeração, então uma obra registrada sob um CEI antigo conserva o número e os dois cadastros são validados do mesmo jeito: 12 dígitos impressos como `00.000.00000/00`, com o dígito verificador calculado sobre os 11 dígitos da base. A Receita Federal não publica a regra do dígito verificador; ela foi confirmada contra os [dados abertos do Cadastro Nacional de Obras (CNO)](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) da Receita Federal: todas as obras do recorte de Minas Gerais desse conjunto passam nesta verificação. A página do catálogo publica apenas a descrição e os links de download do conjunto, não esse resultado. +Valida um número de CNO (Cadastro Nacional de Obras). O CNO substituiu o CEI para obras e manteve a mesma numeração. + +- Mesmas regras de `isValidCei`. ```javascript import { isValidCno } from '@brazilian-utils/brazilian-utils'; @@ -1865,9 +2250,13 @@ isValidCno('110840168063'); // false (dígito verificador inválido) isValidCno('000000000000'); // false (dígitos repetidos) ``` +Fonte: [página do CNO da Receita Federal](https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno) e a [base de dados aberta do CNO](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno). + ### formatCno -Formata um número de CNO (Cadastro Nacional de Obras). O CNO manteve a numeração do CEI, então os dois compartilham a mesma máscara de 12 dígitos, `00.000.00000/00`, a mesma em que as implementações de referência do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCnoOptions`) preenche à esquerda com zeros até 12 dígitos (padrão `false`). +Formata um número de CNO (Cadastro Nacional de Obras). + +- Mesmas regras de `formatCei`: a máscara `00.000.00000/00`, com `pad` em `FormatCnoOptions`. ```javascript import { formatCno } from '@brazilian-utils/brazilian-utils'; @@ -1879,7 +2268,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ### parseCno -Remove a formatação do CNO (Cadastro Nacional de Obras), mantém apenas os dígitos e limita o resultado a 12 dígitos, a numeração que o CNO herdou do CEI. Um valor mais curto passa adiante até onde vai; use `isValidCno` para verificar o número em si. +Remove a formatação do CNO (Cadastro Nacional de Obras), mantém apenas os dígitos e limita o resultado a 12 dígitos, a numeração que o CNO herdou do CEI. ```javascript import { parseCno } from '@brazilian-utils/brazilian-utils'; @@ -1889,7 +2278,10 @@ parseCno('11.113.01373/68'); // '111130137368' ### isValidCaepf -Verifica se um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) é válido. O CAEPF substituiu o CEI para a pessoa física que contrata empregados: 14 dígitos impressos como `000.000.000/000-00`, formados pela base de 9 dígitos do CPF do titular, um número de ordem de 3 dígitos para os vários cadastros do mesmo titular e 2 dígitos verificadores. Os dois dígitos verificadores são o módulo 11 do CNPJ na formulação da referência citada: os pesos vão de 9 até 2 da direita para a esquerda e o dígito é o próprio resto, com o resto 10 lido como 0 — o mesmo dígito que os pesos de 2 a 9 do CNPJ com `11 - resto` produzem. O par resultante é somado a 12, com retorno a zero acima de 99. Uma base cujos 12 dígitos são todos iguais é rejeitada antes do cálculo dos dígitos verificadores, do mesmo jeito que `isValidCei` e `isValidCno` rejeitam um número de CEI/CNO repetido, então o `00000000000012`, que de resto é bem formado, é inválido. A Receita Federal não publica o layout nem a regra dos dígitos verificadores: os dois estão descritos em [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e são implementados do mesmo jeito pelo [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). +Valida um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física). O CAEPF substituiu o CEI para a pessoa física que contrata empregados, como o produtor rural. + +- Layout: 14 dígitos impressos como `000.000.000/000-00`: a base de 9 dígitos do CPF do titular, um número de ordem de 3 dígitos e 2 dígitos verificadores. +- Os dois dígitos verificadores seguem o módulo 11 do CNPJ; o par é então somado a 12, com retorno a zero acima de 99. ```javascript import { isValidCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1902,9 +2294,13 @@ isValidCaepf('00000000000000'); // false (dígitos da base repetidos) isValidCaepf('00000000000012'); // false (dígitos da base repetidos) ``` +Fonte: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) e [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). + ### formatCaepf -Formata um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) na máscara usual `000.000.000/000-00`, a mesma em que as fontes da regra do dígito verificador concordam (a Receita Federal não a publica). Formata progressivamente, até onde os dígitos informados alcançarem, então também pode ser usada como máscara de digitação. `options.pad` (parte de `FormatCaepfOptions`) preenche à esquerda com zeros até 14 dígitos (padrão `false`). +Formata um número de CAEPF (Cadastro de Atividade Econômica da Pessoa Física) na máscara usual `000.000.000/000-00`. + +- Mesmas regras de `formatCei`, com `pad` (`FormatCaepfOptions`) completando até 14 dígitos (padrão `false`). ```javascript import { formatCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1916,7 +2312,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ### parseCaepf -Remove a formatação do CAEPF (Cadastro de Atividade Econômica da Pessoa Física), mantém apenas os dígitos e limita o resultado a 14 dígitos. Um valor mais curto passa adiante até onde vai; use `isValidCaepf` para verificar o número em si. +Remove a formatação do CAEPF (Cadastro de Atividade Econômica da Pessoa Física), mantém apenas os dígitos e limita o resultado a 14 dígitos. ```javascript import { parseCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1928,7 +2324,11 @@ parseCaepf('293.118.610/001-84'); // '29311861000184' ### isValidCbo -Valida se um código CBO (Classificação Brasileira de Ocupações) existe na tabela de ocupações do MTE. Aceita o código com ou sem a máscara de hífen, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 6 dígitos, ou a máscara `NNNN-NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um código CBO sempre tem 6 dígitos e os zeros à esquerda fazem parte dele, então um valor escrito apenas com dígitos é completado com zeros à esquerda até 6, seja ele string ou número, exatamente como `getBankByCode` completa um código de banco: `10205`, `'10205'` e `'010205'` são o mesmo código. Um valor mascarado já carrega os seus separadores e é lido como foi escrito. +Valida um código CBO (Classificação Brasileira de Ocupações) contra a tabela oficial da CBO 2002. + +- Aceita uma string com os 6 dígitos ou com a máscara `NNNN-NN`, ou um número. +- Uma string mascarada precisa de um único separador (espaço, `.`, `-` ou `/`) entre os grupos. Qualquer outra string é rejeitada, em vez de ter seus dígitos extraídos. +- Dígitos sem máscara são completados com zeros à esquerda até 6, como string ou como número. Um valor mascarado é lido como foi escrito. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -1943,11 +2343,13 @@ isValidCbo('2124abc05'); // false (não é uma forma documentada) isValidCbo(-212405); // false (não é um inteiro seguro não negativo) ``` -Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 publicada pelo MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). +Fonte: [tabela de ocupações da CBO 2002 publicada pelo MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ### parseCbo -Remove a formatação do CBO (Classificação Brasileira de Ocupações), mantém apenas os dígitos e limita o resultado a 6 dígitos. Um valor mais curto passa adiante até onde vai e nada é preenchido com zeros à esquerda aqui, então o zero inicial de um código como `010205` precisa ser escrito; use `getCbo` ou `isValidCbo`, que preenchem um código numérico sem máscara, para consultar uma ocupação. +Remove a formatação do CBO (Classificação Brasileira de Ocupações), mantém apenas os dígitos e limita o resultado a 6 dígitos. + +- Nada é completado com zeros à esquerda: o zero inicial de um código como `010205` precisa ser escrito. Use `getCbo` ou `isValidCbo` para consultar uma ocupação. ```javascript import { parseCbo } from '@brazilian-utils/brazilian-utils'; @@ -1957,7 +2359,9 @@ parseCbo('2124-05'); // '212405' ### getCbo -Consulta um código CBO (Classificação Brasileira de Ocupações) e retorna o título oficial da ocupação, no registro `{ code, description }` que toda consulta desta biblioteca devolve. Um valor escrito apenas com dígitos mantém os zeros à esquerda implícitos, tanto como string quanto como número: `getCbo(10205)` e `getCbo('10205')` são lidos como `010205`. Valem as mesmas regras de entrada de `isValidCbo`: uma string precisa estar escrita com os 6 dígitos ou com a máscara `NNNN-NN`, e um número precisa ser um inteiro seguro não negativo. +Consulta um código CBO (Classificação Brasileira de Ocupações) e retorna o título oficial da ocupação. O resultado é um registro `Cbo`: `{ code, description }`. + +- Mesmas regras de `isValidCbo`. Retorna `null` quando o código é desconhecido ou o valor não está em uma forma documentada. ```javascript import { getCbo } from '@brazilian-utils/brazilian-utils'; @@ -1969,11 +2373,13 @@ getCbo('000000'); // null getCbo('2124abc05'); // null (não é uma forma documentada) ``` -Os títulos das ocupações vêm da [tabela oficial de ocupações da CBO 2002 publicada pelo MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). +Fonte: [tabela de ocupações da CBO 2002 publicada pelo MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ### isValidCnae -Valida se um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) existe na [tabela CNAE-Subclasses 2.3 publicada pelo IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), a revisão de subclasses atual da CNAE 2.0. Aceita o código com ou sem a máscara `NNNN-N/NN`, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 7 dígitos, ou a máscara, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um código de subclasse CNAE sempre tem 7 dígitos e os zeros à esquerda fazem parte dele, então um valor escrito apenas com dígitos é completado com zeros à esquerda até 7, seja ele string ou número: `111301`, `'111301'` e `'0111301'` são o mesmo código. Um valor mascarado já carrega os seus separadores e é lido como foi escrito. +Valida um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) contra a tabela CNAE-Subclasses 2.3, a revisão de subclasses atual da CNAE 2.0. + +- Mesmas regras de `isValidCbo`, com 7 dígitos e a máscara `NNNN-N/NN`. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -1987,9 +2393,14 @@ isValidCnae('0111abc301'); // false (não é uma forma documentada) isValidCnae(-111301); // false (não é um inteiro seguro não negativo) ``` +Fonte: [CNAE-Subclasses 2.3 na CONCLA/IBGE](https://concla.ibge.gov.br/busca-online-cnae.html) e a [API de subclasses do IBGE](https://servicodados.ibge.gov.br/api/v2/cnae/subclasses). + ### formatCnae -Formata um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas). `options.pad` (parte de `FormatCnaeOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 7 dígitos de uma subclasse completa, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão: caracteres fora da máscara são descartados e um número é lido como a string dos seus dígitos, sinal e ponto decimal inclusos. Use `isValidCnae` para verificar um código. +Formata um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas). Só a estrutura muda; use `isValidCnae` para conferir um código com a tabela. + +- **Opções** (`FormatCnaeOptions`): `pad` (padrão `false`) completa antes o valor com zeros à esquerda até os 7 dígitos de um código completo. Sem ele a máscara é aplicada até onde o valor vai. +- Caracteres fora da máscara são descartados, e um número é lido como a string dos seus dígitos. Retorna `''` quando não há dígito algum. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; @@ -2005,18 +2416,23 @@ formatCnae(-6201501); // 6201-5/01 ### parseCnae -Remove a formatação do CNAE (Classificação Nacional de Atividades Econômicas), mantém apenas os dígitos e limita o resultado aos 7 dígitos de um código de subclasse completo. Nada é preenchido com zeros à esquerda aqui; use `getCnae` ou `isValidCnae`, que preenchem um código numérico sem máscara, para consultar uma subclasse. +Remove a formatação do CNAE (Classificação Nacional de Atividades Econômicas), mantém apenas os dígitos e limita o resultado aos 7 dígitos de um código de subclasse completo. + +- Mesmas regras de `parseCbo`: nada é completado com zeros à esquerda aqui. ```javascript import { parseCnae } from '@brazilian-utils/brazilian-utils'; parseCnae('6201-5/01'); // '6201501' -parseCnae('62'); // '62' (a partial code is kept as written) +parseCnae('62'); // '62' (um código parcial é mantido como está) ``` ### getCnae -Busca um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) e retorna seu código e a descrição oficial. O `code` volta com os 7 dígitos crus, como em toda consulta desta biblioteca; passe-o para `formatCnae` para obter a forma `NNNN-N/NN`. Um valor escrito apenas com dígitos mantém os zeros à esquerda implícitos, tanto como string quanto como número: `getCnae(111301)` e `getCnae('111301')` são lidos como `0111301`. Valem as mesmas regras de entrada de `isValidCnae`: uma string precisa estar escrita com os 7 dígitos ou com a máscara `NNNN-N/NN`, e um número precisa ser um inteiro seguro não negativo. +Consulta um código de subclasse CNAE (Classificação Nacional de Atividades Econômicas) e retorna seu código e a descrição oficial. O resultado é um registro `Cnae`: `{ code, description }`. + +- Mesmas regras de `getCbo`, com 7 dígitos e a máscara `NNNN-N/NN`. +- `code` retorna com os 7 dígitos sem máscara; passe-o para `formatCnae` para obter a forma `NNNN-N/NN`. ```javascript import { formatCnae, getCnae } from '@brazilian-utils/brazilian-utils'; @@ -2029,9 +2445,13 @@ getCnae('0111abc301'); // null (não é uma forma documentada) formatCnae(getCnae('6201501')?.code); // 6201-5/01 (aplicar a máscara é trabalho do formatador) ``` +Fonte: [CNAE-Subclasses 2.3 na CONCLA/IBGE](https://concla.ibge.gov.br/busca-online-cnae.html) e a [API de subclasses do IBGE](https://servicodados.ibge.gov.br/api/v2/cnae/subclasses). + ### isValidNcm -Valida se um código NCM (Nomenclatura Comum do Mercosul) existe na tabela vigente publicada pelo Siscomex/MDIC. Aceita o código com ou sem a máscara de pontos, ou como número. Uma string só é lida como código quando está escrita em uma dessas formas (os 8 dígitos, ou a máscara `NNNN.NN.NN`, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Um código NCM sempre tem 8 dígitos e os zeros à esquerda fazem parte dele, então um valor escrito apenas com dígitos é completado com zeros à esquerda até 8, seja ele string ou número: `1012100`, `'1012100'` e `'01012100'` são o mesmo código. Um valor mascarado já carrega os seus separadores e é lido como foi escrito. +Valida um código NCM (Nomenclatura Comum do Mercosul) contra a tabela vigente publicada pelo Siscomex/MDIC. + +- Mesmas regras de `isValidCbo`, com 8 dígitos e a máscara `NNNN.NN.NN`. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; @@ -2045,9 +2465,14 @@ isValidNcm('abc01012100'); // false (não é uma forma documentada) isValidNcm(-84713012); // false (não é um inteiro seguro não negativo) ``` +Fonte: [nomenclatura NCM publicada pelo Portal Único Siscomex](https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json). + ### formatNcm -Formata um código NCM (Nomenclatura Comum do Mercosul). `options.pad` (parte de `FormatNcmOptions`) funciona exatamente como em `formatCpf`/`formatCep`: com o padrão `false` a máscara é aplicada progressivamente, até onde o valor vai, que é o que um campo sendo digitado precisa; com `true` o valor é primeiro completado com zeros à esquerda até os 8 dígitos de um código completo, então ele sempre volta com a máscara inteira. Um número é tratado exatamente como a string dos seus dígitos, ou seja, só é completado com `pad: true`. Como todo formatador deste pacote, o valor é lido pelos seus dígitos e a máscara é aplicada até onde eles vão: caracteres fora da máscara são descartados e um número é lido como a string dos seus dígitos, sinal e ponto decimal inclusos. Use `isValidNcm` para verificar um código. +Formata um código NCM (Nomenclatura Comum do Mercosul). Só a estrutura muda; use `isValidNcm` para conferir um código com a tabela. + +- **Opções** (`FormatNcmOptions`): `pad` (padrão `false`) completa antes o valor com zeros à esquerda até os 8 dígitos de um código completo. +- Mesmas regras de `formatCnae`, com a máscara `NNNN.NN.NN`. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; @@ -2062,20 +2487,24 @@ formatNcm(-84713012); // 8471.30.12 ### parseNcm -Remove a formatação do NCM (Nomenclatura Comum do Mercosul), mantém apenas os dígitos e limita o resultado aos 8 dígitos de um código completo. Nada é preenchido com zeros à esquerda aqui; use `isValidNcm`, que preenche um código numérico sem máscara, para verificar um código na tabela oficial. +Remove a formatação do NCM (Nomenclatura Comum do Mercosul), mantém apenas os dígitos e limita o resultado aos 8 dígitos de um código completo. + +- Mesmas regras de `parseCbo`: nada é completado com zeros à esquerda aqui. ```javascript import { parseNcm } from '@brazilian-utils/brazilian-utils'; parseNcm('8471.30.12'); // '84713012' -parseNcm('8471'); // '8471' (a partial code is kept as written) +parseNcm('8471'); // '8471' (um código parcial é mantido como está) ``` ### isValidCfop -Valida se um código CFOP (Código Fiscal de Operações e Prestações) existe na tabela oficial. A tabela é o [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), o texto vigente (redação atual dada pelo Ajuste SINIEF 03/24, última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), e não o texto congelado de 2001 do Ajuste SINIEF 07/01. Só os códigos operáveis contam: os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50` (1000, 1100, 1150, 5350, ...), são títulos de seção e não códigos que um documento pode carregar, então são rejeitados. +Valida um código CFOP (Código Fiscal de Operações e Prestações) contra a tabela oficial, o Anexo II consolidado do Convênio SINIEF s/nº 1970 em vigor. -Uma string só é lida como código quando está escrita em uma das formas documentadas (os 4 dígitos, ou a forma `N.NNN` impressa no anexo, com um único separador entre os grupos e espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. Nenhum código CFOP começa com zero, o seu primeiro dígito é o grupo da operação (1 a 7), então aqui nada é completado: um número e a string dos mesmos dígitos são lidos de forma idêntica. +- Só os códigos operáveis contam: os títulos de grupo e subgrupo, os códigos terminados em `00` e `50`, são rejeitados. +- Aceita uma string com os 4 dígitos ou com a forma `N.NNN`, com um único separador (espaço, `.`, `-` ou `/`), ou um número. Qualquer outra string é rejeitada. +- Nenhum código CFOP começa com zero, então nada é completado. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; @@ -2089,9 +2518,13 @@ isValidCfop('abc5102'); // false (não é uma forma documentada) isValidCfop(-5102); // false (não é um inteiro seguro não negativo) ``` +Fonte: [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). + ### parseCfop -Remove a formatação do CFOP (Código Fiscal de Operações e Prestações), mantém apenas os dígitos e limita o resultado a 4 dígitos. Um valor mais curto passa adiante até onde vai. Nenhum código CFOP começa com zero, o primeiro dígito é o grupo da operação, de 1 a 7, então nada é preenchido com zeros aqui. +Remove a formatação do CFOP (Código Fiscal de Operações e Prestações), mantém apenas os dígitos e limita o resultado a 4 dígitos. + +- Nenhum código CFOP começa com zero, então nada é completado aqui. ```javascript import { parseCfop } from '@brazilian-utils/brazilian-utils'; @@ -2101,7 +2534,9 @@ parseCfop('5.102'); // '5102' ### getCfop -Busca um código CFOP (Código Fiscal de Operações e Prestações) e retorna seu código e a descrição oficial, na redação do [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), no texto vigente, com última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). Os títulos de grupo e subgrupo da nomenclatura oficial, os códigos terminados em `00` e `50`, não estão na tabela e retornam `null`. Valem as mesmas regras de entrada de `isValidCfop`. +Consulta um código CFOP (Código Fiscal de Operações e Prestações) e retorna seu código e a descrição oficial. O resultado é um registro `Cfop`: `{ code, description }`. + +- Mesmas regras de `isValidCfop`. Retorna `null` para um título, um código desconhecido ou um valor fora das formas documentadas. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; @@ -2113,6 +2548,8 @@ getCfop('5350'); // null (título de subgrupo, não é um código operável) getCfop('abc5102'); // null (não é uma forma documentada) ``` +Fonte: [Anexo II consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), última alteração pelo [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). + ### isValidCst Valida um código de CST (Código de Situação Tributária) para um tributo. Informe o tributo em `options.tax`: @@ -2124,13 +2561,9 @@ Valida um código de CST (Código de Situação Tributária) para um tributo. In | `pis` | 2 dígitos | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 dígitos | mesma tabela do `pis` | -`options.tax` (parte de `IsValidCstOptions`) é opcional: omita-o para aceitar um código que exista em qualquer uma das quatro tabelas acima. Um `tax` fora desses quatro valores cai nesse mesmo padrão em tempo de execução, do jeito que toda outra opção escalar desta biblioteca trata um valor que não conhece. - -A Tabela B do ICMS é a vigente: o [Anexo I consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), cuja redação atual veio do [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (efeitos a partir de 01.12.23) e que o [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) alterou suprimindo os itens 12, 13, 52, 72 e 74 (efeitos a partir de 09.07.24) antes que eles chegassem a produzir efeitos: o 39/23 havia adiado a produção de efeitos deles para 1º de outubro de 2024, então a revogação os alcançou antes e esses códigos nunca estiveram em vigor. `02`, `15`, `53` e `61` são seus códigos de monofasia de combustíveis. - -Uma string só é lida como código quando está escrita em uma das formas documentadas (os 2 dígitos de um código da Tabela B, ou os 3 dígitos da forma do ICMS com um único separador opcional depois do dígito de origem, além de espaços em branco opcionais no início e no fim), e um número só quando é um inteiro seguro não negativo. O dígito de origem é a única fronteira que um CST impresso tem, então `'0 10'` e `'1-10'` são lidos, mas `'0-0'`, `'11-0'` e `'00-'` não. - -Um único dígito é mais estreito que qualquer uma das formas documentadas, então ele é completado com zeros à esquerda até os 3 dígitos da forma do ICMS, seja ele string ou número: `0`, `'0'` e `'000'` são todos o código ICMS `000`. Um valor de 2 dígitos já é uma forma documentada, um código da Tabela B, e é lido como foi escrito, ou seja, um código da Tabela B mantém os seus dois dígitos: `'07'`, não `7`, que é o código ICMS `007`. +- **Opções** (`IsValidCstOptions`): `tax` escolhe a tabela. Omitido, ou fora desses quatro valores, todas as tabelas são aceitas. +- Aceita uma string com os 2 dígitos de um código da Tabela B ou os 3 dígitos da forma do ICMS, ou um número. A forma do ICMS pode ter um único separador (espaço, `.`, `-` ou `/`) depois do dígito de origem. +- Um único dígito é completado até a forma de 3 dígitos do ICMS; uma string de 2 dígitos é um código da Tabela B, enquanto o número `7` é o código ICMS `007`. ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; @@ -2149,30 +2582,36 @@ isValidCst('abc110'); // false (não é uma forma documentada) isValidCst(-110); // false (não é um inteiro seguro não negativo) ``` +Fonte: Tabela B do ICMS do [Anexo I do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) alterado pelo [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24); IPI, PIS e COFINS da [IN RFB nº 1.009/2010](https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=15974). + ### isValidCsosn -Valida se um código de CSOSN (Código de Situação da Operação no Simples Nacional) é um dos 10 códigos do [Anexo III-A consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), a tabela instituída pelo Ajuste SINIEF 03/2010: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` ou `900`. +Valida um código de CSOSN (Código de Situação da Operação no Simples Nacional) como um dos 10 códigos da tabela oficial: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` ou `900`. -Uma string só é lida como código quando está escrita como os 3 dígitos puros, com espaços em branco opcionais no início e no fim: um CSOSN não tem agrupamento impresso (a NF-e leva o dígito de origem no seu próprio campo `orig`), então `'1-01'` é rejeitado; um número só é lido quando é um inteiro seguro não negativo. Nenhum código CSOSN começa com zero, a tabela vai de `101` a `900`, então aqui nada é completado: um número e a string dos mesmos dígitos são lidos de forma idêntica. +- Aceita uma string com os 3 dígitos puros, ou um número. Um CSOSN não tem agrupamento impresso, então `'1-01'` é rejeitado. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; isValidCsosn('101'); // true +isValidCsosn(900); // true isValidCsosn('999'); // false isValidCsosn('abc101'); // false (não é uma forma documentada) isValidCsosn(-101); // false (não é um inteiro seguro não negativo) ``` +Fonte: [Anexo III-A consolidado do Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) e [Ajuste SINIEF 03/2010](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10). + ## Texto ### capitalize -Transforma a primeira letra de cada palavra em maiúscula do jeito que se escreve um nome, uma razão social ou um endereço brasileiro, sem precisar de opções. As palavras são separadas por espaço em branco, por `-` e `/`, pelo apóstrofo (`'d'oeste'` vira `'d'Oeste'`) e pela pontuação colada à palavra (`'(empresa)'` vira `'(Empresa)'`, `'bairro:centro'` vira `'Bairro:Centro'`), então `'MOGI-GUAÇU'` vira `'Mogi-Guaçu'`; os separadores ficam onde estão. Toda sequência de espaços em branco (tabs, quebras de linha, espaços repetidos) vira um único espaço, e o espaço no início e no fim é descartado. As partículas de nomes de origem estrangeira (`del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) ficam em minúsculas como as preposições do português, e a partícula elidida `d'` também, onde quer que apareça, sempre que um apóstrofo e uma palavra vierem logo depois (`'dias d'ávila'` vira `'Dias d'Ávila'`); uma letra sozinha logo depois de um apóstrofo é o possessivo do inglês e também fica em minúscula (`"bob's"` vira `"Bob's"`). - -`options.lowerCaseWords` tem como padrão as preposições, artigos e conjunções que permanecem em minúsculas dentro de um nome próprio (`de`, `da`, `do`, `e`, ...), e elas só ficam em minúsculas quando ligam duas palavras: uma delas que seja a primeira palavra, que encerre o valor ou que venha antes de uma pontuação é um designativo e mantém a maiúscula (`'rua a, 100'` vira `'Rua A, 100'` e `'condomínio a, quadra d, lote o'` vira `'Condomínio A, Quadra D, Lote O'`). `options.upperCaseWords` tem como padrão as designações societárias e as abreviações de documentos escritas em maiúsculas no uso brasileiro (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) mais os algarismos romanos que aparecem em nomes e endereços (de `II` a `XXIII`, exceto `VI`, que colide com a forma verbal "vi"). `SA` sem pontuação ficou de fora de propósito, por ser indistinguível do sobrenome "Sá" digitado sem o acento, enquanto `ME` é também o pronome "me", então só fica em maiúsculas na posição de designação, como última palavra do valor (`'fulano comércio me'` vira `'Fulano Comércio ME'`) ou logo antes de outra designação (`'fulano me epp'` vira `'Fulano ME EPP'`); em qualquer outro lugar é uma palavra comum (`'diga-me a verdade'` vira `'Diga-Me a Verdade'`, `'não-me-toque'` vira `'Não-Me-Toque'`). `S/A` e `S/S` são reconhecidos mesmo com a barra no meio, embora a barra separe palavras. Uma palavra de duas letras logo depois de uma `/` vira maiúscula quando é a sigla de um estado brasileiro (`'porto alegre/rs'` vira `'Porto Alegre/RS'`); essa regra é estrutural e continua valendo mesmo com `upperCaseWords` informado, enquanto uma sigla de estado que não venha depois de uma `/` é deixada como está. +Transforma em maiúscula a primeira letra de cada palavra, do jeito que se escreve um nome, uma razão social ou um endereço brasileiro, sem precisar de opções. -Qualquer uma das listas informada em `options` substitui inteiramente a lista padrão correspondente, e a comparação com as duas é case-insensitive (locale pt-BR). As opções são tipadas como `CapitalizeOptions`. As demais palavras são capitalizadas letra a letra: `'İSTANBUL'` vira `'İstanbul'`, e uma primeira letra cuja maiúscula tem duas letras (`ß`, a ligadura `fi`) mantém a forma, então `'straße'` vira `'Straße'` e `'ßa'` continua `'ßa'`. +- **Opções** (`CapitalizeOptions`): `lowerCaseWords`, palavras mantidas em minúsculas entre duas palavras, por padrão preposições e artigos como `de`, `da`, `do`, `e`; `upperCaseWords`, palavras sempre em maiúsculas, por padrão designações societárias e abreviações como `LTDA`, `S.A.`, `ME`, `CNPJ` e algarismos romanos. Uma lista substitui a padrão. +- Palavras se separam em espaços, `-`, `/`, apóstrofos e pontuação colada; espaços repetidos viram um só. +- Palavra minúscula que é a primeira, a última ou precede pontuação é designativo e mantém a maiúscula. +- `ME` só vira maiúsculas como designação (última palavra ou antes de outra); `SA` sem pontos fica como está (o sobrenome Sá). Sigla de estado após `/` vira maiúsculas mesmo com `upperCaseWords` informado. ```javascript import { capitalize } from '@brazilian-utils/brazilian-utils'; @@ -2198,13 +2637,17 @@ capitalize('joão paulo ii'); // João Paulo II capitalize('de'); // De (uma preposição mantém a maiúscula quando é a primeira palavra) capitalize('empresa ltda', { upperCaseWords: [] }); // Empresa Ltda (a lista informada substitui a padrão) capitalize('josé Ama MARIA', { lowerCaseWords: ['ama'] }); // José ama Maria -capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido (comparação case-insensitive) +capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido (comparação sem diferenciar maiúsculas de minúsculas) capitalize(' josé maria '); // José Maria (toda sequência de espaço em branco, tabs e quebras de linha inclusive, vira um único espaço) ``` +Fonte: [Manual de Redação da Presidência da República](https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica/manual-de-redacao.pdf). + ### removeAccents -Remove marcas diacríticas (acentos, tils, cedilhas) de uma string, decompondo cada caractere acentuado em sua letra base mais as marcas de combinação (Unicode NFD) e descartando essas marcas. +Remove marcas diacríticas (acentos, tils, cedilhas) de uma string. + +- Toda marca de combinação (categoria geral M do Unicode) é descartada, então acentos de qualquer escrita são removidos. ```javascript import { removeAccents } from '@brazilian-utils/brazilian-utils'; @@ -2216,30 +2659,52 @@ removeAccents('Açaí'); // 'Acai' removeAccents(''); // '' ``` -## isValidIe +## Inscrição estadual (IE) + +### isValidIe -Valida se a inscrição estadual de um estado é válida. A UF é case-insensitive. Regras notáveis por estado: GO aceita os prefixos `10`, `11` e `15`; PA aceita `15` e `75`-`79`; MS aceita `28` e `50`; SP tem o padrão de produtor rural `P0MMMSSSSD000`; TO usa códigos de tipo de 11 dígitos (`01`, `02`, `03`, `99`). O TO também aceita uma forma de 9 dígitos, aplicando a mesma regra módulo 11 sobre os oito primeiros dígitos; a página do SINTEGRA documenta apenas a de 11 dígitos, então essa forma é comportamento da 2.3.0 mantido por compatibilidade, e não regra publicada. Uma inscrição só de zeros é aceita em todo estado cuja fórmula publicada produz dígito verificador 0 para ela (AM, BA com 8 ou 9 dígitos, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP e TO com 9 dígitos), diferente de `isValidCpf` e `isValidCnpj`, que rejeitam dígitos repetidos. O AM entra nessa lista apenas pelo segundo ramo da fórmula publicada: o primeiro ramo da página, `Se Soma < 11 Então Dígito = 11 - Soma`, dá 11 para a inscrição só de zeros, enquanto o ramo `resto <= 1 ⇒ 0`, o implementado aqui, dá 0. A inscrição e a UF vão juntas num único objeto, tipado como `IsValidIeParams`; a forma da 2.3.0, `isValidIe(stateCode, ie)`, continua funcionando e está deprecada. +Valida uma inscrição estadual para um estado. **Descontinuada:** a forma posicional `isValidIe(stateCode, ie)` continua funcionando, mas está descontinuada; use a forma com objeto `isValidIe({ value, stateCode })`. + +- Recebe um único objeto (`IsValidIeParams`): `value` é a inscrição e `stateCode` o estado ao qual ela pertence (um `StateCode`, sem diferenciar maiúsculas de minúsculas). +- GO, PA, MS, SP, TO, DF, PE, AL e RJ têm casos especiais (prefixos ou formatos extras, ou um desvio da página do SINTEGRA); veja o JSDoc em `src/is-valid-ie` para os detalhes. +- Uma inscrição só de zeros é aceita em todo estado cuja fórmula publicada produz dígito verificador 0 para ela: AM, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE e SP, mais BA com 8 ou 9 dígitos e TO com 9 dígitos. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; +isValidIe({ value: '110042490114', stateCode: 'SP' }); // true +isValidIe({ value: 'P011004243002', stateCode: 'SP' }); // true (produtor rural) isValidIe({ value: '0187634580933', stateCode: 'AC' }); // false -isValidIe({ value: '109161793', stateCode: 'go' }); // true (case-insensitive) +isValidIe({ value: '109161793', stateCode: 'go' }); // true (não diferencia maiúsculas de minúsculas) ``` -## isValidEmail +Fonte: [páginas dos estados no SINTEGRA](http://www.sintegra.gov.br/insc_est.html) e o [roteiro de crítica da SEFAZ-GO](https://goias.gov.br/economia/roteiro-de-critica-da-inscricao-estadual-de-goias/). + +## E-mail + +### isValidEmail + +Valida um endereço de e-mail. Um subconjunto prático da definição do HTML da WHATWG. -Valida se email é válido. O conjunto aceito é um subconjunto prático da definição de [endereço de e-mail válido](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) do HTML da WHATWG, e não da [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322). A parte local é limitada a letras, dígitos e `_'+-.`, e não pode começar com ponto, terminar com ponto ou apóstrofo, nem conter dois pontos seguidos. O domínio precisa ter pelo menos um ponto, e cada rótulo separado por ponto segue a produção `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?` da WHATWG, então um rótulo não pode começar nem terminar com hífen nem passar de 63 caracteres; o rótulo final é alfabético e tem de 2 a 63 letras, então `user@example.c1` é rejeitado. Partes locais entre aspas (`"john doe"@example.com`) e literais de endereço (`john@[127.0.0.1]`) são rejeitadas. +- Parte local: letras, dígitos e `_'+-.`, sem ponto no início ou no fim e sem dois pontos seguidos. +- Domínio: pelo menos um ponto, rótulos de até 63 caracteres, rótulo final de 2 a 63 letras; partes locais entre aspas e literais de endereço são rejeitados. ```javascript import { isValidEmail } from '@brazilian-utils/brazilian-utils'; isValidEmail('john.doe@hotmail.com'); // true +isValidEmail('invalid.email'); // false ``` -## isValidCreditCard +Fonte: [HTML da WHATWG, valid e-mail address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) e [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322). + +## Cartão de crédito + +### isValidCreditCard + +Valida um número de cartão de pagamento (crédito ou débito) com o algoritmo de Luhn. Só a quantidade de dígitos (12 a 19) e o dígito verificador de Luhn são conferidos. Não há detecção de bandeira (Visa, Mastercard, Amex...), consulta de faixa de emissor nem validação de validade/CVV. -Valida se um número de cartão de pagamento é válido usando o algoritmo de Luhn ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Aceita os caracteres de máscara usuais (espaço em branco, `.`, `-` e `/`, o conjunto intercambiável que `isValidCpf` e `isValidCnpj` aceitam) entre dois dígitos quaisquer e espaços ao redor do valor; qualquer outro caractere invalida o valor. Eles são aceitos entre dois dígitos quaisquer, e não em posições fixas, porque o agrupamento impresso de um PAN muda com a bandeira (4-4-4-4 para Visa e Mastercard, 4-6-5 para American Express, 4-6-4 para Diners Club), então não há um único leiaute ao qual prendê-los. Não faz detecção de bandeira (Visa, Mastercard, Amex...), consulta de faixa de emissor nem validação de validade/CVV, verifica apenas a quantidade de dígitos (12 a 19) e o dígito verificador de Luhn. Um `number` só é aceito quando é um inteiro seguro não negativo: qualquer valor acima de `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 dígitos) já chega arredondado para outro número, então passe cartões mais longos como string. Um valor cujos dígitos são todos iguais (`'0000000000000000'`) é rejeitado mesmo passando no cálculo de Luhn, do jeito que todo outro validador deste pacote rejeita um documento de dígitos repetidos (`isValidCpf('00000000000')`, `isValidCns`, `isValidCaepf`, `isValidCei`). +- Aceita uma string ou um número, com os caracteres de máscara (espaço em branco, `.`, `-` e `/`) em qualquer posição entre os dígitos. ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -2248,6 +2713,7 @@ isValidCreditCard('4111111111111111'); // true (número de teste Visa) isValidCreditCard('5555555555554444'); // true (número de teste Mastercard) isValidCreditCard('378282246310005'); // true (número de teste American Express) isValidCreditCard('4111 1111 1111 1111'); // true (máscara com espaços) +isValidCreditCard('4111 - 1111 - 1111 - 1111'); // true (uma sequência de separadores entre os dígitos) isValidCreditCard('4111.1111/1111-1111'); // true (qualquer um dos caracteres de máscara) isValidCreditCard('4111111111111112'); // false (dígito verificador inválido) isValidCreditCard('0000000000000000'); // false (todos os dígitos iguais, ainda que o Luhn feche) @@ -2255,24 +2721,42 @@ isValidCreditCard('4111a1111b1111c1111'); // false (letras entre os dígitos) isValidCreditCard(4111111111111111111); // false (acima de 2^53 - 1, passe como string) ``` -## isValidRegistroProfissional +Fonte: [ISO/IEC 7812-1](https://www.iso.org/standard/70484.html). -Verifica a estrutura de um número de registro/inscrição profissional. Recebe um único objeto, tipado como `IsValidRegistroProfissionalParams`, no mesmo formato do `isValidBankAccount`: `value` é o número do registro, `council` escolhe o conselho emissor (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e o `stateCode` opcional verifica a UF embutida (ignorado para `"CRP"`, cujo prefixo de 2 dígitos é um código regional, não uma UF literal). Qualquer coisa que não seja um objeto, e um objeto sem `value` ou sem `council`, é `false`. Os formatos aceitos são de 4 a 6 dígitos mais a UF para `"OAB"` e `"CRM"`, de 3 a 6 dígitos mais a UF para `"CRO"`, um código regional de 2 dígitos mais 4 a 6 dígitos para `"CRP"`, e a UF mais 6 dígitos, o tipo de registro e um dígito verificador para `"CRC"`. É apenas uma verificação estrutural: a quantidade de dígitos e a UF são validadas, mas nenhum dígito verificador é calculado, mesmo para o CRC, cujo formato inclui um. Um registro no CRC é a UF, 6 dígitos, o tipo de registro (`"O"` Originário ou `"P"` Provisório, que nada diz sobre a categoria profissional) e o dígito verificador, conforme o [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). Um Registro Transferido ou Secundário acrescenta `"T"` ou `"S"` e a UF do CRC de destino **depois** do dígito verificador, conforme esse mesmo item e a [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: os exemplos do próprio Manual são `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` e `PI-111222/O-5 S-AC`. As duas UFs precisam ser códigos reais, e o `stateCode` é comparado com a de origem. O código regional do CRP precisa ser um dos [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) do sistema CFP, de CRP-01 a CRP-24. Só o formato do CRC e esses códigos regionais do CRP se apoiam em fonte publicada: a página do CFP não publica o tamanho do número de inscrição, e a OAB, o CFM e o CFO não publicam formato algum, então as faixas de dígitos aceitas para `"CRP"`, `"OAB"`, `"CRM"` e `"CRO"` são convencionais, não normativas (a busca pública da OAB/SP tem `maxlength="7"`, e o CFM documenta CRMs com prefixo `300` e sufixo `P`, nenhum deles expresso por esses formatos). O CREA não é suportado: seu formato de registro não pôde ser confirmado em uma fonte oficial e publicamente documentada após a unificação nacional de 2016 (RNP). +## Registro profissional + +### isValidRegistroProfissional + +Verifica a estrutura de um número de registro em conselho profissional (registro/inscrição profissional). Só a quantidade de dígitos e a UF são conferidas, nunca o dígito verificador, nem no CRC. + +- Recebe um objeto (`IsValidRegistroProfissionalParams`): `value`, `council` (`RegistroProfissionalCouncil`: `"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` ou `"CRC"`) e `stateCode` opcional (UF esperada). +- `"OAB"` e `"CRM"`: 4 a 6 dígitos mais a UF (`123456/SP`, `123456-SP`); `"CRO"`: 3 a 6 dígitos (`12345/SP`). +- `"CRP"`: código regional de 2 dígitos (`01` a `24`) mais 4 a 6 dígitos (`06/12345`); `stateCode` é ignorado. +- `"CRC"`: UF, 6 dígitos, tipo de registro (`O` ou `P`) e dígito verificador (`SP-123456/O-3`); transferência acrescenta `T` ou `S` e a UF destino (`SP-123456/O-3 T-MG`). `stateCode` confere a UF de origem. +- Formatos de OAB, CRM, CRO e CRP são convencionais (nenhum é publicado); CREA não é coberto. ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; isValidRegistroProfissional({ value: '123456/SP', council: 'OAB' }); // true isValidRegistroProfissional({ value: '123456-RJ', council: 'OAB', stateCode: 'SP' }); // false (UF divergente) +isValidRegistroProfissional({ value: '123456', council: 'OAB' }); // false (sem UF) isValidRegistroProfissional({ value: '06/12345', council: 'CRP' }); // true isValidRegistroProfissional({ value: 'SP-123456/O-3', council: 'CRC' }); // true isValidRegistroProfissional({ value: 'SP-123456/O-3 T-MG', council: 'CRC' }); // true (registro transferido) isValidRegistroProfissional({ value: 'SP-123456/T-3', council: 'CRC' }); // false ("T" não é tipo de registro) ``` -## isValidVin +Fonte: [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf), [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), [regionais do CFP](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/). + +## VIN -Valida se um VIN (Vehicle Identification Number / chassi) é válido. Verifica o tamanho (17 caracteres), as letras excluídas (`I`, `O`, `Q` nunca são válidas; estrutura da [ISO 3779:2009](https://www.iso.org/standard/52200.html)) e o dígito verificador na 9ª posição, calculado e transliterado conforme o [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). Esse dígito verificador é uma exigência norte-americana (49 CFR 565.15 / SAE J853): a [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (que revogou a Resolução CONTRAN nº 24/1998 a partir de 1º de janeiro de 2025) e a ABNT NBR 6066 definem a estrutura do VIN brasileiro, mas não o exigem, então muitos VINs fabricados no Brasil não possuem um dígito verificador correspondente. Esta função é, portanto, uma verificação estrutural no padrão norte-americano, não um validador universal de VINs brasileiros. Não diferencia maiúsculas de minúsculas e remove espaços nas extremidades. Um VIN é impresso como uma sequência única de 17 caracteres, então, diferente dos documentos que este pacote mascara (`isValidCpf`, `isValidCnpj`, `isValidNfeKey`), ele não tem limite de grupo onde escrever um separador e nenhum é aceito: um espaço, `.`, `-` ou `/` entre os caracteres é rejeitado em vez de removido. Um valor cujos 17 caracteres são todos iguais (`'00000000000000000'`) é rejeitado mesmo com o dígito verificador correspondente, do jeito que todo outro validador deste pacote rejeita um documento de dígitos repetidos. +### isValidVin + +Valida um VIN (Vehicle Identification Number / chassi). É uma verificação estrutural no padrão norte-americano, não um validador universal de VINs brasileiros. + +- Confere o tamanho de 17 caracteres, as letras excluídas `I`, `O` e `Q` e o dígito verificador na 9ª posição. +- As normas brasileiras não exigem o dígito verificador, então muitos VINs fabricados no Brasil não passam nele. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; @@ -2282,4 +2766,56 @@ isValidVin('1m8gdm9axkp042788'); // true (dígito verificador X, minúsculo) isValidVin('1HGCM82633A004353'); // false (dígito verificador inválido) isValidVin('00000000000000000'); // false (todos os caracteres iguais, ainda que o dígito feche) isValidVin('1HGCM8263IA004352'); // false (contém a letra excluída I) +isValidVin('1HGCM82633A00435'); // false (16 caracteres) +``` + +Fonte: [ISO 3779:2009](https://www.iso.org/standard/52200.html), [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15) e [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf). + +## Standard Schema + +### toStandardSchema + +Embrulha um utilitário `isValid*` em um [Standard Schema](https://standardschema.dev), o formato de validador que bibliotecas de formulário, roteadores e frameworks de API aceitam: TanStack Form, react-hook-form, tRPC, Hono e outros. + +- `config.options` é repassado ao validador a cada chamada, e `config.message` é a mensagem da issue (padrão `'Invalid value'`). Os dois fazem parte de `ToStandardSchemaOptions`. +- Valida de forma síncrona e não transforma: um valor válido volta como foi passado, um inválido gera uma única issue. +- Validadores que recebem um objeto (`isValidBankAccount`, `isValidRegistroProfissional`, `isValidIe`) funcionam do mesmo jeito. Embrulhe o `isValidIe`, que tem sobrecarga, em uma arrow function: `toStandardSchema((params) => isValidIe(params))`. +- Os tipos da especificação (`StandardSchemaV1`, `StandardSchemaV1Result`, `StandardSchemaV1Issue` e os demais) também são exportados, então nada mais é instalado. + +```javascript +import { isValidCnpj, isValidCpf, toStandardSchema } from '@brazilian-utils/brazilian-utils'; + +const cpf = toStandardSchema(isValidCpf, { message: 'CPF inválido' }); + +cpf['~standard'].validate('123.456.789-09'); // { value: '123.456.789-09' } +cpf['~standard'].validate('123'); // { issues: [{ message: 'CPF inválido' }] } + +const cnpj = toStandardSchema(isValidCnpj, { options: { version: 2 } }); // CNPJ alfanumérico + +// Tudo que recebe um Standard Schema aceita o resultado como está, um campo do TanStack Form por exemplo +; +``` + +Dentro de um schema do Zod ou do Valibot os validadores entram direto, sem wrapper, e o resultado já é um Standard Schema: + +```javascript +import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'; +import { isValidCep, isValidCpf } from '@brazilian-utils/brazilian-utils'; +import { useForm } from 'react-hook-form'; +import * as v from 'valibot'; +import { z } from 'zod'; + +const zodSchema = z.object({ + cpf: z.string().refine(isValidCpf, 'CPF inválido'), + cep: z.string().refine(isValidCep, 'CEP inválido'), +}); + +const valibotSchema = v.object({ + cpf: v.pipe(v.string(), v.check(isValidCpf, 'CPF inválido')), + cep: v.pipe(v.string(), v.check(isValidCep, 'CEP inválido')), +}); + +const form = useForm({ resolver: standardSchemaResolver(zodSchema) }); // ou valibotSchema ``` + +Fonte: [especificação do Standard Schema](https://standardschema.dev). diff --git a/docs/robots.txt b/docs/robots.txt index 9a0726f8a..4f83e52d2 100644 --- a/docs/robots.txt +++ b/docs/robots.txt @@ -1,4 +1,5 @@ User-agent: * Allow: / +Disallow: /snippets/ Sitemap: https://brazilian-utils.com.br/sitemap.xml diff --git a/docs/sitemap.xml b/docs/sitemap.xml deleted file mode 100644 index 30e9345a5..000000000 --- a/docs/sitemap.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - https://brazilian-utils.com.br/ - - - - - https://brazilian-utils.com.br/getting-started - - - - - - https://brazilian-utils.com.br/utilities - - - - - - https://brazilian-utils.com.br/migration-v1-to-v2 - - - - - - https://brazilian-utils.com.br/pt-br/getting-started - - - - - - https://brazilian-utils.com.br/pt-br/utilities - - - - - - https://brazilian-utils.com.br/pt-br/migration-v1-to-v2 - - - - - diff --git a/docs/snippets/address-form/angular/address-by-cep.ts b/docs/snippets/address-form/angular/address-by-cep.ts new file mode 100644 index 000000000..88fc6b5f8 --- /dev/null +++ b/docs/snippets/address-form/angular/address-by-cep.ts @@ -0,0 +1,16 @@ +import { resource, type Signal } from "@angular/core"; +import { getAddressInfoByCep, isValidCep } from "@brazilian-utils/brazilian-utils"; + +/** + * The address of a CEP, looked up as the CEP changes. A resource reloads when what it is about + * changes, drops the answer to a CEP that is no longer the one on screen and stops with the + * component that asked. `getAddressInfoByCep` takes no signal, so the request itself is not + * stopped, only its answer is. + */ +export function addressByCep(cep: Signal) { + return resource({ + // An incomplete CEP is not worth asking about, and a resource with nothing to ask about waits. + params: () => (isValidCep(cep()) ? cep() : undefined), + loader: ({ params }) => getAddressInfoByCep(params), + }); +} diff --git a/docs/snippets/address-form/angular/address-form.ts b/docs/snippets/address-form/angular/address-form.ts new file mode 100644 index 000000000..226bd1012 --- /dev/null +++ b/docs/snippets/address-form/angular/address-form.ts @@ -0,0 +1,71 @@ +import { Component, effect, signal } from "@angular/core"; +import { Field } from "./field"; +import { CepField } from "./cep-field"; +import { addressByCep } from "./address-by-cep"; + +@Component({ + selector: "app-address-form", + imports: [Field, CepField], + template: ` +
+ + + + + + + + + `, +}) +export class AddressForm { + protected readonly cep = signal(""); + protected readonly street = signal(""); + protected readonly neighborhood = signal(""); + protected readonly city = signal(""); + protected readonly state = signal(""); + + private readonly address = addressByCep(this.cep); + + protected status() { + if (this.address.isLoading()) return "Looking it up…"; + + return this.address.error() ? "No address for this CEP" : ""; + } + + // What the lookup found is what the form starts from; it stays editable from there. Asking a + // resource for a value it does not have throws, so it is asked whether it has one first. + private readonly fill = effect(() => { + if (!this.address.hasValue()) return; + + const found = this.address.value(); + + this.street.set(found.street); + this.neighborhood.set(found.neighborhood); + this.city.set(found.city); + this.state.set(found.state); + }); +} diff --git a/docs/snippets/address-form/react/address-form.tsx b/docs/snippets/address-form/react/address-form.tsx new file mode 100644 index 000000000..62949e075 --- /dev/null +++ b/docs/snippets/address-form/react/address-form.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { CepField } from "./cep-field"; +import { Field } from "./field"; +import { useGetAddressByCep } from "./use-get-address-by-cep"; + +const EMPTY = { street: "", neighborhood: "", city: "", state: "" }; +const STATUS = { + idle: "", + loading: "Looking it up…", + found: "", + failed: "No address for this CEP", +}; + +export function AddressForm() { + const [cep, setCep] = useState(""); + const [address, setAddress] = useState(EMPTY); + const lookup = useGetAddressByCep(cep); + + // What the lookup found is what the form starts from; it stays editable from there. + useEffect(() => { + if (lookup.status === "found") setAddress(lookup.address); + if (lookup.status === "failed") setAddress(EMPTY); + }, [lookup]); + + return ( +
event.preventDefault()}> + + + {/* The same field the document field guide builds, told what it is about. */} + setAddress({ ...address, street })} + /> + setAddress({ ...address, neighborhood })} + /> + setAddress({ ...address, city })} + /> + setAddress({ ...address, state })} + /> + + ); +} diff --git a/docs/snippets/address-form/react/use-get-address-by-cep.ts b/docs/snippets/address-form/react/use-get-address-by-cep.ts new file mode 100644 index 000000000..4bf56664d --- /dev/null +++ b/docs/snippets/address-form/react/use-get-address-by-cep.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from "react"; +import { + getAddressInfoByCep, + isValidCep, + type AddressInfo, +} from "@brazilian-utils/brazilian-utils"; + +export type Lookup = + | { status: "idle" } + | { status: "loading" } + | { status: "found"; address: AddressInfo } + | { status: "failed" }; + +/** + * The address of a CEP, looked up as the CEP changes: an incomplete one is not worth asking about, + * and the answer to a CEP that is no longer the one on screen is dropped, as is one that arrives + * after the component is gone. `getAddressInfoByCep` takes no signal, so the request itself is not + * stopped, only its answer is. + */ +export function useGetAddressByCep(cep: string): Lookup { + const [lookup, setLookup] = useState({ status: "idle" }); + + useEffect(() => { + if (!isValidCep(cep)) { + setLookup({ status: "idle" }); + return; + } + + const controller = new AbortController(); + + setLookup({ status: "loading" }); + + getAddressInfoByCep(cep) + .then((address) => { + if (!controller.signal.aborted) setLookup({ status: "found", address }); + }) + .catch(() => { + if (!controller.signal.aborted) setLookup({ status: "failed" }); + }); + + return () => controller.abort(); + }, [cep]); + + return lookup; +} diff --git a/docs/snippets/address-form/vanilla/address-form.html b/docs/snippets/address-form/vanilla/address-form.html new file mode 100644 index 000000000..d3ff5dfc0 --- /dev/null +++ b/docs/snippets/address-form/vanilla/address-form.html @@ -0,0 +1,88 @@ + + + + Address from a CEP + +
+ + + + + + + + + + + + + + + + +
+ + + diff --git a/docs/snippets/address-form/vue/address-form.vue b/docs/snippets/address-form/vue/address-form.vue new file mode 100644 index 000000000..e58ed4e64 --- /dev/null +++ b/docs/snippets/address-form/vue/address-form.vue @@ -0,0 +1,36 @@ + + + diff --git a/docs/snippets/address-form/vue/use-get-address-by-cep.ts b/docs/snippets/address-form/vue/use-get-address-by-cep.ts new file mode 100644 index 000000000..a5649bffd --- /dev/null +++ b/docs/snippets/address-form/vue/use-get-address-by-cep.ts @@ -0,0 +1,48 @@ +import { ref, toValue, watch, type MaybeRefOrGetter } from "vue"; +import { + getAddressInfoByCep, + isValidCep, + type AddressInfo, +} from "@brazilian-utils/brazilian-utils"; + +export type Lookup = + | { status: "idle" } + | { status: "loading" } + | { status: "found"; address: AddressInfo } + | { status: "failed" }; + +/** + * The address of a CEP, looked up as the CEP changes: an incomplete one is not worth asking about, + * and the answer to a CEP that is no longer the one on screen is dropped, as is one that arrives + * after the component is gone. `getAddressInfoByCep` takes no signal, so the request itself is not + * stopped, only its answer is. + */ +export function useGetAddressByCep(cep: MaybeRefOrGetter) { + const lookup = ref({ status: "idle" }); + + watch( + () => toValue(cep), + (current, _previous, onCleanup) => { + if (!isValidCep(current)) { + lookup.value = { status: "idle" }; + return; + } + + const controller = new AbortController(); + + onCleanup(() => controller.abort()); + lookup.value = { status: "loading" }; + + getAddressInfoByCep(current) + .then((address) => { + if (!controller.signal.aborted) lookup.value = { status: "found", address }; + }) + .catch(() => { + if (!controller.signal.aborted) lookup.value = { status: "failed" }; + }); + }, + { immediate: true }, + ); + + return lookup; +} diff --git a/docs/snippets/document-field/_templates/angular/document-field.ts b/docs/snippets/document-field/_templates/angular/document-field.ts new file mode 100644 index 000000000..fa14466d7 --- /dev/null +++ b/docs/snippets/document-field/_templates/angular/document-field.ts @@ -0,0 +1,63 @@ +import { Component, EventEmitter, Input, Output, forwardRef, signal } from "@angular/core"; +import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +@@fieldImports@@ +import { Field } from "./field"; + +/** The field of the form, with what makes it a @@label@@ and nothing else. */ +@Component({ + selector: "app-@@kind@@-field", + imports: [Field], + providers: [ + { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => @@Name@@Field), multi: true }, + ], + template: ` + + `, + // As with the field it wraps: the form around it lays out what is inside. + styles: `:host { display: contents; }`, +}) +export class @@Name@@Field implements ControlValueAccessor { + /** What the form says is wrong with the value, if anything. */ + @Input() errorMessage?: string; + + /** The @@label@@ without its mask, for a field bound with `[(value)]` rather than to a form. */ + @Input("value") set boundValue(value: string) { + this.value.set(value ?? ""); + } + + @Output() readonly valueChange = new EventEmitter(); + + protected readonly mask = { format: @@format@@, parse: @@parse@@ }; + protected readonly value = signal(""); + + protected onChange: (value: string) => void = () => {}; + protected onTouched: () => void = () => {}; + + writeValue(value: string | null): void { + this.value.set(value ?? ""); + } + + registerOnChange(onChange: (value: string) => void): void { + this.onChange = onChange; + } + + registerOnTouched(onTouched: () => void): void { + this.onTouched = onTouched; + } + + protected onValue(value: string) { + this.value.set(value); + this.onChange(value); + this.valueChange.emit(value); + } +} diff --git a/docs/snippets/document-field/_templates/angular/field.ts b/docs/snippets/document-field/_templates/angular/field.ts new file mode 100644 index 000000000..905dd2b76 --- /dev/null +++ b/docs/snippets/document-field/_templates/angular/field.ts @@ -0,0 +1,122 @@ +import { + Component, + EventEmitter, + Input, + Output, + computed, + forwardRef, + signal, +} from "@angular/core"; +import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { MaskDirective, type MaskChange } from "./mask.directive"; + +export type Mask = { + /** Formats what is typed, as it is typed. */ + format: (value: string) => string; + /** Takes the mask off, for whoever holds the value. */ + parse: (value: string) => string; +}; + +/** One id per field on the page, to tie each label and message to their own input. */ +let fields = 0; + +/** A labelled input that says what is wrong with it, masked when it is given a mask. */ +@Component({ + selector: "app-field", + imports: [MaskDirective], + providers: [ + { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Field), multi: true }, + ], + template: ` + + + +

{{ errorMessage }}

+ `, + // A component is an element of its own; this one stands aside so its label, input and message + // are laid out by the form around it, one row of it each, the way they are written here. + styles: `:host { display: contents; }`, +}) +export class Field implements ControlValueAccessor { + @Input({ required: true }) label = ""; + @Input() inputMode?: string; + @Input() autocomplete?: string; + @Input() placeholder?: string; + @Input() errorMessage?: string; + + /** What the field shows, for whoever wraps it. */ + @Input() set value(value: string) { + this.text.set(value ?? ""); + } + + /** What was typed, without its mask, for whoever wraps it. */ + @Output() readonly valueChange = new EventEmitter(); + + /** Left, for whoever wraps this field and answers to a form. */ + @Output() readonly touched = new EventEmitter(); + + /** How to mask the field, when it is a field that is masked. */ + @Input() set mask(mask: Mask | undefined) { + this.masking.set(mask); + } + + protected readonly Boolean = Boolean; + protected readonly id = `field-${(fields += 1)}`; + protected readonly errorId = `${this.id}-error`; + + private readonly masking = signal(undefined); + + /** The value without its mask, the way a form holds it. */ + protected readonly text = signal(""); + protected readonly disabled = signal(false); + protected readonly format = computed( + () => this.masking()?.format ?? ((typed: string) => typed), + ); + protected readonly parse = computed( + () => this.masking()?.parse ?? ((typed: string) => typed), + ); + protected readonly shown = computed(() => this.format()(this.text())); + + protected onChange: (value: string) => void = () => {}; + protected onTouched: () => void = () => {}; + + writeValue(value: string | null): void { + this.text.set(value ?? ""); + } + + registerOnChange(onChange: (value: string) => void): void { + this.onChange = onChange; + } + + registerOnTouched(onTouched: () => void): void { + this.onTouched = onTouched; + } + + setDisabledState(disabled: boolean): void { + this.disabled.set(disabled); + } + + protected onBlur() { + this.onTouched(); + this.touched.emit(); + } + + protected onMasked({ parsedValue }: MaskChange) { + this.text.set(parsedValue); + this.onChange(parsedValue); + this.valueChange.emit(parsedValue); + } +} diff --git a/docs/snippets/document-field/_templates/angular/form.ts b/docs/snippets/document-field/_templates/angular/form.ts new file mode 100644 index 000000000..cf8789892 --- /dev/null +++ b/docs/snippets/document-field/_templates/angular/form.ts @@ -0,0 +1,48 @@ +import { Component } from "@angular/core"; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, + type ValidatorFn, +} from "@angular/forms"; +@@formImports@@ +import { @@Name@@Field } from "./@@kind@@-field"; + +export const @@kind@@Validator: ValidatorFn = (control) => + !control.value || @@validatorControl@@ ? null : { @@kind@@: true }; + +@Component({ + selector: "app-@@kind@@-form", + imports: [ReactiveFormsModule, @@Name@@Field], + template: ` +
+ + + + `, +}) +export class @@Name@@Form { + protected readonly form = new FormGroup({ + @@kind@@: new FormControl("", { + nonNullable: true, + validators: [Validators.required, @@kind@@Validator], + }), + }); + + protected get control() { + return this.form.controls.@@kind@@; + } + + protected errorMessage(): string | undefined { + if (this.control.valid || this.control.untouched) return undefined; + + return this.control.hasError("required") ? "Enter a @@label@@" : "Enter a valid @@label@@"; + } + + protected submit() { + if (this.form.invalid) return this.form.markAllAsTouched(); + + alert(JSON.stringify(this.form.getRawValue(), null, 2)); + } +} diff --git a/docs/snippets/document-field/_templates/angular/mask.ts b/docs/snippets/document-field/_templates/angular/mask.ts new file mode 100644 index 000000000..127740321 --- /dev/null +++ b/docs/snippets/document-field/_templates/angular/mask.ts @@ -0,0 +1,39 @@ +import { Directive, ElementRef, EventEmitter, Input, Output, inject } from "@angular/core"; + +export type MaskChange = { + /** What the field shows. */ + maskedValue: string; + /** The same value without its mask, for a form to hold. */ + parsedValue: string; +}; + +/** + * Masks what is typed into an input, in place, and reports every change masked and without its + * mask, so a form can hold either one: + * ``. + */ +@Directive({ + selector: "[appMask]", + host: { "(input)": "onInput($event)" }, +}) +export class MaskDirective { + /** The formatter of the document being typed. */ + @Input({ required: true, alias: "appMask" }) format!: (value: string) => string; + + /** The parser of the same document, which takes the mask off. */ + @Input({ required: true }) parse!: (value: string) => string; + + @Output() readonly masked = new EventEmitter(); + + private readonly element = inject>(ElementRef); + + protected onInput(event: InputEvent) { + const input = this.element.nativeElement; + const inputType = event.inputType ?? ""; + const format = this.format; + + @@maskBody@@ + + this.masked.emit({ maskedValue: input.value, parsedValue: this.parse(input.value) }); + } +} diff --git a/docs/snippets/document-field/_templates/mask-body.ts b/docs/snippets/document-field/_templates/mask-body.ts new file mode 100644 index 000000000..0cf724ca1 --- /dev/null +++ b/docs/snippets/document-field/_templates/mask-body.ts @@ -0,0 +1,14 @@ +let typed = input.value; +let position = input.selectionStart ?? typed.length; + +// A deleted separator would come straight back: delete the character next to it. +if (inputType.startsWith("delete") && format(typed).length > typed.length) { + if (inputType === "deleteContentBackward") position -= 1; + typed = typed.slice(0, position) + typed.slice(position + 1); +} + +// Formatting what comes before the caret says where the caret goes. +const caret = format(typed.slice(0, position)).length; + +input.value = format(typed); +input.setSelectionRange(caret, caret); diff --git a/docs/snippets/document-field/_templates/react/document-field.tsx b/docs/snippets/document-field/_templates/react/document-field.tsx new file mode 100644 index 000000000..6e8f8e8d4 --- /dev/null +++ b/docs/snippets/document-field/_templates/react/document-field.tsx @@ -0,0 +1,25 @@ +@@fieldImports@@ +import { Field } from "./field"; + +type @@Name@@FieldProps = { + /** The @@label@@ without its mask, the way the form holds it. */ + value: string; + /** Called with the @@label@@ without its mask. */ + onChange: (value: string) => void; + /** What the form says is wrong with the value, if anything. */ + errorMessage?: string; +}; + +/** The field of the form, with what makes it a @@label@@ and nothing else. */ +export function @@Name@@Field(props: @@Name@@FieldProps) { + return ( + + ); +} diff --git a/docs/snippets/document-field/_templates/react/field.tsx b/docs/snippets/document-field/_templates/react/field.tsx new file mode 100644 index 000000000..0d1f64c67 --- /dev/null +++ b/docs/snippets/document-field/_templates/react/field.tsx @@ -0,0 +1,50 @@ +import { useId, type ComponentProps } from "react"; +import { useMask } from "./use-mask"; + +type Mask = { + /** Formats what is typed, as it is typed. */ + format: (value: string) => string; + /** Takes the mask off, for whoever holds the value. */ + parse: (value: string) => string; +}; + +type FieldProps = Omit, "value" | "onChange" | "ref"> & { + label: string; + /** The value without its mask, the way a form holds it. */ + value: string; + /** Called with the value without its mask. */ + onChange: (value: string) => void; + /** What the form says is wrong with the value, if anything. */ + errorMessage?: string; + /** How to mask the field, when it is a field that is masked. */ + mask?: Mask; +}; + +/** A labelled input that says what is wrong with it, masked when it is given a mask. */ +export function Field({ label, value, onChange, errorMessage, mask, ...props }: FieldProps) { + const id = useId(); + const errorId = `${id}-error`; + const ref = useMask({ + format: mask?.format ?? ((typed) => typed), + parse: mask?.parse ?? ((typed) => typed), + value, + onChange: ({ parsedValue }) => onChange(parsedValue), + }); + + return ( + <> + + + {/* On the page from the start, and announced when it gets its text. */} + + + ); +} diff --git a/docs/snippets/document-field/_templates/react/form.tsx b/docs/snippets/document-field/_templates/react/form.tsx new file mode 100644 index 000000000..0e2942d62 --- /dev/null +++ b/docs/snippets/document-field/_templates/react/form.tsx @@ -0,0 +1,27 @@ +import { Controller, useForm } from "react-hook-form"; +@@formImports@@ +import { @@Name@@Field } from "./@@kind@@-field"; + +export function @@Name@@Form() { + const { control, handleSubmit } = useForm({ + defaultValues: { @@kind@@: "" }, + mode: "onTouched", + }); + + return ( +
alert(JSON.stringify(values, null, 2)))}> + @@validator@@ || "Enter a valid @@label@@", + }} + render={({ field, fieldState }) => ( + <@@Name@@Field {...field} errorMessage={fieldState.error?.message} /> + )} + /> + + + ); +} diff --git a/docs/snippets/document-field/_templates/react/mask.ts b/docs/snippets/document-field/_templates/react/mask.ts new file mode 100644 index 000000000..c4d52cdda --- /dev/null +++ b/docs/snippets/document-field/_templates/react/mask.ts @@ -0,0 +1,63 @@ +import { useEffect, useLayoutEffect, useRef } from "react"; + +type UseMaskParams = { + /** The formatter of the document being typed. */ + format: (value: string) => string; + /** The parser of the same document, which takes the mask off. */ + parse: (value: string) => string; + /** What a form holds for the field, without its mask, when a form holds it. */ + value?: string; + /** Called on every change, with the value masked and without its mask. */ + onChange?: (change: { maskedValue: string; parsedValue: string }) => void; +}; + +/** + * Masks what is typed, in place. Returns the ref to put on the input, which is all it needs: the + * input keeps its own value, and every change is reported masked and without its mask, so a form + * can hold either one. + * + * The input is left to the DOM rather than controlled by React, the way the masking libraries do + * it: React writes a controlled input's value again right after an event, which would undo the + * mask. Hand the hook the value a form holds and it writes it in, masked, whenever it changes. + */ +export function useMask({ format, parse, value, onChange }: UseMaskParams) { + const ref = useRef(null); + const latest = useRef({ format, parse, onChange }); + + latest.current = { format, parse, onChange }; + + useEffect(() => { + const input = ref.current; + + if (!input) return; + + const onInput = (event: Event) => { + const inputType = (event as InputEvent).inputType ?? ""; + const { format } = latest.current; + + @@maskBody@@ + + latest.current.onChange?.({ + maskedValue: input.value, + parsedValue: latest.current.parse(input.value), + }); + }; + + input.addEventListener("input", onInput); + + return () => input.removeEventListener("input", onInput); + }, []); + + // What the form holds is what the field shows, masked, whenever the two differ. + useLayoutEffect(() => { + const input = ref.current; + + if (!input || value === undefined) return; + + const masked = latest.current.format(value); + + if (masked !== input.value) input.value = masked; + }, [value]); + + return ref; +} diff --git a/docs/snippets/document-field/_templates/schema/arktype.ts b/docs/snippets/document-field/_templates/schema/arktype.ts new file mode 100644 index 000000000..4aa40d738 --- /dev/null +++ b/docs/snippets/document-field/_templates/schema/arktype.ts @@ -0,0 +1,14 @@ +import { @@validatorFn@@ } from "@brazilian-utils/brazilian-utils"; +import { type } from "arktype"; + +/** A @@label@@, reusable wherever a schema needs one. */ +export const @@kind@@Schema = type("string").narrow( + (@@kind@@, ctx) => @@validatorCtx@@ || ctx.mustBe("a valid @@label@@"), +); + +export const signupSchema = type({ + name: "string > 0", + @@kind@@: @@kind@@Schema, +}); + +export type Signup = typeof signupSchema.infer; diff --git a/docs/snippets/document-field/_templates/schema/standard.ts b/docs/snippets/document-field/_templates/schema/standard.ts new file mode 100644 index 000000000..3668ebaaf --- /dev/null +++ b/docs/snippets/document-field/_templates/schema/standard.ts @@ -0,0 +1,37 @@ +import { toStandardSchema, @@validatorFn@@ } from "@brazilian-utils/brazilian-utils"; +import { sValidator } from "@hono/standard-validator"; +import { FieldApi, FormApi } from "@tanstack/form-core"; +import { initTRPC } from "@trpc/server"; +import { useField } from "vee-validate"; + +const isValid = @@standardValidator@@; + +/** A @@label@@ as a Standard Schema, with no schema library at all. */ +export const @@kind@@Schema = toStandardSchema(isValid, { + message: "Enter a valid @@label@@", +}); + +// Everything that speaks the interface takes it as it is, next to a Zod, Valibot or ArkType +// schema. A few of them, all with the same @@kind@@Schema: + +/** VeeValidate: the rules of a field. */ +export const use@@Name@@Field = () => useField("@@kind@@", @@kind@@Schema); + +/** TanStack Form: a field's validator, `validators={{ onChange: @@kind@@Schema }}`. */ +export const @@kind@@Field = new FieldApi({ + form: new FormApi({ defaultValues: { @@kind@@: "" } }), + name: "@@kind@@", + validators: { onChange: @@kind@@Schema }, +}); + +/** tRPC: what a procedure takes. */ +export const @@kind@@Procedure = initTRPC + .create() + .procedure.input(@@kind@@Schema) + .query(({ input }) => input); + +/** Hono: what a route takes. */ +export const @@kind@@Route = sValidator("param", @@kind@@Schema); + +// react-hook-form takes one through `standardSchemaResolver`, once the schema covers the whole +// form: `useForm({ resolver: standardSchemaResolver(signupSchema) })`. diff --git a/docs/snippets/document-field/_templates/schema/valibot.ts b/docs/snippets/document-field/_templates/schema/valibot.ts new file mode 100644 index 000000000..2fb883368 --- /dev/null +++ b/docs/snippets/document-field/_templates/schema/valibot.ts @@ -0,0 +1,15 @@ +import { @@validatorFn@@ } from "@brazilian-utils/brazilian-utils"; +import * as v from "valibot"; + +/** A @@label@@, reusable wherever a schema needs one. */ +export const @@kind@@Schema = v.pipe( + v.string(), + v.check(@@validatorLambda@@, "Enter a valid @@label@@"), +); + +export const signupSchema = v.object({ + name: v.pipe(v.string(), v.minLength(1, "Enter your name")), + @@kind@@: @@kind@@Schema, +}); + +export type Signup = v.InferOutput; diff --git a/docs/snippets/document-field/_templates/schema/zod.ts b/docs/snippets/document-field/_templates/schema/zod.ts new file mode 100644 index 000000000..7b3d37d1d --- /dev/null +++ b/docs/snippets/document-field/_templates/schema/zod.ts @@ -0,0 +1,12 @@ +import { @@validatorFn@@ } from "@brazilian-utils/brazilian-utils"; +import { z } from "zod"; + +/** A @@label@@, reusable wherever a schema needs one. */ +export const @@kind@@Schema = z.string().refine(@@validatorArrow@@, "Enter a valid @@label@@"); + +export const signupSchema = z.object({ + name: z.string().min(1, "Enter your name"), + @@kind@@: @@kind@@Schema, +}); + +export type Signup = z.infer; diff --git a/docs/snippets/document-field/_templates/vanilla/field.html b/docs/snippets/document-field/_templates/vanilla/field.html new file mode 100644 index 000000000..665ba1b79 --- /dev/null +++ b/docs/snippets/document-field/_templates/vanilla/field.html @@ -0,0 +1,51 @@ + + + +@@label@@ field + +
+ + + +

+ +
+ + diff --git a/docs/snippets/document-field/_templates/vue/document-field.vue b/docs/snippets/document-field/_templates/vue/document-field.vue new file mode 100644 index 000000000..81c823363 --- /dev/null +++ b/docs/snippets/document-field/_templates/vue/document-field.vue @@ -0,0 +1,25 @@ + + + diff --git a/docs/snippets/document-field/_templates/vue/field.vue b/docs/snippets/document-field/_templates/vue/field.vue new file mode 100644 index 000000000..69dc61f7b --- /dev/null +++ b/docs/snippets/document-field/_templates/vue/field.vue @@ -0,0 +1,49 @@ + + + diff --git a/docs/snippets/document-field/_templates/vue/form.vue b/docs/snippets/document-field/_templates/vue/form.vue new file mode 100644 index 000000000..2b59caeba --- /dev/null +++ b/docs/snippets/document-field/_templates/vue/form.vue @@ -0,0 +1,30 @@ + + + diff --git a/docs/snippets/document-field/_templates/vue/mask.ts b/docs/snippets/document-field/_templates/vue/mask.ts new file mode 100644 index 000000000..638aab97d --- /dev/null +++ b/docs/snippets/document-field/_templates/vue/mask.ts @@ -0,0 +1,27 @@ +import type { Directive } from "vue"; + +type MaskOptions = { + /** The formatter of the document being typed. */ + format: (value: string) => string; + /** The parser of the same document, which takes the mask off. */ + parse: (value: string) => string; + /** Called on every change, with the value masked and without its mask. */ + onChange?: (change: { maskedValue: string; parsedValue: string }) => void; +}; + +/** + * Masks what is typed into an input, in place, and reports every change masked and without its + * mask, so a form can hold either one: `v-mask="{ format, parse, onChange }"`. + */ +export const vMask: Directive = { + mounted(input, binding) { + input.addEventListener("input", (event) => { + const { format, parse, onChange } = binding.value; + const inputType = (event as InputEvent).inputType ?? ""; + + @@maskBody@@ + + onChange?.({ maskedValue: input.value, parsedValue: parse(input.value) }); + }); + }, +}; diff --git a/docs/snippets/live/index.html b/docs/snippets/live/index.html new file mode 100644 index 000000000..d7fc9abf6 --- /dev/null +++ b/docs/snippets/live/index.html @@ -0,0 +1,4 @@ + + +Live example + diff --git a/docs/snippets/live/run.js b/docs/snippets/live/run.js new file mode 100644 index 000000000..3416f144f --- /dev/null +++ b/docs/snippets/live/run.js @@ -0,0 +1,260 @@ +/** + * Runs an example of docs/snippets in the browser, so a live demo is exactly the code its page + * shows. One page serves every demo, `live/index.html`, told what to run by its query string: + * `?dir=&example=&usage=` compiles both (Babel for + * TypeScript and JSX, the Vue SFC compiler for a single-file component, TypeScript itself for + * Angular, each from a CDN) and mounts the usage; `?page=` runs a plain HTML example as it + * is. Nothing is built ahead of time. + */ +(function () { + var CDN = "https://cdn.jsdelivr.net/npm/"; + var data = Object.fromEntries(new URLSearchParams(location.search)); + var example = data.example; + + var script = document.createElement("script"); + script.type = "importmap"; + script.textContent = JSON.stringify({ + imports: { + "@brazilian-utils/brazilian-utils": CDN + "@brazilian-utils/brazilian-utils/+esm", + "@brazilian-utils/brazilian-utils/get-states": CDN + "@brazilian-utils/brazilian-utils/get-states/+esm", + "@brazilian-utils/brazilian-utils/get-cities": CDN + "@brazilian-utils/brazilian-utils/get-cities/+esm", + // esm.sh, not jsDelivr, for React: jsDelivr's react-dom imports its own copy of react. + react: "https://esm.sh/react@19.3.0", + "react/jsx-runtime": "https://esm.sh/react@19.3.0/jsx-runtime", + "react-dom/client": "https://esm.sh/react-dom@19.3.0/client?deps=react@19.3.0", + vue: CDN + "vue@3.5.43/dist/vue.esm-browser.prod.js", + "@angular/core": CDN + "@angular/core@22.1.7/+esm", + "@angular/compiler": CDN + "@angular/compiler@22.1.7/+esm", + "@angular/platform-browser": CDN + "@angular/platform-browser@22.1.7/+esm", + "@angular/forms": CDN + "@angular/forms@22.1.7/+esm", + "@angular/core/rxjs-interop": CDN + "@angular/core@22.1.7/rxjs-interop/+esm", + rxjs: CDN + "rxjs@7.8.2/+esm", + "react-hook-form": "https://esm.sh/react-hook-form@7.88.0?external=react", + "vee-validate": "https://esm.sh/vee-validate@4.15.1?external=vue", + }, + }); + document.head.appendChild(script); + + var styles = document.createElement("link"); + styles.rel = "stylesheet"; + styles.href = "../styles.css"; + document.head.appendChild(styles); + + // A plain HTML example is its own page: its markup and its module scripts run here as they are. + var runPage = function (html) { + var page = new DOMParser().parseFromString(html, "text/html"); + var base = document.createElement("base"); + + base.href = new URL("../" + data.page, location.href).href; + document.head.appendChild(base); + document.body.innerHTML = page.body.innerHTML; + + for (var script of page.querySelectorAll("script")) { + var copy = document.createElement("script"); + copy.type = script.type; + copy.textContent = script.textContent; + document.body.appendChild(copy); + } + }; + + // The page that embeds a demo cannot see how tall it is until it renders, and it keeps changing + // as a validation message comes and goes, so the demo reports its own height. + var reportHeight = function () { + var height = Math.ceil(document.body.scrollHeight); + + parent.postMessage({ type: "example-height", height: height }, location.origin); + }; + + var watchHeight = function () { + new ResizeObserver(reportHeight).observe(document.body); + reportHeight(); + }; + + if (document.readyState === "loading") { + addEventListener("DOMContentLoaded", watchHeight); + } else { + watchHeight(); + } + + var fail = function (error) { + document.body.textContent = "Could not load the demo: " + error.message; + }; + + var read = function (name) { + return fetch("../" + (data.dir ? data.dir + "/" : "") + name).then(function (response) { + if (!response.ok) throw new Error(name + ": HTTP " + response.status); + return response.text(); + }); + }; + + // An import of "./mask" is a file whose extension the example leaves out. + // An import without an extension tries these in turn. + var EXTENSIONS = [".ts", ".tsx", ".vue", ".js"]; + + var find = function (name, index) { + var attempt = index || 0; + + if (/\.(tsx?|vue|js)$/.test(name)) { + return read(name).then(function (code) { + return { name: name, code: code }; + }); + } + + return read(name + EXTENSIONS[attempt]).then( + function (code) { + return { name: name + EXTENSIONS[attempt], code: code }; + }, + function (error) { + if (attempt + 1 >= EXTENSIONS.length) throw error; + return find(name, attempt + 1); + }, + ); + }; + + var RELATIVE_IMPORT = /from\s*"\.\/([^"]+)"/g; + var modules = {}; + + // A file of the example and, before it, whatever it imports from beside it: each one becomes a + // module of its own, so an example is read the way it is written. + var moduleOf = function (name) { + if (!modules[name]) { + modules[name] = find(name) + .then(function (file) { + return compile(file.name, file.code); + }) + .then(function (js) { + var imports = []; + var match; + + while ((match = RELATIVE_IMPORT.exec(js)) !== null) imports.push(match[1]); + + return Promise.all( + imports.map(function (imported) { + return moduleOf(imported).then(function (url) { + js = js.split('"./' + imported + '"').join('"' + url + '"'); + }); + }), + ).then(function () { + return toModule(js); + }); + }); + } + + return modules[name]; + }; + + var loadBabel = new Promise(function (resolve, reject) { + var babel = document.createElement("script"); + babel.src = CDN + "@babel/standalone@7.29.9/babel.min.js"; + babel.onload = resolve; + babel.onerror = function () { + reject(new Error("Babel did not load")); + }; + document.head.appendChild(babel); + }); + + // Angular's JIT compiler reads the decorators TypeScript emits, which Babel does not reproduce, + // so an Angular file goes through TypeScript itself. + var transpileTypeScript = function (code, filename) { + return import(CDN + "typescript@5.9.3/+esm").then(function (ts) { + return ts.default.transpileModule(code, { + fileName: filename, + compilerOptions: { + target: ts.default.ScriptTarget.ES2022, + module: ts.default.ModuleKind.ESNext, + experimentalDecorators: true, + useDefineForClassFields: false, + }, + }).outputText; + }); + }; + + var transpile = function (code, filename) { + return window.Babel.transform(code, { + filename: filename, + presets: [["typescript", { allExtensions: true, isTSX: /\.tsx$/.test(filename) }], ["react", { runtime: "automatic" }]], + // Class properties after the decorators, or Angular's signal inputs never register. + plugins: [ + ["proposal-decorators", { legacy: true }], + ["proposal-class-properties", { loose: true }], + ], + }).code; + }; + + var toModule = function (code) { + return URL.createObjectURL(new Blob([code], { type: "text/javascript" })); + }; + + var compileVue = function (name, source) { + return import(CDN + "@vue/compiler-sfc@3.5.43/dist/compiler-sfc.esm-browser.js").then(function (sfc) { + var descriptor = sfc.parse(source, { filename: name }).descriptor; + return sfc.compileScript(descriptor, { id: name, inlineTemplate: true }).content; + }); + }; + + // A file of the example: TypeScript, JSX or a single-file component, compiled to a module. + var compile = function (name, code) { + if (/\.ts$/.test(name)) return transpileTypeScript(code, name); + + var source = /\.vue$/.test(name) ? compileVue(name, code) : Promise.resolve(code); + + return source.then(function (js) { + return transpile(js, name.replace(/\.vue$/, ".ts")); + }); + }; + + // React and Vue want an element to mount into. It takes no part in the layout, so that an + // example lays its rows out against the page the way the Angular one does. + var host = function () { + var element = document.body.appendChild(document.createElement("div")); + element.style.display = "contents"; + return element; + }; + + var mount = function (module) { + if (/\.tsx$/.test(example)) { + return Promise.all([import("react"), import("react-dom/client")]).then(function (react) { + // The example is a controlled component: the demo is the parent that holds its value. + var Demo = function () { + var state = react[0].useState(""); + return react[0].createElement(Object.values(module)[0], { value: state[0], onChange: state[1] }); + }; + react[1].createRoot(host()).render(react[0].createElement(Demo)); + }); + } + + if (/\.vue$/.test(example)) { + return import("vue").then(function (vue) { + vue.createApp(module.default).mount(host()); + }); + } + + return Promise.all([import("@angular/core"), import("@angular/platform-browser")]).then(function (angular) { + var component = Object.values(module)[0]; + document.body.appendChild(document.createElement(angular[0].reflectComponentType(component).selector)); + return angular[1].bootstrapApplication(component, { + providers: [angular[0].provideZonelessChangeDetection()], + }); + }); + }; + + if (data.page) { + read(data.page).then(runPage).catch(fail); + return; + } + + loadBabel + .then(function () { + // Angular's JIT compiler has to be evaluated before any other Angular package, the example + // included: the partially compiled packages look for it as they load. + return /\.ts$/.test(example) ? import("@angular/compiler") : undefined; + }) + .then(function () { + return moduleOf(data.usage || example); + }) + .then(function (url) { + return import(url); + }) + .then(mount) + .catch(fail); +})(); diff --git a/docs/snippets/state-city/angular/cities-of-state.ts b/docs/snippets/state-city/angular/cities-of-state.ts new file mode 100644 index 000000000..80cacccce --- /dev/null +++ b/docs/snippets/state-city/angular/cities-of-state.ts @@ -0,0 +1,22 @@ +import { resource, type Signal } from "@angular/core"; +import type { StateCode } from "@brazilian-utils/brazilian-utils"; + +/** + * The cities of a state, fetched the first time that state's select is opened. The table is + * 154 KB, so nothing is fetched until someone means to pick a city. What it is about is the state + * whose cities were asked for, so picking another state puts the resource back to waiting. + * + * A resource aborts a load it no longer wants and drops its answer, which is what the `abortSignal` + * of the loader is for; `import()` takes none, so the module is not stopped, only what is done + * with it, and here the resource does that by itself. + */ +export function citiesOfState(asked: Signal) { + return resource({ + params: () => asked() || undefined, + loader: async ({ params }) => { + const { getCities } = await import("@brazilian-utils/brazilian-utils/get-cities"); + + return getCities(params as StateCode); + }, + }); +} diff --git a/docs/snippets/state-city/angular/state-city.ts b/docs/snippets/state-city/angular/state-city.ts new file mode 100644 index 000000000..62c2ee5e3 --- /dev/null +++ b/docs/snippets/state-city/angular/state-city.ts @@ -0,0 +1,64 @@ +import { Component, signal } from "@angular/core"; +import { citiesOfState } from "./cities-of-state"; +import { states } from "./states"; + +@Component({ + selector: "app-state-city", + template: ` + + + + + + + `, + // A component is an element of its own; this one stands aside so the page lays out its rows. + styles: `:host { display: contents; }`, +}) +export class StateCity { + protected readonly state = signal(""); + protected readonly askedForStates = signal(false); + + /** The state whose cities were asked for, which is nothing until a city select is opened. */ + protected readonly askedForCities = signal(""); + + protected readonly allStates = states(this.askedForStates); + protected readonly cities = citiesOfState(this.askedForCities); + + // Asking a resource for a value it does not have throws, so it is asked whether it has one. + protected stateList() { + return this.allStates.hasValue() ? this.allStates.value() : []; + } + + protected cityList() { + return this.cities.hasValue() ? this.cities.value() : []; + } + + protected pick(state: string) { + this.state.set(state); + // The cities on screen are another state's; this one's are fetched when they are asked for. + this.askedForCities.set(""); + } +} diff --git a/docs/snippets/state-city/angular/states.ts b/docs/snippets/state-city/angular/states.ts new file mode 100644 index 000000000..b3a7f8e5a --- /dev/null +++ b/docs/snippets/state-city/angular/states.ts @@ -0,0 +1,18 @@ +import { resource, type Signal } from "@angular/core"; + +/** + * The states, fetched the first time the select is opened. 2.5 KB that a page whose visitor never + * opens it does not pay for, and the browser keeps the module once it has it. A resource stops + * with the component that asked and drops an answer it no longer wants. + */ +export function states(asked: Signal) { + return resource({ + // A resource with nothing to ask about waits, which is where this one starts. + params: () => asked() || undefined, + loader: async () => { + const { getStates } = await import("@brazilian-utils/brazilian-utils/get-states"); + + return getStates(); + }, + }); +} diff --git a/docs/snippets/state-city/react/state-city.tsx b/docs/snippets/state-city/react/state-city.tsx new file mode 100644 index 000000000..b979c5f2b --- /dev/null +++ b/docs/snippets/state-city/react/state-city.tsx @@ -0,0 +1,41 @@ +import { useId, useState } from "react"; +import { useCitiesOfState } from "./use-cities-of-state"; +import { useStates } from "./use-states"; + +export function StateCity() { + const id = useId(); + const [state, setState] = useState(""); + const { states, loading: loadingStates, load: loadStates } = useStates(); + const { cities, loading: loadingCities, load: loadCities } = useCitiesOfState(state); + + return ( + <> + + {/* Opening the select is what says the list is wanted, so that is when it is fetched. */} + + + + + + ); +} diff --git a/docs/snippets/state-city/react/use-cities-of-state.ts b/docs/snippets/state-city/react/use-cities-of-state.ts new file mode 100644 index 000000000..a859c6056 --- /dev/null +++ b/docs/snippets/state-city/react/use-cities-of-state.ts @@ -0,0 +1,40 @@ +import { useEffect, useRef, useState } from "react"; +import type { StateCode } from "@brazilian-utils/brazilian-utils"; + +/** + * The cities of a state, fetched the first time that state's select is opened. The table is + * 154 KB, so nothing is fetched until someone means to pick a city, and the browser keeps the + * module once it has it. `import()` takes no signal, so the module is not stopped, only what is + * done with it: a table that arrives for a state that is no longer picked, or after the component + * is gone, is dropped. + */ +export function useCitiesOfState(state: string) { + const [loaded, setLoaded] = useState({ state: "", cities: [] as string[] }); + const [asked, setAsked] = useState(""); + const pending = useRef(null); + + // Both of these are about the state that is picked, so picking another one leaves the cities of + // the old state behind rather than on screen, and its spinner with them. + const cities = loaded.state === state ? loaded.cities : []; + const loading = state !== "" && asked === state && cities.length === 0; + + // What is on its way is dropped when another state is picked and when the component goes. + useEffect(() => () => pending.current?.abort(), [state]); + + const load = async () => { + if (!state || loading || cities.length > 0) return; + + const controller = new AbortController(); + + pending.current = controller; + setAsked(state); + + const { getCities } = await import("@brazilian-utils/brazilian-utils/get-cities"); + + if (controller.signal.aborted) return; + + setLoaded({ state, cities: getCities(state as StateCode) }); + }; + + return { cities, loading, load }; +} diff --git a/docs/snippets/state-city/react/use-states.ts b/docs/snippets/state-city/react/use-states.ts new file mode 100644 index 000000000..dc237ff71 --- /dev/null +++ b/docs/snippets/state-city/react/use-states.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef, useState } from "react"; +import type { State } from "@brazilian-utils/brazilian-utils"; + +/** + * The states, fetched the first time the select is opened. 2.5 KB that a page whose visitor never + * opens it does not pay for, and the browser keeps the module once it has it. `import()` takes no + * signal, so the module is not stopped, only what is done with it: a table that arrives after the + * component is gone is dropped. + */ +export function useStates() { + const [states, setStates] = useState([]); + const [loading, setLoading] = useState(false); + const pending = useRef(null); + + useEffect(() => () => pending.current?.abort(), []); + + const load = async () => { + if (loading || states.length > 0) return; + + const controller = new AbortController(); + + pending.current = controller; + setLoading(true); + + const { getStates } = await import("@brazilian-utils/brazilian-utils/get-states"); + + if (controller.signal.aborted) return; + + setStates(getStates()); + setLoading(false); + }; + + return { states, loading, load }; +} diff --git a/docs/snippets/state-city/vanilla/state-city.html b/docs/snippets/state-city/vanilla/state-city.html new file mode 100644 index 000000000..3eb0f2c5c --- /dev/null +++ b/docs/snippets/state-city/vanilla/state-city.html @@ -0,0 +1,86 @@ + + + + State and city + + + + + + + + + diff --git a/docs/snippets/state-city/vue/state-city.vue b/docs/snippets/state-city/vue/state-city.vue new file mode 100644 index 000000000..9d36062f6 --- /dev/null +++ b/docs/snippets/state-city/vue/state-city.vue @@ -0,0 +1,32 @@ + + + diff --git a/docs/snippets/state-city/vue/use-cities-of-state.ts b/docs/snippets/state-city/vue/use-cities-of-state.ts new file mode 100644 index 000000000..8c876c919 --- /dev/null +++ b/docs/snippets/state-city/vue/use-cities-of-state.ts @@ -0,0 +1,49 @@ +import { computed, ref, toValue, watch, type MaybeRefOrGetter } from "vue"; +import type { StateCode } from "@brazilian-utils/brazilian-utils"; + +/** + * The cities of a state, fetched the first time that state's select is opened. The table is + * 154 KB, so nothing is fetched until someone means to pick a city, and the browser keeps the + * module once it has it. `import()` takes no signal, so the module is not stopped, only what is + * done with it: a table that arrives for a state that is no longer picked, or after the component + * is gone, is dropped. + */ +export function useCitiesOfState(state: MaybeRefOrGetter) { + const loaded = ref({ state: "", cities: [] as string[] }); + const asked = ref(""); + const pending = ref(); + + // Both of these are about the state that is picked, so picking another one leaves the cities of + // the old state behind rather than on screen, and its spinner with them. + const cities = computed(() => + loaded.value.state === toValue(state) ? loaded.value.cities : [], + ); + const loading = computed( + () => toValue(state) !== "" && asked.value === toValue(state) && cities.value.length === 0, + ); + + // What is on its way is dropped when another state is picked and when the scope goes. + watch( + () => toValue(state), + (_current, _previous, onCleanup) => onCleanup(() => pending.value?.abort()), + ); + + const load = async () => { + const current = toValue(state); + + if (!current || loading.value || cities.value.length > 0) return; + + const controller = new AbortController(); + + pending.value = controller; + asked.value = current; + + const { getCities } = await import("@brazilian-utils/brazilian-utils/get-cities"); + + if (controller.signal.aborted) return; + + loaded.value = { state: current, cities: getCities(current as StateCode) }; + }; + + return { cities, loading, load }; +} diff --git a/docs/snippets/state-city/vue/use-states.ts b/docs/snippets/state-city/vue/use-states.ts new file mode 100644 index 000000000..a95d0f269 --- /dev/null +++ b/docs/snippets/state-city/vue/use-states.ts @@ -0,0 +1,34 @@ +import { onScopeDispose, ref } from "vue"; +import type { State } from "@brazilian-utils/brazilian-utils"; + +/** + * The states, fetched the first time the select is opened. 2.5 KB that a page whose visitor never + * opens it does not pay for, and the browser keeps the module once it has it. `import()` takes no + * signal, so the module is not stopped, only what is done with it: a table that arrives after the + * component is gone is dropped. + */ +export function useStates() { + const states = ref([]); + const loading = ref(false); + const pending = ref(); + + onScopeDispose(() => pending.value?.abort()); + + const load = async () => { + if (loading.value || states.value.length > 0) return; + + const controller = new AbortController(); + + pending.value = controller; + loading.value = true; + + const { getStates } = await import("@brazilian-utils/brazilian-utils/get-states"); + + if (controller.signal.aborted) return; + + states.value = getStates(); + loading.value = false; + }; + + return { states, loading, load }; +} diff --git a/docs/snippets/styles.css b/docs/snippets/styles.css new file mode 100644 index 000000000..ae06ac9b9 --- /dev/null +++ b/docs/snippets/styles.css @@ -0,0 +1,88 @@ +/* + * The live demos of the guides: the site's own stylesheet, so a demo looks like the page around it + * (docsify already styles the controls), plus the layout of a form and the state of its fields. + * The examples themselves carry no classes: everything here is addressed by element. + */ +@import url("../styles.css"); + +:root { + --invalid-color: #e5534b; + --muted-color: light-dark(#6e7781, #8b949e); +} + +/* The theme lays a page out to fill the window; a demo is only as tall as its content. */ +html, +body { + height: auto; + min-height: 0; +} + +body { + padding: 20px; +} + +/* One row per field: its label, then the control, then whatever the field has to say. */ +body, +form { + display: grid; + grid-template-columns: max-content minmax(0, 20ch) 1fr; + align-items: center; + gap: 10px 14px; +} + +form { + grid-column: 1 / -1; + padding: 0; +} + +label { + grid-column: 1; + justify-self: end; + font-weight: 600; +} + +input, +select { + grid-column: 2; + width: 100%; + padding: 0.4em 0.65em; + font-family: var(--font-family-mono); +} + +select { + font-family: inherit; +} + +input:focus, +select:focus { + outline: none; + border-color: var(--theme-color); +} + +/* What a field has to say sits next to it, and keeps its row when it has nothing to say. */ +output, +p { + grid-column: 3; + margin: 0; + min-height: 1lh; + font-size: var(--font-size-s, 0.875em); + color: var(--muted-color); +} + +p[role="alert"]:not(:empty) { + color: var(--invalid-color); +} + +output:not(:empty) { + color: var(--theme-color); +} + +button { + grid-column: 2; + justify-self: start; + padding: 0.45em 1.2em; +} + +input[aria-invalid="true"] { + border-color: var(--invalid-color); +} diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 000000000..ece52ea71 --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,61 @@ +/* + * The stylesheet of the docs site, linked by every page shell and imported by the live demos of + * examples.md, so both look the same. docsify 5's core theme with the dark add-on always on, the + * look the site had before v5 (add `screen and (prefers-color-scheme: dark)` to the add-on's + * import to follow the system instead), then the site's own styles. + */ +@import url("https://cdn.jsdelivr.net/npm/docsify@5/dist/themes/core.min.css"); +@import url("https://cdn.jsdelivr.net/npm/docsify@5/dist/themes/addons/core-dark.min.css"); + +:root { + /* The green of the official palette (github.com/brazilian-utils/brand) */ + --theme-color: #009739; + /* The cover page shares the page background instead of the default gradient */ + --cover-bg: var(--color-bg); + --cover-bg-overlay: none; +} + +/* The two questions a guide asks: which document, in words, and then which framework, as tabs */ +.example-picker { + display: flex; flex-wrap: wrap; align-items: center; gap: 0.6em; margin: 1.5em 0 0; +} +.example-picker select { padding: 0.3em 0.5em; font: inherit; } +.example-picker + .example-tabs { margin-top: 0.8em; } +.example-tabs { + display: flex; flex-wrap: wrap; gap: 4px; margin: 1.5em 0 0; + border-bottom: 1px solid var(--border-color); +} +.example-tab-list { display: flex; flex-wrap: wrap; gap: 4px; } +.example-tabs button { + padding: 0.55em 1.1em; border: 0; border-bottom: 2px solid transparent; border-radius: 0; + margin-bottom: -1px; background: none; color: inherit; font: inherit; font-weight: 600; + opacity: 0.7; cursor: pointer; +} +.example-tabs button:hover { opacity: 1; } +.example-tabs button[aria-selected="true"] { + opacity: 1; color: var(--theme-color); border-bottom-color: var(--theme-color); +} +.example-tabs button:focus { box-shadow: none; outline: none; } +.example-tabs button:focus-visible { + box-shadow: none; outline: 2px solid var(--theme-color); outline-offset: -2px; +} +.variant[hidden] { display: none; } +.example[hidden] { display: none; } +.example iframe { + display: block; width: 100%; height: 74px; margin: 1em 0; + border: 1px solid var(--border-color); border-radius: var(--border-radius); +} + +/* The files of an example, shown like an editor's tabs */ +.file-tabs { display: flex; flex-wrap: wrap; gap: 2px; margin: 1.5em 0 0; } +.file-tabs button { + padding: 0.45em 0.9em; border: 0; border-radius: var(--border-radius) var(--border-radius) 0 0; + background: none; color: inherit; cursor: pointer; opacity: 0.65; + font: 0.85em var(--font-family-mono); +} +.file-tabs button:hover { opacity: 1; } +.file-tabs button[aria-selected="true"] { opacity: 1; background: var(--code-bg); } +.file-tabs button:focus { box-shadow: none; outline: none; } +.file-tabs button:focus-visible { outline: 2px solid var(--theme-color); outline-offset: -2px; } +.file[hidden] { display: none; } +.file-tabs + .file pre { margin-top: 0; border-top-left-radius: 0; } diff --git a/docs/utilities.html b/docs/utilities.html deleted file mode 100644 index 987fe725b..000000000 --- a/docs/utilities.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - Utilities · Brazilian Utils - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - diff --git a/docs/utilities.md b/docs/utilities.md index a4b07b62a..ad699c341 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -4,13 +4,27 @@ description: "Every utility of Brazilian Utils, grouped by family (CPF, CNPJ, CE keywords: ["CPF", "CNPJ", "CEP", "boleto", "Pix", "NF-e", "phone", "license plate", "RENAVAM", "PIS", "CNH", "IBAN", "holidays", "business days", "CBO", "CNAE", "NCM", "CFOP", "validator", "formatter", "parser", "generator"] --- -Here you will find all the utilities available for use. +Every function of the package, grouped by family. Each section says what the function does, its options, what it returns on bad input, and shows an example. + +## Conventions + +These rules hold for every function unless its section says otherwise. + +- **Nothing throws on bad input** (`null`, `undefined`, the wrong type): `isValid*` return `false`, `format*` and `parse*` return `''`, single-item `get*` return `null`, list `get*` return `[]`. The only exceptions are the async `getAddressInfoByCep` and `getCepInfoByAddress`, which reject with typed errors. +- **Validators accept the value masked or not**: the usual mask characters (`.`, `-`, `/`) and spaces between or around the groups are ignored, so there is no need to strip formatting first. +- **Formatters mask as far as the value goes**, so they also work as input masks while the user types. `parse*` functions do the reverse and keep only the meaningful characters. +- **Generators use `Math.random()`**, so they are fine for tests and fixtures and never for anything security-related. +- **Getters return a new array or object on every call**, so mutating a result never affects the next call. +- **Every function is synchronous** except `getAddressInfoByCep`, `getCepInfoByAddress` and the deprecated `getMunicipality`. + ## CPF ### isValidCpf -Check if CPF is valid. Accepts the usual mask characters and whitespace between/around groups. +Check if a CPF is valid. + +- Returns `false` for a reserved number (all digits the same, such as `00000000000`) and for a wrong check digit. ```javascript import { isValidCpf } from '@brazilian-utils/brazilian-utils'; @@ -21,7 +35,10 @@ isValidCpf('111 444 777 35'); // true (whitespace mask) ### formatCpf -Format CPF. `options.pad` (part of `FormatCpfOptions`) left-pads the value with zeros up to the 11 slots of the pattern before masking (default `false`). `options.obfuscate` (same type) hides the first 3 digits and the 2 check digits (`***.456.789-**`), the gov.br / Receita Federal display convention, applied after `pad`. It is read for truthiness, the way `pad` is, so any truthy value obfuscates. +Format a CPF. + +- **Options** (`FormatCpfOptions`): `pad` left-pads the value with zeros to 11 digits before masking (default `false`); `obfuscate` hides the first 3 digits and the 2 check digits. +- `obfuscate` is applied after `pad`. ```javascript import { formatCpf } from '@brazilian-utils/brazilian-utils'; @@ -43,7 +60,10 @@ parseCpf('746.506.880-00'); // 74650688000 ### generateCpf -Generate a valid random CPF. Uses `Math.random()` internally, so it is not cryptographically secure. The optional `state` argument (typed as `StateCode`, the two-letter codes of the 27 Brazilian states, e.g. `"SP"`, `"MG"`) ties the CPF to a state by fixing the região fiscal digit in the 9th position to that state's code. Omitted, a random region is used. An unknown code draws a random região fiscal digit instead of throwing, so the result is still a valid CPF. +Generate a valid random CPF. + +- The optional `state` argument (`StateCode`, e.g. `"SP"`) fixes the região fiscal digit (the 9th) to that state's code. +- Without `state`, or with an unknown code, a random região fiscal digit is drawn. ```javascript import { generateCpf } from '@brazilian-utils/brazilian-utils' @@ -53,11 +73,16 @@ generateCpf('SP'); // the 9th digit is 8, the SP região fiscal code generateCpf('MG'); // the 9th digit is 6, the MG região fiscal code ``` +Source: [Receita Federal, "Cadastros: CPF e CNPJ"](https://www.gov.br/receitafederal/pt-br/assuntos/educacao-fiscal/educacao_fiscal/folhetos-orientativos/cadastros-dig.pdf). + ## CNPJ ### isValidCnpj -Check if CNPJ is valid. `options.version` (part of `IsValidCnpjOptions`) picks which format is accepted: `1` (default) the numeric-only format, `2` both the numeric and the alphanumeric one; any other value is read as `1`, the way `formatCnpj` and `parseCnpj` read it. The usual mask characters and whitespace are accepted in either version. Version `2` has no reserved-value list, because the Receita Federal manual defines none for the alphanumeric format: a repeated-character alphanumeric base (all `A`s, say) that passes the checksum is accepted, while the numeric reserved numbers are rejected under version `1`. +Check if a CNPJ is valid. + +- **Options** (`IsValidCnpjOptions`): `version` picks the accepted format: `1` (default) numeric only, `2` numeric and alphanumeric. Any other value is read as `1`. +- A reserved number (all digits the same) is rejected under both versions; version `2` has no reserved list for letters. ```javascript import { isValidCnpj } from '@brazilian-utils/brazilian-utils'; @@ -66,9 +91,15 @@ isValidCnpj('15515147234255'); // false isValidCnpj('q0slfmbd7vx439', { version: 2 }); // true (lowercase alphanumeric) ``` +Source: [Receita Federal, Manual do DV do CNPJ](https://www.gov.br/receitafederal/pt-br/centrais-de-conteudo/publicacoes/documentos-tecnicos/cnpj/manual-dv-cnpj.pdf), [CNPJ alfanumérico](https://www.gov.br/receitafederal/pt-br/acesso-a-informacao/acoes-e-programas/programas-e-atividades/cnpj-alfanumerico). + ### formatCnpj -Format CNPJ. `options.pad` (part of `FormatCnpjOptions`) left-pads the value with zeros up to the 14 slots of the pattern before masking (default `false`). `options.version` (same type) picks which CNPJ format to read: `1` (default) numeric only, `2` alphanumeric. `options.obfuscate` hides the first 2 digits and the 2 check digits (`**.345.678/0001-**`), the gov.br / Receita Federal display convention. It applies to both versions and comes after `pad`, and is read for truthiness, the way `pad` is, so any truthy value obfuscates. +Format a CNPJ. + +- **Options** (`FormatCnpjOptions`): `pad` left-pads the value with zeros to 14 characters before masking (default `false`); `version` picks the format, `1` (default) numeric only, `2` alphanumeric; `obfuscate` hides the first 2 digits and the 2 check digits. +- Version `2` keeps letters (upper-cased) and digits; version `1` keeps digits only. +- `obfuscate` works in both versions and is applied after `pad`. ```javascript import { formatCnpj } from '@brazilian-utils/brazilian-utils'; @@ -81,7 +112,9 @@ formatCnpj('12345678000195', { obfuscate: true }); // **.345.678/0001-** ### parseCnpj -Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. `options.version` (part of `ParseCnpjOptions`) picks which CNPJ format to normalize: `1` (default) keeps digits only, `2` keeps letters and digits, so an alphanumeric CNPJ survives the round trip. +Remove CNPJ formatting, return a normalized value, and cap the result to 14 characters. + +- **Options** (`ParseCnpjOptions`): `version` picks the format: `1` (default) keeps digits only, `2` keeps letters and digits, upper-cased. ```javascript import { parseCnpj } from '@brazilian-utils/brazilian-utils'; @@ -92,7 +125,10 @@ parseCnpj('12.OUT.345/0001-99', { version: 2 }); // 12OUT345000199 ### generateCnpj -Generate a valid random CNPJ. Uses `Math.random()` internally, so it is not cryptographically secure. The first argument is either the version, as before, or a `GenerateCnpjParams` object with the same `version` plus `branch`, the "número de ordem" (filial) block in positions 9 to 12: an integer from 1 to 9999 written zero padded to four characters, random by default. An invalid `branch` is ignored and a random block is used, and the block stays numeric on the alphanumeric version. +Generate a valid random CNPJ. + +- The first argument is either the version, `1` (default) numeric or `2` alphanumeric, or a `GenerateCnpjParams` object with `version` plus `branch`. +- `branch` is the "número de ordem" (filial) block, an integer from 1 to 9999 (random by default). An invalid `branch` is ignored. The block stays numeric in both versions. ```javascript import { generateCnpj } from '@brazilian-utils/brazilian-utils' @@ -107,7 +143,10 @@ generateCnpj({ version: 2, branch: 1 }); // alphanumeric CNPJ whose ordem block ### isValidCep -Check if CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) is valid. Accepts both `string` and `number` input, but a CEP that starts with `0` has to be passed as a string, since a number cannot keep the leading zero (`isValidCep(1310100)` is `false`, `isValidCep('01310100')` is `true`); any spaces, dots and hyphens around/between the 8 digits are ignored, but any other character, a letter in particular, makes the value invalid. +Check if a CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)) is valid. + +- Accepts a `string` or a `number`. A CEP that starts with `0` has to be a string, since a number cannot keep the leading zero. +- Spaces, dots and hyphens are ignored. Any other character makes the value invalid. ```javascript import { isValidCep } from '@brazilian-utils/brazilian-utils'; @@ -123,7 +162,10 @@ isValidCep('12345'); // false (invalid length) ### formatCep -Format CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). `options.pad` (part of `FormatCepOptions`) left-pads the value with zeros to the full 8 digits before masking (default `false`); a CEP that starts with `0` given as a number loses that zero, so pass it as a string or use `pad`. +Format a CEP ([brazilian postal code](https://en.wikipedia.org/wiki/C%C3%B3digo_de_Endere%C3%A7amento_Postal)). + +- **Options** (`FormatCepOptions`): `pad` left-pads the value with zeros to 8 digits before masking (default `false`). +- A CEP that starts with `0` given as a number loses that zero: pass a string or use `pad`. ```javascript import { formatCep } from '@brazilian-utils/brazilian-utils'; @@ -144,7 +186,7 @@ parseCep('92500-000'); // 92500000 ### generateCep -Generate a random CEP. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a random CEP. A CEP has no check digit, so every 8 digit string is structurally valid. ```javascript import { generateCep } from '@brazilian-utils/brazilian-utils'; @@ -154,7 +196,13 @@ generateCep(); // '92500000' ### getAddressInfoByCep -Fetch address information for a given CEP using multiple providers. Defaults to `['viacep', 'brasilapi']`. The `'widenet'` provider is deprecated (its endpoint no longer responds) and excluded from the default list, but it can still be requested explicitly via `options.providers` (typed as `CepProvider[]`). The resolved address is typed as `AddressInfo`. A transient network failure is retried twice per provider, with a 250 ms linear backoff (250 ms, then 500 ms), so a provider that keeps failing is tried up to 3 times and adds about 750 ms before its own failure lands; an HTTP error status or a non-retryable failure is not retried. The providers are started together and raced with `Promise.any`, not queried one after the other, so those retries delay nothing for the other providers, only the moment an all-failed rejection can surface. An `options.providers` that names no known provider rejects with `GetAddressInfoByCepValidationError` ("Nenhum provedor válido especificado"): an empty array, an array of unknown names, and a value that is not an array at all, `null` included. With `providers: ['brasilapi']`, a CEP BrasilAPI does not know rejects with `GetAddressInfoByCepNotFoundError`, since BrasilAPI signals a miss with HTTP 404; any other error status is still a `GetAddressInfoByCepServiceError`. All three extend `GetAddressInfoByCepError`, the base class of every error this util rejects with, so a single `catch` on it covers all of them. +Fetch the address of a CEP from several providers at once and resolve to the first successful answer. The result is an `AddressInfo`: `cep`, `state`, `city`, `neighborhood` and `street`. + +- **Options** (`GetAddressInfoByCepOptions`): `providers` (`CepProvider[]`) lists the providers to race (default `['viacep', 'brasilapi']`). `'widenet'` is deprecated and left out of the default list. +- Accepts a string or a number. A number is left-padded with zeros to 8 digits. +- Retries transient network failures per provider. +- Rejects with `GetAddressInfoByCepValidationError` when the CEP is invalid or `providers` names no known provider, with `GetAddressInfoByCepNotFoundError` when every provider failed and at least one reported the CEP as unknown, and with `GetAddressInfoByCepServiceError` when every provider failed for another reason. +- All three extend `GetAddressInfoByCepError`, so one `catch` covers them. ```javascript import { getAddressInfoByCep } from '@brazilian-utils/brazilian-utils'; @@ -174,7 +222,12 @@ const addressFromNumber = await getAddressInfoByCep(1310100); ### getCepInfoByAddress -Fetch CEPs from an address using ViaCEP. Throws `GetCepInfoByAddressValidationError` when the UF, city or street is missing/invalid — including when the argument is not an object at all (omitted, `null`, a string) and when `federalUnit` is not a string, neither of which leaks a raw `TypeError` — `GetCepInfoByAddressNotFoundError` when no address matches the query, and `GetCepInfoByAddressError` when ViaCEP itself answers with an HTTP error status. A request that cannot be performed at all (a transport failure) rejects with the underlying `fetch` error instead. Each item is typed as `CepAddressInfo` and carries the ViaCEP payload unchanged, under ViaCEP's own field names: `cep`, `logradouro`, `complemento`, `unidade`, `bairro`, `localidade`, `uf`, `estado`, `regiao`, `ibge`, `gia`, `ddd` and `siafi`. A broad street name matches many CEPs, so query as narrowly as the address allows. +Fetch the CEPs of an address from ViaCEP. Resolves to an array of `CepAddressInfo`. + +- The argument (`GetCepInfoByAddressParams`) carries `federalUnit`, `city` and `street`. `federalUnit` may be lowercase; `city` and `street` are trimmed and stripped of accents before the query. +- Rejects with `GetCepInfoByAddressValidationError` when the UF, city or street is missing or invalid, with `GetCepInfoByAddressNotFoundError` when no address matches, and with `GetCepInfoByAddressError` when ViaCEP answers with an HTTP error status. +- Retries transient network failures, as `getAddressInfoByCep` does. +- Each item carries the ViaCEP payload unchanged, under ViaCEP's own field names. ```javascript import { getCepInfoByAddress } from '@brazilian-utils/brazilian-utils'; @@ -208,7 +261,10 @@ const ceps = await getCepInfoByAddress({ ### isValidBoleto -Check if boleto ([brazilian payment method](https://en.wikipedia.org/wiki/Boleto)) is valid. Supports both the 47 digit "cobrança bancária" boleto and the "boleto de arrecadação" (convênio/tributos): either its 48 digit linha digitável or its 44 digit barcode, both starting with `8`. One leniency is kept from 2.3.0: the código de moeda in position 4 of the cobrança bancária barcode is not checked, although Carta-Circular BCB nº 2.926/2000 fixes it at `9` (real), so a slip carrying any other moeda digit still validates. +Check if a boleto ([brazilian payment method](https://en.wikipedia.org/wiki/Boleto)) is valid. + +- Accepts the 47 digit "cobrança bancária" linha digitável and, for the "boleto de arrecadação", either its 48 digit linha digitável or its 44 digit barcode. +- The código de moeda (position 4 of the cobrança bancária barcode) is not checked. ```javascript import { isValidBoleto } from '@brazilian-utils/brazilian-utils'; @@ -217,9 +273,14 @@ isValidBoleto('00190000090114971860168524522114675860000102656'); // true isValidBoleto('846100000005246100291102005460339004695895061080'); // true (boleto de arrecadação) ``` +Source: [Carta-Circular BCB nº 2.926/2000](https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf), [FEBRABAN, Layout Padrão de Arrecadação](https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf). + ### formatBoleto -Format a boleto number. `options.pad` (part of `FormatBoletoOptions`) left-pads the value with zeros up to the number of slots in the pattern before masking (default `false`). The arrecadação (convênio/tributos) mask applies only to the 48 digit linha digitável starting with `8`; the 44 digit arrecadação barcode has no display grouping defined by FEBRABAN and keeps the "cobrança bancária" mask instead. +Format a boleto number. + +- **Options** (`FormatBoletoOptions`): `pad` left-pads the value with zeros to the length of the pattern before masking (default `false`). +- A 48 digit linha digitável starting with `8` gets the arrecadação mask: four blocks of 11 digits, each followed by its check digit. The 44 digit arrecadação barcode keeps the "cobrança bancária" mask. ```javascript import { formatBoleto } from '@brazilian-utils/brazilian-utils'; @@ -230,6 +291,8 @@ formatBoleto('846100000005246100291102005460339004695895061080'); // 84610000000 formatBoleto('84610000000246100291100054603390069589506108'); // 84610.00000 02461.002911 00054.603390 0 69589506108 (44 digit arrecadação barcode keeps the bancária mask) ``` +Source: [FEBRABAN, Layout Padrão de Arrecadação](https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf). + ### parseBoleto Remove boleto formatting, keep only digits, and cap the result to 47 digits (48 for boleto de arrecadação). @@ -242,7 +305,9 @@ parseBoleto('00190.00009 01149.718601 68524.522114 6 75860000102656'); // 001900 ### generateBoleto -Generate a valid random boleto. Pass `{ type: "arrecadacao" }` (typed as `GenerateBoletoParams`) to generate a boleto de arrecadação instead of the default "bancario" (cobrança bancária) type. An arrecadação slip draws its segment from 1 to 7 (segment 9 is the banks' own) and its value identifier from all four values, `6` and `8` for an effective amount and `7` and `9` for a reference quantity, so both `hasEffectiveValue` branches of `getBoletoInfo` are reachable. +Generate a valid random boleto. + +- Pass `{ type: 'arrecadacao' }` (`GenerateBoletoParams`) for a 48 digit boleto de arrecadação instead of the default `'bancario'` (cobrança bancária, 47 digits). ```javascript import { generateBoleto } from '@brazilian-utils/brazilian-utils'; @@ -253,7 +318,12 @@ generateBoleto({ type: 'arrecadacao' }); // "84610000000524610029110200546033900 ### getBoletoInfo -Extract information from a boleto (amount, expiration date, bank code). Returns `null` when `value` is not a valid boleto — `isValidBoleto` is checked first — so the result has to be narrowed before it is read. 2.3.0 returned `undefined` here; every getter of the package now answers an unresolved lookup with `null`, so only a strict `=== undefined` comparison is affected. Accepts an optional `{ referenceDate }` (typed as `GetBoletoInfoOptions`) to resolve the "fator de vencimento" cycle as of a specific date instead of now (the factor's date-base cycle reset on 22/02/2025 per FEBRABAN). Neither FEBRABAN nor the Banco Central publishes a way of telling an old cycle factor from a new cycle one, so every factor resolves to either of two dates 9000 days apart and `referenceDate` picks between them through the library's own safety windows: the same slip can resolve to the other candidate as time passes, so pass `referenceDate` explicitly whenever the answer has to stay stable. The cycle search never goes below the first cycle, so a `referenceDate` older than the scheme itself still resolves a factor to the oldest date that factor can denote rather than to one before the 07/10/1997 base date. For a boleto de arrecadação, the result, typed as `BoletoInfo`, still carries both keys but empty, `bankCode: ''` and `expirationDate: null`, since the slip has neither a bank code nor a fator de vencimento, and adds `type: "arrecadacao"`, `segment`, `value` and `hasEffectiveValue`. +Extract information from a boleto (amount, expiration date, bank code). Returns `null` when the value is not a valid boleto. + +- **Options** (`GetBoletoInfoOptions`): `referenceDate` resolves the "fator de vencimento" cycle as of that date instead of now. +- Returns a `BoletoInfo`: `amount` in cents, `expirationDate` and the three digit `bankCode`. `expirationDate` is `null` when the slip carries no fator de vencimento (a factor below `1000`). +- The fator de vencimento cycle reset on 22/02/2025, so a factor can mean either of two dates 9000 days apart. `referenceDate` picks between them; pass it whenever the answer has to stay stable. +- A boleto de arrecadação has `bankCode: ''` and `expirationDate: null`, plus `type: 'arrecadacao'`, `segment`, `value` (the amount in reais) and `hasEffectiveValue`. ```javascript import { getBoletoInfo } from '@brazilian-utils/brazilian-utils'; @@ -272,11 +342,16 @@ getBoletoInfo('846100000005246100291102005460339004695895061080'); getBoletoInfo('invalid'); // null ``` +Source: [Carta-Circular BCB nº 2.926/2000](https://www.bcb.gov.br/pre/normativos/c_circ/2000/pdf/c_circ_2926_v1_O.pdf), [FEBRABAN, Layout Padrão de Arrecadação](https://cmsarquivos.febraban.org.br/Arquivos/documentos/PDF/Layout%20-%20C%C3%B3digo%20de%20Barras%20-%20Vers%C3%A3o%208%20-%2011_05_2026.pdf). + ## Pix ### isValidPixKey -Check if a Pix key (chave Pix) is valid: a CPF, a CNPJ, an e-mail address, a Brazilian mobile phone number or a random key (EVP), per the DICT key formats. The manual registers a "número de telefone celular", so a landline is not a valid phone key. `options.accept` (typed as `IsValidPixKeyOptions`) restricts which kinds of key are accepted; it defaults to all of them, and `[]` rejects everything. Exports the `PixKeyType` type. +Check if a Pix key (chave Pix) is valid: a CPF, a CNPJ, an e-mail address, a Brazilian mobile phone number or a random EVP key, per the DICT key formats. + +- **Options** (`IsValidPixKeyOptions`): `accept` (`PixKeyType[]`, default all of them) lists the kinds of key that count as valid; `[]` rejects everything. +- Same recognition rules as `getPixKeyInfo`. ```javascript import { isValidPixKey } from '@brazilian-utils/brazilian-utils'; @@ -290,9 +365,16 @@ isValidPixKey('123.456.789-09', { accept: ['email', 'evp'] }); // false isValidPixKey('not a key'); // false ``` +Source: [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf), [DICT API](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html), [pix-api](https://github.com/bacen/pix-api). + ### getPixKeyInfo -Identifies a Pix key and normalizes it to the canonical form the DICT expects inside a BR Code: 11 digit CPF, 14 character CNPJ, lowercased e-mail, E.164 mobile phone (a landline is not a Pix key) or lowercase UUID EVP. An 11 digit value that is valid both as a CPF and as a mobile phone is read as a CPF, unless it was written as a phone number (a `+55`/`0055` prefix or a DDD wrapped in parentheses). The CPF and the phone number are recognized by the way they are written, not only by the digits they carry, so surrounding text is not stripped away and `'abc123.456.789-09'` is not a CPF key. An e-mail key is trimmed and lowercased, and one longer than the 77 characters the DICT allows is rejected. A value whose digits carry a valid CNPJ check digit is read as a CNPJ even when it starts with `0055`, since a phone key inside a BR Code always carries the `+55` prefix. Returns `null` when the value is not a valid Pix key. The result is typed as `PixKeyInfo`. +Identify a Pix key and normalize it to the canonical form the DICT expects inside a BR Code. Returns `null` when the value is not a valid Pix key. + +- Returns a `PixKeyInfo` with the `type` (`PixKeyType`) and the `value`. +- The canonical `value` is digits for a CPF or CNPJ (letters upper-cased), a lowercase e-mail, an E.164 phone or a lowercase UUID. +- An 11 digit value valid as both CPF and mobile phone is read as a CPF, unless written as a phone (`+55` prefix or DDD in parentheses). +- An e-mail longer than 77 characters is rejected. ```javascript import { getPixKeyInfo } from '@brazilian-utils/brazilian-utils'; @@ -307,9 +389,17 @@ getPixKeyInfo('51998259765'); // { type: 'cpf', value: '51998259765' } (also a v getPixKeyInfo('+5551998259765'); // { type: 'phone', value: '+5551998259765' } ``` +Source: [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf), [DICT API](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/API-DICT.html). + ### isValidPixPayload -Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid: well-formed TLV structure, the mandatory objects present, one of the "Merchant Account Information" templates carrying the `br.gov.bcb.pix` GUI with a key or a URL, and a matching CRC-16. The "Point of Initiation Method" object (`01`) is advisory: the Manual do BR Code marks it optional and only assigns a meaning to the value `"12"` ("só pode ser utilizado uma vez"), so it may be absent from either shape and only a value outside `{"11", "12"}` makes the payload invalid. When a payload built around a key carries an amount (`54`), that amount must be greater than zero, unless the payload is a Pix Saque BR Code, i.e. unless it carries the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`) as §2.6 of the Pix manual prescribes; rejecting `"0"`/`"0.00"` without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location makes the payload invalid: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. The key itself is not checked against the DICT formats, use `isValidPixKey` for that. Unreserved Templates (IDs 80 to 99) are ignored: the "QR Code composto" of Pix Automático (Pix recorrente) writes its recurrence location in one of them, and when such a payload also carries a payment location in 26-25, as the composite example of the Pix manual does, it is accepted and read as an ordinary dynamic payload with the recurrence location dropped. Only a payload with no Pix template at all in IDs 26 to 51 is reported as invalid. +Check if a Pix BR Code payload (the string behind a Pix QR Code and behind "Pix copia e cola") is valid. The key itself is not checked; use `isValidPixKey`. + +- The TLV structure, the CRC-16 and the mandatory objects (format indicator, category code, currency, country, merchant name and city) are checked. +- One "Merchant Account Information" template (IDs 26 to 51) must carry the `br.gov.bcb.pix` GUI with a key (static) or a PSP URL (dynamic), never both. +- Objects `01` (Point of Initiation Method) and `62` (Additional Data Field) are optional; `01` must be `11` or `12` when present. +- An amount (`54`) must be greater than zero, except in a Pix Saque BR Code (8 digit `fss` in sub-object 26-03). +- Unreserved Templates (IDs 80 to 99) are ignored. ```javascript import { isValidPixPayload } from '@brazilian-utils/brazilian-utils'; @@ -322,9 +412,16 @@ isValidPixPayload( isValidPixPayload('00020126580014br.gov.bcb.pix...'); // false (broken CRC) ``` +Source: [Manual do BR Code](https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf), [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf). + ### getPixPayloadInfo -Parses a Pix BR Code payload into its fields. The payload is validated by `isValidPixPayload` first, so a malformed structure, a broken CRC or a missing mandatory object returns `null` instead of a partial result. A static payload comes back with `key`, a dynamic one with `url`. The Pix key itself is not validated, since the manual allows a static QR Code built around a key that no longer exists in the DICT; key ownership is only settled at payment time. The "Additional Data Field Template" (ID 62) is mandatory in the BR Code table but optional in the EMV® specification it refers to, so it is accepted when absent. The lengths the manual reserves for the merchant name (25), the merchant city (15), the `txid` (25) and the Pix key field 26-01 (77) are generator side limits, enforced by `generatePixPayload` and not checked here, since payloads in the wild routinely overrun them. The result is typed as `PixPayloadInfo`; `pointOfInitiation` is always present and typed as `PixPointOfInitiation`, `"dynamic"` when the payload carries a PSP location or when the "Point of Initiation Method" object (`01`) is `"12"`, `"static"` otherwise. The merchant account information must carry exactly one of a key or a `url` (checked with the same PSP location rule as `generatePixPayload`); `01` itself is advisory, so it may be absent from either shape and only a value outside `{"11", "12"}` returns `null`. When a payload built around a key carries an amount, that amount must be greater than zero, unless the payload is a Pix Saque BR Code: §2.6 of the Pix manual puts the ISPB of the "facilitador de serviço de saque" in sub-object 26-03 (`fss`), which comes back as `withdrawalFacilitator`, and `54` set to `"0"` or `"0.00"` is accepted alongside it. Rejecting a zero amount without `fss` is a deliberate restriction of this library, not a rule of the manual. A `fss` written next to a PSP location returns `null`: §2.7 of the Manual de Padrões para Iniciação do Pix maps the dynamic QR Code to exactly two sub-objects, `00` (GUI) and `25` (URL), and `fss` belongs to the static template of §2.6. When the payload carries a PSP location the amount and the `txid` are ignored, as the manual mandates. Unreserved Templates (IDs 80 to 99) are ignored: a "QR Code composto" of Pix Automático that also carries a payment location in 26-25 is parsed as an ordinary dynamic payload and its recurrence location is dropped, so a consumer that has to tell the two apart cannot rely on this parser. Only a payload with no Pix template at all in IDs 26 to 51 returns `null`. +Parse a Pix BR Code payload into its fields. Accepts what `isValidPixPayload` accepts and returns `null` for anything else, never a partial result. + +- Returns a `PixPayloadInfo`: `merchantName`, `merchantCity`, `pointOfInitiation` and either `key` (static) or `url` (dynamic). +- `amount`, `txid`, `description` and `withdrawalFacilitator` (the `fss` of a Pix Saque) are present only when the payload carries them. `txid` is absent for the `***` marker. +- `pointOfInitiation` (`PixPointOfInitiation`) is `"dynamic"` when the payload carries a PSP location or object `01` is `"12"`, `"static"` otherwise. +- With a PSP location, `amount` and `txid` are ignored, as the manual mandates. ```javascript import { getPixPayloadInfo } from '@brazilian-utils/brazilian-utils'; @@ -341,11 +438,18 @@ getPixPayloadInfo( // } ``` +Source: [Manual do BR Code](https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf), [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf). + ### generatePixPayload -Generates the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given (part of `GeneratePixPayloadParams`); `null` is returned when both or neither are given. `url` must be a PSP location as the Bacen manual defines it: a host name with a path, without a scheme (`pix.example.com/qr/v2/1234`); a dynamic payload cannot carry `amount` or `txid`, which belong to the PSP location. The amount is written with the two decimal places the BR Code takes, so one that rounds to `0.00` and one that does not survive that round trip (`0.005`, `123.456`) are both rejected rather than written as a different sum. The Pix Saque BR Code, which announces the `fss` of sub-object 26-03, is parsed by `getPixPayloadInfo` but not generated here. +Generate the payload of a Pix BR Code. Exactly one of `params.key` or `params.url` must be given; `null` is returned when both or neither are given. -When `params.key` is given, it is normalized to its DICT canonical form by `getPixKeyInfo` and the payload is static. When `params.url` is given instead (the PSP location, without a URL scheme, e.g. `"pix.example.com/qr/v2/1234"`), the payload is dynamic per the Manual de Padrões para Iniciação do Pix: the URL takes the key's place in the "Merchant Account Information" template and the "Point of Initiation Method" object is set to dynamic (`12`); `params.url` can be at most 77 characters. `merchantName`, `merchantCity` and `description` are folded to printable ASCII (accents dropped) and truncated to what the BR Code allows. `getPixPayloadInfo` already parses both shapes, so `getPixPayloadInfo(generatePixPayload({ url, ... }))` round-trips. +- **Params** (`GeneratePixPayloadParams`): `key` or `url`, `merchantName`, `merchantCity`, and the optional `amount`, `txid` and `description`. +- With `key` the payload is static and the key is normalized by `getPixKeyInfo`. With `url` it is dynamic (object `01` set to `12`) and cannot carry `amount` or `txid`. +- `url` is a PSP location: host and path, no scheme (`pix.example.com/qr/v2/1234`), at most 77 characters. +- `amount` takes two decimal places; `0.005`, `123.456` or a value that rounds to `0.00` is rejected. +- `txid` is 1 to 25 characters of `[A-Za-z0-9]` (default `***`). +- `merchantName`, `merchantCity` and `description` lose their accents and are truncated to 25, 15 and what is left of the template. ```javascript import { generatePixPayload } from '@brazilian-utils/brazilian-utils'; @@ -368,13 +472,28 @@ generatePixPayload({ generatePixPayload({ merchantName: 'Fulano', merchantCity: 'Brasília' }); // null (neither key nor url) ``` +Source: [Manual do BR Code](https://www.bcb.gov.br/content/estabilidadefinanceira/spb_docs/ManualBRCode.pdf), [Manual de Padrões para Iniciação do Pix](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_Pix/II_ManualdePadroesparaIniciacaodoPix.pdf). + ## NF-e key ### isValidNfeKey -Check if a DF-e (Documento Fiscal eletrônico) access key (chave de acesso) is valid. It covers every document whose access key is the same 44 digit string: NF-e (modelo 55), NFC-e (65), CT-e (57, the Conhecimento de Transporte Eletrônico instituted by the cláusula primeira of the [Ajuste SINIEF 09/07](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2007/AJ_009_07)), MDF-e (58), CT-e OS (67, the Conhecimento de Transporte Eletrônico para Outros Serviços instituted by the cláusula primeira of the [Ajuste SINIEF 36/19](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2019/AJ036_19)), GTV-e (64, the CT-e Guia de Transporte de Valores instituted by the cláusula primeira of the [Ajuste SINIEF 03/20](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2020/ajuste-sinief-03-20)), BP-e (63), NF3e (66) and NFCom (62). The CF-e-SAT (59) is out: its 44 position "chave de consulta" is composed differently. The 44 digits may be split into the printed groups of 4 by whitespace, `.`, `-` or `/`, a run of them between two groups included, the same interchangeable mask `isValidCpf` and `isValidCnpj` accept; a separator inside a group of 4, or any other character, is rejected instead of being stripped. The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes found in the `Id` attribute of the document's XML are stripped before that check, along with any whitespace between the prefix and the first group. +Check if a DF-e access key (chave de acesso) is valid. Covers every DF-e with a 44 digit access key; the CF-e-SAT (59) is out. -The emission type (`tpEmis`) is checked against the codes the MOC of that model assigns, so the accepted set changes with the model: 1 to 7 and 9 for NF-e and NFC-e, `{1, 3, 4, 5, 7, 8}` for the CT-e, `{1, 5, 7, 8}` for the CT-e OS, `{1, 2, 7, 8}` for the GTV-e, `{1, 2, 3}` for the MDF-e and `{1, 2}` for the BP-e, the NF3e and the NFCom. Code 8, the authorização pela SVC-SP, is assigned by the [CT-e MOC 4.00](https://dfe-portal.svrs.rs.gov.br/CTE/Documentos) only, never by the NF-e one; the domains of the [BP-e](https://dfe-portal.svrs.rs.gov.br/BPE/Documentos), the [NF3e](https://dfe-portal.svrs.rs.gov.br/NF3e/Documentos) and the [NFCom](https://dfe-portal.svrs.rs.gov.br/NFCOM/Documentos) come from their own manuals. For NF-e and NFC-e the numeric code is also checked against rule B03-10 of the NF-e MOC, which forbids the twenty repeated and sequential `cNF` values it lists and a `cNF` equal to the document number. A document number of all zeros is turned down for every model, following the leiaute rather than a choice of this library: `tiposBasico_v4.00.xsd` of the [NF-e schema package](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) types `nNF` as `TNF`, whose pattern is `[1-9]{1}[0-9]{0,8}`, and the Anexo I of every other model repeats the same regex for its own number field. +- Models: NF-e (55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) and NFCom (62). +- The 44 digits may be grouped in 4 by whitespace, `.`, `-` or `/`. The XML `Id` prefixes (`NFe`, `CTe`, `MDFe`, `BPe`, `NF3e`, `NFCom`) are stripped first. +- `tpEmis` must be one the MOC of that model assigns (table below). +- For NF-e and NFC-e the `cNF` must pass rule B03-10 of the MOC (no repeated or sequential values, not the document number). +- A document number of all zeros is rejected. The check digit is a modulus 11 over the first 43 digits. + +| Model | `tpEmis` accepted | +| --- | --- | +| NF-e (55), NFC-e (65) | 1 to 7 and 9 | +| CT-e (57) | 1, 3, 4, 5, 7, 8 | +| CT-e OS (67) | 1, 5, 7, 8 | +| GTV-e (64) | 1, 2, 7, 8 | +| MDF-e (58) | 1, 2, 3 | +| BP-e (63), NF3e (66), NFCom (62) | 1, 2 | ```javascript import { isValidNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -386,13 +505,20 @@ isValidNfeKey('3517 0458 7165 2300 0119 5500 1000 0000 1210 0012 3458'); // true isValidNfeKey('3517.0458.7165.2300.0119.5500.1000.0000.1210.0012.3458'); // true (any of the mask characters) isValidNfeKey('351 70458716523000119550010000000121000123458'); // false (a separator inside a group of 4) isValidNfeKey('99170458716523000119550010000000121000123458'); // false (invalid cUF) +isValidNfeKey('35170458716523000119010010000000121000123450'); // false (invalid mod) isValidNfeKey('35170458716523000119550010000000128000123455'); // false (the NF-e MOC does not assign tpEmis 8) isValidNfeKey('35170458716523000119550010000000121000000003'); // false (cNF 00000000, rule B03-10) ``` +Source: [MOC NF-e](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf), [NF-e schemas](https://dfe-portal.svrs.rs.gov.br/NFE/Documentos) and the MOCs cited in `src/is-valid-nfe-key/is-valid-nfe-key.ts`. + ### formatNfeKey -Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form every auxiliary document prints it in: the DANFE of the NF-e and the NFC-e, the DACTE of the CT-e, the CT-e OS and the GTV-e, the DAMDFE of the MDF-e, the DABPE of the BP-e, the DANF3E of the NF3e and the DANFE-COM of the NFCom. Like every formatter of this package, the value is read for its digits and grouped as far as they go, so a masked or partial key still being typed is grouped progressively, and anything without a digit (an object, `true`, an object created with `Object.create(null)`) gives `''` instead of throwing. Use `isValidNfeKey` to check a key. `options.pad` (part of `FormatNfeKeyOptions`) left pads the value with zeros up to the 44 digits of a complete access key (default `false`). The parameter is typed as a string because 44 digits are more than a JavaScript number can hold exactly; at runtime a number is read as the string of its digits, like in every formatter of this package. +Format a DF-e (Documento Fiscal eletrônico) access key into groups of 4 digits separated by spaces, the form the DANFE, DACTE, DAMDFE, DABPE, DANF3E and DANFE-COM print it in. + +- **Options** (`FormatNfeKeyOptions`): `pad` left pads the value with zeros up to the 44 digits of a complete access key (default `false`). +- A masked or partial key is grouped as far as its digits go. +- Use `isValidNfeKey` to check a key. ```javascript import { formatNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -408,7 +534,9 @@ formatNfeKey('12345', { pad: true }); ### parseNfeKey -Remove the formatting of a DF-e access key (chave de acesso), keep only digits, and cap the result to 44 digits. The `NFe`, `CTe`, `MDFe`, `BPe`, `NF3e` and `NFCom` prefixes the `Id` attribute of the document XML puts in front of the key are stripped first, since `NF3e` carries a digit of its own; use `isValidNfeKey` to check the key and `getNfeKeyInfo` to read its fields. +Remove the formatting of a DF-e access key (chave de acesso), keep only digits, and cap the result to 44 digits. + +- The XML `Id` prefixes (`NFe`, `CTe`, `MDFe`, `BPe`, `NF3e`, `NFCom`) are stripped first. ```javascript import { parseNfeKey } from '@brazilian-utils/brazilian-utils'; @@ -422,7 +550,10 @@ parseNfeKey('NFe35170458716523000119550010000000121000123458'); ### getNfeKeyInfo -Parses a DF-e access key into its fields (stateCode, year, month, taxId, model, series, number, emissionType, code, checkDigit). Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. The result is typed as `NfeKeyInfo`, whose `model` is an `NfeKeyModel`. NFCom (`'62'`) and NF3e (`'66'`) spend position 36 of the key on `nSiteAutoriz`, the site of the authorizer that received the document, so for those two models the result also carries `authorizationSite` and `code` is 7 digits instead of 8. +Parse a DF-e access key into its fields. Accepts the same input forms as `isValidNfeKey` and returns `null` when the key is not valid. + +- Returns an `NfeKeyInfo`: `stateCode`, `year`, `month`, `taxId`, `model` (`NfeKeyModel`), `series`, `number`, `emissionType`, `code` and `checkDigit`. +- For NFCom and NF3e (models `'62'` and `'66'`) the result also carries `authorizationSite` and `code` is 7 digits instead of 8. ```javascript import { getNfeKeyInfo } from '@brazilian-utils/brazilian-utils'; @@ -442,7 +573,9 @@ getNfeKeyInfo('invalid'); // null ### isValidPhone -Check if phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed before validation, under the rule documented in `parsePhone`. `options.accept` (typed as `PhoneType[]`, part of `IsValidPhoneOptions`) picks which kinds of number count as valid and defaults to `['mobile', 'landline']`; add `'service'` to also accept the non-geographic numbers recognized by `isValidServicePhone`, or pass `[]` to accept none. `options.version` (typed as `PhoneVersion`, part of the same type) is forwarded to `isValidMobilePhone` and picks which mobile numbering rule is enforced: `1` (default) the legacy format, whose first number digit may be 6, 7, 8 or 9, and `2` the current one of Resolução Anatel 749/2022, art. 12, I, "a", which accepts 7, 8 or 9 and rejects the `700` prefix. It only affects mobile numbers; landline and service numbers are unaffected. +Check if a phone number (mobile or landline) is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed first, as in `parsePhone`. + +- **Options** (`IsValidPhoneOptions`): `accept` (`PhoneType[]`, default `['mobile', 'landline']`) picks which kinds of number count as valid; add `'service'` for the numbers `isValidServicePhone` recognizes. `version` (`PhoneVersion`, default `1`) is forwarded to `isValidMobilePhone`. ```javascript import { isValidPhone } from '@brazilian-utils/brazilian-utils'; @@ -456,9 +589,17 @@ isValidPhone('08001234567', { accept: ['service'] }); // true isValidPhone('11900000000', { accept: [] }); // false ``` +Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749). + ### formatPhone -Format phone number according to Brazilian patterns. `options.mask` (typed as `PhoneMask`) accepts `"sn"` (default, subscriber number only, 9 digits, no DDD), `"nanp"` (DDD + subscriber number, `"(00) 00000-0000"` for the 11 digits of a mobile and `"(00) 0000-0000"` for the 10 digits of a landline, any other length keeping the 11 digit grouping), `"e164"` (`"+5511987654321"`), `"international"` (`"+55 11 98765-4321"`, the way a Brazilian number is printed for foreign callers), `"service"` (`"0800 123 4567"` or `"4004-1234"`, the conventional groupings for service numbers) or `"auto"`. `"auto"` picks `"international"` when `value` carries a Brazilian country code (`+55`, `0055` or a bare `55` followed by 10 or 11 digits), `"service"` when `value` is a service number, and otherwise falls back to the digit count: `"nanp"` when `value` has more digits than a bare subscriber number, `"sn"` when it does not. `"e164"` and `"international"` drop the country code from `value` first, under the rule documented in `parsePhone`, and fall back to the `"service"` presentation for a service number, since those have no E.164 form. If `value` includes a DDD, pass `{ mask: 'auto' }` (or `'nanp'`) explicitly, since the default `"sn"` mask assumes no DDD and silently truncates one if present. A `mask` outside the union falls back to the default `"sn"` instead of throwing. +Format a phone number according to Brazilian patterns. If `value` includes a DDD, pass `{ mask: 'auto' }` or `'nanp'`: the default `"sn"` mask assumes no DDD and truncates one. + +- **Options** (`FormatPhoneOptions`): `mask` (`PhoneMask`, default `"sn"`) picks one of the patterns below. An unknown `mask` falls back to `"sn"`. +- `"sn"`: subscriber number only, 9 digits. `"nanp"`: DDD plus subscriber number, 11 digits for a mobile and 10 for a landline; any other length keeps the 11 digit grouping. +- `"e164"` and `"international"` drop the country code first, as `parsePhone` does, and fall back to `"service"` for a service number. +- `"service"`: the Códigos Não Geográficos (`0800 123 4567`) and the abbreviated `300X`/`400X` numbers (`4004-1234`). +- `"auto"`: `"service"` for a service number, `"international"` when `value` carries a country code, otherwise `"nanp"` for more than 9 digits, else `"sn"`. ```javascript import { formatPhone } from '@brazilian-utils/brazilian-utils'; @@ -473,12 +614,17 @@ formatPhone('+5511987654321', { mask: 'international' }); // +55 11 98765-4321 formatPhone('08001234567', { mask: 'service' }); // 0800 123 4567 formatPhone('40041234', { mask: 'service' }); // 4004-1234 formatPhone('+5511987654321', { mask: 'auto' }); // +55 11 98765-4321 ("auto" detects the +55 prefix and picks "international") +formatPhone('5508001234567', { mask: 'auto' }); // 0800 123 4567 ("auto" reads the 0800 number, not a +55 08 one) formatPhone('11900000000'); // 11900-0000 (BEWARE: default "sn" truncates a DDD-prefixed number) ``` +Source: [ITU-T E.164](https://www.itu.int/rec/T-REC-E.164), [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749). + ### parsePhone -Remove phone formatting, keep only digits, and cap the result to 11 digits. A Brazilian country code is stripped first, but only when the digits left behind are exactly 10 or 11 long, i.e. a plausible national number. The rule is length-based, not sign-based, so a number from area code 55 is not mistaken for a country code. +Remove phone formatting, keep only digits, and cap the result to 11 digits. + +- A Brazilian country code (`+55`, `0055` or a bare `55`) is stripped first, but only when 10 or 11 digits are left (DDD plus subscriber number), so area code 55 is not mistaken for it. ```javascript import { parsePhone } from '@brazilian-utils/brazilian-utils'; @@ -491,7 +637,9 @@ parsePhone('55987654321'); // 55987654321 (area code 55, not mistaken for the +5 ### generatePhone -Generate a random Brazilian phone number. Accepts `'mobile'`, `'landline'` or `'service'` (typed as `GeneratePhoneType`); a service number has no DDD. Omitted, it randomly generates a mobile or a landline, never a service number. A generated mobile number always starts with 9, so it passes both `isValidMobilePhone` numbering rules. +Generate a random Brazilian phone number. Accepts `'mobile'`, `'landline'` or `'service'` (`GeneratePhoneType`); when omitted, it generates a mobile or a landline at random, never a service number. + +- A mobile starts with 9 after the DDD (valid under both `isValidMobilePhone` versions); a landline has 8 digits after the DDD, starting with 2 to 6; a service number has no DDD. ```javascript import { generatePhone } from '@brazilian-utils/brazilian-utils'; @@ -504,7 +652,9 @@ generatePhone('service'); // '08001234567' or '40041234' ### isValidMobilePhone -Check if mobile phone number is valid. `options.version` (typed as `PhoneVersion`) controls which mobile numbering rule is enforced: `1` (default) is the pre-Resolução Anatel 749/2022 format, kept for 2.3.0 compatibility, whose first number digit (after the DDD) may be 6, 7, 8 or 9; `2` enforces the resolution's art. 12, I, "a", which places 7, 8 and 9 in the Serviço Móvel Pessoal (SMP), so a leading 6 is Reserva Técnica and is rejected. Version `2` also carves out the `700` prefix, which art. 12, II reserves for the Serviço Móvel Global por Satélite rather than SMP, so `isValidMobilePhone('11700123456', { version: 2 })` is `false`; version `1` does not carve it out and accepts it. +Check if a mobile phone number is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed first, as in `parsePhone`. + +- **Options** (`IsValidMobilePhoneOptions`): `version` (`PhoneVersion`, default `1`) picks the numbering rule: `1` accepts a first digit of 6, 7, 8 or 9; `2` follows Resolução Anatel 749/2022, accepts only 7, 8 or 9 and rejects the `700` series. ```javascript import { isValidMobilePhone } from '@brazilian-utils/brazilian-utils'; @@ -516,19 +666,26 @@ isValidMobilePhone('11612345678', { version: 2 }); // false (6 is Reserva Técni isValidMobilePhone('11700123456', { version: 2 }); // false (the 700 series is satellite) ``` +Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749). + ### isValidLandlinePhone -Check if landline phone number is valid. +Check if a landline phone number is valid. A Brazilian country code (`+55`, `0055` or a bare `55`) is accepted and removed first, as in `parsePhone`. ```javascript import { isValidLandlinePhone } from '@brazilian-utils/brazilian-utils'; isValidLandlinePhone('1130000000'); // true +isValidLandlinePhone('+55 11 3000-0000'); // true (country code accepted) ``` ### isValidServicePhone -Check if a phone number is a valid Brazilian service number, dialed without a DDD: the Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` (11 digits total, so the shorter, extinct `0800` + 6 digit form is rejected), the abbreviated `300X`/`400X` numbers (8 digits), and the 3-digit Códigos de Acesso a Serviços de Utilidade Pública that Anatel has designated (e.g. `190`, `192`), whose consolidated table is the Anexo of [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151). `112` and `911` are rejected: Anatel designates neither, and `911` is not even inside the `1N₂N₁` range art. 13 of [Resolução nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749) destines to public utility services, so the way handsets route them is a GSM convention rather than a numbering designation. Only the structure is checked: the number does not have to be assigned to anyone, and the `0500` rule that encodes a donation amount in the last two digits is not enforced. Anatel withdrew the 4-digit codes instead of allocating them (art. 43 I of [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86) and art. 2º II of the Ato above both ordered them released), so only the conventional `300X` and `400X` roots are recognised: other "Número Único" carrier prefixes in market use, such as `4020` and `4062`, are out of scope and are rejected. +Check if a phone number is a valid Brazilian service number, dialed without a DDD. Only the structure is checked: the number does not have to be assigned to anyone. + +- The Códigos Não Geográficos `0300`, `0303`, `0500`, `0800` and `0900` followed by 7 digits (11 in total). +- The abbreviated `300X`/`400X` numbers, 8 digits. Other carrier prefixes such as `4020` and `4062` are rejected. +- The 3 digit public utility codes Anatel has designated (e.g. `190`, `192`). `112` and `911` are not among them and are rejected. ```javascript import { isValidServicePhone } from '@brazilian-utils/brazilian-utils'; @@ -539,11 +696,14 @@ isValidServicePhone('190'); // true isValidServicePhone('11987654321'); // false (geographic number) ``` +Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151), [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86). + ### getAreaCodeInfo -Get the state (and its region) a Brazilian DDD (area code) belongs to, out of the 67 DDDs in use under the Anatel Plano Geral de Numeração. Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `AreaCodeInfo` type. +Get the state and region a Brazilian DDD (area code) belongs to, out of the 67 DDDs in use under the Anatel Plano Geral de Numeração. Accepts a string or a non-negative integer. -`stateCode` is always a single state: the one the DDD is seated in, the state of the city the code was allocated around, which is not necessarily the state holding most of its municipalities. Four DDDs straddle a state border, and for those `stateCodes` lists the other states too. DDD 61 is the widest of them, serving the Distrito Federal and the twelve Goiás municipalities of the Entorno do Distrito Federal (Águas Lindas de Goiás, Cabeceiras, Cidade Ocidental, Cristalina, Formosa, Luziânia, Novo Gama, Padre Bernardo, Planaltina, Santo Antônio do Descoberto, Valparaíso de Goiás and Vila Boa), so its `stateCode` is `'DF'` even though the Distrito Federal holds only one of its thirteen municipalities, Brasília. The other three are 42, shared by Paraná and Porto União (SC), 47, shared by Santa Catarina and Rio Negro (PR), and 49, shared by Santa Catarina and Barracão (PR), and there the seat does hold every municipality but the one named. +- Returns an `AreaCodeInfo`: `areaCode`, `stateCode`, `stateName`, `regionCode`, `regionName` and `stateCodes`. Returns `null` when the DDD is not in use. +- `stateCode` is the state the DDD is seated in. For the four DDDs that straddle a border (61, 42, 47 and 49) `stateCodes` also lists the other state, the seat first. ```javascript import { getAreaCodeInfo } from '@brazilian-utils/brazilian-utils'; @@ -562,11 +722,14 @@ getAreaCodeInfo(-11); // null getAreaCodeInfo(1.1); // null ``` +Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais). + ### getAreaCodesByState Get every DDD (area code) that serves a given Brazilian state, under the Anatel Plano Geral de Numeração. The match is case-insensitive and the result is sorted in ascending order. -A DDD that straddles a state border is listed under every state it serves, so DDD 61 comes back for both `'DF'` and `'GO'`: it serves the Distrito Federal and the twelve Goiás municipalities of the Entorno do Distrito Federal. The other three are 42, shared by Paraná and Porto União (SC), 47, shared by Santa Catarina and Rio Negro (PR), and 49, shared by Santa Catarina and Barracão (PR). +- Returns `[]` when `stateCode` does not match a Brazilian state. +- A DDD that straddles a border (the same four as `getAreaCodeInfo`) is listed under every state it serves. ```javascript import { getAreaCodesByState } from '@brazilian-utils/brazilian-utils'; @@ -579,11 +742,13 @@ getAreaCodesByState('SC'); // [42, 47, 48, 49] getAreaCodesByState('XX'); // [] ``` +Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais). + ## License plate ### isValidLicensePlate -Check if license plate is valid. Supports the old Brazilian format (ABC-1234) and the Mercosul format (ABC1D23), the single sequence Resolução CONTRAN nº 969/2022 defines for every vehicle, motorcycles included. +Check if a license plate is valid. Accepts the old Brazilian format (`ABC-1234`) and the Mercosul format (`ABC1D23`), with or without a hyphen or space, in any case. ```javascript import { isValidLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -596,9 +761,13 @@ isValidLicensePlate('ABC12D3'); // false (not a Mercosul sequence) isValidLicensePlate('ABC1234EXTRA'); // false (too many characters) ``` +Source: [Resolução CONTRAN nº 969/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf), [Anexos](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf). + ### formatLicensePlate -Format a license plate. Old Brazilian plates (`LLLNNNN`) are returned with a hyphen and Mercosul plates (`LLLNLNN`) stay normalized. Partial values are formatted as far as they go, so it can also be used as an input mask, and a value that cannot start a valid plate gives `''`. +Format a license plate. Old Brazilian plates (`LLLNNNN`) get a hyphen; Mercosul plates (`LLLNLNN`) are returned without a separator. + +- Returns `''` when the value cannot start a valid plate. ```javascript import { formatLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -619,7 +788,9 @@ parseLicensePlate('abc-1234'); // 'ABC1234' ### generateLicensePlate -Generate a random license plate in the chosen format. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random license plate in the chosen format. + +- `format` (`GenerateLicensePlateFormat`): `'LLLNLNN'` (Mercosul, the default) or `'LLLNNNN'` (the old Brazilian format). Any other value falls back to the default. ```javascript import { generateLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -629,11 +800,14 @@ generateLicensePlate('LLLNNNN'); // 'ABC1234' generateLicensePlate('LLLNNLN'); // 'ABC1D23' (a format outside the two in circulation falls back to the default) ``` -A `format` outside the two supported literals falls back to the Mercosul default, the way every other generator in this package treats an option it does not know, so the result is always a plate `isValidLicensePlate` accepts. That default sequence is `LLLNLNN`, from Resolução CONTRAN nº 969/2022, Anexo I item 1.2, the single sequence the resolution defines for every vehicle, motorcycles included. (2.3.0 used an unknown string verbatim, so `generateLicensePlate('LLLNNLN')` produced the withdrawn motorcycle sequence and `generateLicensePlate('bogus')` five digits; neither is a plate.) +Source: [Resolução CONTRAN nº 969/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf). ### getFormatLicensePlate -Detect the normalized format of a license plate. +Detect the normalized format of a license plate: `'LLLNNNN'` for the old Brazilian format, `'LLLNLNN'` for Mercosul. + +- Returns `null` when the value, separators removed, is not 7 letters and digits in one of the two formats. +- Exports the `LicensePlateFormat` type, which `generateLicensePlate` re-exports as `GenerateLicensePlateFormat`. ```javascript import { getFormatLicensePlate } from '@brazilian-utils/brazilian-utils'; @@ -645,11 +819,11 @@ getFormatLicensePlate('INVALID'); // null getFormatLicensePlate('ABC1234EXTRA'); // null (too many characters) ``` -`getFormatLicensePlate` exports the `LicensePlateFormat` type (`"LLLNNNN" | "LLLNLNN"`); `generateLicensePlate` re-exports it as `GenerateLicensePlateFormat`. - ### convertLicensePlateToMercosul -Convert an old format Brazilian license plate (`LLLNNNN`) to the Mercosul format (`LLLNLNN`), following the official conversion table: the digit in the 5th position becomes a letter (`0` through `9` mapping to `A` through `J`). Returns `""` when the value is not a valid old format license plate. +Convert an old format Brazilian license plate (`LLLNNNN`) to the Mercosul format (`LLLNLNN`). The 5th digit becomes a letter, `0` through `9` mapping to `A` through `J`. + +- Returns `""` when the value is not a valid old format license plate. ```javascript import { convertLicensePlateToMercosul } from '@brazilian-utils/brazilian-utils'; @@ -659,11 +833,15 @@ convertLicensePlateToMercosul('abc-1234'); // 'ABC1C34' convertLicensePlateToMercosul('ABC1D23'); // '' (already Mercosul) ``` +Source: [Resolução CONTRAN nº 969/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022.pdf), [Anexo II](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9692022anexos.pdf). + ## RENAVAM ### isValidRenavam -Check if RENAVAM (Registro Nacional de Veículos Automotores) is valid. Supports both the old format (9 digits) and the new format (11 digits). Any spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. A registration whose digits are all the same is rejected as well. +Check if a RENAVAM (Registro Nacional de Veículos Automotores) is valid. Accepts the old format (9 digits) and the new format (11 digits). + +- Spaces, dots and hyphens are ignored; any other character makes the value invalid. ```javascript import { isValidRenavam } from '@brazilian-utils/brazilian-utils'; @@ -678,7 +856,7 @@ isValidRenavam('ab00639884962'); // false (letters are rejected) ### generateRenavam -Generate a valid random RENAVAM: the 11 digit form, ten base digits plus the check digit. A base whose digits are all the same is drawn again, since `isValidRenavam` rejects those. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random RENAVAM in the 11 digit form: ten base digits plus the check digit. ```javascript import { generateRenavam } from '@brazilian-utils/brazilian-utils'; @@ -690,17 +868,22 @@ generateRenavam(); // '12345678900' ### isValidPis -Check if PIS is valid. Accepts the usual mask characters (`.`, `-`, `/`, `(`, `)`, `,`, `*`) and whitespace. +Check if a PIS is valid. Accepts the value masked or not. + +- A value whose digits are all the same is rejected. ```javascript import { isValidPis } from '@brazilian-utils/brazilian-utils'; +isValidPis('12056412847'); // true isValidPis('12056412547'); // false ``` ### formatPis -Format PIS number. `options.pad` (part of `FormatPisOptions`) left-pads the value with zeros to the full 11 digits before masking (default `false`). +Format a PIS. + +- **Options** (`FormatPisOptions`): `pad` left-pads the value with zeros to 11 digits before masking (default `false`). ```javascript import { formatPis } from '@brazilian-utils/brazilian-utils'; @@ -721,7 +904,7 @@ parsePis('123.45678.90-1'); // 12345678901 ### generatePis -Generate a valid random PIS. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random PIS. ```javascript import { generatePis } from '@brazilian-utils/brazilian-utils'; @@ -733,7 +916,10 @@ generatePis(); // '91077906857' ### isValidProcessoJuridico -Validate the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119): the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits and the `J`/`TR` pair, which must identify an existing órgão and tribunal from the closed lists defined by Resolução CNJ nº 65/2008, so a number carrying a correct check digit but a court that does not exist is rejected. The closed lists come from art. 1º, § 4º and § 5º of the resolution, § 5º, III in the wording Resolução CNJ nº 477/2022 gave it to seat the TRF da 6ª Região. The unidade de origem (`OOOO`) is only read as four digits, since art. 1º, § 6º leaves its codification to each tribunal and publishes no central list. The CNJ mask separators (whitespace, `.` and `-`) are accepted between the fields, and whitespace around the value is ignored, but any other character, a letter in particular, makes the value invalid. +Check if a processo jurídico number is valid, per Resolução CNJ nº 65/2008. Three things are checked: the `NNNNNNN-DD.AAAA.J.TR.OOOO` layout, the `DD` check digits (ISO 7064 MOD 97-10) and the `J`/`TR` pair. + +- `J` and `TR` must name an órgão and a tribunal that exist. +- The unidade de origem (`OOOO`) is only checked as four digits. ```javascript import { isValidProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -745,9 +931,13 @@ isValidProcessoJuridico('0000100-23.2008.8.28.0000'); // false (no 28th Tribunal isValidProcessoJuridico('ab00020802520125150049'); // false (letters are rejected) ``` +Source: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ### formatProcessoJuridico -Format the processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119) (mask `NNNNNNN-DD.AAAA.J.TR.OOOO`). `options.pad` (part of `FormatProcessoJuridicoOptions`) left-pads the value with zeros to the full 20 digits before masking (default `false`). +Format a processo jurídico number in the CNJ mask `NNNNNNN-DD.AAAA.J.TR.OOOO`. + +- **Options** (`FormatProcessoJuridicoOptions`): `pad` left-pads the value with zeros to 20 digits before masking (default `false`). ```javascript import { formatProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -756,9 +946,11 @@ formatProcessoJuridico('00020802520125150049'); // 0002080-25.2012.5.15.0049 formatProcessoJuridico('20802520125150049', { pad: true }); // 0002080-25.2012.5.15.0049 ``` +Source: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ### parseProcessoJuridico -Remove processo jurídico formatting, keep only digits, and cap the result to 20 digits. Both the current CNJ mask (`NNNNNNN-DD.AAAA.J.TR.OOOO`) and the older one are accepted, since only the digits are kept. +Remove processo jurídico formatting, keep only digits, and cap the result to 20 digits. ```javascript import { parseProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -768,7 +960,11 @@ parseProcessoJuridico('0002080-25.2012.5.15.0049'); // 00020802520125150049 ### generateProcessoJuridico -Generate a valid random processo jurídico number according to [CNJ's definition](https://atos.cnj.jus.br/atos/detalhar/119). `year` must be between the current year and 9999, `court` between 1 and 9; out-of-range values return `null`. The órgão (`J`) and the tribunal (`TR`) are drawn from the closed lists of art. 1º, § 4º and § 5º, so the pair always names a court that exists: `court` picks the órgão and the `TR` is drawn among the tribunais that órgão has. The unidade de origem (`OOOO`) is drawn freely, since the resolution publishes no central list for it. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random processo jurídico number in the layout of Resolução CNJ nº 65/2008. + +- **Options** (`GenerateProcessoJuridicoParams`): `year` sets the `AAAA` field, an integer from the current year to 9999 (default: the current year); `court` sets the órgão `J`, from 1 to 9 (default: random). +- `TR` is drawn among the tribunais of the chosen órgão, so the pair always names a court that exists. +- Returns `null` when `year` or `court` is out of range. ```javascript import { generateProcessoJuridico } from '@brazilian-utils/brazilian-utils'; @@ -779,11 +975,16 @@ generateProcessoJuridico({ year: 10000 }); // null (year out of range) generateProcessoJuridico({ court: 10 }); // null (no such órgão) ``` +Source: [Resolução CNJ nº 65/2008](https://atos.cnj.jus.br/atos/detalhar/119). + ## Bank accounts and banks ### isValidBankAccount -Check if a Brazilian bank account is valid. The `bankCode` must belong to the Banco Central do Brasil STR participants list (the same dataset used by `getBankByCode`), so an unassigned code such as `'999'` is always invalid. Banks are then validated in one of three ways: by their published check digit algorithm, by structure only (bank exists and the agency/account match the documented digit lengths, for banks that publish no check digit rule) or by a generic mod10/mod11 check, which stays the fallback for every other listed bank. +Check if a Brazilian bank account is valid. The `bankCode` must be a Banco Central STR participant (the list `getBankByCode` uses). + +- **Params** (`IsValidBankAccountParams`, all strings): `bankCode` (3 digits), `agency` (1-5 digits), `account` (1-13 digits) and `digit` (1-2 characters, or `X` for Banco do Brasil and `P` for Bradesco). +- A listed bank is validated in one of three ways: by its published check digit algorithm, by structure only, or by a generic mod10/mod11 fallback. Banks validated by their published check digit algorithm: @@ -799,7 +1000,7 @@ Banks validated by their published check digit algorithm: | HSBC / Kirton Bank | `399` | 4 digits | 6 digits | weights `8,9,2,3,4,5,6,7,8,9` over agency + account; remainder 10 gives `0` | | Citibank | `745` | 4 digits | 10 digits | weights `11..2` over the account; remainder 0 or 1 gives `0` | -Banks validated by structure only, because they publish no check digit rule. The agency (1-5 digits), the account (1-13 digits) and a single numeric `digit` are enough to make the account valid: +Banks validated by structure only (a single numeric `digit` is enough): | Bank | Code | | Bank | Code | | --- | --- | --- | --- | --- | @@ -813,9 +1014,7 @@ Banks validated by structure only, because they publish no check digit rule. The | PagBank | `290` | | Sicredi | `748` | | BMG | `318` | | Sicoob | `756` | -When `digit` has 2 characters, the generic fallback chains mod10 followed by mod11 over the account, the same way CPF/CNPJ check digits are chained. - -Sources: the "Regras de Validação de dígito verificador de agência e conta corrente" compendium, cross checked against `banktools-br` (Ruby), `luizalabs/heimdall` (Python) and `Xerpa/bran_checker` (Elixir). Each shipped algorithm agrees on at least two independent sources. +- Every other listed bank uses the generic fallback: `digit` must match mod10 or mod11 over the account. A 2 character `digit` chains mod10 then mod11. ```javascript import { isValidBankAccount } from '@brazilian-utils/brazilian-utils'; @@ -884,9 +1083,13 @@ isValidBankAccount({ }); // true (Banco ABC Brasil, generic mod10 fallback) ``` +Source: [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv), [Regras de Validação de dígito verificador](https://github.com/eduardokum/laravel-boleto/blob/master/manuais/Regras%20Validacao%20Conta%20Corrente%20VI_EPS.pdf). + ### getBanks -Get every Brazilian bank with a compensation code (COMPE), published by Banco Central do Brasil in the [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Each bank (typed as `Bank`) has a `code` (COMPE, 3 digits), an `ispb` (Identificador do Sistema de Pagamentos Brasileiro, 8 digits) and a `name`. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. +Get every Brazilian bank with a compensation code (COMPE), from the Banco Central do Brasil STR participants list. + +- Each bank (`Bank`) has a `code` (COMPE, 3 digits), an `ispb` (8 digits) and a `name`. ```javascript import { getBanks } from '@brazilian-utils/brazilian-utils'; @@ -900,9 +1103,13 @@ getBanks(); // ] ``` +Source: [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). + ### getBankByCode -Look a Brazilian bank up by its compensation code (COMPE), published by Banco Central do Brasil in the [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Accepts both `string` and `number` input, with or without leading zeros. Returns a fresh copy (typed as `Bank`) of the matching bank, or `null` when no bank has that code. +Look a Brazilian bank up by its compensation code (COMPE), from the Banco Central do Brasil STR participants list. Accepts a `string` or a `number`. + +- Returns the matching `Bank`, or `null` when no bank has that code. ```javascript import { getBankByCode } from '@brazilian-utils/brazilian-utils'; @@ -912,9 +1119,13 @@ getBankByCode(1); // { code: '001', ispb: '00000000', name: 'Banco do Brasil S.A getBankByCode('999'); // null ``` +Source: [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). + ### getBankByIspb -Look a Brazilian bank up by its ISPB (Identificador do Sistema de Pagamentos Brasileiro), the 8 digit code published by Banco Central do Brasil in the [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv). Every SPB participant has an ISPB, but this dataset only carries the institutions that also have a COMPE code, so an ISPB whose institution has no COMPE code of its own returns `null`. Accepts both `string` and `number` input, with or without leading zeros, so `getBankByIspb(0)` finds the same bank as `getBankByIspb('00000000')`. The dataset is generated from that CSV, falling back to [BrasilAPI](https://brasilapi.com.br/api/banks/v1) when the Bacen request fails. Returns a fresh copy (typed as `Bank`) of the matching bank, or `null` when no bank has that ISPB. +Look a Brazilian bank up by its ISPB (Identificador do Sistema de Pagamentos Brasileiro), the 8 digit code of every SPB participant. Accepts a `string` or a `number`, with or without leading zeros. + +- Returns the matching `Bank`, or `null` when no bank has that ISPB. The base only carries institutions that also have a COMPE code. ```javascript import { getBankByIspb } from '@brazilian-utils/brazilian-utils'; @@ -924,11 +1135,17 @@ getBankByIspb('60701190'); // { code: '341', ispb: '60701190', name: 'ITAÚ UNIB getBankByIspb('99999999'); // null ``` +Source: [STR participants list](https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv), [BrasilAPI](https://brasilapi.com.br/api/banks/v1). + ## IBAN ### isValidIban -Check if a Brazilian IBAN (International Bank Account Number) is valid, per Bacen's [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf) (Circular BCB nº 3.625/2013): `BR` + 2 ISO 7064 MOD 97-10 check digits + 8 digit ISPB + 5 digit branch + 10 digit account + 1 letter account type (any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 owner indicator (`1` for the first or only holder up to `9` for the ninth, then `A` to `Z` from the tenth, so `0` is rejected), 29 characters total. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`, since this package does not carry the field layout of the other 90+ ISO 13616 countries. Is case-insensitive and accepts both forms an IBAN is written in: compact (`'BR1500000000000010932840814P2'`) or in the ISO 13616 print format, letters and digits in groups of 4 (the last one shorter), with optional surrounding whitespace either way. The groups may be split by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` and `isValidCnpj` accept. Only a separator away from a group boundary, a run of separators (ISO 13616 prints a single one) or a character outside letters and digits makes the value something other than an IBAN, so it is rejected instead of being stripped. +Check if a Brazilian IBAN (International Bank Account Number) is valid. Only Brazilian IBANs (country code `BR`) are recognized; any other country returns `false`. + +- Layout, 29 characters: `BR`, 2 check digits (ISO 7064 MOD 97-10), 8 digit ISPB, 5 digit branch, 10 digit account, 1 letter account type, 1 owner indicator. +- Account type: any letter, usually `C` or `P`. Owner: `1` to `9`, then `A` to `Z`. +- Accepts the compact form or groups of 4 split by one whitespace, `.`, `-` or `/`, in any case. ```javascript import { isValidIban } from '@brazilian-utils/brazilian-utils'; @@ -941,9 +1158,13 @@ isValidIban('BR15 000 00000 0000 1093 2840 814P 2'); // false (a separator insid isValidIban('DE89370400440532013000'); // false (non Brazilian IBAN) ``` +Source: [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf), [Circular BCB nº 3.625/2013](https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf), [ISO 13616-1:2020](https://www.iso.org/standard/81090.html). + ### formatIban -Format an IBAN in the ISO 13616 print grouping, blocks of 4 characters, the presentation used on statements and bank forms. Does not validate the check digits or the field layout; formats whatever is given, up to the 29 character length of a Brazilian IBAN, as far as it goes, so the function can also be used as an input mask, and an IBAN of another country is grouped the same way up to that length. Use `isValidIban` to check validity. The value may be compact (`'BR1500000000000010932840814P2'`), already in the ISO 13616 print format or a partial value still being typed (`'BR15'`); like every formatter of this package, it is read for its letters and digits and grouped as far as they go, any other character (a hyphen, a dot, extra whitespace) is dropped and the letters are uppercased. Only a value that is not a string gives an empty string. +Format an IBAN in the ISO 13616 print grouping: blocks of 4 characters, the presentation used on statements and bank forms. Does not validate; use `isValidIban` for that. + +- Caps the result at 29 characters, the length of a Brazilian IBAN. ```javascript import { formatIban } from '@brazilian-utils/brazilian-utils'; @@ -956,7 +1177,7 @@ formatIban('BR15 0000-0000.0000/1093 2840 814P-2'); // 'BR15 0000 0000 0000 1093 ### parseIban -Remove IBAN formatting, keep the letters and digits, uppercase the result, and cap it to the 29 characters of a Brazilian IBAN. An IBAN carries letters as well as digits, so the value is read the way `parsePassport` reads a passport number; use `isValidIban` to check the check digits and `getIbanInfo` to read the fields. +Remove IBAN formatting, keep the letters and digits, uppercase the result, and cap it to the 29 characters of a Brazilian IBAN. ```javascript import { parseIban } from '@brazilian-utils/brazilian-utils'; @@ -967,7 +1188,10 @@ parseIban('br15-0000.0000/0000 1093 2840 814p-2'); // 'BR15000000000000109328408 ### getIbanInfo -Parses a Brazilian IBAN into its fields: 2 (country code, always `BR`) + 2 (ISO 7064 MOD 97-10 check digits) + 8 (ISPB) + 5 (branch) + 10 (account) + 1 (account type, any letter, usually `C` for conta corrente or `P` for conta poupança) + 1 (owner indicator, `1` to `9` then `A` to `Z`). Only Brazilian IBANs are supported: the field layout of the other ISO 13616 countries is out of scope, so a well-formed non `BR` IBAN also returns `null`. Accepts the same input forms as `isValidIban`, compact or in the ISO 13616 print format (groups of 4 split by a single whitespace, `.`, `-` or `/`), in either case with optional surrounding whitespace and in any case, and returns `null` whenever `isValidIban` would return `false`, including a value carrying a separator away from a group boundary, a run of separators or any character other than letters and digits. The result is typed as `IbanInfo`, whose `accountType` is a `string`. +Parse a Brazilian IBAN into its fields. Returns an `IbanInfo` object, or `null` whenever `isValidIban` would return `false`. + +- Fields, all strings: `countryCode`, `checkDigits`, `bankIspb`, `branch`, `account`, `accountType` (usually `C` or `P`) and `owner` (`1` to `9`, then `A` to `Z`). +- Same input rules as `isValidIban`. ```javascript import { getIbanInfo } from '@brazilian-utils/brazilian-utils'; @@ -987,11 +1211,17 @@ getIbanInfo('DE89370400440532013000'); // null (non Brazilian IBAN) getIbanInfo('BR15 000 00000 0000 1093 2840 814P 2'); // null (a separator inside a group) ``` +Source: [Diretrizes de Implementação do IBAN no Brasil](https://www.bcb.gov.br/content/estabilidadefinanceira/Documents/sistema_pagamentos_brasileiro/IBAN-Guidelines_%20port.pdf), [Circular BCB nº 3.625/2013](https://www.bcb.gov.br/pre/normativos/circ/2013/pdf/circ_3625_v1_O.pdf), [ISO 13616-1:2020](https://www.iso.org/standard/81090.html). + ## Currency, numbers and dates in words ### formatCurrency -Formats an integer or float to a string in the BRL pattern. A `number` is formatted as-is (sign and decimals preserved). A `string` input is read by the same rule as `parseCurrency`, except that a value written without any separator stays in whole units: the last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator, every other `,` or `.` is a thousands separator, and a `-` written before the first digit is preserved. So `'1.234,56'` formats as `1.234,56`, `'-10.5'` as `-10,50` and `'1234'` as `1.234,00`. `precision` is clamped to `0..20` (the package limit, the bound Node 20 still enforces on `Intl.NumberFormat`), defaults to 2, and falls back to 2 when it is not a finite number. A value that is not a finite number (`NaN`, `Infinity`, `-Infinity`) formats as an empty string, and so does a value that cannot be coerced to a number (a symbol, a plain object, a null-prototype object); `null`, arrays and booleans go through `Number()` as in 2.3.0. `options.symbol` prefixes the result with the `R$` currency symbol (default `false`). Options are typed as `FormatCurrencyOptions`. +Format a number or a numeric string in the BRL pattern (`1.234,56`). A `number` is formatted as is, sign and decimals preserved. + +- **Options** (`FormatCurrencyOptions`): `symbol` (default `false`) prefixes the result with `R$`; `precision` (default 2) sets the decimal places, clamped to 0 to 20. +- A `string` is read as `parseCurrency` reads it, except that a value without any separator stays in whole units: `'1234'` formats as `1.234,00`. +- Returns `''` for a non-finite value or one that cannot be coerced to a number. ```javascript import { formatCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1009,7 +1239,11 @@ formatCurrency(Number.NaN); // "" (non finite numbers format as an empty string) ### parseCurrency -Transforms a string to an integer or float format. The last `,` or `.` followed by 1 to 2 digits (or up to `precision` digits, when that is larger) is the decimal separator; every other `,` or `.` is a thousands separator. So `'R$ 1.234,56'` parses to `1234.56`, `'R$ 1.234'` to `1234`, `'1,5'` to `1.5` and `'12.34'` to `12.34`. A value written without any separator keeps the cents convention and is divided by `10 ** precision`, so `'1234'` parses to `12.34`. A `-` written before the first digit is preserved, so `'-R$ 1,00'` parses to `-1`. `precision` (default 2, clamped to `0..20`, and falling back to 2 when it is not a finite number) controls how many digits are treated as minor units. Options are typed as `ParseCurrencyOptions`. +Parse a BRL currency string into a number. + +- **Options** (`ParseCurrencyOptions`): `precision` (default 2) is the number of digits read as minor units, clamped to 0 to 20. +- The last `,` or `.` followed by 1 to 2 digits (up to `precision`, when larger) is the decimal separator; every other `,` or `.` is a thousands separator. +- A value without any separator is read as cents and divided by `10 ** precision`. ```javascript import { parseCurrency } from '@brazilian-utils/brazilian-utils'; @@ -1027,7 +1261,11 @@ parseCurrency(''); // 0 ### convertNumberToWords -Formats an integer as its Brazilian Portuguese cardinal number words ("por extenso"), e.g. `1235` becomes `"mil duzentos e trinta e cinco"`. Only integers from `-999999999999999` to `999999999999999` (999 trillion in absolute value) are supported; anything outside that range, `NaN` or a non-finite value returns `""`. A non-integer `value` is truncated toward zero before conversion. `options.gender` (part of `ConvertNumberToWordsOptions`) agrees "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies, defaulting to `"masculine"`. An invalid `gender` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. +Write an integer in Brazilian Portuguese cardinal words ("por extenso"): `1235` becomes `"mil duzentos e trinta e cinco"`. + +- **Options** (`ConvertNumberToWordsOptions`): `gender` (default `"masculine"`) agrees "um/dois" and the hundreds ("duzentos/duzentas") with the noun the number qualifies. +- Accepts integers from `-999999999999999` to `999999999999999` (999 trillion). A non-integer is truncated toward zero. +- Returns `""` for a value outside that range or not finite. ```javascript import { convertNumberToWords } from '@brazilian-utils/brazilian-utils'; @@ -1043,7 +1281,10 @@ convertNumberToWords(NaN); // "" ### convertCurrencyToWords -Formats a monetary amount in Brazilian Reais as its "por extenso" textual representation, the style used to write out the amount by hand on cheques and contracts, e.g. `1523.45` becomes `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. `value` is truncated (not rounded) to 2 decimal places. The singular noun is used for exactly 1 ("um real", "um centavo") and "de" is inserted before "reais" when the amount is a round million, billion or trillion of reais. An amount that truncates to nothing becomes `"zero reais"` with no "menos" prefix, any other negative amount is prefixed with "menos", and invalid input returns `""`. Above `Number.MAX_SAFE_INTEGER / 100` reais (about 90 trillion) a double cannot carry cents, so the amount is read as a whole number of reais. It takes no options: the result is always lowercase; apply any other casing to it yourself. +Write an amount in reais in words ("por extenso"), as on cheques and contracts: `1523.45` becomes `"mil quinhentos e vinte e três reais e quarenta e cinco centavos"`. Takes no options. + +- `value` is truncated (not rounded) to 2 decimal places. +- Returns `""` for invalid input or an amount above 999 trillion reais. ```javascript import { convertCurrencyToWords } from '@brazilian-utils/brazilian-utils'; @@ -1059,7 +1300,10 @@ convertCurrencyToWords(-0.001); // "zero reais" (truncates to nothing) ### convertDateToWords -Formats a date as its Brazilian Portuguese "por extenso" textual representation, e.g. `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date` (read by its local calendar date, the same convention used by `isHoliday`) or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. With the default `options.style` of `"full"`, day 1 is written as "primeiro" and every other day uses the cardinal number; with `"month"`, only the month name is spelled out and the day/year are left as digits (day 1 as `"1º"`, e.g. `"2 de março de 2024"`, `"1º de janeiro de 2024"`). Month names are lowercase. In `"full"` style the year is written out as a cardinal number without the thousands comma that `convertNumberToWords`/`convertCurrencyToWords` use (`1999` reads as `"mil novecentos e noventa e nove"`, not `"mil novecentos e noventa e nove"`), matching how a date is read aloud. `options.weekday` (default `false`) prefixes the pt-BR weekday name in lowercase followed by a comma (`"sábado, dois de março de dois mil e vinte e quatro"`), computed from the resolved calendar date. An invalid `style` value is ignored and the default is used. The result is always lowercase; apply any other casing to it yourself. February 29th is accepted on the leap years of the proleptic Gregorian calendar (divisible by 4, except centuries not divisible by 400). Returns `""` for an invalid `Date`, a malformed string, a day/month that does not exist, or a date before year 1. +Write a date in Brazilian Portuguese words ("por extenso"): `"01/01/2024"` becomes `"primeiro de janeiro de dois mil e vinte e quatro"`. Accepts a `Date`, read by its local calendar date, or a string in `"dd/mm/yyyy"` or ISO `"yyyy-mm-dd"` format. + +- **Options** (`ConvertDateToWordsOptions`): `style` (default `"full"`) spells out day, month and year; `"month"` spells out only the month and leaves day and year as digits, day 1 as `"1º"`. `weekday` (default `false`) prefixes the lowercase weekday name and a comma. +- Returns `""` for an invalid `Date`, a malformed string, a day or month that does not exist, or a date before year 1. ```javascript import { convertDateToWords } from '@brazilian-utils/brazilian-utils'; @@ -1080,7 +1324,10 @@ convertDateToWords('29/02/1900'); // "" (1900 is not a leap year) ### getStates -Get all Brazilian states, each with its two-letter code, name, region code, region name and 2-digit IBGE code of the Federative Unit (`cUF`). The list is sorted by name with `localeCompare` in the "pt-BR" locale, so accented names land where a Brazilian reader expects them: Pará, Paraíba, Paraná and Rio de Janeiro, Rio Grande do Norte, Rio Grande do Sul. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. Exports the `State`, `StateCode` and `StateName` types. `State` is a discriminated union with one member per state, so the fields of a state are tied to each other: narrowing a `State` by `code` narrows its `name`, `regionCode`, `regionName` and `ibgeCode` too (`Extract['name']` is `'São Paulo'`), and an impossible combination such as `{ code: 'SP', name: 'Acre' }` is not a `State`. +Get all Brazilian states, each with its two-letter code, name, region code, region name and 2-digit IBGE code (`cUF`). + +- Sorted by name in the "pt-BR" locale. +- Exports the `State`, `StateCode` and `StateName` types. `State` is a discriminated union: narrowing it by `code` also narrows the other fields. ```javascript import { getStates } from '@brazilian-utils/brazilian-utils'; @@ -1117,9 +1364,15 @@ getStates(); // ] ``` +Source: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) + ### getStateByIbgeCode -Get the Brazilian state whose 2-digit IBGE code ("cUF", the Código da Unidade da Federação) matches the given value. This is the same 2-digit UF code found in the first field of every DF-e access key (chave de acesso) issued for any of the models `isValidNfeKey` covers: NF-e (55), NFC-e (65), CT-e (57), MDF-e (58), CT-e OS (67), GTV-e (64), BP-e (63), NF3e (66) and NFCom (62). Accepts a string or a non-negative integer number, stripping any non-digit characters before matching. Exports the `State` type. +Get the Brazilian state whose 2-digit IBGE code (`cUF`, the Código da Unidade da Federação) matches the given value. + +- This is the UF code in the first field of a DF-e access key (chave de acesso), the one `isValidNfeKey` covers. +- Accepts a string or a non-negative integer. +- Returns `null` when the code matches no state. Exports the `State` type. ```javascript import { getStateByIbgeCode } from '@brazilian-utils/brazilian-utils'; @@ -1135,9 +1388,14 @@ getStateByIbgeCode(-35); // null getStateByIbgeCode(3.5); // null ``` +Source: [IBGE Localidades](https://servicodados.ibge.gov.br/api/v1/localidades/estados), [Manual de Orientação do Contribuinte](https://www.confaz.fazenda.gov.br/legislacao/arquivo-manuais/moc7-visao-geral.pdf) + ### getStateCodeByName -Get the two-letter code (sigla) of a Brazilian state given its full name. The match is accent-insensitive, case-insensitive and ignores leading/trailing whitespace, so `'sao paulo'`, `'SÃO PAULO'` and `' São Paulo '` all resolve to `'SP'`. Every run of internal whitespace collapses into a single space too, so `'Rio de Janeiro'` resolves to `'RJ'`, while a name written without the space matches nothing (`'saopaulo'` is not `'São Paulo'`). Exports the `StateCode` type. +Get the two-letter code (sigla) of a Brazilian state from its full name. + +- The match ignores accents, case and surrounding whitespace; internal whitespace collapses into one space. +- Returns `null` when no state matches. Exports the `StateCode` type. ```javascript import { getStateCodeByName } from '@brazilian-utils/brazilian-utils'; @@ -1150,7 +1408,10 @@ getStateCodeByName('Neverland'); // null ### getStateNameByCode -Get the full name of a Brazilian state given its two-letter code (sigla). The match is case-insensitive and ignores leading/trailing whitespace, so `'sp'`, `'SP'` and `' Sp '` all resolve to `'São Paulo'`. Exports the `StateName` type. +Get the full name of a Brazilian state from its two-letter code (sigla). + +- The match ignores case and surrounding whitespace. +- Returns `null` when no state matches. Exports the `StateName` type. ```javascript import { getStateNameByCode } from '@brazilian-utils/brazilian-utils'; @@ -1163,7 +1424,10 @@ getStateNameByCode('ZZ'); // null ### getTimezoneByState -Get the IANA time zone database name (tzdata zone) for a Brazilian state, chosen as the zone of the state capital. The match is case-insensitive and ignores leading/trailing whitespace. Some tzdata zones cover more than one state: `America/Sao_Paulo` also covers DF, GO, MG, ES, RJ, PR, SC and RS besides SP, and `America/Fortaleza` also covers MA, PI, RN and PB besides CE. Pernambuco resolves to `America/Recife`, not `America/Noronha`: Fernando de Noronha is an archipelago district of PE, not a state of its own. +Get the IANA time zone name (tzdata zone) of a Brazilian state: the zone of its capital. + +- The match ignores case and surrounding whitespace. +- Returns `null` when no state matches. ```javascript import { getTimezoneByState } from '@brazilian-utils/brazilian-utils'; @@ -1175,9 +1439,16 @@ getTimezoneByState('PE'); // 'America/Recife' getTimezoneByState('ZZ'); // null ``` +Source: [IANA Time Zone Database](https://www.iana.org/time-zones) + ### getMunicipalities -Get Brazilian municipalities published by the IBGE. Returns all municipalities if no state is provided, or municipalities from a specific state. Each municipality is returned as `{ code, name, stateCode }`, where `code` is the 7-digit IBGE municipality code. Results are sorted by name with `localeCompare` in the "pt-BR" locale. Each call returns a fresh array of fresh objects, so mutating the result never affects subsequent calls. An unknown state code returns an empty array instead of throwing. Only an omitted (or `undefined`) `stateCode` asks for the full list: `getMunicipalities(null)` and `getMunicipalities('')` return `[]`, where the looser `getCities(null)` and `getCities('')` return every city. The state code is matched exactly, case included: `getMunicipalities('sp')` returns `[]` where `getMunicipalities('SP')` returns the 645 São Paulo municipalities. `getMunicipalities` and `getCities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. +Get the Brazilian municipalities published by the IBGE: every municipality, or only those of one state when `stateCode` is given. + +- Each municipality (`Municipality`) is `{ code, name, stateCode }`, where `code` is the 7-digit IBGE code. Sorted by name in the "pt-BR" locale. +- Only an omitted (or `undefined`) `stateCode` asks for the full list: `null` and `''` return `[]`. +- `stateCode` is case-sensitive: `'sp'`, like an unknown code, returns `[]`. +- Embeds all 5571 municipalities. See [Bundle size](getting-started.md#bundle-size) to lazy-load it via `@brazilian-utils/brazilian-utils/get-municipalities`. ```javascript import { getMunicipalities } from '@brazilian-utils/brazilian-utils'; @@ -1207,11 +1478,14 @@ getMunicipalities('SP'); getMunicipalities('ZZ'); // [] ``` -`getMunicipalities` embeds all 5571 IBGE municipalities and their codes, so it carries the same bundle-size cost as `getCities`. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-municipalities` instead of the root import. +Source: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) ### getMunicipalityByCode -Look up a Brazilian municipality by its 7-digit IBGE code. Accepts the code as a string or a number, with any non-digit characters stripped before matching; a code given as a number must be a non-negative integer, so `-3550308` and `355030.8` return `null` instead of being read as `3550308`. Returns `{ code, name, stateCode }`, a fresh object, or `null` when the code is not 7 digits long or does not match any known municipality. +Look up a Brazilian municipality by its 7-digit IBGE code. + +- Accepts the code as a string or a non-negative integer. +- Returns `{ code, name, stateCode }` (`Municipality`), or `null` when the code is not 7 digits long or matches no municipality. ```javascript import { getMunicipalityByCode } from '@brazilian-utils/brazilian-utils'; @@ -1226,9 +1500,16 @@ getMunicipalityByCode('0000000'); // null (unknown code) getMunicipalityByCode('123'); // null (not 7 digits) ``` +Source: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) + ### getCities -Get Brazilian cities. **Deprecated:** use `getMunicipalities` instead. Returns all cities if no state is provided, or cities from a specific state. Each call returns a fresh array, so mutating the result never affects subsequent calls. An unknown state code (or a non-`StateCode` value) returns an empty array instead of throwing, except for a falsy one: `getCities(null)` and `getCities('')` are read as "no state given" and return every city, where the stricter `getMunicipalities` returns `[]` for them. The state code is matched exactly, case included: `getCities('sp')` returns `[]` where `getCities('SP')` returns the 645 São Paulo cities. `getCities` and `getMunicipalities` are the only state-taking lookups that are case-sensitive; `getStateNameByCode`, `getTimezoneByState`, `getAreaCodesByState` and `getMunicipality` all fold case. +Get the names of Brazilian cities: every city, or only those of one state. **Deprecated:** use `getMunicipalities` instead. + +- Sorted in the "pt-BR" locale. +- Any falsy `state` asks for the full list, where `getMunicipalities` returns `[]`. +- `state` is case-sensitive: `'sp'`, like an unknown code, returns `[]`. +- Embeds all 5571 names (~154.2 KB minified, ~49.8 KB gzipped). See [Bundle size](getting-started.md#bundle-size) to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities`. ```javascript import { getCities } from '@brazilian-utils/brazilian-utils'; @@ -1266,11 +1547,16 @@ getCities('SP'); // ] ``` -`getCities` embeds all 5571 IBGE municipality names (~154.2 KB minified, ~49.8 KB gzipped) and is one of the few heavy exceptions in this otherwise tree-shakeable package. See [Bundle size](getting-started.md#bundle-size) for how to lazy-load it via `@brazilian-utils/brazilian-utils/get-cities` instead of the root import. +Source: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) ### getMunicipality -Get municipality information by IBGE code, or get an IBGE code from municipality name and UF. **Deprecated:** use `getMunicipalityByCode` instead, which is synchronous and offline; matching a municipality by name is up to the application, over `getMunicipalities`. A single function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. `code` accepts both `string` and `number` input and must be exactly 7 digits, otherwise the function resolves to `null`. A `code` given as a number must be a non-negative integer: a sign and a decimal point are not digits, so `-3550308` and `355030.8` resolve to `null` instead of being read as `3550308`. Resolution is entirely offline, from a bundled IBGE dataset: no network request is made. The municipality name match ignores accents and casing, and every run of whitespace collapses into a single space, so `'sao paulo'` matches `'São Paulo'` while a name written without the space does not; the casing is folded to upper case, the direction Unicode expands `'ß'` to `'SS'` in, so `'Paßos'` matches `'Passos'`. An unknown municipality, an unknown UF or invalid input all resolve to `null`. The `[name, uf]` pair is a fresh array on every call, so mutating the result never affects subsequent lookups. +Get municipality information by IBGE code, or an IBGE code from a municipality name and UF. **Deprecated:** use `getMunicipalityByCode` instead, which is synchronous and offline; matching a municipality by name is up to the application, over `getMunicipalities`. + +- One function handles both directions, based on whether `options` has a `code` or a `municipalityName`/`uf`. The lookup is offline: no network request is made. +- The name match ignores accents and case, and every run of whitespace collapses into one space. +- Resolves to `null` for an unknown municipality, an unknown UF or invalid input. +- `GetMunicipalityOptions`, `GetMunicipalityByCodeOptions` and `GetMunicipalityByNameOptions` are deprecated aliases of the types below. ```javascript import { getMunicipality } from '@brazilian-utils/brazilian-utils'; @@ -1291,8 +1577,6 @@ await getMunicipality({ code: '123' }); // null (not 7 digits) ``` -In TypeScript the return type follows the direction of the lookup: a `{ code }` query resolves to `[string, string] | null`, a `{ municipalityName, uf }` query resolves to `string | null`, and a query whose direction is only known at run time (a variable typed as `GetMunicipalityParams`) resolves to the union of both. The 2.3.0 names `GetMunicipalityOptions`, `GetMunicipalityByCodeOptions` and `GetMunicipalityByNameOptions` are still exported as deprecated aliases of these. - ```typescript import { getMunicipality, @@ -1314,22 +1598,19 @@ const lookUp = (options: GetMunicipalityParams) => getMunicipality(options); // (options: GetMunicipalityParams) => Promise<[string, string] | string | null> ``` +Source: [IBGE Localidades](https://servicodados.ibge.gov.br/api/docs/localidades) + ## Holidays and business days ### getHolidays -Get Brazilian holidays for a given year. Returns national holidays and optionally state-specific holidays. Each holiday (typed as `Holiday`) has a `type` field (`HolidayType`: `"national"`, `"state"`, `"optional"` or `"religious"`). "Dia da Consciência Negra" (Nov 20) is a national holiday from 2024 onward (Lei nº 14.759/2023). Before that, several states still carry a state-level entry of their own on the same date, under the same `"Dia da Consciência Negra"` name in MT, RJ, AM and SP, and under `"Dia Estadual da Consciência Negra"` in AP, the name that state's own law uses. Commemorative dates that no law turns into a holiday are not listed: RN's "Dia do Rio Grande do Norte" (7 August, Lei RN nº 7.831/2000) is one, and neither is RO's "Dia dos Evangélicos" (18 June), whose law the STF struck down in ADI 3940. Results are memoized per `year`/`stateCode`, but each call still returns a fresh copy. An unknown/invalid `stateCode` is ignored, returning national holidays only; the lookup reads own properties only, so `"__proto__"`, `"constructor"` and the like are unknown state codes rather than a crash. Only the years 1900 through 2099 are supported, the range the business day utilities inherit; a year outside it returns `[]`. +Get the Brazilian holidays of a year: the national ones and, with a `stateCode`, that state's holidays too. Accepts a year or `{ year, stateCode }` (`GetHolidaysParams`). -Only one state holiday per UF is a feriado civil under [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm), art. 1º, II, which authorises "a data magna do Estado fixada em lei estadual" in the singular; the other entries rest on ordinary state laws and are reported because they are observed in practice. Notable per-state rules: - -- **SC** — [Lei SC nº 18.531/2022](http://leis.alesc.sc.gov.br/html/2022/18531_2022_lei.html) moves both state holidays, "Dia do Estado de Santa Catarina" (Aug 11) and "Dia de Santa Catarina de Alexandria" (Nov 25), to the following Sunday whenever they fall Monday to Friday, so Monday Aug 11 2025 is a business day in SC and the holiday lands on Sunday Aug 17. The two dates did not start transferring together. Aug 11 transfers from 2005 on, the year [Lei SC nº 13.408/2005](http://leis.alesc.sc.gov.br/html/2005/13408_2005_lei.html) extended the clause to it (published and in force on Jul 15 2005), and stays on Aug 11 before that. Nov 25 transfers from 1999 on, the year [Lei SC nº 11.213/1999](http://leis.alesc.sc.gov.br/html/1999/11213_1999_lei.html) first introduced the clause (published and in force on Nov 12 1999, thirteen days before that year's Nov 25), with a one-year gap: art. 3º of [Lei SC nº 12.906/2004](http://leis.alesc.sc.gov.br/html/2004/12906_2004_lei.html) revoked that law without restating the clause, so Nov 25 2004 alone stays on the statutory date until Lei SC nº 13.408/2005 reinstated the transfer. So Nov 25 1999 (a Thursday) lands on Sunday Nov 28, Nov 25 2002 (a Monday) on Sunday Dec 1, Nov 25 2004 (a Thursday) stays put, and Nov 25 2005 (a Friday) lands on Sunday Nov 27. -- **DF** — [Lei distrital nº 72/1989](https://www.sinj.df.gov.br/sinj/Norma/18459/Lei_72_27_12_1989.html), art. 1º parágrafo único, declares Corpus Christi a feriado. With `stateCode: 'DF'` the single Corpus Christi entry comes back typed `"state"` instead of `"optional"`; it is replaced, not duplicated. -- **GO** — [Lei GO nº 20.756/2020](https://legisla.casacivil.go.gov.br/pesquisa_legislacao/100979/lei-20756), art. 269, II, lists three feriados estaduais: Jul 26 (Fundação da Cidade de Goiás), Oct 24 (Lançamento da Pedra Fundamental de Goiânia) and Oct 28 (Dia do Servidor Público). -- **AL** — Sep 16 is a feriado estadual from 2024 ([Lei AL nº 9.358/2024](https://sapl.al.al.leg.br/norma/3117)) and only a ponto facultativo (`"optional"`) before that. -- **PB** — Jul 26 ("Morte de João Pessoa") is emitted up to 2015 only: [Lei PB nº 10.601/2015](https://sapl.al.pb.leg.br/norma/11988), art. 2º, revoked its basis. -- **TO** — Mar 18 ("Autonomia do Estado do Tocantins") is emitted up to 2008 only: [Lei TO nº 2.013/2009](https://www.al.to.leg.br/arquivo/15724) rewrote the clause that declared the feriado into a commemorative provision. - -The statutory date is what is returned. SC's shift above is the only observance shift modelled; Acre's Tuesday-to-Thursday shift and the Goiás decrees that may move Jul 26 and Oct 28 are not. +- Each holiday is a `Holiday` whose `type` (`HolidayType`) is `"national"`, `"state"`, `"optional"` or `"religious"`. Holidays are sorted by date. +- "Dia da Consciência Negra", Nov 20, is national from 2024 on. +- Per-state rules (SC's Sunday shift, DF's Corpus Christi, dates that stopped being holidays) follow each state's law; see the source for the list. +- An unknown or non-string `stateCode` is ignored and only national holidays are returned. +- Returns `[]` when the year is not an integer from 1900 to 2099, or when the argument is neither a number nor an object. ```javascript import { getHolidays } from '@brazilian-utils/brazilian-utils'; @@ -1350,9 +1631,15 @@ getHolidays({ year: 2024, stateCode: 'SP' }); // Includes national holidays plus state-specific holidays (e.g., "Revolução Constitucionalista") ``` +Source: `src/get-holidays/constants.ts`, [Lei nº 662/1949](https://www.planalto.gov.br/ccivil_03/leis/l0662.htm), [Lei nº 9.093/1995](https://www.planalto.gov.br/ccivil_03/leis/l9093.htm). + ### isHoliday -Check if a specific date is a Brazilian holiday. The check compares `targetDate`'s local calendar date (year/month/day as read locally), not its underlying UTC instant. Returns `false` when `targetDate` is missing or not a valid `Date`. An invalid `stateCode` is treated in two different ways: a string that is not a known state code is ignored and only national holidays are considered, the same as `getHolidays`, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for a national holiday. +Check if a date is a Brazilian holiday. Accepts `{ targetDate, stateCode? }` (`IsHolidayParams`). + +- The check uses `targetDate`'s local calendar date, not its UTC instant. +- `stateCode` also considers that state's holidays. An unknown code is ignored, as in `getHolidays`. +- Returns `false` when `targetDate` is missing or not a valid `Date`, or when `stateCode` is present and not a string. ```javascript import { isHoliday } from '@brazilian-utils/brazilian-utils'; @@ -1364,7 +1651,10 @@ isHoliday(); // false ### isBusinessDay -Check if a date is a Brazilian business day (dia útil). Returns `false` for Saturdays, Sundays, and Brazilian holidays returned by `getHolidays` for `value`'s local calendar day (year/month/day as read locally), the same convention used by `isHoliday`. `options.includeOptional` (part of `BusinessDayOptions`, the option type every business day utility shares) defaults to `true`, so optional-type holidays (`Holiday.type === "optional"`, i.e. Carnaval and Corpus Christi) also count as non-business days; pass `false` to only treat statutory holidays this way. `options.stateCode` also considers that state's holidays; a string that is not a known state code is ignored, falling back to national holidays only, while a `stateCode` that is present and is not a string at all (a number, `null`, an object) is rejected and makes the call return `false` even for an ordinary weekday, the same split `isHoliday` makes and the value `addBusinessDays`, `subBusinessDays` and `differenceInBusinessDays` reject with `null`. A `value` that is not a valid `Date` returns `false`. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `false`. +Check if a date is a Brazilian business day (dia útil): not a Saturday, a Sunday or a holiday `getHolidays` lists for its local calendar day. + +- **Options** (`BusinessDayOptions`, shared by every business day util): `includeOptional` (default `true`) also counts the `"optional"` holidays, Carnaval and Corpus Christi, as non-business days; `stateCode` also counts that state's holidays. +- Returns `false` when `value` is not a valid `Date` or its year is outside 1900 to 2099, or when `stateCode` is present and not a string. ```javascript import { isBusinessDay } from '@brazilian-utils/brazilian-utils'; @@ -1381,7 +1671,12 @@ isBusinessDay(new Date('not a date')); // false ### addBusinessDays -Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and Brazilian holidays exactly as `isBusinessDay` defines them (same `BusinessDayOptions`: `options.includeOptional`, default `true`, and `options.stateCode` work exactly as they do there). The signature is date-fns': `addBusinessDays(date, amount, options?)`. Returns a new `Date`; the input `date` is never mutated, and its time-of-day is preserved in the result. An `amount` of `0` returns a new `Date` equal to `date`, unchanged, even when `date` itself falls on a weekend or holiday, this mirrors the verified behavior of [date-fns' `addBusinessDays(date, 0)`](https://date-fns.org/docs/addBusinessDays), which also does not roll the input to the next business day. A negative `amount` walks backwards, one business day at a time, also like date-fns. Returns `null` on bad input: a `date` that is not a valid `Date`, an `amount` that is not a finite integer, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored, exactly as `isBusinessDay` ignores it. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it, or a walk that leaves it, returns `null`. +Add a number of Brazilian business days (dias úteis) to a date, skipping Saturdays, Sundays and the holidays `isBusinessDay` skips. Signature: `addBusinessDays(date, amount, options?)`, the same as date-fns. + +- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `stateCode` also skips that state's holidays. +- Returns a new `Date`, time of day preserved; `date` is never mutated. +- An `amount` of `0` returns the same date, even on a weekend or holiday. A negative `amount` walks backwards. +- Returns `null` when `date` is invalid, `amount` is not a finite integer, `stateCode` is not a string, or the result leaves the years 1900 to 2099. ```javascript import { addBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1397,7 +1692,9 @@ addBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ### subBusinessDays -Subtract a number of Brazilian business days (dias úteis) from a date: `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`, which is exactly how it is implemented, so every detail above (the preserved time-of-day, the untouched input, an `amount` of `0` returning the date unchanged, the 1900-2099 range and the `null` cases) holds here too, `options.stateCode` and `options.includeOptional` included. A negative `amount` walks forwards. +Subtract a number of Brazilian business days (dias úteis) from a date. `subBusinessDays(date, amount, options?)` is `addBusinessDays(date, -amount, options)`. + +- Same rules as `addBusinessDays`, `BusinessDayOptions` included. A negative `amount` walks forwards. ```javascript import { subBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1414,7 +1711,12 @@ subBusinessDays(new Date(2024, 0, 2), 1.5); // null (not an integer) ### differenceInBusinessDays -Count the number of Brazilian business days (dias úteis) between two dates, mirroring the semantics of [date-fns' `differenceInBusinessDays`](https://date-fns.org/docs/differenceInBusinessDays) (verified against its source), argument order included: `differenceInBusinessDays(laterDate, earlierDate, options?)`. The walk starts at `earlierDate` and stops just before `laterDate`, so `earlierDate` is counted when it is itself a business day, `laterDate` is never counted, and every business day strictly in between is counted once. Only the calendar day of each `Date` matters, the time of day is ignored. Business days are determined exactly like `isBusinessDay` (same `BusinessDayOptions`), `options.includeOptional` (default `true`) and `options.stateCode` included. The result is positive when `laterDate` is after `earlierDate` and negative when it is before it; two dates on the same calendar day return `0`. Returns `null` on bad input: a date that is not a valid `Date`, or a `stateCode` that is not a string; an `options` that is not an object at all is ignored. Only years from 1900 through 2099 are supported, the range `getHolidays` computes; a date outside it returns `null`. +Count the Brazilian business days (dias úteis) between two dates. Signature: `differenceInBusinessDays(laterDate, earlierDate, options?)`, the same as date-fns. + +- **Options** (`BusinessDayOptions`, shared with `isBusinessDay`): `includeOptional` (default `true`) also skips Carnaval and Corpus Christi; `stateCode` also skips that state's holidays. +- Counts `earlierDate` when it is a business day and every business day strictly between the two dates; `laterDate` is never counted. The time of day is ignored. +- The result is negative when `laterDate` is before `earlierDate`, and `0` on the same calendar day. +- Returns `null` when either date is not a valid `Date` or is outside the years 1900 to 2099, or `stateCode` is not a string. ```javascript import { differenceInBusinessDays } from '@brazilian-utils/brazilian-utils'; @@ -1431,7 +1733,9 @@ differenceInBusinessDays(new Date(), new Date('not a date')); // null ### isValidPassport -Check if a Brazilian passport number is valid (2 letters followed by 6 digits). Accepts both `string` and `number` input; the input is case-insensitive and any non-alphanumeric characters (spaces, dots, hyphens) are ignored. A number is accepted for symmetry with `formatPassport`/`parsePassport` but is never valid: the decimal form of a number never starts with the two letters a passport number needs. +Check if a Brazilian passport number is valid: 2 letters followed by 6 digits. + +- There is no check digit, so a well-formed number is not necessarily a real passport. ```javascript import { isValidPassport } from '@brazilian-utils/brazilian-utils'; @@ -1442,9 +1746,11 @@ isValidPassport('AB-123.456'); // true (symbols are ignored) isValidPassport('12345678'); // false ``` +Source: [Polícia Federal](https://www.gov.br/pf/pt-br/assuntos/passaporte) and its [FAQ](https://www.gov.br/pf/pt-br/assuntos/passaporte/ajuda/duvidas_/caderneta/caderneta-numero-onde-fica-e). + ### formatPassport -Format a Brazilian passport number (uppercase, without symbols, capped to 8 characters). A non-string input returns an empty string. +Format a Brazilian passport number: uppercase, without symbols, capped to 8 characters. It is the same operation as `parsePassport`, of which it is an alias. ```javascript import { formatPassport } from '@brazilian-utils/brazilian-utils'; @@ -1455,7 +1761,7 @@ formatPassport('AB-123.456'); // 'AB123456' ### parsePassport -Remove all non-alphanumeric characters from a passport number, uppercase the result, and cap it to 8 characters. A non-string input returns an empty string. +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'; @@ -1466,7 +1772,7 @@ parsePassport(' AB 123 456 '); // 'AB123456' ### generatePassport -Generate a random valid Brazilian passport number. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a random valid Brazilian passport number. ```javascript import { generatePassport } from '@brazilian-utils/brazilian-utils'; @@ -1478,7 +1784,10 @@ generatePassport(); // 'RY393097' ### isValidCnh -Check if CNH is valid. Spaces, dots and hyphens around/between the digits are ignored, but any other character, a letter in particular, makes the value invalid. A value whose 11 digits are all the same is rejected before the check digits are computed, so `'11111111111'` is invalid. +Check if a CNH is valid. Spaces, dots and hyphens are ignored; any other character makes the value invalid. + +- A value whose 11 digits are all the same is rejected, so `'11111111111'` is invalid. +- The first check digit keeps a remainder of 1 as `1`, as real registry numbers do. Resolução CONTRAN nº 886/2021 says `0`. ```javascript import { isValidCnh } from '@brazilian-utils/brazilian-utils'; @@ -1488,9 +1797,13 @@ isValidCnh('000000001-19'); // true (hyphen before the check digits) isValidCnh('ab00000000119'); // false (letters are rejected) ``` +Source: [Resolução CONTRAN nº 886/2021, art. 4º](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/Resolucao8862021F.pdf); weights per [siga0984](https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-cnh/). + ### formatCnh -Format CNH. `options.pad` (part of `FormatCnhOptions`) left-pads the value with zeros to the full 11 digits before masking (default `false`). +Format a CNH. + +- **Options** (`FormatCnhOptions`): `pad` left-pads the value with zeros to the full 11 digits before masking (default `false`). ```javascript import { formatCnh } from '@brazilian-utils/brazilian-utils'; @@ -1501,7 +1814,7 @@ formatCnh('2650306461', { pad: true }); // 026503064-61 ### parseCnh -Remove CNH formatting, keep only digits, and cap the result to 11 digits. Returns `''` when there is no digit at all. +Remove CNH formatting, keep only digits, and cap the result to 11 digits. ```javascript import { parseCnh } from '@brazilian-utils/brazilian-utils'; @@ -1511,7 +1824,7 @@ parseCnh('026503064-61'); // '02650306461' ### generateCnh -Generate a valid random CNH. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random CNH. ```javascript import { generateCnh } from '@brazilian-utils/brazilian-utils'; @@ -1523,7 +1836,9 @@ generateCnh(); // '02650306461' ### isValidLegalNature -Check if a legal nature code exists in the official list. The table follows IBGE/CONCLA's "Natureza Jurídica 2021": the 92 codes in force plus the 8 a past revision of the table retired, kept because they still appear in records filed while they were in force. Use `getLegalNature` to tell the two apart: a retired code comes back with `legacy: true` and the `currentCode` it corresponds to today. Only the usual mask characters (hyphens, dots, whitespace) are tolerated around the 4 digits, so `'2062a'` is rejected instead of being read as `'2062'`. +Check if a legal nature code exists in the official list, the IBGE/CONCLA "Natureza Jurídica 2021" table. Only hyphens, dots and whitespace are tolerated around the 4 digits. + +- The 92 codes in force are accepted, plus the 8 a past revision retired. `getLegalNature` tells them apart (`legacy: true`). ```javascript import { isValidLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1533,9 +1848,13 @@ isValidLegalNature('2208'); // true (retired by a past revision, still accepted) isValidLegalNature('9999'); // false ``` +Source: [CONCLA, Natureza Jurídica 2021](https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021) and its [detailed structure PDF](https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-EstruturaDetalhada.pdf). + ### formatLegalNature -Format a legal nature code. `options.pad` (part of `FormatLegalNatureOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes; with `true` the value is first left padded with zeros to the 4 digits of a complete code. Use `isValidLegalNature` to check a code. +Format a legal nature code. Use `isValidLegalNature` to check a code. + +- **Options** (`FormatLegalNatureOptions`): `pad` first left-pads the value with zeros to the 4 digits of a complete code (default `false`). ```javascript import { formatLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1558,7 +1877,7 @@ parseLegalNature('206-2'); // '2062' ### generateLegalNature -Generate a random valid legal nature code. Only the 92 codes in force are drawn, never one of the 8 a past revision retired. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a random valid legal nature code. Only the 92 codes in force are drawn, never a retired one. ```javascript import { generateLegalNature } from '@brazilian-utils/brazilian-utils'; @@ -1568,9 +1887,10 @@ generateLegalNature(); // '2062' ### getLegalNature -Look a legal nature code up in the official IBGE/CONCLA table. The entry also carries the CONCLA category the code is listed under, taken from its first digit. No legal nature code starts with a zero, that first digit is the category (1 to 5), so nothing is ever padded here: a number and the string of the same digits are read identically. +Look a legal nature code up in the official IBGE/CONCLA table. Returns `null` for an unknown code. -A code a past revision of the table retired is still looked up, because it keeps appearing in records filed while it was in force, and comes back with `legacy: true` and the `currentCode` it corresponds to today, per the CONCLA correspondence spreadsheets. The 92 codes in force have `legacy: false` and no `currentCode`. +- The entry (`LegalNature`) also carries the CONCLA category of the code, given by its first digit. +- A code a past revision retired comes back with `legacy: true` and the `currentCode` it corresponds to today, or `currentCode: null` when there is no successor. Codes in force have `legacy: false` and no `currentCode`. | Retired code | Description | Corresponds to | | --- | --- | --- | @@ -1607,9 +1927,13 @@ getLegalNature(206.2)?.category.description; // 'Entidades Empresariais' getLegalNature('0000'); // null ``` +Source: [CONCLA, Natureza Jurídica 2021](https://concla.ibge.gov.br/estrutura/natjur-estrutura/natureza-juridica-2021). + ### getLegalNatures -Get the legal nature map keyed by code. Only the 92 codes of the CONCLA 2021 table, the ones in force, are listed by default; pass `{ includeLegacy: true }` (`GetLegalNaturesParams`) to add the 8 a past revision of the table retired. +Get the legal nature map keyed by code. Only the 92 codes in force are listed by default. + +- **Options** (`GetLegalNaturesParams`): `includeLegacy` (default `false`) adds the 8 retired codes. ```javascript import { getLegalNatures } from '@brazilian-utils/brazilian-utils'; @@ -1624,7 +1948,11 @@ getLegalNatures({ includeLegacy: true })['2208']; // 'Entidade Binacional Itaipu ### getLegalNaturesByCategory -Get every legal nature of a CONCLA category, the group given by the first digit of the code: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas and `5` Organizações Internacionais e Outras Instituições Extraterritoriais. The category is accepted as a string or as a number, the entries come back sorted by code, and an unknown category gives `[]`. Only the codes in force are listed by default; pass `{ includeLegacy: true }` (`GetLegalNaturesByCategoryOptions`) to add the retired codes of the category, in code order. +Get every legal nature of a CONCLA category, the group given by the first digit of the code. The category is accepted as a string or as a number. + +- Categories: `1` Administração Pública, `2` Entidades Empresariais, `3` Entidades sem Fins Lucrativos, `4` Pessoas Físicas and `5` Organizações Internacionais e Outras Instituições Extraterritoriais. +- **Options** (`GetLegalNaturesByCategoryOptions`): `includeLegacy` (default `false`) adds the retired codes of the category. +- The entries come back sorted by code. An unknown category returns `[]`. ```javascript import { getLegalNaturesByCategory } from '@brazilian-utils/brazilian-utils'; @@ -1642,11 +1970,14 @@ getLegalNaturesByCategory('2', { includeLegacy: true }).length; // 33 getLegalNaturesByCategory('9'); // [] ``` -## Voter ID +## Voter ID (título de eleitor) ### isValidVoterId -Check if a voter ID number is valid. Accepts both the standard 12-digit id and the 13-digit id issued by São Paulo (UF `01`) and Minas Gerais (UF `02`). Whitespace and dots are accepted around and between the `0000 0000 00 00` groups, but any other character, a letter in particular, makes the value invalid. +Check if a voter ID number is valid. Accepts the standard 12-digit id and the 13-digit id issued by São Paulo (UF `01`) and Minas Gerais (UF `02`). + +- A voter ID is an 8-digit sequential number, a 2-digit federative union code (`01` to `28`) and 2 check digits. +- Whitespace and dots are accepted around and between the groups. Any other character, a hyphen included, makes the value invalid. ```javascript import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1654,11 +1985,19 @@ import { generateVoterId, isValidVoterId } from '@brazilian-utils/brazilian-util const voterId = generateVoterId('SP'); isValidVoterId(voterId); // true +isValidVoterId('102385010671'); // true (12 digits) +isValidVoterId('1234567880191'); // true (13 digits, São Paulo) +isValidVoterId('123456780124'); // false (invalid check digits) ``` +Source: [Resolução TSE nº 23.659/2021, art. 36](https://www.tse.jus.br/legislacao/compilada/res/2021/resolucao-no-23-659-de-26-de-outubro-de-2021), [brutils](https://github.com/brazilian-utils/python/blob/main/brutils/voter_id.py) and [siga0984](https://siga0984.wordpress.com/2019/05/01/algoritmos-validacao-de-titulo-de-eleitor/). + ### formatVoterId -Format a voter ID number. Uses the 12-digit grouping `0000 0000 00 00` by default; the 13-digit grouping `0000 0000 0 00 00` is used only when the sanitized value has more than 12 digits **and** its federative union code (the 10th and 11th digits) is `01` (São Paulo) or `02` (Minas Gerais), the two states whose voter ids may carry a 9-digit sequential number. +Format a voter ID number with the 12-digit grouping `0000 0000 00 00`. + +- The 13-digit grouping `0000 0000 0 00 00` is used only when the value has more than 12 digits and its UF code (the 10th and 11th digits) is `01` or `02`. +- Digits past the last slot of the pattern are dropped. ```javascript import { formatVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1680,7 +2019,10 @@ parseVoterId('1234 5678 8 01 91'); // '1234567880191' (13-digit SP/MG voter id) ### generateVoterId -Generate a valid random voter ID number. You can optionally provide a state code; an unknown state code falls back to `"ZZ"` (issued abroad) instead of throwing. Uses `Math.random()` internally, so it is not cryptographically secure. +Generate a valid random voter ID number. The optional `state` argument (`StateCode`, or `"ZZ"` for a voter ID issued abroad) sets the federative union code. + +- An unknown state, or a value that is not a string, falls back to `"ZZ"` (UF `28`). +- The result always has 12 digits, never the 13-digit São Paulo or Minas Gerais form. ```javascript import { generateVoterId } from '@brazilian-utils/brazilian-utils'; @@ -1694,23 +2036,30 @@ generateVoterId('XX'); // falls back to "ZZ" instead of throwing ### isValidCns -Check if a CNS (Cartão Nacional de Saúde) number is valid, the unique SUS (Sistema Único de Saúde) user identifier. Definitive cards (starting with 1 or 2) are validated over an embedded 11 digit PIS/PASEP/NIS derived base weighted 15 down to 5; when the raw digit computes to 10, DATASUS raises the weighted sum by 2, recomputes the digit and marks the card with the suffix `001` instead of `000`. Provisional cards (starting with 7, 8 or 9) are validated instead by a single weighted sum (weights 15 down to 1) that must be a multiple of 11. The value has to be written as the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace, `.`, `-` or `/`, the interchangeable mask characters `isValidCpf` and `isValidCnpj` accept, a run of them between two groups included; letters among the digits, or a separator inside a group, are rejected instead of being read past. +Check if a CNS (Cartão Nacional de Saúde) number is valid, the SUS (Sistema Único de Saúde) identifier of a user, health professional or health facility. The value must be the 15 digits, optionally split into the printed groups of 3-4-4-4 by whitespace, `.`, `-` or `/`. -The two routines come from the [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/), which sits behind a bot filter and answers HTTP 403 to non-browser clients. The [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html) documents the same algorithm and is reachable without a browser, but applies the provisional routine to numbers starting with 5, 7, 8 or 9; this implementation follows ANVISA and rejects a 5-prefixed number even when its weighted sum checks out. +- Definitive cards start with 1 or 2, provisional ones with 7, 8 or 9; each has its own modulus 11 rule. +- A number starting with 5 is rejected, following ANVISA. ```javascript import { isValidCns } from '@brazilian-utils/brazilian-utils'; isValidCns('123456789010000'); // true (definitive) +isValidCns('100000000060018'); // true (definitive, raw check digit 10, suffix 001) isValidCns('700000000000005'); // true (provisional) isValidCns('123.4567-8901/0000'); // true (any of the mask characters) +isValidCns('123456789010001'); // false (wrong check digit) isValidCns('12345678901'); // false (wrong length) isValidCns('abc123456789010000'); // false (not written as a CNS) ``` +Source: [ANVISA CNS validation page](https://rni-docs.anvisa.gov.br/docs/regras_gerais/validacoes/validacaoCNS/) and the [e-SUS APS page](https://integracao.esusab.ufsc.br/ledi/documentacao/regras/algoritmo_CNS.html). + ### formatCns -Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. `options.pad` (part of `FormatCnsOptions`) left-pads the value with zeros up to the 15 slots of the pattern before masking (default `false`). +Format a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 digits separated by spaces. + +- **Options** (`FormatCnsOptions`): `pad` left-pads the value with zeros up to the 15 slots of the pattern before masking (default `false`). ```javascript import { formatCns } from '@brazilian-utils/brazilian-utils'; @@ -1722,7 +2071,7 @@ formatCns('89010001', { pad: true }); // '000 0000 8901 0001' ### parseCns -Remove CNS (Cartão Nacional de Saúde) formatting, keep only digits, and cap the result to 15 digits. A partial value passes through as far as it goes, so it can also strip the mask off an input still being typed; use `isValidCns` to check the number itself. +Remove CNS (Cartão Nacional de Saúde) formatting, keep only digits, and cap the result to 15 digits. ```javascript import { parseCns } from '@brazilian-utils/brazilian-utils'; @@ -1730,13 +2079,29 @@ import { parseCns } from '@brazilian-utils/brazilian-utils'; parseCns('123 4567 8901 0000'); // '123456789010000' ``` -## Certidão +## Certidão (civil registry certificate) ### isValidCertidao -Check if the matrícula of a certidão de registro civil (nascimento, casamento, óbito and the other acts kept by a serventia de registro civil das pessoas naturais) is valid. The matrícula has 32 digits laid out as 6 (CNS da serventia) + 2 (acervo) + 2 (serviço) + 4 (ano) + 1 (tipo do livro) + 5 (livro) + 3 (folha) + 7 (termo) + 2 (dígitos verificadores), and both check digits are modulus 11 with the weights cycling from 2 to 10 and back through 0: the first pass starts at 2 over the 30 base digits, the second at 1 over the 31 digits that include the first check digit, and in both a remainder of 10 is read as 1. Accepts the usual mask characters and whitespace between/around groups. The layout is the one [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) (Provimento CNJ nº 149/2023) currently publishes, with inciso II and §§ 1º and 3º to 5º in the redação of the Provimento CN nº 237/2026 and the rest of the article, § 2º included, in that of the Provimento CN nº 182/2024; the matrícula itself was instituted by the now revoked [Provimento CNJ nº 2/2009](https://atos.cnj.jus.br/atos/detalhar/1311) and got its digit structure from the also revoked [Provimento CNJ nº 3/2009, art. 7º](https://atos.cnj.jus.br/atos/detalhar/1310). The check digits are detailed by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented by [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) and [validator-docs](https://github.com/geekcom/validator-docs/blob/master/src/validator-docs/Rules/Certidao.php). +Check if the matrícula of a certidão de registro civil (birth, marriage, death and the other acts of a registro civil das pessoas naturais) is valid. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. -The serviço digits are fixed at `55`, the code [art. 473, III](https://atos.cnj.jus.br/atos/detalhar/5243) assigns to the registro civil das pessoas naturais, so a matrícula carrying any other pair in the ninth and tenth positions is rejected however good its check digits are. The book-type digit always has to name one of the nine book types (the same `CertidaoType` returned by `getCertidaoInfo`), so a matrícula whose digit is `0` is rejected however good its check digits are, the same way `getCertidaoInfo` returns `null` for it. `options.accept` (part of `IsValidCertidaoOptions`) narrows that to the listed types; it defaults to every type, and a value that is not an array falls back to that default. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +The matrícula has 32 digits, printed as `000000 00 00 0000 0 00000 000 0000000 00`: + +| Digits | Field | +| --- | --- | +| 6 | CNS da serventia | +| 2 | acervo | +| 2 | serviço, always `55` | +| 4 | ano | +| 1 | tipo do livro | +| 5 | livro | +| 3 | folha | +| 7 | termo | +| 2 | dígitos verificadores | + +- **Options** (`IsValidCertidaoOptions`): `accept` narrows the valid book types (`CertidaoType`) to the listed ones (default: every type). +- The serviço must be `55`, and the book-type digit must be one of the nine books (`0` is rejected). +- Accepts the value masked or not, with whitespace between and around the groups. ```javascript import { isValidCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1750,9 +2115,14 @@ isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['birth'] isValidCertidao('104539 01 55 2013 1 00012 021 0000123 21', { accept: ['death'] }); // false ``` +Source: [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243); check digits per [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts). + ### formatCertidao -Format the matrícula of a certidão de registro civil into the printed mask of the Provimento, the 32 digits grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. `options.pad` (part of `FormatCertidaoOptions`) left pads the value with zeros up to 32 digits (default `false`). The mask is the one of [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243). A number is accepted and read as the string of its digits, like in `formatCpf`, but a full 32 digit matrícula has to be a string: that many digits are more than a JavaScript number can hold exactly. At runtime the value is read for its digits and masked as far as they go, like in every formatter of this package, so a partial matrícula still being typed is masked progressively. +Format the matrícula of a certidão de registro civil into the printed mask of art. 473. The 32 digits are grouped as 6 2 2 4 1 5 3 7 2 and separated by spaces. + +- **Options** (`FormatCertidaoOptions`): `pad` left-pads the value with zeros up to 32 digits (default `false`). +- A number is accepted, but a full 32-digit matrícula has to be a string. ```javascript import { formatCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1763,9 +2133,11 @@ formatCertidao('1552010100020112000012087', { pad: true }); // 000000 01 55 2010 formatCertidao(104539015520); // 104539 01 55 20 (a number is read as the string of its digits) ``` +Source: [art. 473 of the Código Nacional de Normas](https://atos.cnj.jus.br/atos/detalhar/5243). + ### parseCertidao -Remove the formatting of the matrícula of a certidão de registro civil, keep only digits, and cap the result to 32 digits. This only takes the mask off: use `isValidCertidao` to check the matrícula and `getCertidaoInfo` to read its fields. +Remove the formatting of the matrícula of a certidão de registro civil, keep only digits, and cap the result to 32 digits. ```javascript import { parseCertidao } from '@brazilian-utils/brazilian-utils'; @@ -1776,7 +2148,25 @@ parseCertidao('104539 01 55 2013 1 00012 021 0000123 21'); ### getCertidaoInfo -Parse the matrícula of a certidão de registro civil into its fields, returning `null` when the matrícula is not valid, which includes a book code that is not one of the nine books. A serviço other than the `55` that art. 473, III fixes for the registro civil das pessoas naturais also gives `null`. [Art. 473, V of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243) lists the codes 1 to 7; no CNJ primary text reachable today publishes the other two, the Anexo IV of the revoked Provimento CNJ nº 63/2017 included, which lists the same seven. The codes 8 (emancipação) and 9 (interdição) come from the references the check digit rule rests on: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts) both print the nine book list. They are kept because matrículas carrying them circulate. Only a string is accepted: the 32 digits of a matrícula are more than a JavaScript number can hold. +Parse the matrícula of a certidão de registro civil into its fields. Accepts the same input forms as `isValidCertidao` and returns `null` when the matrícula is not valid. + +- Returns `null` also for a serviço other than `55` and for a book code outside 1 to 9. +- Art. 473, V lists only the book codes 1 to 7. The codes 8 (emancipação) and 9 (interdição) are also accepted. + +The `CertidaoInfo` result carries: + +| Key | Description | +| --- | --- | +| `registryCns` | The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. | +| `acervo` | Acervo the book belongs to: `"01"` the serventia's own, `"02"` and up one per acervo it absorbed. Art. 473, §§ 3º to 5º splits the absorbed ones by the date the origin serventia was extinguished or deactivated. Up to 31/12/2009: the CNS of the incorporating unit and an acervo code from `"02"` up, one per incorporation. From 01/01/2010 on: the CNS of the incorporated unit itself and the code `"01"`, counted as that unit's own acervo. An acervo split between two or more successor serventias gets each successor's own CNS with the code `"02"`. | +| `service` | Service rendered by the serventia, always `"55"`, the registro civil das pessoas naturais. | +| `year` | Four digit year the act was recorded. | +| `type` | The book the act belongs to: `"birth"`, `"marriage"`, `"religious-marriage"`, `"death"`, `"stillbirth"`, `"banns"`, `"other"`, `"emancipation"` or `"interdiction"`. | +| `typeCode` | Raw book code, 1 to 9, as printed in the fifteenth position of the matrícula. | +| `book` | The 5 digit book (livro) number, zero padded. | +| `page` | The 3 digit page (folha) number, zero padded. | +| `term` | The 7 digit term (termo) number, zero padded. | +| `checkDigits` | The 2 modulus 11 check digits of the matrícula. | ```javascript import { getCertidaoInfo } from '@brazilian-utils/brazilian-utils'; @@ -1798,26 +2188,15 @@ getCertidaoInfo('104539 01 55 2013 1 00012 021 0000123 21'); getCertidaoInfo('invalid'); // null ``` -The `CertidaoInfo` result carries: - -| Key | Description | -| --- | --- | -| `registryCns` | The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. | -| `acervo` | Acervo the book belongs to: `"01"` the serventia's own, `"02"` and up one per acervo it absorbed. [Art. 473, §§ 3º to 5º](https://atos.cnj.jus.br/atos/detalhar/5243) splits the absorbed ones by the date the origin serventia was extinguished or deactivated: up to 31/12/2009 the matrícula carries the CNS of the incorporating unit and an acervo code from `"02"` up, one per incorporation; from 01/01/2010 on it carries the CNS of the incorporated unit itself and the code `"01"`, counted as that unit's own acervo; and an acervo split between two or more successor serventias gets each successor's own CNS with the code `"02"`. | -| `service` | Service rendered by the serventia, always `"55"`, the registro civil das pessoas naturais. | -| `year` | Four digit year the act was recorded. | -| `type` | The book the act belongs to: `"birth"`, `"marriage"`, `"religious-marriage"`, `"death"`, `"stillbirth"`, `"banns"`, `"other"`, `"emancipation"` or `"interdiction"`. | -| `typeCode` | Raw book code, 1 to 9, as printed in the fifteenth position of the matrícula. | -| `book` | The 5 digit book (livro) number, zero padded. | -| `page` | The 3 digit page (folha) number, zero padded. | -| `term` | The 7 digit term (termo) number, zero padded. | -| `checkDigits` | The 2 modulus 11 check digits of the matrícula. | +Source: [art. 473 of the Código Nacional de Normas da Corregedoria Nacional de Justiça](https://atos.cnj.jus.br/atos/detalhar/5243); book codes 8 and 9 per [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [validation-br](https://github.com/klawdyo/validation-br/blob/feat-certidao/src/certidao.ts). ## CEI, CNO and CAEPF ### isValidCei -Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer: 12 digits printed as `00.000.00000/00`, the last one a check digit calculated over the 11 base digits with the weights 7, 4, 1, 8, 5, 2, 1, 6, 3, 7 and 4. Accepts the usual mask characters and whitespace between/around groups, a run of them between two groups included. The Receita Federal does not publish this check digit rule, so it follows the reference implementations of [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php) and [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs), cross-checked against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal. +Check if a CEI (Cadastro Específico do INSS) number is valid. The CEI identifies an employer with no CNPJ, such as a construction work or a rural producer. + +- Layout: 12 digits printed as `00.000.00000/00`, 11 base digits and one check digit. ```javascript import { isValidCei } from '@brazilian-utils/brazilian-utils'; @@ -1829,9 +2208,13 @@ isValidCei('24.985.96743/68'); // false (invalid check digit) isValidCei('000000000000'); // false (repeated digits) ``` +Source: [yii2-br-validator](https://github.com/yiibr/yii2-br-validator/blob/master/src/CeiValidator.php), [Bigai.Documentos.Brasil](https://github.com/marcos-cruz/Documento/blob/master/src/Bigai.Documentos.Brasil/Cei/Cei.cs) and the [CNO open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno). + ### formatCei -Format a CEI (Cadastro Específico do INSS) number according to the usual `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCeiOptions`) left pads the value with zeros up to 12 digits (default `false`). +Format a CEI (Cadastro Específico do INSS) number with the usual `00.000.00000/00` mask. + +- **Options** (`FormatCeiOptions`): `pad` left-pads the value with zeros up to 12 digits (default `false`). ```javascript import { formatCei } from '@brazilian-utils/brazilian-utils'; @@ -1843,7 +2226,7 @@ formatCei('249', { pad: true }); // 00.000.00002/49 ### parseCei -Remove CEI (Cadastro Específico do INSS) formatting, keep only digits, and cap the result to 12 digits. A partial value passes through as far as it goes; use `isValidCei` to check the number itself. +Remove CEI (Cadastro Específico do INSS) formatting, keep only digits, and cap the result to 12 digits. ```javascript import { parseCei } from '@brazilian-utils/brazilian-utils'; @@ -1853,7 +2236,9 @@ parseCei('27.729.71181/87'); // '277297118187' ### isValidCno -Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering, so a work registered under a legacy CEI keeps the same number and both registries validate identically: 12 digits printed as `00.000.00000/00` with a check digit calculated over the 11 base digits. The Receita Federal does not publish the check digit rule; it was confirmed against the [Cadastro Nacional de Obras (CNO) open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno) of the Receita Federal: every work in the Minas Gerais extract of that dataset passes this check. The catalogue page itself publishes only the dataset's description and download links, not that result. +Check if a CNO (Cadastro Nacional de Obras) number is valid. The CNO replaced the CEI for construction works and kept its numbering. + +- Same rules as `isValidCei`. ```javascript import { isValidCno } from '@brazilian-utils/brazilian-utils'; @@ -1865,9 +2250,13 @@ isValidCno('110840168063'); // false (invalid check digit) isValidCno('000000000000'); // false (repeated digits) ``` +Source: [CNO page of the Receita Federal](https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cno) and the [CNO open dataset](https://dados.gov.br/dados/conjuntos-dados/cadastro-nacional-de-obras-cno). + ### formatCno -Format a CNO (Cadastro Nacional de Obras) number. The CNO kept the CEI's numbering, so both share the same 12 digit, `00.000.00000/00` mask, the one the reference implementations of the check digit agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCnoOptions`) left pads the value with zeros up to 12 digits (default `false`). +Format a CNO (Cadastro Nacional de Obras) number. + +- Same rules as `formatCei`: the `00.000.00000/00` mask, with `pad` in `FormatCnoOptions`. ```javascript import { formatCno } from '@brazilian-utils/brazilian-utils'; @@ -1879,7 +2268,7 @@ formatCno('979', { pad: true }); // 00.000.00009/79 ### parseCno -Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. A shorter value passes through as far as it goes; use `isValidCno` to check the number itself. +Remove CNO (Cadastro Nacional de Obras) formatting, keep only digits, and cap the result to 12 digits, the numbering the CNO kept from the CEI. ```javascript import { parseCno } from '@brazilian-utils/brazilian-utils'; @@ -1889,7 +2278,10 @@ parseCno('11.113.01373/68'); // '111130137368' ### isValidCaepf -Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees: 14 digits printed as `000.000.000/000-00`, formed by the 9 digit CPF base of the holder, a 3 digit sequence for the holder's several registrations and 2 check digits. Both check digits are the CNPJ's modulus 11 in the formulation of the cited reference: the weights cycle from 9 down to 2 from the right and the check digit is the remainder itself, with a remainder of 10 read as 0 — the same digit the CNPJ's 2-to-9 weights with `11 - remainder` produce. The resulting pair is then shifted by 12, wrapping around 100. A base whose 12 digits are all the same is rejected before the check digits are computed, the way `isValidCei` and `isValidCno` reject a repeated CEI/CNO number, so the otherwise well-formed `00000000000012` is invalid. The Receita Federal does not publish the layout or the check digit rule: both are described by [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and implemented the same way by [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). +Check if a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number is valid. The CAEPF replaced the CEI for individuals who hire employees, such as rural producers. + +- Layout: 14 digits printed as `000.000.000/000-00`: the 9-digit CPF base of the holder, a 3-digit sequence and 2 check digits. +- Both check digits follow the CNPJ's modulus 11; the pair is then shifted by 12, wrapping around 100. ```javascript import { isValidCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1902,9 +2294,13 @@ isValidCaepf('00000000000000'); // false (repeated base digits) isValidCaepf('00000000000012'); // false (repeated base digits) ``` +Source: [ghiorzi.org](http://ghiorzi.org/DVnew.htm) and [brazilian-values](https://github.com/VitorLuizC/brazilian-values/blob/master/src/validators/isCAEPF.ts). + ### formatCaepf -Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the usual `000.000.000/000-00` mask, the one the sources of the check digit rule agree on (the Receita Federal does not print it). Formats progressively, as far as the digits given go, so it can also be used as an input mask. `options.pad` (part of `FormatCaepfOptions`) left pads the value with zeros up to 14 digits (default `false`). +Format a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number with the usual `000.000.000/000-00` mask. + +- Same rules as `formatCei`, with `pad` (`FormatCaepfOptions`) padding up to 14 digits (default `false`). ```javascript import { formatCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1916,7 +2312,7 @@ formatCaepf('184', { pad: true }); // 000.000.000/001-84 ### parseCaepf -Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. A shorter value passes through as far as it goes; use `isValidCaepf` to check the number itself. +Remove CAEPF (Cadastro de Atividade Econômica da Pessoa Física) formatting, keep only digits, and cap the result to 14 digits. ```javascript import { parseCaepf } from '@brazilian-utils/brazilian-utils'; @@ -1928,7 +2324,11 @@ parseCaepf('293.118.610/001-84'); // '29311861000184' ### isValidCbo -Check if a CBO (Classificação Brasileira de Ocupações) code exists in the MTE occupation table. Accepts the code with or without the hyphen mask, or as a number. A string is only read as a code when it is written in one of those forms (the 6 digits, or the `NNNN-NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A CBO code is always 6 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 6 whether it comes as a string or as a number, exactly like `getBankByCode` pads a bank code: `10205`, `'10205'` and `'010205'` are the same code. A masked value already carries its separators and is read as written. +Check if a CBO (Classificação Brasileira de Ocupações) code exists in the official CBO 2002 table. + +- Accepts a string with the 6 digits or with the `NNNN-NN` mask, or a number. +- A masked string needs a single separator (space, `.`, `-` or `/`) between the groups. Any other string is rejected instead of having its digits picked out. +- Bare digits are left padded with zeros to 6, as a string or as a number. A masked value is read as written. ```javascript import { isValidCbo } from '@brazilian-utils/brazilian-utils'; @@ -1943,11 +2343,13 @@ isValidCbo('2124abc05'); // false (not a documented form) isValidCbo(-212405); // false (not a non-negative safe integer) ``` -The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). +Source: [CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ### parseCbo -Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. A shorter value passes through as far as it goes and nothing is left padded here, so the leading zero of a code such as `010205` has to be written out; use `getCbo` or `isValidCbo`, which do pad a bare numeric code, to look an occupation up. +Remove CBO (Classificação Brasileira de Ocupações) formatting, keep only digits, and cap the result to 6 digits. + +- Nothing is left padded: the leading zero of a code such as `010205` has to be written out. Use `getCbo` or `isValidCbo` to look an occupation up. ```javascript import { parseCbo } from '@brazilian-utils/brazilian-utils'; @@ -1957,7 +2359,9 @@ parseCbo('2124-05'); // '212405' ### getCbo -Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title, in the `{ code, description }` record every lookup of this library returns. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCbo(10205)` and `getCbo('10205')` are both read as `010205`. Same input rules as `isValidCbo`: a string has to be written as the 6 digits or with the `NNNN-NN` mask, and a number has to be a non-negative safe integer. +Look a CBO (Classificação Brasileira de Ocupações) code up and get its official occupation title. The result is a `Cbo` record: `{ code, description }`. + +- Same rules as `isValidCbo`. Returns `null` when the code is unknown or the value is not in a documented form. ```javascript import { getCbo } from '@brazilian-utils/brazilian-utils'; @@ -1969,11 +2373,13 @@ getCbo('000000'); // null getCbo('2124abc05'); // null (not a documented form) ``` -The occupation titles come from the [official CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). +Source: [CBO 2002 occupation table published by the MTE](https://www.gov.br/trabalho-e-emprego/pt-br/assuntos/cbo/servicos/downloads/cbo2002-ocupacao.csv). ### isValidCnae -Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the [CNAE-Subclasses 2.3 table published by IBGE](https://concla.ibge.gov.br/busca-online-cnae.html), the current subclass revision of CNAE 2.0. Accepts the code with or without the `NNNN-N/NN` mask, or as a number. A string is only read as a code when it is written in one of those forms (the 7 digits, or the mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. A CNAE subclass code is always 7 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 7 whether it comes as a string or as a number: `111301`, `'111301'` and `'0111301'` are the same code. A masked value already carries its separators and is read as written. +Check if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code exists in the CNAE-Subclasses 2.3 table, the current subclass revision of CNAE 2.0. + +- Same rules as `isValidCbo`, with 7 digits and the `NNNN-N/NN` mask. ```javascript import { isValidCnae } from '@brazilian-utils/brazilian-utils'; @@ -1987,9 +2393,14 @@ isValidCnae('0111abc301'); // false (not a documented form) isValidCnae(-111301); // false (not a non-negative safe integer) ``` +Source: [CNAE-Subclasses 2.3 at CONCLA/IBGE](https://concla.ibge.gov.br/busca-online-cnae.html) and the [IBGE subclasses API](https://servicodados.ibge.gov.br/api/v2/cnae/subclasses). + ### formatCnae -Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. `options.pad` (part of `FormatCnaeOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 7 digits of a complete subclass code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Like every formatter of this package, the value is read for its digits and masked as far as they go: characters outside the mask are dropped and a number is read as the string of its digits, sign and decimal point included. Use `isValidCnae` to check a code. +Format a CNAE (Classificação Nacional de Atividades Econômicas) subclass code. Only the structure changes; use `isValidCnae` to check a code against the table. + +- **Options** (`FormatCnaeOptions`): `pad` (default `false`) first left pads the value with zeros to the 7 digits of a complete code. Without it the mask is applied as far as the value goes. +- Characters outside the mask are dropped, and a number is read as the string of its digits. Returns `''` when there is no digit at all. ```javascript import { formatCnae } from '@brazilian-utils/brazilian-utils'; @@ -2005,7 +2416,9 @@ formatCnae(-6201501); // 6201-5/01 ### parseCnae -Remove CNAE (Classificação Nacional de Atividades Econômicas) formatting, keep only digits, and cap the result to the 7 digits of a complete subclass code. Nothing is left padded here; use `getCnae` or `isValidCnae`, which do pad a bare numeric code, to look a subclass up. +Remove CNAE (Classificação Nacional de Atividades Econômicas) formatting, keep only digits, and cap the result to the 7 digits of a complete subclass code. + +- Same rules as `parseCbo`: nothing is left padded here. ```javascript import { parseCnae } from '@brazilian-utils/brazilian-utils'; @@ -2016,7 +2429,10 @@ parseCnae('62'); // '62' (a partial code is kept as written) ### getCnae -Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. `code` comes back as the 7 bare digits, like every other lookup of this library; pass it to `formatCnae` for the `NNNN-N/NN` form. A value written as bare digits keeps its implied leading zeros, as a string as much as a number: `getCnae(111301)` and `getCnae('111301')` are both read as `0111301`. Same input rules as `isValidCnae`: a string has to be written as the 7 digits or with the `NNNN-N/NN` mask, and a number has to be a non-negative safe integer. +Look a CNAE (Classificação Nacional de Atividades Econômicas) subclass code up and get its code and official description. The result is a `Cnae` record: `{ code, description }`. + +- Same rules as `getCbo`, with 7 digits and the `NNNN-N/NN` mask. +- `code` comes back as the 7 bare digits; pass it to `formatCnae` for the `NNNN-N/NN` form. ```javascript import { formatCnae, getCnae } from '@brazilian-utils/brazilian-utils'; @@ -2029,9 +2445,13 @@ getCnae('0111abc301'); // null (not a documented form) formatCnae(getCnae('6201501')?.code); // 6201-5/01 (the mask is the formatter's job) ``` +Source: [CNAE-Subclasses 2.3 at CONCLA/IBGE](https://concla.ibge.gov.br/busca-online-cnae.html) and the [IBGE subclasses API](https://servicodados.ibge.gov.br/api/v2/cnae/subclasses). + ### isValidNcm -Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. Accepts the code with or without the dotted mask, or as a number. A string is only read as a code when it is written in one of those forms (the 8 digits, or the `NNNN.NN.NN` mask, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. An NCM code is always 8 digits and its leading zeros are part of it, so a value written as bare digits is left padded with zeros to 8 whether it comes as a string or as a number: `1012100`, `'1012100'` and `'01012100'` are the same code. A masked value already carries its separators and is read as written. +Check if an NCM (Nomenclatura Comum do Mercosul) code exists in the current table published by Siscomex/MDIC. + +- Same rules as `isValidCbo`, with 8 digits and the `NNNN.NN.NN` mask. ```javascript import { isValidNcm } from '@brazilian-utils/brazilian-utils'; @@ -2045,9 +2465,14 @@ isValidNcm('abc01012100'); // false (not a documented form) isValidNcm(-84713012); // false (not a non-negative safe integer) ``` +Source: [NCM nomenclature published by the Portal Único Siscomex](https://portalunico.siscomex.gov.br/classif/api/publico/nomenclatura/download/json). + ### formatNcm -Format an NCM (Nomenclatura Comum do Mercosul) code. `options.pad` (part of `FormatNcmOptions`) works exactly like it does in `formatCpf`/`formatCep`: with the default `false` the mask is applied progressively, as far as the value goes, which is what an input being typed into needs; with `true` the value is first left padded with zeros to the 8 digits of a complete code, so it always comes back fully masked. A number is treated exactly like the string of its digits, so it is only padded under `pad: true`. Like every formatter of this package, the value is read for its digits and masked as far as they go: characters outside the mask are dropped and a number is read as the string of its digits, sign and decimal point included. Use `isValidNcm` to check a code. +Format an NCM (Nomenclatura Comum do Mercosul) code. Only the structure changes; use `isValidNcm` to check a code against the table. + +- **Options** (`FormatNcmOptions`): `pad` (default `false`) first left pads the value with zeros to the 8 digits of a complete code. +- Same rules as `formatCnae`, with the `NNNN.NN.NN` mask. ```javascript import { formatNcm } from '@brazilian-utils/brazilian-utils'; @@ -2062,7 +2487,9 @@ formatNcm(-84713012); // 8471.30.12 ### parseNcm -Remove NCM (Nomenclatura Comum do Mercosul) formatting, keep only digits, and cap the result to the 8 digits of a complete code. Nothing is left padded here; use `isValidNcm`, which does pad a bare numeric code, to check a code against the official table. +Remove NCM (Nomenclatura Comum do Mercosul) formatting, keep only digits, and cap the result to the 8 digits of a complete code. + +- Same rules as `parseCbo`: nothing is left padded here. ```javascript import { parseNcm } from '@brazilian-utils/brazilian-utils'; @@ -2073,9 +2500,11 @@ parseNcm('8471'); // '8471' (a partial code is kept as written) ### isValidCfop -Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table. The table is the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), the text in force (current wording given by Ajuste SINIEF 03/24, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25)), not the frozen 2001 text of Ajuste SINIEF 07/01. Only operable codes count: the group and subgroup headings of the official nomenclature, the codes ending in `00` and `50` (1000, 1100, 1150, 5350, ...), are section titles rather than codes a document can carry, so they are rejected. +Check if a CFOP (Código Fiscal de Operações e Prestações) code exists in the official table, the consolidated Anexo II of Convênio SINIEF s/nº 1970 in force. -A string is only read as a code when it is written in one of the documented forms (the 4 digits, or the `N.NNN` form the annex prints, with a single separator between the groups and optional surrounding whitespace), and a number only when it is a non-negative safe integer. No CFOP code starts with a zero, its first digit is the operation group (1 to 7), so nothing is ever padded here: a number and the string of the same digits are read identically. +- Only operable codes count: the group and subgroup headings, the codes ending in `00` and `50`, are rejected. +- Accepts a string with the 4 digits or with the `N.NNN` form, with a single separator (space, `.`, `-` or `/`), or a number. Any other string is rejected. +- No CFOP code starts with a zero, so nothing is padded. ```javascript import { isValidCfop } from '@brazilian-utils/brazilian-utils'; @@ -2089,9 +2518,13 @@ isValidCfop('abc5102'); // false (not a documented form) isValidCfop(-5102); // false (not a non-negative safe integer) ``` +Source: [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). + ### parseCfop -Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. A shorter value passes through as far as it goes. No CFOP code starts with a zero, its first digit is the operation group from 1 to 7, so nothing is ever padded here. +Remove CFOP (Código Fiscal de Operações e Prestações) formatting, keep only digits, and cap the result to 4 digits. + +- No CFOP code starts with a zero, so nothing is padded here. ```javascript import { parseCfop } from '@brazilian-utils/brazilian-utils'; @@ -2101,7 +2534,9 @@ parseCfop('5.102'); // '5102' ### getCfop -Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description, as the [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24) words it, in the text in force, last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). The group and subgroup headings of the official nomenclature, the codes ending in `00` and `50`, are not in the table and give `null`. Same input rules as `isValidCfop`. +Look a CFOP (Código Fiscal de Operações e Prestações) code up and get its code and official description. The result is a `Cfop` record: `{ code, description }`. + +- Same rules as `isValidCfop`. Returns `null` for a heading, an unknown code or a value not in a documented form. ```javascript import { getCfop } from '@brazilian-utils/brazilian-utils'; @@ -2113,6 +2548,8 @@ getCfop('5350'); // null (a subgroup heading, not an operable code) getCfop('abc5102'); // null (not a documented form) ``` +Source: [consolidated Anexo II of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cfop_cvsn_1-6.24), last amended by [Ajuste SINIEF 39/25](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2025/AJ039_25). + ### isValidCst Check if a CST (Código de Situação Tributária) code is valid for a given tax. Pass the tax through `options.tax`: @@ -2124,13 +2561,9 @@ Check if a CST (Código de Situação Tributária) code is valid for a given tax | `pis` | 2 digits | `01`-`09`, `49`, `50`-`56`, `60`-`67`, `70`-`75`, `98`, `99` | | `cofins` | 2 digits | same table as `pis` | -`options.tax` (part of `IsValidCstOptions`) is optional: omit it to accept a code that exists in any one of the four tables above. A `tax` outside those four values falls back to that same default at runtime, the way every other scalar option of this library treats a value it does not know. - -The ICMS Tabela B is the one in force: the [consolidated Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), whose current wording came from [Ajuste SINIEF 39/23](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2023/ajuste-sinief-39-23) (effective 01.12.23) and which [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24) amended by striking items 12, 13, 52, 72 and 74 (effects from 09.07.24) before they ever took effect: 39/23 had deferred their effect to 1º de outubro de 2024, so the revocation reached them first and those codes were never in force. `02`, `15`, `53` and `61` are its monofasia de combustíveis codes. - -A string is only read as a code when it is written in one of the documented forms (the 2 digits of a Tabela B code, or the 3 digits of the ICMS form with an optional single separator after the origin digit, plus optional surrounding whitespace), and a number only when it is a non-negative safe integer. The origin digit is the only boundary a printed CST has, so `'0 10'` and `'1-10'` are read while `'0-0'`, `'11-0'` and `'00-'` are not. - -A single digit is narrower than either documented form, so it is left padded with zeros to the 3 digits of the ICMS form, whether it comes as a string or as a number: `0`, `'0'` and `'000'` are all the ICMS code `000`. A 2 digit value is already a documented form, a Tabela B code, and is read as written, so a Tabela B code keeps its own two digits: `'07'`, not `7`, which is the ICMS code `007`. +- **Options** (`IsValidCstOptions`): `tax` picks the table. Omitted, or outside those four values, every table is accepted. +- Accepts a string with the 2 digits of a Tabela B code or the 3 digits of the ICMS form, or a number. The ICMS form may have a single separator (space, `.`, `-` or `/`) after the origin digit. +- A single digit is padded to the 3-digit ICMS form; a 2-digit string is a Tabela B code, while the number `7` is the ICMS code `007`. ```javascript import { isValidCst } from '@brazilian-utils/brazilian-utils'; @@ -2149,30 +2582,36 @@ isValidCst('abc110'); // false (not a documented form) isValidCst(-110); // false (not a non-negative safe integer) ``` +Source: ICMS Tabela B from [Anexo I of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) as amended by [Ajuste SINIEF 20/24](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2024/AJ020_24); IPI, PIS and COFINS from [IN RFB nº 1.009/2010](https://normas.receita.fazenda.gov.br/sijut2consulta/link.action?idAto=15974). + ### isValidCsosn -Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70), the table Ajuste SINIEF 03/2010 instituted: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. +Check if a CSOSN (Código de Situação da Operação no Simples Nacional) code is one of the 10 codes of the official table: `101`, `102`, `103`, `201`, `202`, `203`, `300`, `400`, `500` or `900`. -A string is only read as a code when it is written as the bare 3 digits with optional surrounding whitespace: a CSOSN has no printed grouping (the NF-e carries the origin digit in its own `orig` field), so `'1-01'` is rejected; a number is read only when it is a non-negative safe integer. No CSOSN code starts with a zero, the table runs from `101` to `900`, so nothing is ever padded here: a number and the string of the same digits are read identically. +- Accepts a string with the bare 3 digits, or a number. A CSOSN has no printed grouping, so `'1-01'` is rejected. ```javascript import { isValidCsosn } from '@brazilian-utils/brazilian-utils'; isValidCsosn('101'); // true +isValidCsosn(900); // true isValidCsosn('999'); // false isValidCsosn('abc101'); // false (not a documented form) isValidCsosn(-101); // false (not a non-negative safe integer) ``` +Source: [consolidated Anexo III-A of Convênio SINIEF s/nº 1970](https://www.confaz.fazenda.gov.br/legislacao/ajustes/sinief/cvsn_70) and [Ajuste SINIEF 03/2010](https://www.confaz.fazenda.gov.br/legislacao/ajustes/2010/aj_003_10). + ## Text ### capitalize -Transforms the first letter into a capital one of each word, the way a Brazilian name, company name or address is written, with no options needed. Words are separated by whitespace, by `-` and `/`, by the apostrophe (`'d'oeste'` becomes `'d'Oeste'`) and by punctuation that touches a word (`'(empresa)'` becomes `'(Empresa)'`, `'bairro:centro'` becomes `'Bairro:Centro'`), so `'MOGI-GUAÇU'` becomes `'Mogi-Guaçu'`; the separators are kept where they are. Every run of whitespace (tabs, newlines, repeated spaces) collapses into a single space, and the leading and trailing whitespace is dropped. The particles of foreign-origin names (`del`, `della`, `di`, `du`, `van`, `von`, `der`, `den`) stay lower case like the Portuguese prepositions, and so does the elided `d'`, wherever it appears, whenever an apostrophe and a word follow it (`'dias d'ávila'` becomes `'Dias d'Ávila'`); a single letter written right after an apostrophe is the English possessive and stays lower case too (`"bob's"` becomes `"Bob's"`). - -`options.lowerCaseWords` defaults to the Portuguese prepositions, articles and conjunctions that stay in lower case inside a proper name (`de`, `da`, `do`, `e`, ...), and they are only written in lower case when they link two words: one of them that is the first word, that ends the value, or that is followed by punctuation is a designator instead and keeps its capital (`'rua a, 100'` becomes `'Rua A, 100'` and `'condomínio a, quadra d, lote o'` becomes `'Condomínio A, Quadra D, Lote O'`). `options.upperCaseWords` defaults to the company designations and document abbreviations written in upper case in Brazilian usage (`LTDA`, `S.A.`, `S/A`, `S.S.`, `S/S`, `ME`, `EPP`, `MEI`, `EIRELI`, `CIA`, `SCP`, `CNPJ`, `CPF`, `RG`, `CEP`, `UF`) plus the roman numerals that appear in names and addresses (`II` through `XXIII`, except `VI`, which collides with the pt-BR verb form "vi"). `SA` without punctuation is deliberately absent, since it is indistinguishable from the surname "Sá" typed without its accent, while `ME` is also the pronoun "me", so it is only written in upper case in the designation position, as the last word of the value (`'fulano comércio me'` becomes `'Fulano Comércio ME'`) or right before another designation (`'fulano me epp'` becomes `'Fulano ME EPP'`); anywhere else it is an ordinary word (`'diga-me a verdade'` becomes `'Diga-Me a Verdade'`, `'não-me-toque'` becomes `'Não-Me-Toque'`). `S/A` and `S/S` are matched across the slash even though a slash separates words. A two letter word that follows a `/` is upper-cased when it is the code of a Brazilian state (`'porto alegre/rs'` becomes `'Porto Alegre/RS'`); that rule is structural and stays on even when `upperCaseWords` is given, while a state code that does not follow a `/` is left alone. +Capitalize the first letter of each word, the way a Brazilian name, company name or address is written, with no options needed. -Either list given in `options` replaces its default entirely, and the comparison against both is case-insensitive (pt-BR locale). Options are typed as `CapitalizeOptions`. Every other word is capitalized letter by letter: `'İSTANBUL'` becomes `'İstanbul'`, and a first letter whose upper case is two letters (`ß`, the `fi` ligature) keeps its case, so `'straße'` becomes `'Straße'` and `'ßa'` stays `'ßa'`. +- **Options** (`CapitalizeOptions`): `lowerCaseWords`, words kept in lower case between two words, by default prepositions and articles such as `de`, `da`, `do`, `e`; `upperCaseWords`, words always in upper case, by default company designations and abbreviations such as `LTDA`, `S.A.`, `ME`, `CNPJ` and roman numerals. A list replaces its default. +- Words split at whitespace, `-`, `/`, apostrophes and adjoining punctuation; whitespace runs collapse into one space. +- A lower-case word that is first, last or followed by punctuation is a designator and keeps its capital. +- `ME` is upper-cased only as a designation (last word, or before another designation); `SA` without dots is left alone (the surname Sá). A state code after a `/` is upper-cased even with `upperCaseWords` given. ```javascript import { capitalize } from '@brazilian-utils/brazilian-utils'; @@ -2202,9 +2641,13 @@ capitalize('doc inválido', { upperCaseWords: ['DOC'] }); // DOC Inválido (case capitalize(' josé maria '); // José Maria (every run of whitespace, tabs and newlines included, collapses into one space) ``` +Source: [Manual de Redação da Presidência da República](https://www4.planalto.gov.br/centrodeestudos/assuntos/manual-de-redacao-da-presidencia-da-republica/manual-de-redacao.pdf). + ### removeAccents -Remove diacritical marks (accents, tildes, cedillas) from a string, decomposing every accented character into its base letter plus combining marks (Unicode NFD) and dropping the combining marks. +Remove diacritical marks (accents, tildes, cedillas) from a string. + +- Every combining mark (Unicode general category M) is dropped, so accents from any script go. ```javascript import { removeAccents } from '@brazilian-utils/brazilian-utils'; @@ -2216,30 +2659,52 @@ removeAccents('Açaí'); // 'Acai' removeAccents(''); // '' ``` -## isValidIe +## Inscrição estadual (IE) + +### isValidIe -Check if inscrição estadual (state registration) is valid. The state code is case-insensitive. Notable per-state rules: GO accepts prefixes `10`, `11` and `15`; PA accepts `15` and `75`-`79`; MS accepts `28` and `50`; SP has a produtor rural pattern `P0MMMSSSSD000`; TO uses 11-digit type codes (`01`, `02`, `03`, `99`). TO also accepts a 9-digit form, applying the same modulus 11 rule to the first eight digits; the SINTEGRA page documents only the 11-digit one, so that shape is 2.3.0 behaviour kept for compatibility rather than a published rule. An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it (AM, BA with 8 or 9 digits, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE, SP and TO with 9 digits), unlike `isValidCpf` and `isValidCnpj`, which reject repeated digits. AM is on that list through the second branch of its published formula only: the page's first branch, `Se Soma < 11 Então Dígito = 11 - Soma`, gives 11 for an all-zero registration, while the `resto <= 1 ⇒ 0` branch, the one implemented here, gives 0. The registration and the state code go together in a single object, typed as `IsValidIeParams`; the 2.3.0 form, `isValidIe(stateCode, ie)`, still works and is deprecated. +Check if an inscrição estadual (state registration) is valid for a state. **Deprecated:** the positional form `isValidIe(stateCode, ie)` still works but is deprecated; use the object form `isValidIe({ value, stateCode })`. + +- Takes a single object (`IsValidIeParams`): `value` is the registration and `stateCode` the state it belongs to (a `StateCode`, case-insensitive). +- GO, PA, MS, SP, TO, DF, PE, AL and RJ have special cases (extra prefixes or formats, or a deviation from the SINTEGRA page); see the JSDoc in `src/is-valid-ie` for the details. +- An all-zero registration is accepted wherever the published formula yields a check digit of 0 for it: AM, CE, ES, MG, MT, PB, PE, PI, PR, RJ, RS, SC, SE and SP, plus BA with 8 or 9 digits and TO with 9 digits. ```javascript import { isValidIe } from '@brazilian-utils/brazilian-utils'; +isValidIe({ value: '110042490114', stateCode: 'SP' }); // true +isValidIe({ value: 'P011004243002', stateCode: 'SP' }); // true (produtor rural) isValidIe({ value: '0187634580933', stateCode: 'AC' }); // false isValidIe({ value: '109161793', stateCode: 'go' }); // true (case-insensitive) ``` -## isValidEmail +Source: [SINTEGRA state pages](http://www.sintegra.gov.br/insc_est.html) and the [SEFAZ-GO roteiro de crítica](https://goias.gov.br/economia/roteiro-de-critica-da-inscricao-estadual-de-goias/). + +## Email + +### isValidEmail + +Check if an email address is valid. A practical subset of the WHATWG HTML definition. -Check if email is valid. The accepted set is a practical subset of the WHATWG HTML [valid e-mail address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) definition, not of [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322). The local part is limited to letters, digits and `_'+-.`, and may not start with a dot, end with a dot or an apostrophe, or contain two dots in a row. The domain must carry at least one dot, and each dotted label follows the WHATWG production `[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`, so a label may neither start nor end with a hyphen nor exceed 63 characters; the final label is alphabetic and 2 to 63 letters long, so `user@example.c1` is rejected. Quoted local parts (`"john doe"@example.com`) and address literals (`john@[127.0.0.1]`) are rejected. +- Local part: letters, digits and `_'+-.`, with no leading or trailing dot and no two dots in a row. +- Domain: at least one dot, labels of up to 63 characters, final label 2 to 63 letters; quoted local parts and address literals are rejected. ```javascript import { isValidEmail } from '@brazilian-utils/brazilian-utils'; isValidEmail('john.doe@hotmail.com'); // true +isValidEmail('invalid.email'); // false ``` -## isValidCreditCard +Source: [WHATWG HTML, valid e-mail address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address) and [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322). + +## Credit card + +### isValidCreditCard + +Check if a payment card number (credit or debit) is valid using the Luhn algorithm. Only the digit count (12 to 19) and the Luhn check digit are checked. There is no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks. -Check if a payment card number is valid using the Luhn algorithm ([ISO/IEC 7812-1](https://www.iso.org/standard/70484.html)). Accepts the usual mask characters (whitespace, `.`, `-` and `/`, the interchangeable set `isValidCpf` and `isValidCnpj` accept) between any two digits and whitespace around the value; any other character makes the value invalid. They are accepted between any two digits rather than at fixed positions because the printed grouping of a PAN changes with the brand (4-4-4-4 for Visa and Mastercard, 4-6-5 for American Express, 4-6-4 for Diners Club), so there is no single layout to pin them to. Performs no brand detection (Visa, Mastercard, Amex...), issuer range lookup or expiration/CVV checks, only the digit count (12 to 19) and the Luhn check digit. A `number` is only accepted when it is a non-negative safe integer: anything above `Number.MAX_SAFE_INTEGER` (2^53 - 1, 16 digits) has already been rounded to a different number before the function sees it, so pass a longer PAN as a string. A value whose digits are all the same (`'0000000000000000'`) is rejected even when it passes the Luhn check, the way every other validator of this package rejects a repeated-digit document (`isValidCpf('00000000000')`, `isValidCns`, `isValidCaepf`, `isValidCei`). +- Accepts a string or a number, with the mask characters (whitespace, `.`, `-` and `/`) anywhere between the digits. ```javascript import { isValidCreditCard } from '@brazilian-utils/brazilian-utils'; @@ -2248,6 +2713,7 @@ isValidCreditCard('4111111111111111'); // true (Visa test number) isValidCreditCard('5555555555554444'); // true (Mastercard test number) isValidCreditCard('378282246310005'); // true (American Express test number) isValidCreditCard('4111 1111 1111 1111'); // true (spaced mask) +isValidCreditCard('4111 - 1111 - 1111 - 1111'); // true (a run of separators between the digits) isValidCreditCard('4111.1111/1111-1111'); // true (any of the mask characters) isValidCreditCard('4111111111111112'); // false (bad check digit) isValidCreditCard('0000000000000000'); // false (every digit the same, though the Luhn check passes) @@ -2255,24 +2721,42 @@ isValidCreditCard('4111a1111b1111c1111'); // false (letters between the digits) isValidCreditCard(4111111111111111111); // false (above 2^53 - 1, pass it as a string) ``` -## isValidRegistroProfissional +Source: [ISO/IEC 7812-1](https://www.iso.org/standard/70484.html). -Check the structure of a professional council registration number (registro/inscrição profissional). It takes a single object, typed as `IsValidRegistroProfissionalParams`, the shape `isValidBankAccount` takes: `value` is the registration number, `council` picks the issuing council (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`) and the optional `stateCode` checks the embedded UF (ignored for `"CRP"`, whose 2 digit prefix is a regional code, not a literal UF). Anything that is not an object, and an object missing `value` or `council`, is `false`. The accepted shapes are 4 to 6 digits plus the UF for `"OAB"` and `"CRM"`, 3 to 6 digits plus the UF for `"CRO"`, a 2 digit regional code plus 4 to 6 digits for `"CRP"`, and the UF plus 6 digits, the tipo de registro and one check digit for `"CRC"`. This is a structural check only: digit counts and the UF are validated, but no check digit is computed, even for CRC, whose format includes one. A CRC registration is the UF, 6 digits, the tipo de registro (`"O"` Originário or `"P"` Provisório, which says nothing about the professional category) and the check digit, as published in the [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf) (item 1.1). A Registro Transferido or Secundário appends `"T"` or `"S"` and the UF of the destination CRC **after** the check digit, per that same item and [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), art. 5º parágrafo único: the Manual's own examples are `SP-123456/O-3 T-MG`, `TO-654321/P-8 T-SC` and `PI-111222/O-5 S-AC`. Both UFs must be real state codes, and `stateCode` is compared against the originating one. A CRP regional code has to be one of the [24 Conselhos Regionais](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/) of the CFP system, CRP-01 to CRP-24. Only the CRC shape and those CRP regional codes rest on a published source: the CFP page publishes no length for the inscription number itself, and the OAB, the CFM and the CFO publish no format at all, so the digit ranges accepted for `"CRP"`, `"OAB"`, `"CRM"` and `"CRO"` are conventional rather than normative (the OAB/SP public search field is `maxlength="7"`, and the CFM documents `300`-prefixed and `P`-suffixed CRMs, none of which these shapes express). CREA is not supported: its registration format could not be confirmed from an official, publicly documented source after the 2016 national unification (RNP). +## Professional registration + +### isValidRegistroProfissional + +Check the structure of a professional council registration number (registro/inscrição profissional). Only the digit count and the UF are checked, never a check digit, even for CRC. + +- Takes an object (`IsValidRegistroProfissionalParams`): `value`, `council` (`"OAB"`, `"CRM"`, `"CRO"`, `"CRP"` or `"CRC"`, a `RegistroProfissionalCouncil`) and an optional `stateCode` (expected UF). +- `"OAB"` and `"CRM"`: 4 to 6 digits plus the UF (`123456/SP`, `123456-SP`); `"CRO"`: 3 to 6 digits (`12345/SP`). +- `"CRP"`: a 2-digit regional code (`01` to `24`) plus 4 to 6 digits (`06/12345`); `stateCode` is ignored. +- `"CRC"`: UF, 6 digits, tipo de registro (`O` or `P`) and check digit (`SP-123456/O-3`); a transfer appends `T` or `S` and the destination UF (`SP-123456/O-3 T-MG`). `stateCode` matches the originating UF. +- The OAB, CRM, CRO and CRP shapes are conventional (no published format). CREA is not covered. ```javascript import { isValidRegistroProfissional } from '@brazilian-utils/brazilian-utils'; isValidRegistroProfissional({ value: '123456/SP', council: 'OAB' }); // true isValidRegistroProfissional({ value: '123456-RJ', council: 'OAB', stateCode: 'SP' }); // false (UF mismatch) +isValidRegistroProfissional({ value: '123456', council: 'OAB' }); // false (no UF) isValidRegistroProfissional({ value: '06/12345', council: 'CRP' }); // true isValidRegistroProfissional({ value: 'SP-123456/O-3', council: 'CRC' }); // true isValidRegistroProfissional({ value: 'SP-123456/O-3 T-MG', council: 'CRC' }); // true (registro transferido) isValidRegistroProfissional({ value: 'SP-123456/T-3', council: 'CRC' }); // false ("T" is not a tipo de registro) ``` -## isValidVin +Source: [Manual de Registro do Sistema CFC/CRCs](https://cfc.org.br/wp-content/uploads/2018/04/1_manual_registro.pdf), [Resolução CFC nº 1.707/2023](https://www1.cfc.org.br/sisweb/SRE/docs/Res_1707.pdf), [CFP regional councils](https://site.cfp.org.br/cfp/sistema-conselhos/conselhos-pelo-brasil/). + +## VIN -Check if a VIN (Vehicle Identification Number / chassi) is valid. Checks the length (17 characters), the excluded letters (`I`, `O`, `Q` are never valid; [ISO 3779:2009](https://www.iso.org/standard/52200.html) structure) and the check digit at the 9th position, with the check digit and transliteration computed per [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15). That check digit is a North-American requirement (49 CFR 565.15 / SAE J853): [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf) (which revoked Resolução CONTRAN nº 24/1998 from 1 January 2025) and ABNT NBR 6066 define the Brazilian VIN structure but do not mandate it, so many Brazilian-built VINs do not carry a matching check digit. This function is therefore a North-American-style structural check, not a universal validator of Brazilian VINs. Case-insensitive and trims surrounding whitespace. A VIN is printed as one unbroken run of 17 characters, so, unlike the documents this package masks (`isValidCpf`, `isValidCnpj`, `isValidNfeKey`), it has no group boundary to write a separator at and none is accepted: a space, `.`, `-` or `/` among the characters is rejected instead of being stripped. A value whose 17 characters are all the same (`'00000000000000000'`) is rejected even when it carries a matching check digit, the way every other validator of this package rejects a repeated-digit document. +### isValidVin + +Check if a VIN (Vehicle Identification Number / chassi) is valid. This is a North-American-style structural check, not a universal validator of Brazilian VINs. + +- Checks the 17-character length, the excluded letters `I`, `O` and `Q`, and the check digit at position 9. +- Brazilian rules do not mandate the check digit, so many Brazilian-built VINs fail it. ```javascript import { isValidVin } from '@brazilian-utils/brazilian-utils'; @@ -2282,4 +2766,56 @@ isValidVin('1m8gdm9axkp042788'); // true (check digit X, lowercase) isValidVin('1HGCM82633A004353'); // false (bad check digit) isValidVin('00000000000000000'); // false (every character the same, though the check digit matches) isValidVin('1HGCM8263IA004352'); // false (contains the excluded letter I) +isValidVin('1HGCM82633A00435'); // false (16 characters) +``` + +Source: [ISO 3779:2009](https://www.iso.org/standard/52200.html), [49 CFR 565.15](https://www.ecfr.gov/current/title-49/section-565.15) and [Resolução CONTRAN nº 968/2022](https://www.gov.br/transportes/pt-br/assuntos/transito/conteudo-contran/resolucoes/resolucao9682022.pdf). + +## Standard Schema + +### toStandardSchema + +Wrap an `isValid*` utility in a [Standard Schema](https://standardschema.dev), the validator format that form libraries, routers and API frameworks accept: TanStack Form, react-hook-form, tRPC, Hono and more. + +- `config.options` is handed to the validator on every call, and `config.message` is the message of the issue (default `'Invalid value'`). Both are part of `ToStandardSchemaOptions`. +- It validates synchronously and does not transform: a valid value comes back as it was given, an invalid one yields a single issue. +- Validators that take an object (`isValidBankAccount`, `isValidRegistroProfissional`, `isValidIe`) work the same way. Wrap the overloaded `isValidIe` in an arrow function: `toStandardSchema((params) => isValidIe(params))`. +- The types of the specification (`StandardSchemaV1`, `StandardSchemaV1Result`, `StandardSchemaV1Issue` and the rest) are exported too, so nothing else is installed. + +```javascript +import { isValidCnpj, isValidCpf, toStandardSchema } from '@brazilian-utils/brazilian-utils'; + +const cpf = toStandardSchema(isValidCpf, { message: 'CPF inválido' }); + +cpf['~standard'].validate('123.456.789-09'); // { value: '123.456.789-09' } +cpf['~standard'].validate('123'); // { issues: [{ message: 'CPF inválido' }] } + +const cnpj = toStandardSchema(isValidCnpj, { options: { version: 2 } }); // alphanumeric CNPJ + +// Anything that takes a Standard Schema takes it as is, a TanStack Form field for example +; +``` + +Inside a Zod or Valibot schema the validators plug in directly, with no wrapper, and the result is itself a Standard Schema: + +```javascript +import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'; +import { isValidCep, isValidCpf } from '@brazilian-utils/brazilian-utils'; +import { useForm } from 'react-hook-form'; +import * as v from 'valibot'; +import { z } from 'zod'; + +const zodSchema = z.object({ + cpf: z.string().refine(isValidCpf, 'CPF inválido'), + cep: z.string().refine(isValidCep, 'CEP inválido'), +}); + +const valibotSchema = v.object({ + cpf: v.pipe(v.string(), v.check(isValidCpf, 'CPF inválido')), + cep: v.pipe(v.string(), v.check(isValidCep, 'CEP inválido')), +}); + +const form = useForm({ resolver: standardSchemaResolver(zodSchema) }); // or valibotSchema ``` + +Source: [Standard Schema specification](https://standardschema.dev). diff --git a/jsr.json b/jsr.json new file mode 100644 index 000000000..4b41130dd --- /dev/null +++ b/jsr.json @@ -0,0 +1,151 @@ +{ + "name": "@brazilian-utils/brazilian-utils", + "version": "2.4.0", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./add-business-days": "./src/add-business-days/add-business-days.ts", + "./capitalize": "./src/capitalize/capitalize.ts", + "./convert-currency-to-words": "./src/convert-currency-to-words/convert-currency-to-words.ts", + "./convert-date-to-words": "./src/convert-date-to-words/convert-date-to-words.ts", + "./convert-license-plate-to-mercosul": "./src/convert-license-plate-to-mercosul/convert-license-plate-to-mercosul.ts", + "./convert-number-to-words": "./src/convert-number-to-words/convert-number-to-words.ts", + "./difference-in-business-days": "./src/difference-in-business-days/difference-in-business-days.ts", + "./format-boleto": "./src/format-boleto/format-boleto.ts", + "./format-caepf": "./src/format-caepf/format-caepf.ts", + "./format-cei": "./src/format-cei/format-cei.ts", + "./format-cep": "./src/format-cep/format-cep.ts", + "./format-certidao": "./src/format-certidao/format-certidao.ts", + "./format-cnae": "./src/format-cnae/format-cnae.ts", + "./format-cnh": "./src/format-cnh/format-cnh.ts", + "./format-cno": "./src/format-cno/format-cno.ts", + "./format-cnpj": "./src/format-cnpj/format-cnpj.ts", + "./format-cns": "./src/format-cns/format-cns.ts", + "./format-cpf": "./src/format-cpf/format-cpf.ts", + "./format-currency": "./src/format-currency/format-currency.ts", + "./format-iban": "./src/format-iban/format-iban.ts", + "./format-legal-nature": "./src/format-legal-nature/format-legal-nature.ts", + "./format-license-plate": "./src/format-license-plate/format-license-plate.ts", + "./format-ncm": "./src/format-ncm/format-ncm.ts", + "./format-nfe-key": "./src/format-nfe-key/format-nfe-key.ts", + "./format-passport": "./src/format-passport/format-passport.ts", + "./format-phone": "./src/format-phone/format-phone.ts", + "./format-pis": "./src/format-pis/format-pis.ts", + "./format-processo-juridico": "./src/format-processo-juridico/format-processo-juridico.ts", + "./format-voter-id": "./src/format-voter-id/format-voter-id.ts", + "./generate-boleto": "./src/generate-boleto/generate-boleto.ts", + "./generate-cep": "./src/generate-cep/generate-cep.ts", + "./generate-cnh": "./src/generate-cnh/generate-cnh.ts", + "./generate-cnpj": "./src/generate-cnpj/generate-cnpj.ts", + "./generate-cpf": "./src/generate-cpf/generate-cpf.ts", + "./generate-legal-nature": "./src/generate-legal-nature/generate-legal-nature.ts", + "./generate-license-plate": "./src/generate-license-plate/generate-license-plate.ts", + "./generate-passport": "./src/generate-passport/generate-passport.ts", + "./generate-phone": "./src/generate-phone/generate-phone.ts", + "./generate-pis": "./src/generate-pis/generate-pis.ts", + "./generate-pix-payload": "./src/generate-pix-payload/generate-pix-payload.ts", + "./generate-processo-juridico": "./src/generate-processo-juridico/generate-processo-juridico.ts", + "./generate-renavam": "./src/generate-renavam/generate-renavam.ts", + "./generate-voter-id": "./src/generate-voter-id/generate-voter-id.ts", + "./get-address-info-by-cep": "./src/get-address-info-by-cep/get-address-info-by-cep.ts", + "./get-area-code-info": "./src/get-area-code-info/get-area-code-info.ts", + "./get-area-codes-by-state": "./src/get-area-codes-by-state/get-area-codes-by-state.ts", + "./get-bank-by-code": "./src/get-bank-by-code/get-bank-by-code.ts", + "./get-bank-by-ispb": "./src/get-bank-by-ispb/get-bank-by-ispb.ts", + "./get-banks": "./src/get-banks/get-banks.ts", + "./get-boleto-info": "./src/get-boleto-info/get-boleto-info.ts", + "./get-cbo": "./src/get-cbo/get-cbo.ts", + "./get-cep-info-by-address": "./src/get-cep-info-by-address/get-cep-info-by-address.ts", + "./get-certidao-info": "./src/get-certidao-info/get-certidao-info.ts", + "./get-cfop": "./src/get-cfop/get-cfop.ts", + "./get-cities": "./src/get-cities/get-cities.ts", + "./get-cnae": "./src/get-cnae/get-cnae.ts", + "./get-format-license-plate": "./src/get-format-license-plate/get-format-license-plate.ts", + "./get-holidays": "./src/get-holidays/get-holidays.ts", + "./get-iban-info": "./src/get-iban-info/get-iban-info.ts", + "./get-legal-nature": "./src/get-legal-nature/get-legal-nature.ts", + "./get-legal-natures": "./src/get-legal-natures/get-legal-natures.ts", + "./get-legal-natures-by-category": "./src/get-legal-natures-by-category/get-legal-natures-by-category.ts", + "./get-municipalities": "./src/get-municipalities/get-municipalities.ts", + "./get-municipality": "./src/get-municipality/get-municipality.ts", + "./get-municipality-by-code": "./src/get-municipality-by-code/get-municipality-by-code.ts", + "./get-nfe-key-info": "./src/get-nfe-key-info/get-nfe-key-info.ts", + "./get-pix-key-info": "./src/get-pix-key-info/get-pix-key-info.ts", + "./get-pix-payload-info": "./src/get-pix-payload-info/get-pix-payload-info.ts", + "./get-state-by-ibge-code": "./src/get-state-by-ibge-code/get-state-by-ibge-code.ts", + "./get-state-code-by-name": "./src/get-state-code-by-name/get-state-code-by-name.ts", + "./get-state-name-by-code": "./src/get-state-name-by-code/get-state-name-by-code.ts", + "./get-states": "./src/get-states/get-states.ts", + "./get-timezone-by-state": "./src/get-timezone-by-state/get-timezone-by-state.ts", + "./is-business-day": "./src/is-business-day/is-business-day.ts", + "./is-holiday": "./src/is-holiday/is-holiday.ts", + "./is-valid-bank-account": "./src/is-valid-bank-account/is-valid-bank-account.ts", + "./is-valid-boleto": "./src/is-valid-boleto/is-valid-boleto.ts", + "./is-valid-caepf": "./src/is-valid-caepf/is-valid-caepf.ts", + "./is-valid-cbo": "./src/is-valid-cbo/is-valid-cbo.ts", + "./is-valid-cei": "./src/is-valid-cei/is-valid-cei.ts", + "./is-valid-cep": "./src/is-valid-cep/is-valid-cep.ts", + "./is-valid-certidao": "./src/is-valid-certidao/is-valid-certidao.ts", + "./is-valid-cfop": "./src/is-valid-cfop/is-valid-cfop.ts", + "./is-valid-cnae": "./src/is-valid-cnae/is-valid-cnae.ts", + "./is-valid-cnh": "./src/is-valid-cnh/is-valid-cnh.ts", + "./is-valid-cno": "./src/is-valid-cno/is-valid-cno.ts", + "./is-valid-cnpj": "./src/is-valid-cnpj/is-valid-cnpj.ts", + "./is-valid-cns": "./src/is-valid-cns/is-valid-cns.ts", + "./is-valid-cpf": "./src/is-valid-cpf/is-valid-cpf.ts", + "./is-valid-credit-card": "./src/is-valid-credit-card/is-valid-credit-card.ts", + "./is-valid-csosn": "./src/is-valid-csosn/is-valid-csosn.ts", + "./is-valid-cst": "./src/is-valid-cst/is-valid-cst.ts", + "./is-valid-email": "./src/is-valid-email/is-valid-email.ts", + "./is-valid-iban": "./src/is-valid-iban/is-valid-iban.ts", + "./is-valid-ie": "./src/is-valid-ie/is-valid-ie.ts", + "./is-valid-landline-phone": "./src/is-valid-landline-phone/is-valid-landline-phone.ts", + "./is-valid-legal-nature": "./src/is-valid-legal-nature/is-valid-legal-nature.ts", + "./is-valid-license-plate": "./src/is-valid-license-plate/is-valid-license-plate.ts", + "./is-valid-mobile-phone": "./src/is-valid-mobile-phone/is-valid-mobile-phone.ts", + "./is-valid-ncm": "./src/is-valid-ncm/is-valid-ncm.ts", + "./is-valid-nfe-key": "./src/is-valid-nfe-key/is-valid-nfe-key.ts", + "./is-valid-passport": "./src/is-valid-passport/is-valid-passport.ts", + "./is-valid-phone": "./src/is-valid-phone/is-valid-phone.ts", + "./is-valid-pis": "./src/is-valid-pis/is-valid-pis.ts", + "./is-valid-pix-key": "./src/is-valid-pix-key/is-valid-pix-key.ts", + "./is-valid-pix-payload": "./src/is-valid-pix-payload/is-valid-pix-payload.ts", + "./is-valid-processo-juridico": "./src/is-valid-processo-juridico/is-valid-processo-juridico.ts", + "./is-valid-registro-profissional": "./src/is-valid-registro-profissional/is-valid-registro-profissional.ts", + "./is-valid-renavam": "./src/is-valid-renavam/is-valid-renavam.ts", + "./is-valid-service-phone": "./src/is-valid-service-phone/is-valid-service-phone.ts", + "./is-valid-vin": "./src/is-valid-vin/is-valid-vin.ts", + "./is-valid-voter-id": "./src/is-valid-voter-id/is-valid-voter-id.ts", + "./parse-boleto": "./src/parse-boleto/parse-boleto.ts", + "./parse-caepf": "./src/parse-caepf/parse-caepf.ts", + "./parse-cbo": "./src/parse-cbo/parse-cbo.ts", + "./parse-cei": "./src/parse-cei/parse-cei.ts", + "./parse-cep": "./src/parse-cep/parse-cep.ts", + "./parse-certidao": "./src/parse-certidao/parse-certidao.ts", + "./parse-cfop": "./src/parse-cfop/parse-cfop.ts", + "./parse-cnae": "./src/parse-cnae/parse-cnae.ts", + "./parse-cnh": "./src/parse-cnh/parse-cnh.ts", + "./parse-cno": "./src/parse-cno/parse-cno.ts", + "./parse-cnpj": "./src/parse-cnpj/parse-cnpj.ts", + "./parse-cns": "./src/parse-cns/parse-cns.ts", + "./parse-cpf": "./src/parse-cpf/parse-cpf.ts", + "./parse-currency": "./src/parse-currency/parse-currency.ts", + "./parse-iban": "./src/parse-iban/parse-iban.ts", + "./parse-legal-nature": "./src/parse-legal-nature/parse-legal-nature.ts", + "./parse-license-plate": "./src/parse-license-plate/parse-license-plate.ts", + "./parse-ncm": "./src/parse-ncm/parse-ncm.ts", + "./parse-nfe-key": "./src/parse-nfe-key/parse-nfe-key.ts", + "./parse-passport": "./src/parse-passport/parse-passport.ts", + "./parse-phone": "./src/parse-phone/parse-phone.ts", + "./parse-pis": "./src/parse-pis/parse-pis.ts", + "./parse-processo-juridico": "./src/parse-processo-juridico/parse-processo-juridico.ts", + "./parse-voter-id": "./src/parse-voter-id/parse-voter-id.ts", + "./remove-accents": "./src/remove-accents/remove-accents.ts", + "./sub-business-days": "./src/sub-business-days/sub-business-days.ts", + "./to-standard-schema": "./src/to-standard-schema/to-standard-schema.ts" + }, + "publish": { + "include": ["LICENSE", "README.md", "jsr.json", "src/**/*.ts"], + "exclude": ["src/**/*.test.ts", "src/_internals/test/**"] + } +} diff --git a/package.json b/package.json index 475e1de5c..bce2d837d 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,9 @@ "build:data": "node ./scripts/data.ts", "build:llms": "node ./scripts/llms.ts", "build:site": "node ./scripts/site.ts", + "build:jsr": "node ./scripts/jsr.ts", + "build:examples": "node ./scripts/examples.ts", + "build:docs": "npm run build:examples && npm run build:llms && npm run build:site", "prepublishOnly": "vp run build", "check:dependencies": "node -e \"const d=require('./package.json').dependencies||{};if(Object.keys(d).length){console.error('runtime dependencies are not allowed:',Object.keys(d).join(', '));process.exit(1)}\"" }, diff --git a/release-please-config.json b/release-please-config.json index 35182664a..35b4d413d 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -22,6 +22,8 @@ { "type": "style", "section": "Style", "hidden": true } ], "packages": { - ".": {} + ".": { + "extra-files": [{ "type": "json", "path": "jsr.json", "jsonpath": "$.version" }] + } } } diff --git a/reports/api/brazilian-utils.api.md b/reports/api/brazilian-utils.api.md index 31135c54f..75b328bda 100644 --- a/reports/api/brazilian-utils.api.md +++ b/reports/api/brazilian-utils.api.md @@ -979,6 +979,55 @@ export type RegistroProfissionalCouncil = "OAB" | "CRM" | "CRO" | "CRP" | "CRC"; // @public export const removeAccents: (value: string) => string; +// @public +export type StandardSchemaV1 = { + readonly "~standard": StandardSchemaV1Props; +}; + +// @public +export type StandardSchemaV1FailureResult = { + readonly issues: readonly StandardSchemaV1Issue[]; +}; + +// @public +export type StandardSchemaV1Issue = { + readonly message: string; + readonly path?: readonly (PropertyKey | StandardSchemaV1PathSegment)[]; +}; + +// @public +export type StandardSchemaV1Options = { + readonly libraryOptions?: Record; +}; + +// @public +export type StandardSchemaV1PathSegment = { + readonly key: PropertyKey; +}; + +// @public +export type StandardSchemaV1Props = { + readonly version: 1; + readonly vendor: string; + readonly validate: (value: unknown, options?: StandardSchemaV1Options) => StandardSchemaV1Result | Promise>; + readonly types?: StandardSchemaV1Types; +}; + +// @public +export type StandardSchemaV1Result = StandardSchemaV1SuccessResult | StandardSchemaV1FailureResult; + +// @public +export type StandardSchemaV1SuccessResult = { + readonly value: Output; + readonly issues?: undefined; +}; + +// @public +export type StandardSchemaV1Types = { + readonly input: Input; + readonly output: Output; +}; + // @public export type State = { readonly code: "AC"; @@ -1153,6 +1202,15 @@ export type StateName = State["name"]; // @public export const subBusinessDays: (date: Date, amount: number, options?: BusinessDayOptions) => Date | null; +// @public +export const toStandardSchema: (validate: (value: Value, options?: Options) => boolean, config?: ToStandardSchemaOptions) => StandardSchemaV1; + +// @public +export type ToStandardSchemaOptions = { + options?: Options; + message?: string; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/scripts/data-summary.ts b/scripts/data-summary.ts new file mode 100644 index 000000000..3dd872471 --- /dev/null +++ b/scripts/data-summary.ts @@ -0,0 +1,118 @@ +#!/usr/bin/env node + +/** + * Writes the Markdown body of the dataset refresh pull request: for every generated dataset file + * that `npm run build:data` changed, how many entries were added and removed and a sample of + * them, read from `git diff`. The generated files hold about one entry per line, so a line + * diff is an entry diff: a changed entry shows up once as removed and once as added. + * + * Usage: + * node scripts/data-summary.ts [output.md] + * Print the summary, or write it to `output.md`. + */ + +import { execFileSync } from "node:child_process"; +import { writeFileSync } from "node:fs"; + +/** The source each generated file is rebuilt from, as named in the file's own header. */ +const DATASETS: Record = { + "src/_internals/constants/banks.ts": "Banks (Banco Central, STR participants)", + "src/_internals/constants/cbo.ts": "CBO 2002 occupations (Ministério do Trabalho e Emprego)", + "src/_internals/constants/cfop.ts": "CFOP codes (CONFAZ, Convênio SINIEF s/nº 1970)", + "src/_internals/constants/cities.ts": "Municipalities (IBGE)", + "src/_internals/constants/cnae.ts": "CNAE subclasses (IBGE/CONCLA)", + "src/_internals/constants/states.ts": "States (IBGE)", + "src/is-valid-legal-nature/constants.ts": "Legal natures (IBGE/CONCLA)", + "src/is-valid-ncm/constants.ts": "NCM codes (Siscomex)", +}; + +const SAMPLE_SIZE = 15; + +/** + * Runs a command of the toolchain (resolved from `PATH`, as `scripts/data.ts` does with `node` and + * `vp`: the script only ever runs in a checkout, by a maintainer or by CI) and returns its output. + * @param {string} command - The executable. + * @param {string[]} args - Its arguments. + * @returns {string} What it printed. + */ +const capture = (command: string, args: string[]): string => + execFileSync(command, args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 }); + +/** + * @param {string} file - A path relative to the repository root. + * @returns {{ added: string[]; removed: string[] }} The lines the working tree adds to and removes + * from the committed file, without their `+`/`-` marker. + */ +const changedLines = (file: string): { added: string[]; removed: string[] } => { + const diff = capture("git", ["diff", "--unified=0", "--no-color", "--", file]); + const added: string[] = []; + const removed: string[] = []; + + for (const line of diff.split("\n")) { + if (line.startsWith("+++") || line.startsWith("---")) continue; + if (line.startsWith("+")) added.push(line.slice(1).trim()); + if (line.startsWith("-")) removed.push(line.slice(1).trim()); + } + + return { added: added.filter(Boolean), removed: removed.filter(Boolean) }; +}; + +/** + * @param {string} title - "Added" or "Removed". + * @param {string[]} lines - The entries. + * @returns {string[]} A collapsed Markdown block with up to `SAMPLE_SIZE` of them. + */ +const sample = (title: string, lines: string[]): string[] => { + if (lines.length === 0) return []; + + const rest = lines.length - SAMPLE_SIZE; + + return [ + `
${title} (${lines.length})`, + "", + "```text", + ...lines.slice(0, SAMPLE_SIZE), + ...(rest > 0 ? [`... and ${rest} more`] : []), + "```", + "", + "
", + "", + ]; +}; + +const sections: string[] = []; + +for (const [file, label] of Object.entries(DATASETS)) { + const { added, removed } = changedLines(file); + + if (added.length === 0 && removed.length === 0) continue; + + sections.push( + `### ${label}`, + "", + `\`${file}\`: ${added.length} line(s) added, ${removed.length} removed. A changed entry counts once on each side.`, + "", + ...sample("Added", added), + ...sample("Removed", removed), + ); +} + +const body = [ + "Automated refresh of the datasets the package embeds, rebuilt from their official sources by", + "`npm run build:data`. The check and the test suite passed on the result.", + "", + "Review each table against its source for plausibility before merging: a table that lost a", + "large share of its rows is a broken scraper or a source outage, not news.", + "", + "## What changed", + "", + ...(sections.length > 0 ? sections : ["No dataset file changed."]), +].join("\n"); + +const [output] = process.argv.slice(2); + +if (output === undefined) { + console.log(body); +} else { + writeFileSync(output, `${body}\n`); +} diff --git a/scripts/examples.ts b/scripts/examples.ts new file mode 100644 index 000000000..b56568e44 --- /dev/null +++ b/scripts/examples.ts @@ -0,0 +1,350 @@ +#!/usr/bin/env node + +/** + * Writes the examples the document field guide shows (`docs/guides/document-field.md`): one + * focused example per document and framework, so a reader copies the CPF field, not a generic one + * that has to be narrowed down first. Every file comes from a template in + * `docs/snippets/document-field/_templates`, filled in from the table below, together with the + * `mask` function they share. One page runs them all in the browser, `docs/snippets/live`, told + * which files to compile by its query string. The Check workflow fails when these are stale. + * + * Usage: + * node scripts/examples.ts + */ + +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = join(import.meta.dirname, ".."); +const EXAMPLE_DIR = join(ROOT, "docs", "snippets", "document-field"); +const TEMPLATE_DIR = join(EXAMPLE_DIR, "_templates"); + +type Document = { + /** The kebab-case name of the document, which names its folder and its files. */ + kind: string; + /** The name in the code, `Cpf` in `CpfField`. */ + name: string; + /** What the reader sees: the label of the field and of the tab. */ + label: string; + /** The complete format, which also tells the field when the value is complete. */ + placeholder: string; + /** The formatter of the package, and the arguments after the value, if any. */ + format: string; + /** The validator of the package, and the arguments after the value, if any. */ + validator: string; + /** The parser of the package, which takes the mask off, and its arguments after the value. */ + parser: string; + /** What a browser may fill the field with, `off` when there is no token for the document. */ + autocomplete: string; + /** The `inputmode` of the field: an alphanumeric CNPJ needs a keyboard with letters. */ + inputMode: "numeric" | "text"; +}; + +const DOCUMENTS: Document[] = [ + { + kind: "cpf", + name: "Cpf", + label: "CPF", + placeholder: "000.000.000-00", + format: "formatCpf", + validator: "isValidCpf", + parser: "parseCpf", + autocomplete: "off", + inputMode: "numeric", + }, + { + kind: "cnpj", + name: "Cnpj", + label: "CNPJ", + placeholder: "00.ABC.000/0001-00", + format: "formatCnpj, { version: 2 }", + validator: "isValidCnpj, { version: 2 }", + parser: "parseCnpj, { version: 2 }", + autocomplete: "off", + inputMode: "text", + }, + { + kind: "cep", + name: "Cep", + label: "CEP", + placeholder: "00000-000", + format: "formatCep", + validator: "isValidCep", + parser: "parseCep", + autocomplete: "postal-code", + inputMode: "numeric", + }, + { + kind: "phone", + name: "Phone", + label: "Phone", + placeholder: "(00) 00000-0000", + format: 'formatPhone, { mask: "nanp" }', + validator: "isValidPhone", + parser: "parsePhone", + autocomplete: "tel-national", + inputMode: "numeric", + }, +]; + +/** The schema libraries the schema tab shows, each a template of its own. */ +const SCHEMAS = ["zod", "valibot", "arktype", "standard"] as const; + +/** + * The frameworks, each with the extension of its field and form files and the name its mask file + * takes: a hook, a directive or a module, whatever that framework reaches for. + */ +const FRAMEWORKS = [ + { name: "react", field: "tsx", form: "tsx", mask: "use-mask.ts", base: "field.tsx" }, + { name: "angular", field: "ts", form: "ts", mask: "mask.directive.ts", base: "field.ts" }, + { name: "vue", field: "vue", form: "vue", mask: "mask.ts", base: "field.vue" }, +] as const; + +const PLACEHOLDER_PATTERN = /@@(\w+)@@/g; +const MASK_BODY_PATTERN = /^([ \t]*)@@maskBody@@$/m; + +/** + * @param {string} source - A function and the arguments after the value, `formatCnpj, { version: 2 }`. + * @returns {{ fn: string; rest: string }} The function on its own, and the remaining arguments. + */ +function split(source: string): { fn: string; rest: string } { + const [fn = "", ...args] = source.split(", "); + + return { fn, rest: args.length === 0 ? "" : `, ${args.join(", ")}` }; +} + +/** + * @param {{ fn: string; rest: string }} target - A function and its arguments after the value. + * @param {string} value - What to pass as the value. + * @returns {string} The call, `formatCnpj(value, { version: 2 })`. + */ +function call({ fn, rest }: { fn: string; rest: string }, value: string): string { + return `${fn}(${value}${rest})`; +} + +/** + * @param {Document} document - The document the example is about. + * @returns {Record} What every `@@name@@` of a template stands for. + */ +function values(document: Document): Record { + const format = split(document.format); + const validator = split(document.validator); + const parser = split(document.parser); + // A function that takes options is wrapped, so that the mask can call it with a value alone. + const parserExpression = + parser.rest === "" ? parser.fn : `(value: string) => ${call(parser, "value")}`; + const formatter = format.rest === "" ? format.fn : `(value: string) => ${call(format, "value")}`; + + return { + kind: document.kind, + Name: document.name, + label: document.label, + placeholder: document.placeholder, + length: String(document.placeholder.length), + inputMode: document.inputMode, + autocomplete: document.autocomplete, + names: [format.fn, validator.fn, parser.fn].join(", "), + fieldImports: `import { ${[format.fn, parser.fn].join(", ")} } from "@brazilian-utils/brazilian-utils";`, + imports: `import { ${[format.fn, validator.fn].join(", ")} } from "@brazilian-utils/brazilian-utils";`, + formImports: `import { ${validator.fn} } from "@brazilian-utils/brazilian-utils";`, + format: formatter, + // The plain JavaScript example takes the same formatter without the type of its parameter. + formatJs: formatter.replace("(value: string)", "(value)"), + formatCall: call(format, "value"), + formatValue: call(format, "value"), + formatValueVue: call(format, "value.value"), + formatSignal: call(format, "this.value()"), + validator: call(validator, "value"), + validatorPlain: call(validator, "value"), + validatorValue: call(validator, "value.value"), + validatorText: call(validator, "text.value"), + validatorSignal: call(validator, "this.value()"), + validatorControl: call(validator, "control.value"), + validatorFn: validator.fn, + // The schema is a schema of strings, so a validator that takes more than that is narrowed. + standardValidator: + validator.rest === "" ? validator.fn : `(value: string) => ${call(validator, "value")}`, + // Inside a schema the validator is called on the value alone, wrapped when it takes options. + validatorArrow: + validator.rest === "" + ? validator.fn + : `(${document.kind}) => ${call(validator, document.kind)}`, + // Valibot's `check` takes a validator of `string` alone, so the call is always wrapped. + validatorLambda: `(${document.kind}) => ${call(validator, document.kind)}`, + validatorCtx: call(validator, document.kind), + validatorOptions: validator.rest === "" ? "" : `\n options:${validator.rest.slice(1)},`, + parseInput: call(parser, "input.value"), + parseMaskedEvent: call(parser, "event.currentTarget.value"), + parseMasked: call(parser, "masked"), + parse: parserExpression, + parseMaskValue: call(parser, "maskValue(event)"), + parseMaskedEventVue: call(parser, "(event.target as HTMLInputElement).value"), + parseMaskedEventAngular: call(parser, "(event.target as HTMLInputElement).value"), + }; +} + +/** + * @param {string} name - A file of `docs/snippets/document-field/_templates`. + * @returns {string} Its contents. + */ +function readTemplate(name: string): string { + return readFileSync(join(TEMPLATE_DIR, name), "utf8"); +} + +/** + * @param {string} code - A block of code. + * @param {number} spaces - How far to indent it. + * @returns {string} The block, with every non-empty line indented. + */ +function indent(code: string, spaces: number): string { + return code + .split("\n") + .map((line) => (line.trim() === "" ? line : " ".repeat(spaces) + line)) + .join("\n"); +} + +/** The mask, written once and inlined into each hook, directive and listener. */ +const maskBody = readTemplate("mask-body.ts").trim(); + +/** + * @param {string} template - The template, with `@@name@@` placeholders. + * @param {Record} substitutions - What each placeholder stands for. + * @returns {string} The filled in template. + */ +function fill(template: string, substitutions: Record): string { + // The shared body is written once, indented to where the template asks for it. + const body = template.replace(MASK_BODY_PATTERN, (_match, spaces: string) => + indent(maskBody, spaces.length), + ); + + return body.replace(PLACEHOLDER_PATTERN, (match, name: string) => { + const value = substitutions[name]; + + if (value === undefined) throw new Error(`No value for ${match}`); + + return value; + }); +} + +rmSync(join(EXAMPLE_DIR, "generated"), { force: true, recursive: true }); + +for (const document of DOCUMENTS) { + const folder = join(EXAMPLE_DIR, "generated", document.kind); + + mkdirSync(folder, { recursive: true }); + + for (const framework of FRAMEWORKS) { + // A folder per framework: a reader copies one, and two of them name a file the same way. + const frameworkFolder = join(folder, framework.name); + + mkdirSync(frameworkFolder, { recursive: true }); + + writeFileSync( + join(frameworkFolder, framework.base), + fill(readTemplate(`${framework.name}/${framework.base}`), values(document)), + ); + + writeFileSync( + join(frameworkFolder, framework.mask), + fill(readTemplate(`${framework.name}/mask.ts`), { + ...values(document), + }), + ); + + for (const [part, extension] of [ + ["field", framework.field], + ["form", framework.form], + ] as const) { + const template = readTemplate( + `${framework.name}/${part === "field" ? "document-field" : part}.${extension}`, + ); + const file = `${document.kind}-${part}.${extension}`; + + writeFileSync(join(frameworkFolder, file), fill(template, { ...values(document) })); + } + } + + mkdirSync(join(folder, "schema"), { recursive: true }); + + for (const schema of SCHEMAS) { + const template = readTemplate(`schema/${schema}.ts`); + + writeFileSync( + join(folder, "schema", `${document.kind}-${schema}.ts`), + fill(template, values(document)), + ); + } + + mkdirSync(join(folder, "vanilla"), { recursive: true }); + + writeFileSync( + join(folder, "vanilla", `${document.kind}-field.html`), + fill(readTemplate("vanilla/field.html"), values(document)), + ); +} + +// The address guide types a CEP into the very field the document field guide builds, so that +// field and its mask are written there too, from the same templates. The guide shows neither: it +// says where they come from and gets on with the lookup. +const ADDRESS_DIR = join(ROOT, "docs", "snippets", "address-form"); +const cep = DOCUMENTS.find((document) => document.kind === "cep"); + +if (cep === undefined) throw new Error("The address guide needs the CEP of the documents table"); + +for (const framework of FRAMEWORKS) { + const folder = join(ADDRESS_DIR, framework.name); + + writeFileSync( + join(folder, framework.mask), + fill(readTemplate(`${framework.name}/mask.ts`), values(cep)), + ); + + writeFileSync( + join(folder, framework.base), + fill(readTemplate(`${framework.name}/${framework.base}`), values(cep)), + ); + + writeFileSync( + join(folder, `cep-field.${framework.field}`), + fill(readTemplate(`${framework.name}/document-field.${framework.field}`), values(cep)), + ); +} + +// A page that points at a file which is not there renders whatever the server answers with, so +// the pages that show these examples are checked against what was just written. +const PAGES = [ + join(ROOT, "docs", "guides", "document-field.md"), + join(ROOT, "docs", "pt-br", "guides", "document-field.md"), + join(ROOT, "docs", "guides", "address-form.md"), + join(ROOT, "docs", "pt-br", "guides", "address-form.md"), + join(ROOT, "docs", "guides", "state-city.md"), + join(ROOT, "docs", "pt-br", "guides", "state-city.md"), + join(ROOT, "docs", "guides", "schema.md"), + join(ROOT, "docs", "pt-br", "guides", "schema.md"), +]; +const INCLUDE_PATTERN = /\]\((\.[^ )]+) ':include/g; +const FILE_DIV_PATTERN = /
/g; + +for (const page of PAGES) { + const folder = join(page, ".."); + + const markdown = readFileSync(page, "utf8"); + + for (const [, target] of markdown.matchAll(INCLUDE_PATTERN)) { + const file = join(folder, target ?? ""); + + if (!existsSync(file)) throw new Error(`${page} includes ${target}, which is not there`); + } + + // A file block left open swallows the next one, which then cannot be shown on its own. + let open = 0; + + for (const [tag] of markdown.matchAll(FILE_DIV_PATTERN)) { + open += tag === "
" ? -1 : 1; + + if (open > 1) throw new Error(`${page} has a file block inside another one`); + if (open < 0) open = 0; + } +} + +console.log(`Wrote ${DOCUMENTS.length} documents × ${FRAMEWORKS.length + 1} frameworks`); diff --git a/scripts/jsr.ts b/scripts/jsr.ts new file mode 100644 index 000000000..ea7dc977e --- /dev/null +++ b/scripts/jsr.ts @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +/** + * Keeps the `exports` of `jsr.json` in step with `src/`: the package root plus one subpath per + * utility folder, the same folders `vite.config.ts` turns into the npm subpaths, so + * `jsr:@brazilian-utils/brazilian-utils/is-valid-cpf` resolves like its npm counterpart. JSR + * publishes the TypeScript sources as they are, so an export points at the `.ts` file. + * + * Everything else in `jsr.json` is left alone, the `version` in particular: release-please bumps + * it together with `package.json` (see `extra-files` in `release-please-config.json`). The Check + * workflow fails when the file is stale. + */ + +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = join(import.meta.dirname, ".."); +const SRC_DIR = join(ROOT, "src"); +const JSR_PATH = join(ROOT, "jsr.json"); + +const utilNames = readdirSync(SRC_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")) + .map((entry) => entry.name) + .filter((name) => existsSync(join(SRC_DIR, name, `${name}.ts`))) + .toSorted(); + +const exports: Record = { ".": "./src/index.ts" }; + +for (const name of utilNames) { + exports[`./${name}`] = `./src/${name}/${name}.ts`; +} + +// Only the `exports` block is rewritten, in place: the rest of the file keeps the layout the +// formatter gave it, so a second run (the staleness check of the Check workflow) finds nothing to +// change, and the `version` release-please maintains is never touched. +const EXPORTS_BLOCK = /"exports": \{[^}]*\}/; +const current = readFileSync(JSR_PATH, "utf8"); + +if (!EXPORTS_BLOCK.test(current)) { + throw new Error("jsr.json has no exports block"); +} + +const block = JSON.stringify(exports, null, "\t").replaceAll("\n", "\n\t"); + +writeFileSync( + JSR_PATH, + current.replace(EXPORTS_BLOCK, () => `"exports": ${block}`), +); diff --git a/scripts/llms.ts b/scripts/llms.ts index a97d9c013..6b0ea9ce9 100644 --- a/scripts/llms.ts +++ b/scripts/llms.ts @@ -225,7 +225,7 @@ function buildLlmsTxt(utils: UtilSection[], datasetUtils: string[]): string { return `# Brazilian Utils -> Brazilian Utils is a zero-dependency JavaScript/TypeScript library of small, focused utilities for the day-to-day problems of building software for Brazilian businesses: validating, formatting, parsing and generating documents (CPF, CNPJ, CEP, Pix, boleto, NF-e, phone numbers, license plates and more). +> Brazilian Utils is a zero-dependency JavaScript/TypeScript library of small, focused utilities for the day-to-day problems of building software for Brazil: validating, formatting, parsing and generating CPF, CNPJ, CEP, Pix, boleto, NF-e, phone numbers, license plates and more. The package has **zero runtime dependencies**, is fully tree-shakeable and runs on Node.js \`^20.19.0 || >=22.12.0\`, Bun, Deno and modern browsers (including a UMD \`