@felipeteko/curp
Version:
Genera y valida el CURP (Clave Única de Registro de Población) mexicano.
582 lines (533 loc) • 12.6 kB
JavaScript
;
const curp = {};
const GENERO = {
MASCULINO: 'H',
FEMENINO: 'M',
NO_BINARIO: 'X',
};
const ESTADO = {
AGUASCALIENTES: 'AS',
BAJA_CALIFORNIA: 'BC',
BAJA_CALIFORNIA_SUR: 'BS',
CAMPECHE: 'CC',
COAHUILA: 'CL',
COLIMA: 'CM',
CHIAPAS: 'CS',
CHIHUAHUA: 'CH',
DISTRITO_FEDERAL: 'DF',
CDMX: 'DF',
DURANGO: 'DG',
GUANAJUATO: 'GT',
GUERRERO: 'GR',
HIDALGO: 'HG',
JALISCO: 'JC',
ESTADO_DE_MEXICO: 'MC',
NO_ESPECIFICADO: 'NE',
MICHOACAN: 'MN',
MORELOS: 'MS',
NAYARIT: 'NT',
NUEVO_LEON: 'NL',
OAXACA: 'OC',
PUEBLA: 'PL',
QUERETARO: 'QT',
QUINTANA_ROO: 'QR',
SAN_LUIS_POTOSI: 'SP',
SINALOA: 'SL',
SONORA: 'SR',
TABASCO: 'TC',
TAMAULIPAS: 'TS',
TLAXCALA: 'TL',
VERACRUZ: 'VZ',
YUCATAN: 'YN',
ZACATECAS: 'ZS',
};
const comunes = [
'MARIA DEL ',
'MARIA DE LOS ',
'MARIA ',
'JOSE DE ',
'JOSE ',
'MA. ',
'MA ',
'M. ',
'J. ',
'J ',
'M ',
];
const malasPalabras = {
BACA: 'BXCA',
BAKA: 'BXKA',
BUEI: 'BXEI',
BUEY: 'BXEY',
CACA: 'CXCA',
CACO: 'CXCO',
CAGA: 'CXGA',
CAGO: 'CXGO',
CAKA: 'CXKA',
CAKO: 'CXKO',
COGE: 'CXGE',
COGI: 'CXGI',
COJA: 'CXJA',
COJE: 'CXJE',
COJI: 'CXJI',
COJO: 'CXJO',
COLA: 'CXLA',
CULO: 'CXLO',
FALO: 'FXLO',
FETO: 'FXTO',
GETA: 'GXTA',
GUEI: 'GXEI',
GUEY: 'GXEY',
JETA: 'JXTA',
JOTO: 'JXTO',
KACA: 'KXCA',
KACO: 'KXCO',
KAGA: 'KXGA',
KAGO: 'KXGO',
KAKA: 'KXKA',
KAKO: 'KXKO',
KOGE: 'KXGE',
KOGI: 'KXGI',
KOJA: 'KXJA',
KOJE: 'KXJE',
KOJI: 'KXJI',
KOJO: 'KXJO',
KOLA: 'KXLA',
KULO: 'KXLO',
LILO: 'LXLO',
LOCA: 'LXCA',
LOCO: 'LXCO',
LOKA: 'LXKA',
LOKO: 'LXKO',
MAME: 'MXME',
MAMO: 'MXMO',
MEAR: 'MXAR',
MEAS: 'MXAS',
MEON: 'MXON',
MIAR: 'MXAR',
MION: 'MXON',
MOCO: 'MXCO',
MOKO: 'MXKO',
MULA: 'MXLA',
MULO: 'MXLO',
NACA: 'NXCA',
NACO: 'NXCO',
PEDA: 'PXDA',
PEDO: 'PXDO',
PENE: 'PXNE',
PIPI: 'PXPI',
PITO: 'PXTO',
POPO: 'PXPO',
PUTA: 'PXTA',
PUTO: 'PXTO',
QULO: 'QXLO',
RATA: 'RXTA',
ROBA: 'RXBA',
ROBE: 'RXBE',
ROBO: 'RXBO',
RUIN: 'RXIN',
SENO: 'SXNO',
TETA: 'TXTA',
VACA: 'VXCA',
VAGA: 'VXGA',
VAGO: 'VXGO',
VAKA: 'VXKA',
VUEI: 'VXEI',
VUEY: 'VXEY',
WUEI: 'WXEI',
WUEY: 'WXEY',
};
/* eslint max-params: ["error", 6] */
/* eslint-env es6 */
class Persona {
constructor(
nombre,
apellidoPaterno,
apellidoMaterno,
genero,
estado,
fechaNacimiento,
) {
this.nombre = nombre;
this.apellidoPaterno = apellidoPaterno;
this.apellidoMaterno = apellidoMaterno;
this.genero = genero;
this.estado = estado;
this.fechaNacimiento = fechaNacimiento;
}
}
function getPersona() {
return new Persona();
}
function generar(persona) {
validaDatos(persona);
let curp = '';
const pad = zeropad.bind(null, 2);
const nombre = ajustaCompuesto(
normalizaString(persona.nombre.toUpperCase()),
).trim();
const apellidoPaterno = ajustaCompuesto(
normalizaString(persona.apellidoPaterno.toUpperCase()),
).trim();
let apellidoMaterno = persona.apellidoMaterno || '';
apellidoMaterno = ajustaCompuesto(
normalizaString(apellidoMaterno.toUpperCase()),
).trim();
const nombreUsar = obtenerNombreUsar(nombre);
const inicialNombre = nombreUsar.substring(0, 1);
let vocalApellido = apellidoPaterno
.substring(1)
.replace(/[BCDFGHJKLMNÑPQRSTVWXYZ]/g, '')
.substring(0, 1)
.trim();
vocalApellido = vocalApellido === '' ? 'X' : vocalApellido;
let primeraLetraPaterno = apellidoPaterno.substring(0, 1);
primeraLetraPaterno = primeraLetraPaterno === 'Ñ' ? 'X' : primeraLetraPaterno;
let primeraLetraMaterno = '';
if (!apellidoMaterno || apellidoMaterno === '') {
primeraLetraMaterno = 'X';
} else {
primeraLetraMaterno = apellidoMaterno.substring(0, 1);
primeraLetraMaterno =
primeraLetraMaterno === 'Ñ' ? 'X' : primeraLetraMaterno;
}
let posicionUnoCuatro = [
primeraLetraPaterno,
vocalApellido,
primeraLetraMaterno,
inicialNombre,
].join('');
posicionUnoCuatro = removerMalasPalabras(filtraCaracteres(posicionUnoCuatro));
const posicionCatorceDieciseis = [
primerConsonante(apellidoPaterno),
primerConsonante(apellidoMaterno),
primerConsonante(nombreUsar),
].join('');
const fechaNacimiento = persona.fechaNacimiento.split('-');
curp = [
posicionUnoCuatro,
pad(fechaNacimiento[2] - 1900),
pad(fechaNacimiento[1]),
pad(fechaNacimiento[0]),
persona.genero,
persona.estado,
filtraCaracteres(posicionCatorceDieciseis),
].join('');
curp += getSpecialChar(fechaNacimiento[2]);
curp += agregaDigitoVerificador(curp);
return curp;
}
/**
* AjustaCompuesto()
* Cuando el nombre o los apellidos son compuestos y tienen
* proposiciones, contracciones o conjunciones, se deben eliminar esas palabras
* a la hora de calcular el CURP.
* @param {string} str - String donde se eliminarán las partes que lo hacen compuesto
*/
function ajustaCompuesto(str) {
const compuestos = [
/\bDA\b/,
/\bDAS\b/,
/\bDE\b/,
/\bDEL\b/,
/\bDER\b/,
/\bDI\b/,
/\bDIE\b/,
/\bDD\b/,
/\bEL\b/,
/\bLA\b/,
/\bLOS\b/,
/\bLAS\b/,
/\bLE\b/,
/\bLES\b/,
/\bMAC\b/,
/\bMC\b/,
/\bVAN\b/,
/\bVON\b/,
/\bY\b/,
];
compuestos.forEach((compuesto) => {
if (compuesto.test(str)) {
str = str.replace(compuesto, '');
}
});
return str;
}
/**
* Zeropad()
* Rellena con ceros un string, para que quede de un ancho determinado.
* @param {number} ancho - Ancho deseado.
* @param {number} num - Numero que sera procesado.
*/
function zeropad(ancho, num) {
const pad = Array.apply(0, Array.call(0, ancho))
.map(() => 0)
.join('');
return (pad + num).replace(new RegExp('^.*([0-9]{' + ancho + '})$'), '$1');
}
/**
* PrimerConsonante()
* Saca la primer consonante interna del string, y la devuelve.
* Si no hay una consonante interna, devuelve X.
* @param {string} str - String del cual se va a sacar la primer consonante.
*/
function primerConsonante(str) {
str = str
.substring(1)
.replace(/[AEIOU]/gi, '')
.substring(0, 1)
.trim();
return str === '' || str === 'Ñ' ? 'X' : str;
}
/**
* FiltraCaracteres()
* Filtra convirtiendo todos los caracteres no alfabeticos a X.
* @param {string} str - String el cual sera convertido.
*/
function filtraCaracteres(str) {
return str.toUpperCase().replace(/[\d_\-./\\,]/g, 'X');
}
/**
* NormalizaString()
* Elimina los acentos, eñes y diéresis que pudiera tener el nombre.
* @param {string} str - String con el nombre o los apellidos.
*/
function normalizaString(str) {
const origen = [
'Ã',
'À',
'Á',
'Ä',
'Â',
'È',
'É',
'Ë',
'Ê',
'Ì',
'Í',
'Ï',
'Î',
'Ò',
'Ó',
'Ö',
'Ô',
'Ù',
'Ú',
'Ü',
'Û',
'ã',
'à',
'á',
'ä',
'â',
'è',
'é',
'ë',
'ê',
'ì',
'í',
'ï',
'î',
'ò',
'ó',
'ö',
'ô',
'ù',
'ú',
'ü',
'û',
'Ç',
'ç',
];
const destino = [
'A',
'A',
'A',
'A',
'A',
'E',
'E',
'E',
'E',
'I',
'I',
'I',
'I',
'O',
'O',
'O',
'O',
'U',
'U',
'U',
'U',
'a',
'a',
'a',
'a',
'a',
'e',
'e',
'e',
'e',
'i',
'i',
'i',
'i',
'o',
'o',
'o',
'o',
'u',
'u',
'u',
'u',
'c',
'c',
];
str = str.split('');
const salida = str.map((char) => {
const pos = origen.indexOf(char);
return pos > -1 ? destino[pos] : char;
});
return salida.join('');
}
/**
* AgregaDigitoVerificador()
* Agrega el dígito que se usa para validar el CURP.
* @param {string} curpStr - String que contiene los primeros 17 caracteres del CURP.
*/
function agregaDigitoVerificador(incompleteCurp) {
const dictionary = '0123456789ABCDEFGHIJKLMNÑOPQRSTUVWXYZ';
let lnSum = 0.0;
let lnDigt = 0.0;
for (let i = 0; i < 17; i++) {
lnSum += dictionary.indexOf(incompleteCurp.charAt(i)) * (18 - i);
}
lnDigt = 10 - (lnSum % 10);
if (lnDigt === 10) return 0;
return lnDigt;
}
function getSpecialChar(bornYear) {
if (bornYear[0] === '1') {
return '0';
}
return 'A';
}
/**
* Obtiene el nombre que se debe utilizar en la generacion del curp.
* si tiene mas de 1 nombre Y el primer nombre es uno de la lista de nombres comunes,
* se usa el segundo nombre.
* @param {string} nombre - String que representa todos los nombres (excepto los apellidos) separados por espacio
* @author Israel Perales.
*/
function obtenerNombreUsar(nombre) {
const nombres = nombre.trim().split(/\s+/);
if (nombres.length === 1) return nombres[0];
const esNombreComun = comunes.some(
(nombreComun) => nombre.indexOf(nombreComun) === 0,
);
if (esNombreComun) return nombres[1];
return nombres[0];
}
function removerMalasPalabras(palabra) {
if (malasPalabras[palabra]) {
return malasPalabras[palabra];
}
return palabra;
}
/**
* Valida los datos requeridos para generar el CURP.
* @param {*} persona
*/
function validaDatos(persona) {
if (!persona.nombre) throw new Error('Nombre es requerido');
if (!persona.apellidoPaterno)
throw new Error('Apellido Paterno es requerido');
if (!persona.fechaNacimiento)
throw new Error('Fecha de nacimiento es requerido');
if (!persona.genero) throw new Error('Genero es requerido');
if (!persona.estado) throw new Error('Estado es requerido');
}
/**
* Valida que el curp cumpla con el formato y el digito verificador.
* @param {string} curpToValidate
* @returns true de ser valido, false de ser invalido.
*/
function validar(curpToValidate) {
const regex =
/^([A-Z][AEIOUX][A-Z]{2}\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])[HMX](?:AS|B[CS]|C[CLMSH]|D[FG]|G[TR]|HG|JC|M[CNS]|N[ETL]|OC|PL|Q[TR]|S[PLR]|T[CSL]|VZ|YN|ZS)[B-DF-HJ-NP-TV-Z]{3}[A-Z\d])(\d)$/;
const validado = curpToValidate.match(regex);
return (
validado &&
parseInt(validado[2], 10) === agregaDigitoVerificador(validado[1])
);
}
/**
* Capitaliza la primera letra de cada palabra en una cadena de texto.
* Convierte todas las letras a minúsculas primero y luego pone en mayúscula
* la primera letra de cada palabra.
*
* @param {string} str - La cadena de texto que se va a capitalizar.
* @returns {string} La cadena con la primera letra de cada palabra en mayúscula.
*/
function capitalizeWords(str) {
return str.toLowerCase().replace(/\b\w/g, (char) => char.toUpperCase());
}
/**
* Obtiene una lista de los estados con sus nombres capitalizados y los valores
* correspondientes. Utiliza la función `capitalizeWords` para asegurar que los
* nombres de los estados estén en formato adecuado y los ordena alfabéticamente.
*
* @returns {Array<Object>} Una lista de objetos con las propiedades `label` y `value` para cada estado,
* ordenada alfabéticamente por el label.
*/
function getEstados() {
return Object.entries(ESTADO)
.reduce((acc, [key, value]) => {
// Si ya existe un estado con este value, no lo agregamos de nuevo
if (!acc.some((estado) => estado.value === value)) {
// Si es CDMX o DF, usamos "Ciudad de México" como label
if (key === 'DISTRITO_FEDERAL' || key === 'CDMX') {
acc.push({
label: 'Ciudad de Mexico',
value,
});
} else {
acc.push({
label: capitalizeWords(key.replace(/_/g, ' ')),
value,
});
}
}
return acc;
}, [])
.sort((a, b) => a.label.localeCompare(b.label));
}
/**
* Obtiene una lista de los géneros con sus etiquetas y valores
* correspondientes. Los ordena alfabéticamente por etiqueta.
*
* @returns {Array<Object>} Una lista de objetos con las propiedades `label` y `value` para cada género,
* ordenada alfabéticamente por el label.
*/
function getGeneros() {
return Object.entries(GENERO)
.map(([key, value]) => ({
label: capitalizeWords(key.replace(/_/g, ' ')),
value,
}))
.sort((a, b) => a.label.localeCompare(b.label));
}
curp.validar = validar;
curp.getPersona = getPersona;
curp.generar = generar;
curp.getEstados = getEstados;
curp.getGeneros = getGeneros;
curp.GENERO = GENERO;
curp.ESTADO = ESTADO;
/**
* Si no usa Node.js no se exporta curp.
*/
/* istanbul ignore next */
if (typeof module !== 'undefined') module.exports = curp;