Validar IBAN con C++
Me estoy entreteniendo en picar código para una aplicación de gestión y contabilidad en Linux. A veces pienso que alimento a un monstruo (por lo grande y compleja que se me antoja) pero todavía me sigue yendo la marcha, así que tiro para adelante sin complejos. Ya voy por el tercer intento y me gustaría decir en serio que va empezado a tomar forma, pero no me atrevo. Sea como sea es bastante probable que nunca termine la aplicación por completo, a medida que envejezco voy perdiendo ganas de programar.
Pues uno de los problemas a los que me he enfrentado, teclado en ristre, ha sido el de validar un código de cuenta IBAN. Como se explica aquí, la cosa consiste en convertirlo a un número entero y comprobarlo módulo-97. Las reglas son las siguientes:
- Comprobar que su longitud es correcta (máximo 34 caracteres).
- Mover los 4 primeros caracteres al final de la cadena.
- Sustituir cada letra por dos dígitos, donde A=10, B=11 … Z=35.
- Calcular el resto de la división del número obtenido entre 97.
El desafío más sobresaliente es el de obtener el resto de una división, cuyo dividendo podría llegar a estar compuesto por más de cuarenta dígitos, sin que en C/C++ exista un tipo de dato que pueda manejar esas magnitudes con total precisión. La solución está al final de la web antes enlazada, solo es cuestión de pasarla a código empezando por utilizar una cadena de caracteres (string) para contener y procesar el código IBAN. Los pasos a seguir serían los siguientes:
- Formar un grupo alfanumérico con los 9 primeros dígitos de la cadena.
- Eliminar de la cadena los dígitos pasados al grupo.
- Obtener el resto de dividir entre 97 el valor numérico del grupo.
- Asignar al grupo el valor alfanumérico del resto.
- Completar los 9 dígitos del grupo, añadiento los 8 primeros de la cadena si el resto tiene un dígito (10 > resto) o los 7 primeros en otro caso.
- Repetir los pasos 2-5 hasta que todos los dígitos hayan sido procesados.
Si te preguntas por la utilidad de incluir esta validación en una aplicación real, la respuesta es obvia: proporcionar un plus de confianza en que no hemos cometido errores al teclear un número de cuenta bastante largo. Y como las cuentas bancarias no mueven otra cosa que dinero, cuantas más precauciones se tomen para evitar errores en su tratamiento, mejor.
Vamos ahora con el código propiamente dicho. He escrito una pequeña clase llamada Iban a modo de ejemplo y un main para comprobar su funcionamiento. El archivo cabecera es el siguiente:
Iban.hpp
#ifndef IBAN_HPP
#define IBAN_HPP
#include<iostream>
class Iban
{
public:
bool
validar(std::string &codigoIban);
private:
void
soloAlfanumericos(std::string &cadena);
void
ponerEnMayusculas(std::string &cadena);
std::string
digitosLetra(const char letra) const;
bool
modulo97(const std::string &cadena) const;
unsigned
cadenaAEntero(const std::string &cadena) const;
std::string
enteroACadena(const int entero) const;
};
#endif
Solo hay un método público: validar(). Los métodos soloAlfanumericos()
y ponerEnMayusculas() reciben el parámetro como referencia para poder modificarlo,
ya que podría contener separadores como puntos, guiones, espacios… y letras
minúsculas. El resto de métodos están declarados en modo estanco
, si se me
permite la expresión, ya que así no pueden causar efecto alguno fuera de ellos mismos.
Veamos la implementación uno por uno.
Iban.cpp
#include "Iban.hpp"
#include <array>
#include <algorithm>
#include <cmath>
bool
Iban::validar(std::string &codigoIban)
{
soloAlfanumericos(codigoIban);
if (10 <= codigoIban.length() || 34 >= codigoIban.length()) {
ponerEnMayusculas(codigoIban);
std::string cadena(codigoIban.substr(4) + codigoIban.substr(0, 4));
for (unsigned pos = 0; pos < cadena.length(); ++pos) {
if (!std::isdigit(cadena[pos])) {
cadena.replace(pos, 1, digitosLetra(cadena[pos]));
}
}
return modulo97(cadena);
}
return false;
}
Primero nos quedamos únicamente con los caracteres alfanuméricos, a continuación comprobamos que el tamaño está dentro del rango permitido, asignándole diez caracteres al mínimo porque nos ha parecido bien, y nos aseguramos de que todas las letras están en mayúsculas. Como en C++ siempre hay más de una manera —incluso demasiadas— de hacer las cosas, si no te apetece escribir un método para convertir a mayúsculas puedes copiar esto que encontré publicado en una web (sustituyendo theString por codigoIban, obviamente):
std::transform(theString.begin(), theString.end(), theString.begin(), toupper);
Debes tener en cuenta que en este contexto toupper es una función C; no está precedida por std:: porque pertenece al espacio de nombres llamado global pero, si se quiere, puede ser precedida por el operador de resolución de ámbito (::toupper). Si tienes más curiosidad, aquí te dejo un enlace a transform. Estamos hablando de C++14 (por lo menos).
Una vez depurado el código IBAN, empezamos el proceso de validación propiamiente dicho creando una nueva cadena con los cuatro primeros caracteres movidos al final. El bucle for se encarga de sustituir todas las letras por sus valores numéricos de dos dígitos, dejándola lista para ser enviada a la comprobación modulo-97. Si todo ha ido bien, la respuesta será true y tendremos un IBAN validado.
Iban.cpp
void
Iban::soloAlfanumericos(std::string &cadena)
{
for (std::string::iterator it = cadena.begin();
it != cadena.end(); ++it) {
if (!std::isalnum(*it)) {
*it = 0x20;
}
}
cadena.erase(std::remove_if(cadena.begin(), cadena.end(),
isspace), cadena.end());
}
Suelo utilizar los iteradores de la STL para recorrer secuencialmente una colección de valores del mismo tipo, tal vez porque me recuerdan los incrementos/decrementos de punteros en C. Cada vez que aparece un carácter que no sea alfanumérico es sustituido por el espacio y, una vez recorrida la cadena completa, se eliminan todos los espacios con una línea parecida a la de la conversión a mayúsculas, y ya tenemos el código IBAN limpio de caracteres indeseados: solo quedan dígitos y letras.
Iban.cpp
void
Iban::ponerEnMayusculas(std::string &cadena)
{
for (std::string::iterator it = cadena.begin();
it != cadena.end(); ++it) {
if (std::isalpha(*it)) {
*it = std::toupper(*it);
}
}
}
Como me gusta entender el código que escribo y escribir el código que entiendo, he preferido dejarte este sencillo método como ejemplo para convertir caracteres a mayúsculas, aunque probablemente ya tendrás el tuyo propio. La línea que mencioné antes (usando transform) la considero adecuada para demostrar las capacidades de la STL, pero poco más.
Iban.cpp
std::string
Iban::digitosLetra(const char letra) const
{
const std::array<const char *, 26> valoresLetras {
"A10", "B11", "C12", "D13", "E14", "F15", "G16", "H17", "I18",
"J19", "K20", "L21", "M22", "N23", "O24", "P25", "Q26", "R27",
"S28", "T29", "U30", "V31", "W32", "X33", "Y34", "Z35"
};
std::string digitos;
for (std::array<const char *, 26>::const_iterator it = valoresLetras.begin();
it < valoresLetras.end(); ++it) {
if (*it[0] == letra) {
digitos.assign(std::string(*it).substr(1));
break;
}
}
return digitos;
}
Supongo que esto no tiene mucho misterio para tí, pero por si acaso no estuviese del todo claro para alguien con menos preparación, diré que utilizo un array de punteros a caracteres para no crear veintiséis instancias de la clase std::string, así tengo directamente lo que necesito sin más añadidos. El bucle for va leyendo dicho array y, cuando encuentra una coincidencia entre la letra del parámetro y el primer carácter de uno de los char* estilo C, lo convierte a std::string, copia los dos caracteres dígitos que siguen a la letra e interrumpe el bucle para devolverlos, puesto que ya se ha conseguido el objetivo. Podría haber declarado static el array para crearlo solo en la primera llamada, pero soy reacio a dejar cosas en la memoria a sabiendas y no creo que la diferencia en rendimiento sea significativa. Otra posibilidad es pasar como parámetro la cadena completa y cambiar todas las letras de una tacada, ya sea directamente si se utiliza una referencia, o mediante una copia y devolviendo otra cadena. Mas estamos con C++; si no te gusta esto, puedes utilizar un map o cualquier otra cosa que se te ocurra, incluida una búsqueda exaustiva por la STL para localizar, o inventar, alguna frikada como la anterior del transform. Aquí solo intento dejar un código que pueda servir como muestra o ejemplo, sin pretensiones de idoneidad.
Iban.cpp
bool
Iban::modulo97(const std::string &cadena) const
{
std::string cad(cadena);
std::string grupo;
int numDigitos = 9;
int resto = 0;
while (!cad.empty()) {
grupo.append(cad.substr(0, numDigitos));
cad.erase(0, numDigitos);
resto = cadenaAEntero(grupo) % 97;
grupo.assign(enteroACadena(resto));
numDigitos = 10 > resto ? 8 : 7;
}
return 1 == resto;
}
Esto ya ha sido explicado, no merece la pena extenderse más.
Iban.cpp
unsigned
Iban::cadenaAEntero(const std::string &cadena) const
{
unsigned entero = 0;
for (int i = cadena.length(); 0 < i; --i) {
int digito = cadena[i - 1] - '0';
entero += digito * pow(10.0, cadena.length() - i);
}
return entero;
}
Aquí tenemos una forma de pasar una cadena de caracteres dígitos a su equivalente numérico. En este caso no utilizo iteradores, sino un índice y los corchetes porque necesito conocer la posición del dígito en la cadena, ya que de la misma depende la potencia de diez por la que multiplicar para ir componiendo el número. El valor numérico de un dígito se obtiene, en este caso, restando al valor ASCII del dígito el valor ASCII del cero (48). Una vez convertido el dígito a número, se multiplica por la potencia de diez obtenida de restar a la longitud de la cadena la posición que ocupa el dígito en la misma comenzando por 1 de izquierda a derecha.
Iban.cpp
std::string
Iban::enteroACadena(const int entero) const
{
std::string cadena;
if (9 < entero) {
cadena.push_back(static_cast<char>(entero / 10) + '0');
}
cadena.push_back(static_cast<char>(entero % 10) + '0');
return cadena;
}
Una vez hemos recordado lo del valor ASCII, esto está meridiamente claro. Puede que el casting sobre, pero así me gusta más. Como aquí solo vamos a necesitar convertir un entero de dos dígitos como mucho (el residuo de una división entre 97), he utilizado esta versión reducida para pasar un entero a cadena. Una versión completa empezaría por obtener la cifra de las unidades tomando el resto de la división del entero entre diez y sumándole el '0' (48), para añadirla a la cadena. A continuación, para obtener la siguiente cifra se le resta al número la última obtenida, con lo que la cifra de las unidades será el cero, y se divide entre diez para convertir la cifra de las decenas en la nueva cifra de las unidades y así hasta completar el número. La cadena se puede ir formando en orden inverso (con push_back() por ejemplo), en cuyo caso habrá que reordenarla al terminar, o bien insertando cada dígito a partir del segundo en la posición cero.
También te voy a dejar un sencillo main() para hacer pruebas de validación en un terminal de texto.
Main.cpp
#include "Iban.hpp"
int
main()
{
std::string codigoIban;
Iban iban;
std::cout << "Código IBAN: ";
std::getline(std::cin, codigoIban);
if (iban.validar(codigoIban)) {
for (unsigned i = 4; i < codigoIban.length(); i += 5) { // Grupos de 4 caracteres
codigoIban.insert(i, " "); // separados por espacios.
}
std::cout << "IBAN válido: " << codigoIban << std::endl;
} else {
std::cout << "Eso no es un IBAN válido." << std::endl;
}
return 0;
}
Para compilar y ejecutar este programita (en un sistema basado en Linux) hay que situarse en la misma carpeta donde se hayan creado los tres archivos (Iban.hpp, Iban.cpp, Main.cpp) y ejecutar las siguientes órdenes:
~$ g++ -Wall Iban.cpp Main.cpp -o iban
Con lo que hemos creado un archivo ejecutable llamado iban, y ahora con
~$ ./iban
lanzamos el programa para comprobar una cuenta.
Finalizaré con unos enlaces donde aparecen ejemplos de cuentas IBAN que puedes usar para poner a prueba el código:
