From b0633150b0576c3db0b377e439104d7ebc3bdeeb Mon Sep 17 00:00:00 2001
From: Kelvin <kelvingremista2015@hotmail.com>
Date: Wed, 2 Apr 2025 00:10:09 -0300
Subject: [PATCH 1/3] feat: added isCPF validator

---
 src/index.js            |  3 +++
 src/lib/isCPF.js        | 51 +++++++++++++++++++++++++++++++++++
 test/validators.test.js | 59 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 113 insertions(+)
 create mode 100644 src/lib/isCPF.js

diff --git a/src/index.js b/src/index.js
index c47674595..20baffb54 100644
--- a/src/index.js
+++ b/src/index.js
@@ -130,6 +130,8 @@ import isStrongPassword from './lib/isStrongPassword';
 
 import isVAT from './lib/isVAT';
 
+import isCPF from './lib/isCPF';
+
 const version = '13.15.0';
 
 const validator = {
@@ -245,6 +247,7 @@ const validator = {
   isLicensePlate,
   isVAT,
   ibanLocales,
+  isCPF,
 };
 
 export default validator;
diff --git a/src/lib/isCPF.js b/src/lib/isCPF.js
new file mode 100644
index 000000000..23145af82
--- /dev/null
+++ b/src/lib/isCPF.js
@@ -0,0 +1,51 @@
+import assertString from './util/assertString';
+
+
+export default function isCPF(cpf) {
+  assertString(cpf);
+  if (cpf.length !== 11) return false;
+  if (cpf === '00000000000' || cpf === '11111111111' || cpf === '22222222222' || cpf === '33333333333' || cpf === '44444444444' || cpf === '55555555555' || cpf === '66666666666' || cpf === '77777777777' || cpf === '88888888888' || cpf === '99999999999') return false;
+
+  const paramD1 = {
+    10: 0, 9: 1, 8: 2, 7: 3, 6: 4, 5: 5, 4: 6, 3: 7, 2: 8,
+  };
+  const paramD2 = {
+    11: 0, 10: 1, 9: 2, 8: 3, 7: 4, 6: 5, 5: 6, 4: 7, 3: 8, 2: 9,
+  };
+  let firstNineCharacters = cpf.slice(0, 9);
+  let d1 = 0;
+  let d2 = 0;
+  let d1AndD2 = '';
+
+  for (let i = 10; i >= 2; i--) {
+    if (Number.isNaN(Number(firstNineCharacters[paramD1[i]]))) return false;
+    d1 += Number(firstNineCharacters[paramD1[i]]) * i;
+  }
+
+  if (d1 % 11 < 2) {
+    d1 = 0;
+  } else {
+    d1 = 11 - (d1 % 11);
+  }
+
+  firstNineCharacters += String(d1);
+
+  for (let i = 11; i >= 2; i--) {
+    d2 += Number(firstNineCharacters[paramD2[i]]) * i;
+  }
+
+  if (d2 % 11 < 2) {
+    d2 = 0;
+  } else {
+    d2 = 11 - (d2 % 11);
+  }
+
+  d1AndD2 += d1;
+  d1AndD2 += d2;
+
+  if (d1AndD2 === cpf.slice(cpf.length - 2)) {
+    return true;
+  }
+
+  return false;
+}
diff --git a/test/validators.test.js b/test/validators.test.js
index d6e948a41..26edaba14 100644
--- a/test/validators.test.js
+++ b/test/validators.test.js
@@ -15440,4 +15440,63 @@ describe('Validators', () => {
       ],
     });
   });
+  it('should validate a CPF', () => {
+    test({
+      validator: 'isCPF',
+      valid: [
+        '15709954861',
+        '55734947598',
+        '60142555835',
+        '54945762139',
+        '36718123897',
+        '43849941922',
+        '97989392984',
+        '98273014037',
+        '66603094703',
+        '40668005130',
+        '50699975565',
+        '98798876465',
+        '81661188907',
+        '52991780002',
+        '03703980761',
+        '62281573370',
+        '05575125572',
+        '03085770860',
+        '28686002129',
+        '24234579793',
+      ],
+      invalid: [
+        '296.231.440-69',
+        '62424843039',
+        '477.887.094-96',
+        '41526890821',
+        '861.803.242-00',
+        '57751926579',
+        '034.928.141-63',
+        '66437339822',
+        '178.714.111-57',
+        '27507501358',
+        '769.259.949-19',
+        '43661169739',
+        '646.438.057-95',
+        '49403410474',
+        '113.512.907-25',
+        '81322401108',
+        '458.825.895-23',
+        '92472311425',
+        '585.251.576-48',
+        '00002283074',
+        '796.940.600-52',
+        '08228682581',
+        '906.765.489-12',
+        '31437427245',
+        '045.504.051-45',
+        '21383151886',
+        '519.213.751-90',
+        '06712261581',
+        '884.593.736-07',
+        '42188048376',
+      ],
+    });
+  });
 });

From 22961a9c135da6f7e26b89c567b363506b54a266 Mon Sep 17 00:00:00 2001
From: Kelvin <kelvingremista2015@hotmail.com>
Date: Wed, 2 Apr 2025 00:28:41 -0300
Subject: [PATCH 2/3] update README

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index a64ca450f..d8ca90eb9 100644
--- a/README.md
+++ b/README.md
@@ -174,6 +174,7 @@ Validator                               | Description
 **isVAT(str, countryCode)**             | check if the string is a [valid VAT number][VAT Number] if validation is available for the given country code matching [ISO 3166-1 alpha-2][ISO 3166-1 alpha-2]. <br/><br/>`countryCode` is one of `['AL', 'AR', 'AT', 'AU', 'BE', 'BG', 'BO', 'BR', 'BY', 'CA', 'CH', 'CL', 'CO', 'CR', 'CY', 'CZ', 'DE', 'DK', 'DO', 'EC', 'EE', 'EL', 'ES', 'FI', 'FR', 'GB', 'GT', 'HN', 'HR', 'HU', 'ID', 'IE', 'IL', 'IN', 'IS', 'IT', 'KZ', 'LT', 'LU', 'LV', 'MK', 'MT', 'MX', 'NG', 'NI', 'NL', 'NO', 'NZ', 'PA', 'PE', 'PH', 'PL', 'PT', 'PY', 'RO', 'RS', 'RU', 'SA', 'SE', 'SI', 'SK', 'SM', 'SV', 'TR', 'UA', 'UY', 'UZ', 'VE']`.
 **isWhitelisted(str, chars)**           | check if the string consists only of characters that appear in the whitelist `chars`.
 **matches(str, pattern [, modifiers])** | check if the string matches the pattern.<br/><br/>Either `matches('foo', /foo/i)` or `matches('foo', 'foo', 'i')`.
+**isCPF(str)**                          | check if the string follows the rules defined by the Brazilian Federal Revenue Service for CPF verification digit calculation.
 
 ## Sanitizers
 

From ac697165da0775bc796400c7d589a7f755b8eba0 Mon Sep 17 00:00:00 2001
From: Kelvin <kelvingremista2015@hotmail.com>
Date: Wed, 2 Apr 2025 23:53:22 -0300
Subject: [PATCH 3/3] feat: remove mask if exist

---
 src/lib/isCPF.js        | 25 +++++++++++++------------
 test/validators.test.js | 20 ++++++++++----------
 2 files changed, 23 insertions(+), 22 deletions(-)

diff --git a/src/lib/isCPF.js b/src/lib/isCPF.js
index 23145af82..d9ddf789f 100644
--- a/src/lib/isCPF.js
+++ b/src/lib/isCPF.js
@@ -1,10 +1,19 @@
 import assertString from './util/assertString';
 
+const isMask = /[.-]/g;
+const repeatedDigitsRegex = /^(\d)\1{10}$/;
 
 export default function isCPF(cpf) {
   assertString(cpf);
-  if (cpf.length !== 11) return false;
-  if (cpf === '00000000000' || cpf === '11111111111' || cpf === '22222222222' || cpf === '33333333333' || cpf === '44444444444' || cpf === '55555555555' || cpf === '66666666666' || cpf === '77777777777' || cpf === '88888888888' || cpf === '99999999999') return false;
+
+  let cleanedCPF = String(cpf);
+
+  if (isMask.test(cleanedCPF)) {
+    cleanedCPF = cleanedCPF.replace(isMask, '');
+  }
+
+  if (cleanedCPF.length !== 11) return false;
+  if (repeatedDigitsRegex.test(cleanedCPF)) return false;
 
   const paramD1 = {
     10: 0, 9: 1, 8: 2, 7: 3, 6: 4, 5: 5, 4: 6, 3: 7, 2: 8,
@@ -12,10 +21,9 @@ export default function isCPF(cpf) {
   const paramD2 = {
     11: 0, 10: 1, 9: 2, 8: 3, 7: 4, 6: 5, 5: 6, 4: 7, 3: 8, 2: 9,
   };
-  let firstNineCharacters = cpf.slice(0, 9);
+  let firstNineCharacters = cleanedCPF.slice(0, 9);
   let d1 = 0;
   let d2 = 0;
-  let d1AndD2 = '';
 
   for (let i = 10; i >= 2; i--) {
     if (Number.isNaN(Number(firstNineCharacters[paramD1[i]]))) return false;
@@ -40,12 +48,5 @@ export default function isCPF(cpf) {
     d2 = 11 - (d2 % 11);
   }
 
-  d1AndD2 += d1;
-  d1AndD2 += d2;
-
-  if (d1AndD2 === cpf.slice(cpf.length - 2)) {
-    return true;
-  }
-
-  return false;
+  return cleanedCPF.slice(-2) === `${d1}${d2}`;
 }
diff --git a/test/validators.test.js b/test/validators.test.js
index 26edaba14..e8dcf2b00 100644
--- a/test/validators.test.js
+++ b/test/validators.test.js
@@ -15444,25 +15444,25 @@ describe('Validators', () => {
     test({
       validator: 'isCPF',
       valid: [
-        '15709954861',
+        '157.099.548-61',
         '55734947598',
-        '60142555835',
+        '601.425.558-35',
         '54945762139',
-        '36718123897',
+        '367.181.238-97',
         '43849941922',
-        '97989392984',
+        '979.893.929-84',
         '98273014037',
-        '66603094703',
+        '666.030.947-03',
         '40668005130',
-        '50699975565',
+        '506.999.755-65',
         '98798876465',
-        '81661188907',
+        '816.611.889-07',
         '52991780002',
-        '03703980761',
+        '037.039.807-61',
         '62281573370',
-        '05575125572',
+        '055.751.255-72',
         '03085770860',
-        '28686002129',
+        '286.860.021-29',
         '24234579793',
       ],
       invalid: [