diff --git a/README.md b/README.md index b6da602..de92e60 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,37 @@ # npi-validator -Validate NPI numbers for healthcare physicians or organizations + +Validation function for National Provider Identifier (NPI) numbers. Works for healthcare physicians and organizations in the United States or other countries that use the NPI standard. + +This is an implementation of the Luhn formula (mod 10 double add double) check digit, see the [CMS.gov specification](https://www.cms.gov/Regulations-and-Guidance/Administrative-Simplification/NationalProvIdentStand/Downloads/NPIcheckdigit.pdf) for more information. + +## Install + +```sh +npm install npi-validator +``` + +## Usage + +```js +import npiValid from 'npi-validator'; + +const npi = 1234567893; +const valid = npiValid(npi); + +console.log(valid); +//=> true +``` + +## API + +### `function npiValid(id: string | number, prefix?: string | number): boolean` + +```js +import npiValid from 'npi-validator'; + +const npi = 1234567893; +const valid = npiValid(npi); + +console.log(valid); +//=> true +``` diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..2d9f382 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,18 @@ +/** +Validates NPI numbers. See the [CMS.gov specification](https://www.cms.gov/Regulations-and-Guidance/Administrative-Simplification/NationalProvIdentStand/Downloads/NPIcheckdigit.pdf) for more information. + +@example +``` +import npiValid from 'npi-validator'; + +const npi = 1234567893; +const valid = npiValid(npi); + +console.log(valid); +//=> true +``` + +@param id - npi number; must be 10 digits +@param prefix - prefix number; defaults to 80840; must start with 80,the health prefix, and be followed by the 3 digit country code +*/ +export function npiValid(id: string | number, prefix?: string | number): boolean; diff --git a/index.js b/index.js new file mode 100644 index 0000000..49bd95b --- /dev/null +++ b/index.js @@ -0,0 +1,47 @@ +const {ceil, floor} = Math; +const toInt = s => Number.parseInt(s, 10); + +const sumDigits = int => { + let sum = 0; + while (int) { + sum += int % 10; + int = floor(int / 10); + } + + return sum; +}; + +const doubleAlternate = (value, index) => { + let pos = index ?? value.length - 1; + let sum = 0; + let alt = true; + for (; pos >= 0; pos--) { + const cur = toInt(value.charAt(pos)); + sum += alt ? sumDigits(cur * 2) : cur; + alt = !alt; + } + + return sum; +}; + +export function npiValid(id, prefix = '80840') { + const prefixRegex = /^80\d{3}$/; + const npiRegex = /^\d{10}$/; + const usPrefixSum = 24; // Precalculated value for 80840 (United States) + const npi = id?.toString() ?? ''; + const pre = prefix?.toString() ?? ''; + + if (!npiRegex.test(npi) || !prefixRegex.test(pre)) { + return false; + } + + const check = toInt(npi.charAt(9)); + const npiSum = doubleAlternate(npi, 8); + const preSum = pre === '80840' ? usPrefixSum : doubleAlternate(pre, 3); + const total = npiSum + preSum; + const totalCeil = ceil(total / 10) * 10; + + return totalCeil - total === check; +} + +export default npiValid; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b2fbf7d --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "npi-validator", + "version": "1.0.0", + "description": "Validate NPI numbers for healthcare physicians or organizations.", + "homepage": "https://github.com/coryasilva/npi-validator#readme", + "bugs": { + "url": "https://github.com/coryasilva/npi-validator/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/coryasilva/npi-validator.git" + }, + "author": { + "name": "Cory Silva", + "url": "https://corysilva.com" + }, + "keywords": [ + "healthcare", + "npi", + "validation" + ], + "engines": { + "node": ">=18" + }, + "type": "module", + "sideEffects": false, + "main": "index.js", + "exports": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "scripts": { + "test": "xo && ava && tsc index.d.ts" + }, + "devDependencies": { + "ava": "^6.0.1", + "typescript": "^5.3.3", + "xo": "^0.56.0" + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..2e17a68 --- /dev/null +++ b/test.js @@ -0,0 +1,38 @@ +/* eslint-disable unicorn/numeric-separators-style */ +import test from 'ava'; +import npiValid from './index.js'; + +test('NPI validation with default United States prefix', t => { + t.is(npiValid(1234567893), true); + t.is(npiValid('1234567893'), true); + t.is(npiValid(1234567890), false); + + // Bad inputs + t.is(npiValid(''), false); + t.is(npiValid(' '), false); + t.is(npiValid(undefined), false); + t.is(npiValid(null), false); + t.is(npiValid({}), false); + t.is(npiValid([]), false); + t.is(npiValid(true), false); + t.is(npiValid(false), false); + t.is(npiValid(), false); +}); + +test('NPI validation with custom prefix', t => { + t.is(npiValid(1234567894, 80123), true); + t.is(npiValid(1234567894, '80123'), true); + t.is(npiValid(1234567894, 8012), false); + t.is(npiValid(1234567894, 81840), false); + + // Bad inputs + t.is(npiValid(1234567893, ''), false); + t.is(npiValid(1234567893, ' '), false); + t.is(npiValid(1234567893, '80 '), false); + t.is(npiValid(1234567893, undefined), true); + t.is(npiValid(1234567893, null), false); + t.is(npiValid(1234567893, {}), false); + t.is(npiValid(1234567893, []), false); + t.is(npiValid(1234567893, true), false); + t.is(npiValid(1234567893, false), false); +});