miércoles, 27 de enero de 2010

STL string, parseando un número de versión

En esta ocasión vamos a ver cómo usar la clase std::string para convertir una cadena de caracteres (como "1.2.3") en una clase "Version" que tenga una interfaz simple de utilizar (por ejemplo, que permita comparar versiones).

Los pasos son simples:
  1. tenemos como entrada una cadena de caracteres,
  2. dividimos la cadena según el caracter '.' (punto),
  3. convertimos cada parte a entero,
  4. guardamos cada número entero de forma ordenada en un vector.
El punto 2 es en el cual nos vamos a enfocar más. El código mostrado aquí es a modo de ejemplo, y está lejos de ser el más óptimo.

Antes de comenzar ¿Podemos usar strcmp para comparar dos versiones? No, no podemos. Ejemplo: strcmp("1.9", "1.10") > 0, cuando en realidad la versión 1.9 es menor a 1.10.

Imaginemos que tenemos una cadena:
std::string ver = "1.10.2.3";
La clase std::string tiene la función miembro find, la cual podemos usar para buscar casi cualquier cosa dentro de la cadena. ¿Qué debemos buscar? Los puntos, sabiendo donde está cada punto, podemos ir recortando la cadena en sus distintas partes ("1", "10", "2", y "3"). Buscamos la ubicación del primer punto:
size_t i = ver.find('.');

if (i != std::string::npos)
  std::printf("El punto fue encontrado en la posición %d\n", i);
else
  std::printf("No hay punto en la cadena\n");
Ejecutando el código anterior, deberíamos obtener el mensaje:
El punto fue encontrado en la posición 1
¿Qué es size_t? Es como el tipo "unsigned int", el tipo de dato retornado por el operador sizeof() y utilizado como índice en las funciones de std::string.

¿Qué es std::string::npos? Es el máximo valor posible de un size_t y se utiliza para indicar (en este caso) que la función find falló (no encontró el punto).

Una vez que tenemos la posición del punto, podemos obtener la porción de texto que contiene la primer cifra, para eso usamos la función miembro substr:
std::string primer_cifra = ver.substr(0, i);
Esto significa: che vos, función substr, devolveme el pedazo de cadena de caracteres que va desde el índice 0, y tiene una longitud de i-caracteres.

El proceso puede ser repetido tantas veces como queramos para seguir obteniendo cifras. Por ejemplo, para la siguiente cifra debemos buscar (find) el siguiente punto, pero comenzando desde el que encontramos hace un rato:
i++;     // ir a la posición siguiente del punto '.'
size_t j = ver.find('.', i);
¿Qué es (o hace) el segundo argumento de find? Le indica a la función desde donde debe comenzar a buscar. La primera vez que usamos find este argumento no se especificó, porque por omisión toma el valor 0, es decir, buscar desde el inicio de la cadena.

¿Por qué i++? Porque si comenzáramos a buscar un punto desde i, nos devolvería la misma posición i (porque justamente, en i, está el primer punto que encontramos). Entonces debemos avanzar una posición.

Ahora, debemos recortar la segunda cifra:
std::string segunda_cifra = ver.substr(i, j-i);
La segunda cifra, comienza desde i y tiene una longitud igual a j-i. Para comprender esto, vea la siguiente figura:


De esta forma, ver.substr(i, j-i) nos devuelve la cadena "10". La versión completa del algoritmo puede quedar algo así:
#include <vector>  // Por std::vector
#include <string>  // Por std::string
#include <cstdlib> // Por std::strtol
#include <cstdio>  // Por std::printf

int main()
{
  std::string ver = "1.10.2.3";

  // Vector con cada cifra.
  // Luego del procesamiento esto debería ser = { 1, 10, 2, 3 }
  std::vector<int> cifras;

  size_t i = 0;   // Comenzamos desde i=0
  size_t j = 0;

  // Repetir hasta no llegar al final de la cadena
  while (j != std::string::npos) {
    // Buscar próximo punto
    j = ver.find('.', i);

    std::string cifra;

    // Si se encontró un punto
    if (j != std::string::npos) {
      // Recortamos desde i hasta j
      cifra = ver.substr(i, j-i);

      // El nuevo comienzo para buscar puntos será "j+1"
      i = j+1;
    }
    // Si no se encontró un punto
    else {
      // Recortamos desde "i" hasta el final de la cadena
      cifra = ver.substr(i);
    }

    // Obtener el valor entero de la cifra
    int cifra_int = std::strtol(cifra.c_str(), NULL, 10);

    // Agregar la cifra al vector
    cifras.push_back(cifra_int);
  }

  for (size_t i=0; i<cifras.size(); ++i)
    printf("cifras[%d] = %d\n", i, cifras[i]);

  return 0;
}
La salida del anterior programa es:
cifras[0] = 1
cifras[1] = 10
cifras[2] = 2
cifras[3] = 3
Con este código, podríamos implementar una clase "Version" con la siguiente interfaz:
class Version
{
public:
  Version(const char*);
  Version(const std::string&);

  bool operator==(const Version& u) const;
  bool operator<(const Version& u) const;
  // ...
};
En un próximo post voy a colocar una posible implementación de la clase "Version".