diff --git a/README.md b/README.md
index b2896b73f..ae39d74ec 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,7 @@ Validator | Description
**isISO31661Alpha3(str)** | check if the string is a valid [ISO 3166-1 alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) officially assigned country code.
**isISRC(str)** | check if the string is a [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code).
**isISSN(str [, options])** | check if the string is an [ISSN](https://en.wikipedia.org/wiki/International_Standard_Serial_Number).
`options` is an object which defaults to `{ case_sensitive: false, require_hyphen: false }`. If `case_sensitive` is true, ISSNs with a lowercase `'x'` as the check digit are rejected.
+**isCPF(str [, options])** | check if the string is a valid brazilian [CPF](https://en.wikipedia.org/wiki/CPF_number). The CPF is analogous to the USA's SSN.
`options` is an object which defaults to `{ separators: 'optional' }` which by default validates CPFs with or without separators; other valid values for `separators` are `require` and `none`;
**isJSON(str [, options])** | check if the string is valid JSON (note: uses JSON.parse).
`options` is an object which defaults to `{ allow_primitives: false }`. If `allow_primitives` is true, the primitives 'true', 'false' and 'null' are accepted as valid JSON values.
**isJWT(str)** | check if the string is valid JWT token.
**isLatLong(str [, options])** | check if the string is a valid latitude-longitude coordinate in the format `lat,long` or `lat, long`.
`options` is an object that defaults to `{ checkDMS: false }`. Pass `checkDMS` as `true` to validate DMS(degrees, minutes, and seconds) latitude-longitude format.
diff --git a/src/index.js b/src/index.js
index e91c582da..bdbadf126 100644
--- a/src/index.js
+++ b/src/index.js
@@ -76,6 +76,7 @@ import isEAN from './lib/isEAN';
import isISIN from './lib/isISIN';
import isISBN from './lib/isISBN';
import isISSN from './lib/isISSN';
+import isCPF from './lib/isCPF';
import isTaxID from './lib/isTaxID';
import isMobilePhone, { locales as isMobilePhoneLocales } from './lib/isMobilePhone';
@@ -187,6 +188,7 @@ const validator = {
isISIN,
isISBN,
isISSN,
+ isCPF,
isMobilePhone,
isMobilePhoneLocales,
isPostalCode,
diff --git a/src/lib/isCPF.js b/src/lib/isCPF.js
new file mode 100644
index 000000000..d435d5b91
--- /dev/null
+++ b/src/lib/isCPF.js
@@ -0,0 +1,35 @@
+import assertString from './util/assertString';
+
+const defaultOptions = { separators: 'optional' }; // 'require', 'none' or 'optional' (default)
+
+const formatSeparators = /^(\d{3})\.(\d{3})\.(\d{3})-(\d{2})$/; // 000.000.000-00
+const formatPlain = /^(\d{3})(\d{3})(\d{3})(\d{2})$/; // 00000000000
+const formatBoth = /^(\d{3})\.?(\d{3})\.?(\d{3})-?(\d{2})$/; // both formats above, i.e., optional separators
+
+const weightsFirstDigit = [10, 9, 8, 7, 6, 5, 4, 3, 2];
+const weightsSecondDigit = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
+
+function calculateDigit(values, weights) {
+ const sum = values.reduce((acc, cur, i) => acc + (cur * weights[i]), 0);
+ const remainder = sum % 11;
+ return String(remainder < 2 ? 0 : 11 - remainder);
+}
+
+export default function isCPF(str, options = defaultOptions) {
+ assertString(str);
+ const { separators } = { ...defaultOptions, ...options };
+
+ let format = formatBoth;
+ if (separators === 'require') format = formatSeparators;
+ else if (separators === 'none') format = formatPlain;
+
+ const match = format.exec(str);
+ if (!match) return false;
+ const cpf = Array.from(match[1] + match[2] + match[3]);
+
+ const digit1 = calculateDigit(cpf, weightsFirstDigit);
+ const digit2 = calculateDigit(cpf.concat(digit1), weightsSecondDigit);
+
+ const [first, second] = match[4];
+ return first === digit1 && second === digit2;
+}
diff --git a/test/validators.js b/test/validators.js
index b9899b626..8fe204ad6 100644
--- a/test/validators.js
+++ b/test/validators.js
@@ -4758,6 +4758,75 @@ describe('Validators', () => {
});
});
+ it('should validate CPFs', () => {
+ test({
+ validator: 'isCPF',
+ valid: [
+ '844.186.675-98',
+ '216.414.064-88',
+ '391.757.780-17',
+ '73112521722',
+ '53253165167',
+ '24462635183',
+ ],
+ invalid: [
+ '',
+ ' ',
+ '84.4186.675-89',
+ '216.414.064',
+ '391 757 780 17',
+ '73112521729',
+ '53253165169',
+ '24462635189',
+ ],
+ });
+ test({
+ validator: 'isCPF',
+ args: [{ separators: 'require' }],
+ valid: [
+ '844.186.675-98',
+ '216.414.064-88',
+ '391.757.780-17',
+ '179.518.290-30',
+ ],
+ invalid: [
+ '',
+ ' ',
+ '84.4186.675-89',
+ '216.414.064',
+ '391 757 780 17',
+ '73112521722',
+ '53253165167',
+ '24462635183',
+ '73112521729',
+ '53253165169',
+ '24462635189',
+ ],
+ });
+ test({
+ validator: 'isCPF',
+ args: [{ separators: 'none' }],
+ valid: [
+ '73112521722',
+ '53253165167',
+ '24462635183',
+ ],
+ invalid: [
+ '',
+ ' ',
+ '84.4186.675-89',
+ '216.414.064',
+ '391 757 780 17',
+ '73112521729',
+ '53253165169',
+ '24462635189',
+ '844.186.675-98',
+ '216.414.064-88',
+ '391.757.780-17',
+ ],
+ });
+ });
+
it('should validate JSON', () => {
test({
validator: 'isJSON',