Validar datos en C++

Validación de datos

En aplicaciones de consola C++, uno de los puntos más problemáticos es la lectura de datos por parte del usuario.

Las funciones de entrada C y los métodos del stream cin resultan poco potentes a la hora de hacer lecturas discriminadas de número enteros, o en coma flotante, fechas o cadenas que se ajuste a un formato determinado.

Después de hacer algunos programas, todos nos damos cuenta de que es mala idea usar el operador >> de cin para leer un número. Si el usuario introduce una cadena no numérica, el valor leído es cero, pero la cadena permanece en el buffer de entrada del teclado, y se intenta leer en sucesivas lecturas.

El resultado es que no se leen los datos que se pretenden leer, y generalmente el programa termina por entrar en un bucle infinito o, en el mejor de los datos, da un resultado incorrecto.

Cuando leemos cadenas también existe peligro, concretamente, de overbuffering, es decir, de sobrepasar el espacio de memoria correspondiente a la cadena a leer.

Esto es más cierto cuando leemos cadenas C, en forma de array de caracteres terminadas en nulo. Si usamos objetos de tipo string este peligro no existe.

Con esto en mente, parece claro que es mejor leer siempre cadenas. En el caso de querer capturar valores numéricos, se debe validar la cadena leída. Si la validación tiene éxito se convierte el valor leído a número, y si no, se repite el proceso.

Generalizando, para validar cualquier formato de dato, el proceso es el siguiente:

  • Leer una cadena.
  • Verificar si el formato es correcto.
    • Si es correcto: retornar el valor leído.
    • Si no es correcto: volver al principio.

Leer una cadena

Parece una tarea sencilla, ¿no?

Tal vez no tanto...

Veamos. Si usamos el operador >> de cin para leer un objeto de la clase string se pueden presentar algunos problemas. Por ejemplo, el operador >> deja de leer cuando encuentra un espacio, y además, no lo retira del buffer.

Si intentamos leer un número, y el usuario escribe "varias palabras y un numero 34", se leerá "varias" en la cadena, y el resto quedará en el buffer para siguientes lecturas.

Si usamos el método getline, pronto descubriremos que no sirve para leer objetos de la clase string, sino sólo cadenas terminadas en nulo.

Existe una función getline que evita todos estos inconvenientes: lee objetos string y lee espacios. En el primer argumento debemos indicar el objeto cin, y en el segundo, la cadena a leer:

    string cad;
    getline(cin, cad);

De este modo nos aseguramos de que cad contiene una cadena con todo lo introducido por el usuario, y podemos pasar al siguiente paso.

Verificar el formato correcto

Por supuesto, esta tarea es diferente dependiendo del formato del dato a leer.

Si se trata de un entero, y no somos demasiado exigentes con el formato, podemos usar un stream de cadena para verificar la entrada del usuario.

    int i;
    stringstream mystream(cad);
    if(mystream >> i) cout << "entero leido" << endl;
    else cout << "error" << endl;

En este ejemplo usamos un stringstream creado a partir de la cadena leída, y después intentamos leer un entero a partir de ella. Si al comienzo de la cadena hay un entero, su valor pasa a i, el caso contrario indicará un error.

Digo que "si no somos demasiado exigentes", porque hay muchas cadenas que darán como resultado un entero, aunque no contengan sólo un puntero, por ejemplo "123.3", "123,4" o "123abc".

Si se trata de un número en coma flotante, nos sirve el mismo método, pero usando una variable float o double para la lectura. Este método interpretará correctamente entradas en notación científica, como "1e3", o "1.3e2".

Insistir hasta tener un resultado válido

El último paso es repetir la lectura si el valor leído no es válido.

Para ello pondremos todo el código anterior dentro de un bucle. Por ejemplo, para leer un entero:

    int i;
    string cad;

    while(true) {
        cout << "Introduce un entero: ";
        getline(cin, cad);
        stringstream mystream(cad);
        if(mystream >> i) break;
        cout << "error" << endl;
    }
    cout << "Valor: " << i << endl;

Podemos introducir todo este código en una función, si tenemos que leer varios números, en la que se acepte como entrada una cadena, y como valor de retorno un entero.

Verificación general

El método anterior es suficiente en los casos generales de lectura de números, pero a menudo tenemos que verificar otros tipos de datos, como fechas, o cadenas que se ajusten a un formato concreto.

Estas verificaciones generalmente requieren verificar cada carácter de la cadena capturada para comprobar si se ajusta a un patrón determinado. Por ejemplo, una fecha debe constar de dos dígitos, seguidos de un carácter separador, otros dos dígitos, otro carácter separador y finalmente otros cuatro dígitos. Además los dos primeros dígitos deben estar en el intervalo entre 1 y el número de días correspondiente al mes y año, los segundos entre 1 y 12, ya que se refieren a un mes, y los últimos cuatro, deben estar en el rango de fechas válidas para nuestra aplicación. Esto será variable, dependiendo de cada caso.

Otras lecturas deben ajustarse a otros patrones, a veces más simples y otras más complejos.

Si disponemos de soporte para expresiones regulares, la primera parte de la verificación puede ser más sencilla, y en muchos casos no necesitaremos más.

C++11 tiene soporte para expresiones regulares, dentro de las bibliotecas estándar, concretamente, en <regex>.

En cualquier caso, existen bibliotecas que implementan expresiones regulares para C, por ejemplo, pcre.

Sin embargo, la mayor parte de las veces no es necesario usar esta herramienta, y podemos simplemente verificar si la cadena se ajusta a determinadas reglas o no.

Como ejemplo, veamos una función de validación para capturar valores enteros dentro de un intervalo.

int Intervalo(string msg, int v1, int v2) {
    int i;
    string cad;
    bool valido = false;

    do {
        cout << msg;
        getline(cin, cad);
        stringstream mystream(cad);
        if(mystream >> i) {
            if(i >= v1 && i <= v2) valido = true;
        }
        if(!valido) cout << "error" << endl;
    } while(!valido);
    return i;
}
...
    i = Intervalo("Introduce un entero entre 100 y 200: ", 100, 200);
    cout << "Valor: " << i << endl;

Fuentes: cplusplus.com