Skip to content
Open
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
41 changes: 30 additions & 11 deletions src/formatters/formatToCNPJ.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import mapToNumeric from '../helpers/mapToNumeric';
const REGEX_MASK_CHARACTERS = /[.\/-]/g
const BASE_LENGTH = 12
const DV_LENGTH = 2

const normalizeCNPJ = (value: string): string =>
value
.replace(REGEX_MASK_CHARACTERS, '')
.split('')
.reduce<[string, string]>(
([base, dv], c) =>
base.length < BASE_LENGTH && /[A-Z\d]/i.test(c) ? [base + c, dv] :
dv.length < DV_LENGTH && /\d/.test(c) ? [base, dv + c] :
[base, dv],
['', ''],
)
.join('')

/**
* Formats step-by-step a `string` value into a CNPJ.
* Formats step-by-step a `string` value into a CNPJ (AA.AAA.AAA/AAAA-DV).
* Base: 12 alphanumeric chars. DV: 2 numeric digits.
* @example ```js
* formatToCNPJ('128781')
* //=> '12.878.1'
Expand All @@ -11,17 +27,20 @@ import mapToNumeric from '../helpers/mapToNumeric';
*
* formatToCNPJ('00.0.000.00.00--00-00')
* //=> '00.000.000/0000-00'
*
* formatToCNPJ('1A2B3C4D5E6F78')
* //=> '1A.2B3.C4D/5E6F-78'
* ```
* @param value - A `string` value of a CNPJ.
*/
const formatToCNPJ = (
value: string,
): string => (
mapToNumeric(value)
.replace(/(\d{2})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1/$2')
.replace(/(\d{4})(\d{1,2})$/, '$1-$2')
);
const formatToCNPJ = (value: string): string => {
const n = normalizeCNPJ(value)
let result = n.slice(0, 2)
if (n.length > 2) result += '.' + n.slice(2, 5)
if (n.length > 5) result += '.' + n.slice(5, 8)
if (n.length > 8) result += '/' + n.slice(8, 12)
if (n.length > 12) result += '-' + n.slice(12, 14)
return result
}

export default formatToCNPJ;
5 changes: 3 additions & 2 deletions src/formatters/formatToCPFOrCNPJ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import formatToCNPJ from './formatToCNPJ';
import formatToCPF from './formatToCPF';

/**
* Check if a `string` value can be formatted to CPF.
* Check if a `string` value can be formatted to CPF (digits-only, max 11; else CNPJ).
* @param value - A `string` value of a CPF or CNPJ.
*/
const canFormatToCPF = (
value: string,
): boolean => (value.match(/\d/g)?.length ?? 0) <= 11
): boolean =>
(value.replace(/\D/g, '').length <= 11) && !/[/A-Za-z]/.test(value)

/**
* Formats step-by-step a `string` value to CPF or CNPJ depending on its length.
Expand Down
89 changes: 65 additions & 24 deletions src/validators/isCNPJ.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
import generateCheckSums from '../helpers/generateCheckSums';
import getRemaining from '../helpers/getRemaining';
import isRepeatedArray from '../helpers/isRepeatedValue';
import mapToNumbers from '../helpers/mapToNumbers';
const BASE_LENGTH = 12
const REGEX_BASE_CNPJ = /^[A-Z\d]{12}$/i
const REGEX_FULL_CNPJ = /^[A-Z\d]{12}\d{2}$/i
const REGEX_MASK_CHARACTERS = /[.\/-]/g
const REGEX_INVALID_CHARACTERS = /[^A-Z\d.\/-]/i
const ASCII_ZERO = '0'.charCodeAt(0)
const CHECK_DIGIT_WEIGHT = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
const ZEROED_BASE = '000000000000'

const removeMask = (cnpj: string): string =>
cnpj.replace(REGEX_MASK_CHARACTERS, '')

const calculateCheckDigits = (baseCNPJ: string): string => {
if (REGEX_INVALID_CHARACTERS.test(baseCNPJ))
throw new Error('CNPJ contains invalid characters')

const raw = removeMask(baseCNPJ)

if (!REGEX_BASE_CNPJ.test(raw) || raw === ZEROED_BASE)
throw new Error('Invalid base CNPJ for check digits calculation')

const digits = raw.toUpperCase().split('').map((char: string) => char.charCodeAt(0) - ASCII_ZERO)
const sum1 = digits.reduce(
(acc: number, digit: number, index: number) => acc + digit * CHECK_DIGIT_WEIGHT[index + 1],
0,
)
const dv1 = sum1 % 11 < 2 ? 0 : 11 - (sum1 % 11)
const sum2 =
digits.reduce(
(acc: number, digit: number, index: number) => acc + digit * CHECK_DIGIT_WEIGHT[index],
0,
) +
dv1 * CHECK_DIGIT_WEIGHT[BASE_LENGTH]
const dv2 = sum2 % 11 < 2 ? 0 : 11 - (sum2 % 11)

return `${dv1}${dv2}`
}

/**
* Pattern to match formatted CNPJ (99.999.999/9999-99) or 14 numbers.
* Pattern to match formatted CNPJ (AA.AAA.AAA/AAAA-DV or 12 alphanumeric + 2 digits).
*/
export const CNPJ_PATTERN = /^(\d{14}|\d{2}\.\d{3}\.\d{3}\/\d{4}\-\d{2})$/;
export const CNPJ_PATTERN = /^([A-Z\d]{12}\d{2}|[A-Z\d]{2}\.[A-Z\d]{3}\.[A-Z\d]{3}\/[A-Z\d]{4}-\d{2})$/i

/**
* Check if value is a valid CNPJ.
* Check if value is a valid CNPJ (base 12 alphanumeric + 2 numeric check digits).
* @example ```js
* isCNPJ('41142260000189')
* //=> true
Expand All @@ -25,20 +58,28 @@ export const CNPJ_PATTERN = /^(\d{14}|\d{2}\.\d{3}\.\d{3}\/\d{4}\-\d{2})$/;
* ```
* @param value - A text containing a CNPJ.
*/
const isCNPJ = (
value: string,
): boolean => {
if (!CNPJ_PATTERN.test(value))
return false;
const numbers = mapToNumbers(value);
if (isRepeatedArray(numbers))
return false;
const validators = [ 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2 ];
const checkers = generateCheckSums(numbers, validators);
return (
numbers[12] === getRemaining(checkers[0]) &&
numbers[13] === getRemaining(checkers[1])
);
};

export default isCNPJ;
const isCNPJ = (value: string): boolean => {
if (REGEX_INVALID_CHARACTERS.test(value))
return false

const raw = removeMask(value)

if (!REGEX_FULL_CNPJ.test(raw))
return false

const base = raw.slice(0, BASE_LENGTH)
if (base === ZEROED_BASE)
return false

const providedCheckDigits = raw.slice(BASE_LENGTH)
let calculatedCheckDigits: string
try {
calculatedCheckDigits = calculateCheckDigits(base)
} catch {
return false
}

return providedCheckDigits === calculatedCheckDigits
}

export default isCNPJ
2 changes: 1 addition & 1 deletion src/validators/isCPFOrCNPJ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const PATTERNS = [CPF_PATTERN, CNPJ_PATTERN];
const isCPFOrCNPJ = (value: string): boolean => {
const matches = PATTERNS.map((pattern) => pattern.test(value));

if (!matches.includes(true)) return false;
if (matches.indexOf(true) === -1) return false;

return matches[0] ? isCPF(value) : isCNPJ(value);
};
Expand Down
7 changes: 7 additions & 0 deletions test/formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ test('formatToCNPJ', (context) => {
context.is(formatToCNPJ('128781'), '12.878.1');
context.is(formatToCNPJ('32284981000138'), '32.284.981/0001-38');
context.is(formatToCNPJ('00.0.000.00.00--00-00'), '00.000.000/0000-00');
context.is(formatToCNPJ('15.216.260/0001-76'), '15.216.260/0001-76');
context.is(formatToCNPJ('6A3EDMWP000168'), '6A.3ED.MWP/0001-68');
context.is(formatToCNPJ('Q2.GRO.WP7/0001-85'), 'Q2.GRO.WP7/0001-85');
context.is(formatToCNPJ('1A2B3C4D5E6F78'), '1A.2B3.C4D/5E6F-78');
context.is(formatToCNPJ('1A.2B3.C4D/5E6F-78'), '1A.2B3.C4D/5E6F-78');
});

test('formatToCPF', (context) => {
Expand All @@ -91,6 +96,8 @@ test('formatToCPFOrCNPJ', (context) => {
context.is(formatToCPFOrCNPJ('366.418.768-70'), '366.418.768-70');
context.is(formatToCPFOrCNPJ('32284981000138'), '32.284.981/0001-38');
context.is(formatToCPFOrCNPJ('00.0.000.00.00--00-00'), '00.000.000/0000-00');
context.is(formatToCPFOrCNPJ('6A3EDMWP000168'), '6A.3ED.MWP/0001-68');
context.is(formatToCPFOrCNPJ('1A2B3C4D5E6F78'), '1A.2B3.C4D/5E6F-78');
});

test('formatToDate', (context) => {
Expand Down
8 changes: 8 additions & 0 deletions test/validators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ test('isCEP', (context) => {
test('isCNPJ', (context) => {
context.true(isCNPJ('41142260000189'));
context.true(isCNPJ('45.723.174/0001-10'));
context.true(isCNPJ('15.216.260/0001-76'));
context.true(isCNPJ('6A3EDMWP000168'));
context.true(isCNPJ('D7YDDI5H000162'));
context.true(isCNPJ('Q2.GRO.WP7/0001-85'));
context.false(isCNPJ('41142260007182'));
context.false(isCNPJ('19.981.127/0001-10'));
context.false(isCNPJ('64.637.agsvs009/0001-90'));
context.false(isCNPJ('1A2B3C4D5E6F99'));
});

test('isCAEPF', (context) => {
Expand All @@ -52,6 +57,9 @@ test('isCPF', (context) => {
test('isCPFOrCNPJ', (context) => {
context.true(isCPFOrCNPJ("41142260000189"));
context.true(isCPFOrCNPJ("45.723.174/0001-10"));
context.true(isCPFOrCNPJ("15.216.260/0001-76"));
context.true(isCPFOrCNPJ("6A3EDMWP000168"));
context.true(isCPFOrCNPJ("Q2.GRO.WP7/0001-85"));
context.false(isCPFOrCNPJ("41142260007182"));
context.false(isCPFOrCNPJ("19.981.127/0001-10"));
context.false(isCPFOrCNPJ("64.637.agsvs009/0001-90"));
Expand Down