Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spec/bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions spec/bridge/conformance/cases/format-cnpj.ts
Original file line number Diff line number Diff line change
@@ -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;
},
};
56 changes: 56 additions & 0 deletions spec/bridge/source/_internals/mask.ts
Original file line number Diff line number Diff line change
@@ -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;
};
63 changes: 63 additions & 0 deletions spec/bridge/source/format-cnpj.ts
Original file line number Diff line number Diff line change
@@ -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));
};
Loading