DEV Community

APIEmpresas
APIEmpresas

Posted on

Cómo validar un CIF español en JavaScript, Python y PHP (y por qué no basta)

Si tienes un formulario de alta de clientes, un ERP o un programa de facturación en España, tarde o temprano necesitas validar un CIF. Parece fácil, pero hay tres detalles que casi todas las funciones que circulan por ahí hacen mal:

  1. Algunas letras exigen control numérico y otras control con letra. Una función que acepte cualquiera de los dos da por buenos CIF que no lo son.
  2. Los usuarios escriben el CIF como quieren: B-12.345.674, b12345674, con espacios…
  3. Un CIF bien formado no significa que la empresa exista. Esto es lo que más problemas da al facturar.

Vamos por partes. Al final tienes una librería con las tres versiones y los tests.

La estructura del CIF

Un CIF (oficialmente, NIF de persona jurídica desde 2008) tiene 9 caracteres:

  • Una letra que indica la forma jurídica: A sociedad anónima, B limitada, F cooperativa, G asociación o fundación, Q organismo público…
  • 7 números.
  • Un carácter de control, número o letra, calculado a partir de los 7 números.

El cálculo del control

Con los 7 números:

  1. Suma los que están en posición par (2.º, 4.º y 6.º).
  2. Multiplica por 2 los de posición impar (1.º, 3.º, 5.º y 7.º). Si el resultado tiene dos cifras, súmalas (7 × 2 = 14 → 1 + 4 = 5).
  3. Suma los dos totales y quédate con la última cifra. Si es 0, el control es 0; si no, 10 menos esa cifra.
  4. Si el control va en letra, es la de esa posición en JABCDEFGHI (0 = J, 1 = A, 2 = B…).

Ejemplo con A46103834:

Números:      4 6 1 0 3 8 3
Pares:        6 + 0 + 8 = 14
Impares ×2:   8, 2, 6, 6 → 22
Total:        36 → última cifra 6 → control 10 − 6 = 4   ✓
Enter fullscreen mode Exit fullscreen mode

Y el detalle que se suele olvidar:

Letra inicial Control
A, B, E, H Siempre número
N, P, Q, R, S, W Siempre letra
Resto (C, D, F, G, J, U, V) Número o letra

JavaScript

const LETRAS_CONTROL = 'JABCDEFGHI';

function limpiar(valor) {
  return String(valor ?? '').replace(/[\s.\-\/]/g, '').toUpperCase();
}

function validarCif(valor) {
  const m = limpiar(valor).match(/^([ABCDEFGHJNPQRSUVW])(\d{7})([0-9A-J])$/);
  if (!m) return false;
  const [, letra, numeros, control] = m;

  let suma = 0;
  for (let i = 0; i < 7; i++) {
    let n = Number(numeros[i]);
    if (i % 2 === 0) {        // posiciones impares (1.ª, 3.ª…): doblar y sumar cifras
      n *= 2;
      if (n > 9) n -= 9;
    }
    suma += n;
  }
  const digito = (10 - (suma % 10)) % 10;
  const letraControl = LETRAS_CONTROL[digito];

  if ('ABEH'.includes(letra)) return control === String(digito);
  if ('NPQRSW'.includes(letra)) return control === letraControl;
  return control === String(digito) || control === letraControl;
}

validarCif('A46103834');     // true
validarCif('a-46.103.834');  // true
validarCif('A46103835');     // false
Enter fullscreen mode Exit fullscreen mode

n > 9 ? n - 9 es un atajo: para un número de dos cifras entre 10 y 18, restar 9 da lo mismo que sumar sus cifras.

Python

import re

def limpiar(valor):
    return re.sub(r"[\s.\-/]", "", "" if valor is None else str(valor)).upper()

def validar_cif(valor):
    m = re.fullmatch(r"([ABCDEFGHJNPQRSUVW])(\d{7})([0-9A-J])", limpiar(valor))
    if not m:
        return False
    letra, numeros, control = m.groups()

    suma = 0
    for i, c in enumerate(numeros):
        n = int(c)
        if i % 2 == 0:
            n *= 2
            n = n - 9 if n > 9 else n
        suma += n
    digito = (10 - suma % 10) % 10
    letra_control = "JABCDEFGHI"[digito]

    if letra in "ABEH":
        return control == str(digito)
    if letra in "NPQRSW":
        return control == letra_control
    return control in (str(digito), letra_control)
Enter fullscreen mode Exit fullscreen mode

PHP

function validarCif(?string $valor): bool
{
    $cif = strtoupper(preg_replace('/[\s.\-\/]/', '', (string) $valor));
    if (!preg_match('/^([ABCDEFGHJNPQRSUVW])(\d{7})([0-9A-J])$/', $cif, $m)) {
        return false;
    }
    [, $letra, $numeros, $control] = $m;

    $suma = 0;
    for ($i = 0; $i < 7; $i++) {
        $n = (int) $numeros[$i];
        if ($i % 2 === 0) {
            $n *= 2;
            $n = $n > 9 ? $n - 9 : $n;
        }
        $suma += $n;
    }
    $digito = (10 - $suma % 10) % 10;
    $letraControl = 'JABCDEFGHI'[$digito];

    if (strpos('ABEH', $letra) !== false) return $control === (string) $digito;
    if (strpos('NPQRSW', $letra) !== false) return $control === $letraControl;
    return $control === (string) $digito || $control === $letraControl;
}
Enter fullscreen mode Exit fullscreen mode

DNI y NIE, de regalo

La letra del DNI es la del resto de dividir el número entre 23 en TRWAGMYFPDXBNJZSQVHLCKE. En el NIE se cambia la primera letra por un número (X = 0, Y = 1, Z = 2) y se hace el mismo cálculo:

const LETRAS_DNI = 'TRWAGMYFPDXBNJZSQVHLCKE';

function validarDni(valor) {
  const m = limpiar(valor).match(/^(\d{8})([A-Z])$/);
  return !!m && LETRAS_DNI[Number(m[1]) % 23] === m[2];
}

function validarNie(valor) {
  const m = limpiar(valor).match(/^([XYZ])(\d{7})([A-Z])$/);
  return !!m && LETRAS_DNI[Number('XYZ'.indexOf(m[1]) + m[2]) % 23] === m[3];
}
Enter fullscreen mode Exit fullscreen mode

La librería

Lo he empaquetado todo, con tests y el mismo resultado en los tres lenguajes (comprobado con 200.000 CIF aleatorios), sin dependencias y con licencia MIT:

npm install validar-cif
pip install validar-cif
composer require apiempresas/validar-cif
Enter fullscreen mode Exit fullscreen mode
const { validarCif, validarNif, tipoNif, tipoEntidad } = require('validar-cif');

validarNif('X1234567L');     // true (CIF, DNI o NIE)
tipoNif('12345678Z');        // 'dni'
tipoEntidad('B00000000');    // 'Sociedad de responsabilidad limitada'
Enter fullscreen mode Exit fullscreen mode

Código y documentación: github.com/abasmartinc/validar-cif.

Por qué no basta con validar el formato

La fórmula es pública: cualquiera puede inventarse un CIF que la cumpla. B12345674 es un CIF perfectamente válido y, casi seguro, no es de nadie.

Si lo validas para facturar, esto importa. La factura tiene que llevar el NIF y la razón social exacta del cliente, y con Verifactu (2027) y la factura electrónica entre empresas los errores se van a notar más: una factura a un NIF que no existe, o a una sociedad extinguida, te toca rectificarla.

Para saber si la empresa existe hay que consultarla en una fuente de datos. Yo mantengo APIEmpresas, una API con los datos del Registro Mercantil (BORME) que devuelve la razón social, el estado y el resto de la ficha a partir del CIF:

curl "https://apiempresas.es/api/v1/companies?cif=A46103834" \
  -H "X-API-KEY: TU_API_KEY"
Enter fullscreen mode Exit fullscreen mode

Tiene un plan gratis de 100 consultas, sin tarjeta. Si solo quieres comprobar alguno a mano, está el validador de CIF online, que hace el mismo cálculo paso a paso y te dice si la empresa existe.

Resumen

  • Valida el formato en el cliente para dar feedback inmediato, con el control según la letra inicial.
  • Limpia la entrada (guiones, puntos, espacios, minúsculas) antes de validar.
  • Si el dato acaba en una factura, comprueba además que la empresa existe y sigue activa.

¿Usas otra regla que se me haya escapado? Cuéntamelo en los comentarios.

Top comments (0)