├── .github ├── FUNDING.yml └── workflows │ ├── purge-cache.yml │ └── tests.yml ├── CHANGELOG.md ├── src ├── index.d.ts ├── __tests__ │ ├── getCheckDigit.test.js │ └── index.test.js ├── validStates.js ├── getCheckDigit.js ├── forbiddenWords.json └── index.js ├── .eslintrc ├── rollup.config.js ├── LICENSE ├── package.json ├── .gitignore ├── dist ├── index.js └── index.js.map └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: manuelmhtr 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [1.0.0] - 2022-07-24 5 | ### Added 6 | - First version of the project. 7 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export default function validateCurp(curp: T): { 2 | isValid: boolean, 3 | curp?: T, 4 | errors?: ('INVALID_FORMAT' | 'INVALID_DATE' | 'INVALID_STATE' | 'INVALID_VERIFICATION_DIGIT' | 'FORBIDDEN_WORD')[] 5 | }; 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb-base", 4 | "plugin:jest/recommended" 5 | ], 6 | "plugins": [ 7 | "jest" 8 | ], 9 | "env": { 10 | "node": true, 11 | "jest/globals": true 12 | }, 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "ignorePatterns": [ 17 | "dist/**", 18 | "esm/**" 19 | ] 20 | } -------------------------------------------------------------------------------- /.github/workflows/purge-cache.yml: -------------------------------------------------------------------------------- 1 | name: Purge CDN cache 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | purge-cache: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: curl 13 | uses: wei/curl@v1 14 | with: 15 | args: https://purge.jsdelivr.net/gh/manuelmhtr/validate-curp@latest/dist/index.js 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 16.x 16 | - run: yarn install 17 | - run: yarn build 18 | - run: yarn test 19 | env: 20 | CI: true 21 | -------------------------------------------------------------------------------- /src/__tests__/getCheckDigit.test.js: -------------------------------------------------------------------------------- 1 | const getCheckDigit = require('../getCheckDigit'); 2 | 3 | describe('.getVerificationDigit', () => { 4 | const testCases = { 5 | SABC560626MDFLRN01: '1', 6 | MOTR930411HJCRMN03: '3', 7 | MOTR930411HJCRMG73: '0', 8 | }; 9 | 10 | Object.keys(testCases).forEach((curp) => { 11 | const digit = testCases[curp]; 12 | 13 | it(`returns "${digit}" as verification digit for CURP "${curp}"`, () => { 14 | expect(getCheckDigit(curp)).toEqual(digit); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import json from '@rollup/plugin-json'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | 6 | export default { 7 | input: 'src/index.js', 8 | output: { 9 | file: 'dist/index.js', 10 | format: 'umd', 11 | name: 'validateCurp', 12 | sourcemap: true, 13 | }, 14 | plugins: [ 15 | commonjs(), 16 | json(), 17 | babel({ 18 | babelHelpers: 'bundled', 19 | presets: ['@babel/env'], 20 | }), 21 | terser(), 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /src/validStates.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'AS', // AGUASCALIENTES 3 | 'BC', // BAJA CALIFORNIA 4 | 'BS', // BAJA CALIFORNIA SUR 5 | 'CC', // CAMPECHE 6 | 'CL', // COAHUILA 7 | 'CM', // COLIMA 8 | 'CS', // CHIAPAS 9 | 'CH', // CHIHUAHUA 10 | 'DF', // DISTRITO FEDERAL 11 | 'DG', // DURANGO 12 | 'GT', // GUANAJUATO 13 | 'GR', // GUERRERO 14 | 'HG', // HIDALGO 15 | 'JC', // JALISCO 16 | 'MC', // MÉXICO 17 | 'MN', // MICHOACÁN 18 | 'MS', // MORELOS 19 | 'NT', // NAYARIT 20 | 'NL', // NUEVO LE”N 21 | 'OC', // OAXACA 22 | 'PL', // PUEBLA 23 | 'QT', // QUERÉTARO 24 | 'QR', // QUINTANA ROO 25 | 'SP', // SAN LUIS POTOSÍ 26 | 'SL', // SINALOA 27 | 'SR', // SONORA 28 | 'TC', // TABASCO 29 | 'TS', // TAMAULIPAS 30 | 'TL', // TLAXCALA 31 | 'VZ', // VERACRUZ 32 | 'YN', // YUCATÁN 33 | 'ZS', // ZACATECAS 34 | 'NE', // NACIDO EN EL EXTRANJERO 35 | ]; 36 | -------------------------------------------------------------------------------- /src/getCheckDigit.js: -------------------------------------------------------------------------------- 1 | const VALUES_MAP = { 2 | 0: 0, 3 | 1: 1, 4 | 2: 2, 5 | 3: 3, 6 | 4: 4, 7 | 5: 5, 8 | 6: 6, 9 | 7: 7, 10 | 8: 8, 11 | 9: 9, 12 | A: 10, 13 | B: 11, 14 | C: 12, 15 | D: 13, 16 | E: 14, 17 | F: 15, 18 | G: 16, 19 | H: 17, 20 | I: 18, 21 | J: 19, 22 | K: 20, 23 | L: 21, 24 | M: 22, 25 | N: 23, 26 | Ñ: 24, 27 | O: 25, 28 | P: 26, 29 | Q: 27, 30 | R: 28, 31 | S: 29, 32 | T: 30, 33 | U: 31, 34 | V: 32, 35 | W: 33, 36 | X: 34, 37 | Y: 35, 38 | Z: 36, 39 | }; 40 | 41 | const getScore = (string) => string.split('').reduce((sum, char, i) => { 42 | const index = 18 - i; 43 | const value = VALUES_MAP[char] || 0; 44 | return sum + value * index; 45 | }, 0); 46 | 47 | module.exports = (curp) => { 48 | const base = curp.slice(0, -1); 49 | const score = getScore(base); 50 | const mod = score % 10; 51 | if (mod === 0) return '0'; 52 | return String(10 - mod); 53 | }; 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Manuel de la Torre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/forbiddenWords.json: -------------------------------------------------------------------------------- 1 | [ 2 | "BACA", 3 | "BAKA", 4 | "BUEI", 5 | "BUEY", 6 | "CACA", 7 | "CACO", 8 | "CAGA", 9 | "CAGO", 10 | "CAKA", 11 | "CAKO", 12 | "COGE", 13 | "COGI", 14 | "COJA", 15 | "COJE", 16 | "COJI", 17 | "COJO", 18 | "COLA", 19 | "CULO", 20 | "FALO", 21 | "FETO", 22 | "GETA", 23 | "GUEI", 24 | "GUEY", 25 | "JETA", 26 | "JOTO", 27 | "KACA", 28 | "KACO", 29 | "KAGA", 30 | "KAGO", 31 | "KAKA", 32 | "KAKO", 33 | "KOGE", 34 | "KOGI", 35 | "KOJA", 36 | "KOJE", 37 | "KOJI", 38 | "KOJO", 39 | "KOLA", 40 | "KULO", 41 | "LILO", 42 | "LOCA", 43 | "LOCO", 44 | "LOKA", 45 | "LOKO", 46 | "MAME", 47 | "MAMO", 48 | "MEAR", 49 | "MEAS", 50 | "MEON", 51 | "MIAR", 52 | "MION", 53 | "MOCO", 54 | "MOKO", 55 | "MULA", 56 | "MULO", 57 | "NACA", 58 | "NACO", 59 | "PEDA", 60 | "PEDO", 61 | "PENE", 62 | "PIPI", 63 | "PITO", 64 | "POPO", 65 | "PUTA", 66 | "PUTO", 67 | "QULO", 68 | "RATA", 69 | "ROBA", 70 | "ROBE", 71 | "ROBO", 72 | "RUIN", 73 | "SENO", 74 | "TETA", 75 | "VACA", 76 | "VAGA", 77 | "VAGO", 78 | "VAKA", 79 | "VUEI", 80 | "VUEY", 81 | "WUEI", 82 | "WUEY" 83 | ] 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validate-curp", 3 | "version": "1.0.0", 4 | "description": "A simple library to validate Mexican CURPs (Personal ID)", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "./node_modules/rollup/dist/bin/rollup -c", 8 | "test": "jest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/manuelmhtr/validate-curp.git" 13 | }, 14 | "keywords": [ 15 | "CURP", 16 | "validator", 17 | "validar", 18 | "validador", 19 | "verificar", 20 | "verify", 21 | "Clave Única de Registro de Población", 22 | "Mexico", 23 | "Mexican" 24 | ], 25 | "author": "Manuel de la Torre", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/manuelmhtr/validate-curp/issues" 29 | }, 30 | "homepage": "https://github.com/manuelmhtr/validate-curp#readme", 31 | "jest": { 32 | "testEnvironment": "node" 33 | }, 34 | "devDependencies": { 35 | "@babel/cli": "^7.18.9", 36 | "@babel/core": "^7.18.9", 37 | "@babel/preset-env": "^7.18.9", 38 | "@rollup/plugin-babel": "^5.3.1", 39 | "@rollup/plugin-commonjs": "^22.0.1", 40 | "@rollup/plugin-json": "^4.1.0", 41 | "eslint": "^8.2.0", 42 | "eslint-config-airbnb-base": "15.0.0", 43 | "eslint-plugin-import": "^2.25.2", 44 | "eslint-plugin-jest": "^26.6.0", 45 | "jest": "^28.1.3", 46 | "rollup": "^2.77.0", 47 | "rollup-plugin-terser": "^7.0.2" 48 | }, 49 | "dependencies": {} 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # IDEs config 87 | .vscode 88 | 89 | # dotenv environment variables file 90 | .env 91 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | !function(A,O){"object"==typeof exports&&"undefined"!=typeof module?module.exports=O():"function"==typeof define&&define.amd?define(O):(A="undefined"!=typeof globalThis?globalThis:A||self).validateCurp=O()}(this,(function(){"use strict";var A={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,G:16,H:17,I:18,J:19,K:20,L:21,M:22,N:23,"Ñ":24,O:25,P:26,Q:27,R:28,S:29,T:30,U:31,V:32,W:33,X:34,Y:35,Z:36},O=function(O){var e=O.slice(0,-1),n=e.split("").reduce((function(O,e,n){return O+(A[e]||0)*(18-n)}),0)%10;return 0===n?"0":String(10-n)},e=["BACA","BAKA","BUEI","BUEY","CACA","CACO","CAGA","CAGO","CAKA","CAKO","COGE","COGI","COJA","COJE","COJI","COJO","COLA","CULO","FALO","FETO","GETA","GUEI","GUEY","JETA","JOTO","KACA","KACO","KAGA","KAGO","KAKA","KAKO","KOGE","KOGI","KOJA","KOJE","KOJI","KOJO","KOLA","KULO","LILO","LOCA","LOCO","LOKA","LOKO","MAME","MAMO","MEAR","MEAS","MEON","MIAR","MION","MOCO","MOKO","MULA","MULO","NACA","NACO","PEDA","PEDO","PENE","PIPI","PITO","POPO","PUTA","PUTO","QULO","RATA","ROBA","ROBE","ROBO","RUIN","SENO","TETA","VACA","VAGA","VAGO","VAKA","VUEI","VUEY","WUEI","WUEY"],n=["AS","BC","BS","CC","CL","CM","CS","CH","DF","DG","GT","GR","HG","JC","MC","MN","MS","NT","NL","OC","PL","QT","QR","SP","SL","SR","TC","TS","TL","VZ","YN","ZS","NE"],t=/^[A-Z][AEIOUX][A-Z]{2}[0-9]{6}[HM][A-Z]{2}[B-DF-HJ-NP-TV-Z]{3}[A-Z\d]\d$/,r=function(A){var r=[],i=t.test(A),u=!i||function(A){var O=A.slice(4,10),e=O.slice(0,2),n=O.slice(2,4),t=O.slice(4,6),r=new Date("20".concat(e,"-").concat(n,"-").concat(t));return!Number.isNaN(r.getTime())}(A),C=!i||function(A){var O=(A||"").slice(11,13);return n.includes(O)}(A),c=!i||function(A){var e=A.slice(-1);return O(A)===e}(A);return i||r.push("INVALID_FORMAT"),u||r.push("INVALID_DATE"),C||r.push("INVALID_STATE"),c||r.push("INVALID_CHECK_DIGIT"),function(A){var O=(A||"").slice(0,4);return e.includes(O)}(A)&&r.push("FORBIDDEN_WORD"),r};return function(A){var O=function(A){return String(A).trim().toUpperCase().replace(/[^0-9A-Z]/g,"")}(A),e=r(O);return 0===e.length?function(A){return{isValid:!0,curp:A}}(O):function(A){return{isValid:!1,curp:null,errors:A}}(e)}})); 2 | //# sourceMappingURL=index.js.map 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const getCheckDigit = require('./getCheckDigit'); 2 | const forbiddenWords = require('./forbiddenWords.json'); 3 | const validStates = require('./validStates'); 4 | 5 | const CURP_REGEXP = /^[A-Z][AEIOUX][A-Z]{2}[0-9]{6}[HM][A-Z]{2}[B-DF-HJ-NP-TV-Z]{3}[A-Z\d]\d$/; 6 | const INVALID_FORMAT_ERROR = 'INVALID_FORMAT'; 7 | const INVALID_DATE_ERROR = 'INVALID_DATE'; 8 | const INVALID_STATE_ERROR = 'INVALID_STATE'; 9 | const INVALID_CHECK_DIGIT_ERROR = 'INVALID_CHECK_DIGIT'; 10 | const FORBIDDEN_WORD_ERROR = 'FORBIDDEN_WORD'; 11 | 12 | const parseInput = (input) => String(input) 13 | .trim() 14 | .toUpperCase() 15 | .replace(/[^0-9A-Z]/g, ''); 16 | 17 | const validateDate = (curp) => { 18 | const dateStr = curp.slice(4, 10); 19 | const year = dateStr.slice(0, 2); 20 | const month = dateStr.slice(2, 4); 21 | const day = dateStr.slice(4, 6); 22 | const date = new Date(`20${year}-${month}-${day}`); 23 | return !Number.isNaN(date.getTime()); 24 | }; 25 | 26 | const validateCheckDigit = (curp) => { 27 | const digit = curp.slice(-1); 28 | const expected = getCheckDigit(curp); 29 | return expected === digit; 30 | }; 31 | 32 | const validateState = (curp) => { 33 | const state = (curp || '').slice(11, 13); 34 | return validStates.includes(state); 35 | }; 36 | 37 | const hasForbiddenWords = (curp) => { 38 | const prefix = (curp || '').slice(0, 4); 39 | return forbiddenWords.includes(prefix); 40 | }; 41 | 42 | const validate = (curp) => { 43 | const errors = []; 44 | const hasValidFormat = CURP_REGEXP.test(curp); 45 | const hasValidDate = hasValidFormat ? validateDate(curp) : true; 46 | const hasValidState = hasValidFormat ? validateState(curp) : true; 47 | const hasValidDigit = hasValidFormat ? validateCheckDigit(curp) : true; 48 | if (!hasValidFormat) errors.push(INVALID_FORMAT_ERROR); 49 | if (!hasValidDate) errors.push(INVALID_DATE_ERROR); 50 | if (!hasValidState) errors.push(INVALID_STATE_ERROR); 51 | if (!hasValidDigit) errors.push(INVALID_CHECK_DIGIT_ERROR); 52 | if (hasForbiddenWords(curp)) errors.push(FORBIDDEN_WORD_ERROR); 53 | return errors; 54 | }; 55 | 56 | const getValidResponse = (curp) => ({ 57 | isValid: true, 58 | curp, 59 | }); 60 | 61 | const getInvalidResponse = (errors) => ({ 62 | isValid: false, 63 | curp: null, 64 | errors, 65 | }); 66 | 67 | module.exports = (input) => { 68 | const curp = parseInput(input); 69 | const errors = validate(curp); 70 | const isValid = errors.length === 0; 71 | 72 | return isValid ? getValidResponse(curp) : getInvalidResponse(errors); 73 | }; 74 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const validateCurp = require('../index'); 2 | 3 | describe('.validateCurp', () => { 4 | describe('when CURP is valid', () => { 5 | it('returns true for a valid person CURP', () => { 6 | const curp = 'MOTR930411HJCRMN03'; 7 | const response = validateCurp(curp); 8 | expect(response).toEqual({ 9 | curp, 10 | isValid: true, 11 | }); 12 | }); 13 | 14 | it('works with lowercase and symbols', () => { 15 | const curp = ' motr-930411/HjCR*mn03 '; 16 | const response = validateCurp(curp); 17 | expect(response).toEqual({ 18 | curp: 'MOTR930411HJCRMN03', 19 | isValid: true, 20 | }); 21 | }); 22 | }); 23 | 24 | describe('support for special cases', () => { 25 | it('return is valid for "foreign" state (NE)', () => { 26 | const curp = 'MOTR930411HNERMN03'; 27 | const response = validateCurp(curp); 28 | expect(response).toEqual({ 29 | curp, 30 | isValid: true, 31 | }); 32 | }); 33 | }); 34 | 35 | describe('when CURP is not valid', () => { 36 | it('should return false when CURP has Ñ', () => { 37 | const curp = 'MOTR930411HJCRMÑ03'; 38 | const response = validateCurp(curp); 39 | expect(response).toEqual({ 40 | curp: null, 41 | isValid: false, 42 | errors: ['INVALID_FORMAT'], 43 | }); 44 | }); 45 | 46 | it('should return not valid and specify errors when input is not a string', () => { 47 | const curp = null; 48 | const response = validateCurp(curp); 49 | expect(response).toEqual({ 50 | curp: null, 51 | isValid: false, 52 | errors: ['INVALID_FORMAT'], 53 | }); 54 | }); 55 | 56 | it('should return not valid and specify errors when format is incorrect', () => { 57 | const curp = 'INVALID_CURP'; 58 | const response = validateCurp(curp); 59 | expect(response).toEqual({ 60 | curp: null, 61 | isValid: false, 62 | errors: ['INVALID_FORMAT'], 63 | }); 64 | }); 65 | 66 | it('should return not valid and specify errors when format is correct but date is not', () => { 67 | const curp = 'MOTR931311HJCRMN02'; 68 | const response = validateCurp(curp); 69 | expect(response).toEqual({ 70 | curp: null, 71 | isValid: false, 72 | errors: ['INVALID_DATE'], 73 | }); 74 | }); 75 | 76 | it('should return not valid and specify errors when format is correct but state is not', () => { 77 | const curp = 'MOTR930411HXXRMN06'; 78 | const response = validateCurp(curp); 79 | expect(response).toEqual({ 80 | curp: null, 81 | isValid: false, 82 | errors: ['INVALID_STATE'], 83 | }); 84 | }); 85 | 86 | it('should return not valid and specify errors when check digit is not correct', () => { 87 | const curp = 'MOTR930411HJCRMN06'; 88 | const response = validateCurp(curp); 89 | expect(response).toEqual({ 90 | curp: null, 91 | isValid: false, 92 | errors: ['INVALID_CHECK_DIGIT'], 93 | }); 94 | }); 95 | 96 | it('should return not valid and specify errors when contains a forbidden word', () => { 97 | const curp = 'FETO930411HJCRMN01'; 98 | const response = validateCurp(curp); 99 | expect(response).toEqual({ 100 | curp: null, 101 | isValid: false, 102 | errors: ['FORBIDDEN_WORD'], 103 | }); 104 | }); 105 | 106 | it('should return multiple errors when is required', () => { 107 | const curp = 'FETO931311HJCRMN06'; 108 | const response = validateCurp(curp); 109 | expect(response).toEqual({ 110 | curp: null, 111 | isValid: false, 112 | errors: ['INVALID_DATE', 'INVALID_CHECK_DIGIT', 'FORBIDDEN_WORD'], 113 | }); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Validate CURP 2 | 3 | ![](https://img.shields.io/badge/build-passing-green?style=flat) 4 | ![](https://img.shields.io/npm/dm/validate-curp) 5 | ![](https://img.shields.io/github/license/manuelmhtr/validate-curp?color=blue) 6 | 7 | A simple and lightweight library to validate [Mexican CURPs](https://es.wikipedia.org/wiki/Clave_%C3%9Anica_de_Registro_de_Poblaci%C3%B3n) (Personal ID). 8 | 9 | 10 | ## Install 11 | 12 | ### NodeJS 13 | 14 | Use NPM: 15 | 16 | ```shell 17 | $ npm install --save validate-curp 18 | ``` 19 | 20 | Or YARN: 21 | 22 | ```shell 23 | $ yarn add validate-curp 24 | ``` 25 | 26 | ### Browser 27 | 28 | Add the script to your project: 29 | 30 | ```html 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | ``` 43 | 44 | 45 | ## API 46 | 47 | The library only exposes a single function (`.validateCurp`). 48 | 49 | 50 | ### .validateCurp(curp) 51 | 52 | 53 | Checks whether a string is a valid CURP and returns validation details. 54 | 55 | 56 | **Parameters** 57 | 58 | | Parameter | Type | Description | 59 | | --------- | ---- | ----------- | 60 | |`curp`|String|The CURP to be validated.| 61 | |`options`|Object| Settings (Optional).| 62 | 63 | **Response** 64 | 65 | It returns a plain object with the values: 66 | 67 | | Parameter | Type | Description | 68 | | --------- | ---- | ----------- | 69 | |`isValid`|Boolean|Indicates if the string is a valid CURP.| 70 | |`curp`|String|The formatted CURP (uppercase, with no white spaces or symbols). Returns `null` when input is an invalid CURP.| 71 | |`errors`|Array[String]|In case the CURP is invalid, the reasons why the CURP is invalid will be listed here.| 72 | 73 | Possible `errors` values and they description are: 74 | 75 | | Error | Descripción | 76 | | ----- | ----------- | 77 | |`INVALID_FORMAT`|The format is invalid, that means, the string does not meet with the required length or expected structure. Eg: `XYZ` because clearly is not an CURP. | 78 | |`INVALID_DATE`|The string may have the correct format, but digits generate an invalid date. Eg: `MOTR935511HJCRMN03` because it refers to month `55`.| 79 | |`INVALID_STATE`|The string may have the correct format, but letters for state don't match with a valid one. Eg: `MOTR9390411HXXRMN03` because it refers to state `XX`, which does not exist. See the valid states list [here](/src/validStates.js).| 80 | |`INVALID_CHECK_DIGIT`|The string has a valid format, but the last character (check digit) is invalid. Eg: `MOTR930411HJCRMN09` ends with `9` but it is expected to end with `3`.| 81 | |`FORBIDDEN_WORD`|The string contains one of the inconvenient words that cannot be included in a CURP. Eg: `FETO930411HJCRMN03` the initials make the word `FETO` (fetus, LOL). Find the full list of words in [this document](http://www.ordenjuridico.gob.mx/Federal/PE/APF/APC/SEGOB/Instructivos/InstructivoNormativo.pdf).| 82 | 83 | 84 | **Example** 85 | 86 | ```js 87 | const validateCurp = require('validate-curp'); 88 | 89 | const response = validateCurp('motr930411hjcrmn03'); 90 | console.log(response); 91 | 92 | /* 93 | Prints: 94 | 95 | { 96 | isValid: true, 97 | curp: 'MOTR930411HJCRMN03' 98 | } 99 | 100 | */ 101 | 102 | const response = validateCurp('This is not a CURP'); 103 | console.log(response); 104 | 105 | /* 106 | Prints: 107 | 108 | { 109 | isValid: false, 110 | curp: null, 111 | errors: ['INVALID_FORMAT'] 112 | } 113 | 114 | */ 115 | ``` 116 | 117 | 118 | ## Tests 119 | 120 | Run the test with the command: 121 | 122 | ```shell 123 | $ yarn test 124 | ``` 125 | 126 | 127 | ## Related 128 | 129 | * [validate-rfc](https://github.com/manuelmhtr/validate-rfc) 130 | * You need to check if an RFC is registered in SAT or is blacklisted? Try with [Verifier](https://rapidapi.com/manuelmhtr/api/verifier). 131 | 132 | ## Licencia 133 | 134 | MIT 135 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":["../src/getCheckDigit.js","../src/index.js","../src/validStates.js"],"sourcesContent":["const VALUES_MAP = {\n 0: 0,\n 1: 1,\n 2: 2,\n 3: 3,\n 4: 4,\n 5: 5,\n 6: 6,\n 7: 7,\n 8: 8,\n 9: 9,\n A: 10,\n B: 11,\n C: 12,\n D: 13,\n E: 14,\n F: 15,\n G: 16,\n H: 17,\n I: 18,\n J: 19,\n K: 20,\n L: 21,\n M: 22,\n N: 23,\n Ñ: 24,\n O: 25,\n P: 26,\n Q: 27,\n R: 28,\n S: 29,\n T: 30,\n U: 31,\n V: 32,\n W: 33,\n X: 34,\n Y: 35,\n Z: 36,\n};\n\nconst getScore = (string) => string.split('').reduce((sum, char, i) => {\n const index = 18 - i;\n const value = VALUES_MAP[char] || 0;\n return sum + value * index;\n}, 0);\n\nmodule.exports = (curp) => {\n const base = curp.slice(0, -1);\n const score = getScore(base);\n const mod = score % 10;\n if (mod === 0) return '0';\n return String(10 - mod);\n};\n","const getCheckDigit = require('./getCheckDigit');\nconst forbiddenWords = require('./forbiddenWords.json');\nconst validStates = require('./validStates');\n\nconst CURP_REGEXP = /^[A-Z][AEIOUX][A-Z]{2}[0-9]{6}[HM][A-Z]{2}[B-DF-HJ-NP-TV-Z]{3}[A-Z\\d]\\d$/;\nconst INVALID_FORMAT_ERROR = 'INVALID_FORMAT';\nconst INVALID_DATE_ERROR = 'INVALID_DATE';\nconst INVALID_STATE_ERROR = 'INVALID_STATE';\nconst INVALID_CHECK_DIGIT_ERROR = 'INVALID_CHECK_DIGIT';\nconst FORBIDDEN_WORD_ERROR = 'FORBIDDEN_WORD';\n\nconst parseInput = (input) => String(input)\n .trim()\n .toUpperCase()\n .replace(/[^0-9A-Z]/g, '');\n\nconst validateDate = (curp) => {\n const dateStr = curp.slice(4, 10);\n const year = dateStr.slice(0, 2);\n const month = dateStr.slice(2, 4);\n const day = dateStr.slice(4, 6);\n const date = new Date(`20${year}-${month}-${day}`);\n return !Number.isNaN(date.getTime());\n};\n\nconst validateCheckDigit = (curp) => {\n const digit = curp.slice(-1);\n const expected = getCheckDigit(curp);\n return expected === digit;\n};\n\nconst validateState = (curp) => {\n const state = (curp || '').slice(11, 13);\n return validStates.includes(state);\n};\n\nconst hasForbiddenWords = (curp) => {\n const prefix = (curp || '').slice(0, 4);\n return forbiddenWords.includes(prefix);\n};\n\nconst validate = (curp) => {\n const errors = [];\n const hasValidFormat = CURP_REGEXP.test(curp);\n const hasValidDate = hasValidFormat ? validateDate(curp) : true;\n const hasValidState = hasValidFormat ? validateState(curp) : true;\n const hasValidDigit = hasValidFormat ? validateCheckDigit(curp) : true;\n if (!hasValidFormat) errors.push(INVALID_FORMAT_ERROR);\n if (!hasValidDate) errors.push(INVALID_DATE_ERROR);\n if (!hasValidState) errors.push(INVALID_STATE_ERROR);\n if (!hasValidDigit) errors.push(INVALID_CHECK_DIGIT_ERROR);\n if (hasForbiddenWords(curp)) errors.push(FORBIDDEN_WORD_ERROR);\n return errors;\n};\n\nconst getValidResponse = (curp) => ({\n isValid: true,\n curp,\n});\n\nconst getInvalidResponse = (errors) => ({\n isValid: false,\n curp: null,\n errors,\n});\n\nmodule.exports = (input) => {\n const curp = parseInput(input);\n const errors = validate(curp);\n const isValid = errors.length === 0;\n\n return isValid ? getValidResponse(curp) : getInvalidResponse(errors);\n};\n","module.exports = [\n 'AS', // AGUASCALIENTES\n 'BC', // BAJA CALIFORNIA\n 'BS', // BAJA CALIFORNIA SUR\n 'CC', // CAMPECHE\n 'CL', // COAHUILA\n 'CM', // COLIMA\n 'CS', // CHIAPAS\n 'CH', // CHIHUAHUA\n 'DF', // DISTRITO FEDERAL\n 'DG', // DURANGO\n 'GT', // GUANAJUATO\n 'GR', // GUERRERO\n 'HG', // HIDALGO\n 'JC', // JALISCO\n 'MC', // MÉXICO\n 'MN', // MICHOACÁN\n 'MS', // MORELOS\n 'NT', // NAYARIT\n 'NL', // NUEVO LE”N\n 'OC', // OAXACA\n 'PL', // PUEBLA\n 'QT', // QUERÉTARO\n 'QR', // QUINTANA ROO\n 'SP', // SAN LUIS POTOSÍ\n 'SL', // SINALOA\n 'SR', // SONORA\n 'TC', // TABASCO\n 'TS', // TAMAULIPAS\n 'TL', // TLAXCALA\n 'VZ', // VERACRUZ\n 'YN', // YUCATÁN\n 'ZS', // ZACATECAS\n 'NE', // NACIDO EN EL EXTRANJERO\n];\n"],"names":["VALUES_MAP","A","B","C","D","E","F","G","H","I","J","K","L","M","N","Ñ","O","P","Q","R","S","T","U","V","W","X","Y","Z","getCheckDigit","curp","base","slice","mod","split","reduce","sum","char","i","String","forbiddenWords","validStates","CURP_REGEXP","validate","errors","hasValidFormat","test","hasValidDate","dateStr","year","month","day","date","Date","concat","Number","isNaN","getTime","validateDate","hasValidState","state","includes","validateState","hasValidDigit","digit","validateCheckDigit","push","prefix","hasForbiddenWords","input","trim","toUpperCase","replace","parseInput","length","isValid","getValidResponse","getInvalidResponse"],"mappings":"6OAAA,IAAMA,EAAa,CACjB,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACH,EAAG,EACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,IAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,GACHC,EAAG,ICrCCC,ED8CW,SAACC,GAChB,IAAMC,EAAOD,EAAKE,MAAM,GAAI,GAEtBC,EADiBF,EARWG,MAAM,IAAIC,QAAO,SAACC,EAAKC,EAAMC,GAG/D,OAAOF,GADOnC,EAAWoC,IAAS,IADpB,GAAKC,KAGlB,GAKmB,GACpB,OAAY,IAARL,EAAkB,IACfM,OAAO,GAAKN,IClDfO,2jBACAC,ECFW,CACf,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,MD7BIC,EAAc,2EAqCdC,EAAW,SAACb,GAChB,IAAMc,EAAS,GACTC,EAAiBH,EAAYI,KAAKhB,GAClCiB,GAAeF,GA5BF,SAACf,GACpB,IAAMkB,EAAUlB,EAAKE,MAAM,EAAG,IACxBiB,EAAOD,EAAQhB,MAAM,EAAG,GACxBkB,EAAQF,EAAQhB,MAAM,EAAG,GACzBmB,EAAMH,EAAQhB,MAAM,EAAG,GACvBoB,EAAO,IAAIC,KAAJ,KAAAC,OAAcL,EAAQC,KAAAA,OAAAA,EAASC,KAAAA,OAAAA,IAC5C,OAAQI,OAAOC,MAAMJ,EAAKK,WAsBYC,CAAa5B,GAC7C6B,GAAgBd,GAdF,SAACf,GACrB,IAAM8B,GAAS9B,GAAQ,IAAIE,MAAM,GAAI,IACrC,OAAOS,EAAYoB,SAASD,GAYWE,CAAchC,GAC/CiC,GAAgBlB,GArBG,SAACf,GAC1B,IAAMkC,EAAQlC,EAAKE,OAAO,GAE1B,OADiBH,EAAcC,KACXkC,EAkBmBC,CAAmBnC,GAM1D,OALKe,GAAgBD,EAAOsB,KA1CD,kBA2CtBnB,GAAcH,EAAOsB,KA1CD,gBA2CpBP,GAAef,EAAOsB,KA1CD,iBA2CrBH,GAAenB,EAAOsB,KA1CK,uBA4BR,SAACpC,GACzB,IAAMqC,GAAUrC,GAAQ,IAAIE,MAAM,EAAG,GACrC,OAAOQ,EAAeqB,SAASM,GAa3BC,CAAkBtC,IAAOc,EAAOsB,KA1CT,kBA2CpBtB,UAcQ,SAACyB,GAChB,IAAMvC,EAxDW,SAACuC,GAAD,OAAW9B,OAAO8B,GAClCC,OACAC,cACAC,QAAQ,aAAc,IAqDVC,CAAWJ,GAClBzB,EAASD,EAASb,GAGxB,OAFkC,IAAlBc,EAAO8B,OAdA,SAAC5C,GAAD,MAAW,CAClC6C,SAAS,EACT7C,KAAAA,GAciB8C,CAAiB9C,GAXT,SAACc,GAAD,MAAa,CACtC+B,SAAS,EACT7C,KAAM,KACNc,OAAAA,GAQ0CiC,CAAmBjC"} --------------------------------------------------------------------------------