From dbff7d8508a46335742d36cb9238c37a5a92976e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 04:00:23 +0000 Subject: [PATCH] feat(spec): port formatCnpj to the bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second utility, and the one that shows what sharing costs. It reads the same two character classes `isValidCnpj` reads, out of the same `source/_internals/cnpj.ts`, and adds a mask helper of its own under `source/_internals/mask.ts` — written once, for every `format*` utility that comes after it. What each target ends up with is still one self-contained module: the generated `format-cnpj` carries the mask helper and the two classes it uses, and not a line of the check digit code, because the compiler prunes what the utility cannot reach. That is the property the npm package needs, and it is why the helpers are inlined rather than emitted as a shared module per target. Three options that interact — `pad`, `version`, `obfuscate` — over the same corpus, so all seven combinations are replayed against every input. typescript 4025/4025 python 4025/4025 ruby 4025/4025 go 3983/3983 rust 3983/3983 java 3983/3983 csharp 3983/3983 cabi 3983/3983 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UX1gTGeMTyoXQyr1qUoQKd --- spec/bridge/README.md | 1 + spec/bridge/conformance/cases/format-cnpj.ts | 28 +++++++++ spec/bridge/source/_internals/mask.ts | 56 +++++++++++++++++ spec/bridge/source/format-cnpj.ts | 63 ++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 spec/bridge/conformance/cases/format-cnpj.ts create mode 100644 spec/bridge/source/_internals/mask.ts create mode 100644 spec/bridge/source/format-cnpj.ts diff --git a/spec/bridge/README.md b/spec/bridge/README.md index cad288ba..964fc225 100644 --- a/spec/bridge/README.md +++ b/spec/bridge/README.md @@ -149,6 +149,7 @@ be handed it, so nothing is skipped for being inconvenient, and the count is vis | utility | what makes it worth porting | | ------------- | ----------------------------------------------------------------- | | `isValidCnpj` | regular expressions, check digits, JavaScript's own type coercion | +| `formatCnpj` | a shared mask helper, and three options that interact | ## Adding a utility diff --git a/spec/bridge/conformance/cases/format-cnpj.ts b/spec/bridge/conformance/cases/format-cnpj.ts new file mode 100644 index 00000000..6363e141 --- /dev/null +++ b/spec/bridge/conformance/cases/format-cnpj.ts @@ -0,0 +1,28 @@ +/** + * What `formatCnpj` is replayed with: the whole CNPJ corpus, under every option combination. + */ +import { type Recorder } from "../cases.ts"; +import { cnpjCorpus } from "./_cnpj-corpus.ts"; + +const OPTIONS = [ + undefined, + { version: 2 }, + { pad: true }, + { obfuscate: true }, + { pad: true, version: 2 }, + { obfuscate: true, version: 2 }, + { obfuscate: true, pad: true }, +]; + +export const recorder: Recorder = { + module: "format-cnpj", + inputs: (shipped) => { + const corpus = cnpjCorpus(shipped["generateCnpj"] as (version?: 1 | 2) => string); + const found: unknown[][] = []; + + for (const input of corpus) + for (const options of OPTIONS) found.push(options === undefined ? [input] : [input, options]); + + return found; + }, +}; diff --git a/spec/bridge/source/_internals/mask.ts b/spec/bridge/source/_internals/mask.ts new file mode 100644 index 00000000..092e94f1 --- /dev/null +++ b/spec/bridge/source/_internals/mask.ts @@ -0,0 +1,56 @@ +/** + * Laying a value over a mask, which is what every `format*` utility does. + * + * The mask is a string: `0` is a slot the value fills, `*` is a slot the value fills and the + * mask hides, and anything else is a separator, written only while there is still value left + * to write. + */ + +/** + * Lays a value over a pattern. + * + * @param {string} value - The sanitized value. + * @param {string} pattern - The mask. + * @param {boolean} pad - Whether to left pad the value with zeros up to the number of slots. + * @returns {string} The masked value, cut short where the value runs out. + */ +export const layout = (value: string, pattern: string, pad: boolean): string => { + let slots = 0; + + for (let index = 0; index < pattern.length; index++) { + if (pattern.charCodeAt(index) === 48 || pattern.charCodeAt(index) === 42) { + slots += 1; + } + } + + let padded = value; + + if (pad) { + padded = value.padStart(slots, "0"); + } + + let formatted = ""; + let cursor = 0; + + for (let index = 0; index < pattern.length; index++) { + const slot = pattern.charCodeAt(index); + + if (slot === 48 || slot === 42) { + if (cursor >= padded.length) { + return formatted; + } + + if (slot === 42) { + formatted += "*"; + } else { + formatted += padded.slice(cursor, cursor + 1); + } + + cursor += 1; + } else if (cursor < padded.length) { + formatted += pattern.slice(index, index + 1); + } + } + + return formatted; +}; diff --git a/spec/bridge/source/format-cnpj.ts b/spec/bridge/source/format-cnpj.ts new file mode 100644 index 00000000..104ee34a --- /dev/null +++ b/spec/bridge/source/format-cnpj.ts @@ -0,0 +1,63 @@ +/** + * `formatCnpj`, written once. + * + * The boundary guards are the handwritten package's, not a tidier version of them: a nullish + * value answers the empty string, and a value the mask outruns is cut short rather than + * padded, because that is what the JavaScript this repository ships already does. + * + * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cnpj + * @see Official: https://www.gov.br/receitafederal/pt-br/acesso-a-informacao/acoes-e-programas/programas-e-atividades/cnpj-alfanumerico + */ +import { NON_ALPHANUMERIC, NON_DIGIT } from "./_internals/cnpj.ts"; +import { layout } from "./_internals/mask.ts"; +import { asString, isTruthy } from "./_std.ts"; + +/** Options of `formatCnpj`. */ +export type FormatCnpjOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; + /** Which CNPJ format to read: `1` numeric only, `2` alphanumeric (default: `1`). */ + version?: 1 | 2; + /** Whether to hide the first 2 digits and the 2 check digits with `*` (default: `false`). */ + obfuscate?: boolean; +}; + +const PATTERN = "00.000.000/0000-00"; +const OBFUSCATED_PATTERN = "**.000.000/0000-**"; + +/** + * Formats a given CNPJ (Cadastro Nacional da Pessoa Jurídica) value. + * + * @param {string} value - The CNPJ value to be formatted. + * @param {FormatCnpjOptions} [options] - Optional configuration for formatting the CNPJ. + * @returns {string} The formatted CNPJ string in the pattern "00.000.000/0000-00". + * + * @example + * ```typescript + * formatCnpj("12345678000195"); // "12.345.678/0001-95" + * formatCnpj("12345678", { pad: true }); // "00.000.012/3456-78" + * formatCnpj("q0SLFMBD7VX439", { version: 2 }); // "Q0.SLF.MBD/7VX4-39" + * formatCnpj("12345678000195", { obfuscate: true }); // "**.345.678/0001-**" + * ``` + */ +export const formatCnpj = (value: string | number, options?: FormatCnpjOptions): string => { + if (value === null || value === undefined) { + return ""; + } + + const text = asString(value); + + let cleaned = text.replaceAll(NON_DIGIT, ""); + + if (options?.version === 2) { + cleaned = text.replaceAll(NON_ALPHANUMERIC, "").toUpperCase(); + } + + let pattern = PATTERN; + + if (isTruthy(options?.obfuscate)) { + pattern = OBFUSCATED_PATTERN; + } + + return layout(cleaned, pattern, isTruthy(options?.pad)); +};