Repository: validatorjs/validator.js
Pull request #1974 (2022-05-22 16:57:52)

feat(isLuhnValid): Expose isLuhnValid independently from isCreditCard
Currently the luhn checking algorithm is only used inside isCreditCard, but the luhn algorithm is also used in/for other "numbers", so it would be nice if the `isLuhnValid` could be exposed independently.

> is a simple [checksum](https://en.wikipedia.org/wiki/Checksum) formula used to validate a variety of identification numbers, such as [credit card numbers](https://en.wikipedia.org/wiki/Payment_card_number), [IMEI numbers](https://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity), [National Provider Identifier numbers](https://en.wikipedia.org/wiki/National_Provider_Identifier) in the United States, [Canadian](https://en.wikipedia.org/wiki/Canada) [Social Insurance Numbers](https://en.wikipedia.org/wiki/Social_Insurance_Number), [Israeli](https://en.wikipedia.org/wiki/Israeli_identity_card) ID Numbers, [South African](https://en.wikipedia.org/wiki/South_Africa) ID Numbers, [Swedish](https://en.wikipedia.org/wiki/Sweden) [National identification numbers](https://en.wikipedia.org/wiki/National_identification_number), [Swedish](https://en.wikipedia.org/wiki/Sweden) Corporate Identity Numbers (OrgNr), [Greek](https://en.wikipedia.org/wiki/Greece) Social Security Numbers (ΑΜΚΑ), SIM card numbers and survey codes appearing on [McDonald's](https://en.wikipedia.org/wiki/File:McDonalds_Receipt_Luhn_Algorithm.png), [Taco Bell](https://en.wikipedia.org/wiki/File:Taco_Bell_Receipt_Luhn_Algorithm.png), and [Tractor Supply Co.](https://en.wikipedia.org/wiki/File:Tractor_Supply_Receipt_Luhn_Algorithm.png) receipts.

Source: https://en.wikipedia.org/wiki/Luhn_algorithm

The code is copied from the existing `isCreditCard` check.

## Checklist

- [x] PR contains only changes related; no stray files, etc.
- [x] README updated (where applicable)
- [x] Tests written (where applicable) - ~~Tests from `isCreditCard` apply, if any exist.~~



Patch:
diff --git a/README.md b/README.md
index 9674841..629933c 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,7 @@ Validator                               | Description
 **isLicensePlate(str [, locale])**     | check if string matches the format of a country's license plate.<br/><br/>(locale is one of `['cs-CZ', 'de-DE', 'de-LI', 'fi-FI', 'pt-BR', 'pt-PT', 'sq-AL', 'sv-SE']` or `any`)
 **isLocale(str)**                       | check if the string is a locale
 **isLowercase(str)**                    | check if the string is lowercase.
+**isLuhnValid(str)**                    | check if the string passes the luhn check.
 **isMACAddress(str [, options])**                   | check if the string is a MAC address.<br/><br/>`options` is an object which defaults to `{no_separators: false}`. If `no_separators` is true, the validator will allow MAC addresses without separators. Also, it allows the use of hyphens, spaces or dots e.g  '01 02 03 04 05 ab', '01-02-03-04-05-ab' or '0102.0304.05ab'. The options also allow a `eui` property to specify if it needs to be validated against EUI-48 or EUI-64. The accepted values of `eui` are: 48, 64.
 **isMagnetURI(str)**                      | check if the string is a [magnet uri format](https://en.wikipedia.org/wiki/Magnet_URI_scheme).
 **isMD5(str)**                          | check if the string is a MD5 hash.<br/><br/>Please note that you can also use the `isHash(str, 'md5')` function. Keep in mind that MD5 has some collision weaknesses compared to other algorithms (e.g., SHA).
diff --git a/src/index.js b/src/index.js
index ec6fe52..42e1e8b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -69,6 +69,7 @@ import isBefore from './lib/isBefore';
 
 import isIn from './lib/isIn';
 
+import isLuhnValid from './lib/isLuhnValid';
 import isCreditCard from './lib/isCreditCard';
 import isIdentityCard from './lib/isIdentityCard';
 
@@ -183,6 +184,7 @@ const validator = {
   isAfter,
   isBefore,
   isIn,
+  isLuhnValid,
   isCreditCard,
   isIdentityCard,
   isEAN,
diff --git a/src/lib/isCreditCard.js b/src/lib/isCreditCard.js
index b11c4cc..53c80eb 100644
--- a/src/lib/isCreditCard.js
+++ b/src/lib/isCreditCard.js
@@ -1,4 +1,5 @@
 import assertString from './util/assertString';
+import isLuhnValid from './isLuhnValid';
 
 /* eslint-disable max-len */
 const creditCard = /^(?:4[0-9]{12}(?:[0-9]{3,6})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12,15}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11}|6[27][0-9]{14}|^(81[0-9]{14,17}))$/;
@@ -10,24 +11,5 @@ export default function isCreditCard(str) {
   if (!creditCard.test(sanitized)) {
     return false;
   }
-  let sum = 0;
-  let digit;
-  let tmpNum;
-  let shouldDouble;
-  for (let i = sanitized.length - 1; i >= 0; i--) {
-    digit = sanitized.substring(i, (i + 1));
-    tmpNum = parseInt(digit, 10);
-    if (shouldDouble) {
-      tmpNum *= 2;
-      if (tmpNum >= 10) {
-        sum += ((tmpNum % 10) + 1);
-      } else {
-        sum += tmpNum;
-      }
-    } else {
-      sum += tmpNum;
-    }
-    shouldDouble = !shouldDouble;
-  }
-  return !!((sum % 10) === 0 ? sanitized : false);
+  return isLuhnValid(str);
 }
diff --git a/src/lib/isLuhnValid.js b/src/lib/isLuhnValid.js
new file mode 100644
index 0000000..da20527
--- /dev/null
+++ b/src/lib/isLuhnValid.js
@@ -0,0 +1,26 @@
+import assertString from './util/assertString';
+
+export default function isLuhnValid(str) {
+  assertString(str);
+  const sanitized = str.replace(/[- ]+/g, '');
+  let sum = 0;
+  let digit;
+  let tmpNum;
+  let shouldDouble;
+  for (let i = sanitized.length - 1; i >= 0; i--) {
+    digit = sanitized.substring(i, (i + 1));
+    tmpNum = parseInt(digit, 10);
+    if (shouldDouble) {
+      tmpNum *= 2;
+      if (tmpNum >= 10) {
+        sum += ((tmpNum % 10) + 1);
+      } else {
+        sum += tmpNum;
+      }
+    } else {
+      sum += tmpNum;
+    }
+    shouldDouble = !shouldDouble;
+  }
+  return !!((sum % 10) === 0 ? sanitized : false);
+}


Test patch:
diff --git a/test/validators.js b/test/validators.js
index 16fb6b8..1ef34d7 100644
--- a/test/validators.js
+++ b/test/validators.js
@@ -4931,6 +4931,34 @@ describe('Validators', () => {
     });
   });
 
+  it('should validate luhn numbers', () => {
+    test({
+      validator: 'isLuhnValid',
+      valid: [
+        '0',
+        '5421',
+        '01234567897',
+        '0123456789012345678906',
+        '0123456789012345678901234567891',
+        '123456789012345678906',
+        '375556917985515',
+        '36050234196908',
+        '4716461583322103',
+        '4716-2210-5188-5662',
+        '4929 7226 5379 7141',
+      ],
+      invalid: [
+        '',
+        '1',
+        '5422',
+        'foo',
+        'prefix6234917882863855',
+        '623491788middle2863855',
+        '6234917882863855suffix',
+      ],
+    });
+  });
+
   it('should validate credit cards', () => {
     test({
       validator: 'isCreditCard',