Publi

Operaciones básicas con cadenas en C++: capitalización, conversiones, recorte, recorrido y más

Como ha sucedido con otros lenguajes, C++ también ha evolucionado. Ha madurado mucho desde aquellos códigos que programábamos hace años y se nota. Por un lado, podemos pensar que al sumar abstracción en ciertos aspectos nos separa de la máquina y hace nuestro código más lento. Suma comprobaciones, hace más callbacks y en definitiva, una sencilla tarea que completaba en pocos cientos de operaciones, ahora son pocos miles. Aunque en su favor, podemos decir que aquello que programábamos en 15 o 20 líneas de código se ha reducido a una o dos, reduciendo así los puntos de ruptura, posibles bugs y calentamientos de cabeza futuros.

Eso sí, si queremos siempre podemos volver a los orígenes. Aunque será mucho más seguro utilizar estas nuevas formas de trabajo, si estamos seguros de lo que hacemos y queremos que el rendimiento de nuestro programa sea mucho mayor, podemos hacerlo. Es más, muchos programadores, todavía a 2017, suelen preferir programar en C cuando quieren ver un rendimiento superior en sus desarrollos.

He hecho esta pequeña recopilación de chuletas para realizar ciertas operaciones en C++ moderno, que podréis compilar si contáis con soporte para C++11 o superior. He decidido incluir los programas completos, no sólo la parte en que quiero hacer hincapié , para que podáis copiar, pegar y compilar, y así lo veis y lo sentís como lo hago yo.

Nota: Tenemos Glib, Boost y cientos de bibliotecas de C++ que pueden hacer cosas muy chulas (y lo que están preparando para C++17, que hasta finales de año no estará listo y que podremos disfrutar en su totalidad en 2018), pero en este post he querido centrarme en lo que podemos hacer con las herramientas que nos da el lenguaje sin utilizar bibliotecas de terceros

Pasar una cadena completa a mayúsculas/minúsculas

Recordemos los tiempos en los que en C pasábamos a mayúsculas y minúsculas una cadena. La recorríamos por completo y evaluábamos para cada letra cuál sería su mayúscula. No empezaremos con una novedad del lenguaje (podremos compilar con compiladores antiguos, pero sólo estamos abriendo boca). Cuando sólo tenemos un alfabeto inglés podemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>

int main()
{
    std::string s = "HoLa MuNDo DeSDe PoeSía BiNaRia";

    std::transform(s.begin(), s.end(), s.begin(), ::toupper);

    cout << s << endl;
}

Utilizaremos transform() para que recorra la cadena y llame a toupper con cada uno de los caracteres, lo típico, toupper de cctype. Y de la misma manera que llamamos a toupper, lo podemos hacer con tolower para las minúsculas.

Mayúsculas y minúsculas en un mundo plurilingüe

Pero, a estas alturas, un ordenador tiene que poder hablar inglés, español, francés y hasta ruso, y todos tenemos derecho a nuestras letras mayúsculas y minúsculas. Ahora tenemos que poner en práctica el uso de la locale con la que queremos hacer la transformación. Eso sí, en lugar de un string, también tendremos que utilizar un wstring, porque en codificaciones como UTF-8, un solo byte no siempre equivale a un carácter. Necesitaremos alguien que sea capaz de leer caracteres de nuestros bytes (y programarlo nosotros es horrible). Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>
#include <algorithm>
#include <locale>

using namespace std;

int main()
{
    std::wstring s = L"HoLa MuNDo DeSDe PoeSía BiNaRia";
    std::locale::global(std::locale("es_ES.UTF-8"));

    std::transform(s.begin(), s.end(), s.begin(), [](wchar_t c){
            return std::toupper(c, std::locale()); });

    wcout << "RESULTADO:"<<endl;
    wcout << s << endl;
}

O también:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include <algorithm>
#include <locale>

int main()
{
    std::wstring s = L"HoLa MuNDo DeSDe PoeSía BiNaRia";
    std::locale::global(std::locale("es_ES.UTF-8"));

    std::use_facet<std::ctype<wchar_t>>(std::locale()).toupper(&s[0], &s[0] + s.size());

    std::wcout << "RESULTADO:"<<std::endl;
    std::wcout << s << std::endl;
}

O si queremos que el código quede más limpio, podemos sustituir esta línea tan larga por:

1
2
auto& f = std::use_facet<std::ctype<wchar_t>>(std::locale());
f.toupper(&s[0], &s[0] + s.size());

Caracteres al principio y al final

Para saber el primer carácter de una cadena s, podíamos utilizar:

  • s[0] : Primera posición del string, que está muy bien, pero damos una posición numérica y no decimos de forma explícita “lo primero que hay”.
  • *s.begin() : El valor del iterador al primer elemento. Aunque como programadores de C++ y no de C, ver muchos asteriscos nos agobia…

Para la última letra, ya lo tendríamos más complicado, tendríamos que usar:

  • *(s.end()-1) : El iterador al último carácter de la cadena será el terminador, pues resolvemos el puntero al carácter anterior. Esto queda un poco feo, hay asteriscos y hacemos cuentas con punteros. No es muy seguro.
  • s[s.length()-1] : Aunque no tenemos asteriscos. pedimos la longitud de la cadena, lo que puede que implica una llamada, aunque muchos compiladores están muy bien optimizados.

En C++11 tenemos front():

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <string>

int main()
{
    std::string s = "Hola Mundo";

    std::cout << "Primera: "<<s.front() << std::endl
                << "Última: "<<s.back()<< std::endl;
}

Si queremos, podemos añadir o eliminar caracteres al final con las funciones push_back() y pop_back, así:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>

int main()
{
    std::string s = "Hola Mundo";

    s.push_back('!');
    std::cout << s << std::endl;
    s.pop_back();
    std::cout << s << std::endl;
}

Comprobar que una cadena contiene un número

Vamos, lo que queremos hacer es saber si el contenido de una cadena, es numérico. Para ello, debemos asegurarnos de que todos los caracteres de la misma son números:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
#include <algorithm>

bool isNumeric(const std::string& input)
{
    return std::all_of(input.begin(), input.end(), ::isdigit);
}

int main()
{
    std::string s = "1239812357";
    std::cout << ((isNumeric(s))?"Sí":"No") << std::endl;
}

Sí, precioso, pero ¿qué pasa con los negativos? Bueno, podemos modificar isNumeric un poco para comprobar el primer carácter de la cadena:

1
2
3
4
5
6
7
bool isNumeric(const std::string& input)
{
    auto ib = input.begin();
    if ((input.front()=='+') || (input.front()=='-') )
        ib++;
    return std::all_of(ib, input.end(), ::isdigit);
}

Bueno, ya cubrimos los números enteros, y si comprobamos esto, podremos ver que daría igual la longitud de la cadena, es decir, pueden ser números muy grandes, números que desbordarían un long, pero al menos podemos comprobar su validez.
Pero, ¿qué pasaría con los números hexadecimales? ¿o los binarios? Aquí podemos especificar la base:

1
2
3
4
5
6
7
8
bool isNumeric(std::string in, int base=10 )
{
    std::string validos = "0123456789ABCDEF";
   
    return (in.find_first_not_of(validos.substr(0, base),
                                                             ((in.front()=='+') || (in.front()=='-') )
                    ) == std::string::npos);
}

Eso sí, ¿qué pasaría con los decimales? Nos estamos poniendo caprichosos ya, pero podríamos comprobar la existencia de la coma decimal y ver si hay más números a partir de ahí:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>
#include <algorithm>
#include <locale>

bool isNumeric(std::string in, int base=10 )
{
    std::string validos = "0123456789ABCDEF";
   
    auto entera = in.find_first_not_of(validos.substr(0, base),
                                                                         ((in.front()=='+') || (in.front()=='-') ));

    // Podíamos haber utilizado == '.' pero no sabemos el idioma del usuario.
    // Para los decimales puede utilizar . , o cualquier otra cosa.
    if (in[entera] == std::use_facet< std::numpunct<char> >(std::locale()).decimal_point())
        return (in.find_first_not_of(validos.substr(0, base),
                                                                 entera+1) == std::string::npos);
    else
        return (entera == std::string::npos);
}

int main()
{
    std::locale::global(std::locale("es_ES.UTF-8"));
    std::string s = "-123982357,";

    std::cout << ((isNumeric(s, 16))?"Sí":"No") << std::endl;
}

Comprobar que una cadena contiene un número II

Bueno, y como es C++ moderno, tenemos un extra muy interesante, ¡expresiones regulares! Pues nada, creemos una expresión regular para comprobar cadenas numéricas. Total, en otros lenguajes lo hacemos sin miedo, aunque, ¡cuidado! El tamaño del ejecutable puede crecer mucho.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
#include <algorithm>
#include <regex>

bool isNumeric(std::string in )
{
    std::regex num_regex("^[+|-]?(([0-9]*)|(([0-9]*)\\.([0-9]*)))$");
    return std::regex_match(in, num_regex);
}

int main()
{
    std::string s = "123982357";

    std::cout << ((isNumeric(s))?"Sí":"No") << std::endl;
}

Convertir de cadena a número

Si pensamos en C. Aunque tenemos atoi(), atod(), strtol() y demás familiares y derivados, debemos recorrer la cadena por completo y analizar carácter a carácter si es numérico y si lo es, darle un valor con respecto a la posición que ocupa dentro de la cadena. También es cierto que atoi() y demás pueden ser inseguras ya que C no comprueba tamaños de cadenas y si no controlamos el dato, los resultados pueden ser inesperados. Es más, atoi() es la función que nunca se queja, tanto si le pasas una cadena que no contiene un número (que devuelve 0) como si le pasas una cadena muy larga con números muy largos que desbordan la variable (ok, se desborda, devuelve lo que quiere, pero no falla). Aunque strtol() / strtod() nos dan más control, pero no son estilo C++.

En C++ podemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>

int main()
{
    std::string numero = "12347665";
    try
        {          
            std::cout << std::stoi(numero)<<std::endl;
        }
    catch (std::logic_error e)
        {
            std::cout << "Problemas!!"<< std::endl;
        }
}

Eso sí, stoi(), stol() y stoll() funcionan con strtol() y strtoll() por detrás, para eso de tener excepciones. Si queremos velocidad, seguramente nos vaya mejor utilizando las funciones de C, pero si queremos estilo y excepciones, las funciones de C++ nos vendrán bien.

Al igual que stoi(), tenemos también stof() para float, stod() para double y stold() para long double. Eso sí, ¿quieres números extremadamente grandes? Puedes probar GMP, que tiene una interfaz para C++. Aquí tienes un ejemplo de uso en C.

Convertir de número a cadena

En C, los que aprendimos hace mucho tiempo teníamos una función, itoa(), pero no era ANSI-C. De hecho, yo la conocí en Turbo C, y no la vemos en muchos compiladores. Aunque tenía el mismo problema de siempre: no hay comprobación del tamaño de cadenas, porque en C no es muy fácil.

De todas formas, aunque en C terminábamos haciendo las conversiones número-cadena con sprintf() o snprintf() (mejor esta segunda), antiguamente en C++, podíamos utilizar sstream, pero muchísimas veces da una pereza enorme y nos obligaba a tener wrappers a meno con estas funciones siempre cerca. Pero desde C++11 tenemos una nueva función para sacarnos las castañas del fuego, to_string(), fácil y preciosa, a la que le da igual que le pasemos un int, double, uint16_t, char o lo que queramos, siempre que sea numérico:

1
2
3
4
5
6
7
8
#include <iostream>
#include <string>

int main()
{
    uint16_t zz = 1239;
    std::cout << std::to_string(zz);
}

Dividir cadenas (split, tokenize…)

Una ardua tarea es siempre separar una cadena en varios trozos, o bien por palabras, o con algún carácter comodín, o cualquier otro criterio. En C tenemos strtok(), con la inseguridad que conlleva, además porque en un mundo multihilo puede darnos problemas (y por eso vino su hermano strtok_r()), pero es otro cantar. En C++ tenemos a nuestra disposición multitud de estructuras de datos, y algo que hacemos en otros lenguajes como Javascript de forma muy fácil (un split de toda la vida), en C++ se nos puede atravesar un poco. Y tenemos varias opciones:

Separar palabras por espacios

En realidad, por los caracteres que utiliza cin para separar elementos. Lo haremos con istream_iterator y meteremos los valores en un vector. Lo bueno es que podemos utilizar strings, int, o cualquier cosa que podamos utilizar en un stream:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string>
#include <vector>
#include <sstream>

int main()
{
    std::istringstream iss("Estoy probando un\n\n\n\ntokenizer desde Poesía Binaria");
    std::vector<std::string> results { std::istream_iterator<std::string>(iss), std::istream_iterator<std::string>() };
   
    for (auto vi : results)
        std::cout << vi <<std::endl;
}

…¡¡con expresiones regulares!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
#include <vector>
#include <regex>

std::vector<std::string> split(const std::string& str, const std::string& regex)
{
    std::regex _regex(regex);
    return { std::sregex_token_iterator (str.begin(), str.end(), _regex, -1), std::sregex_token_iterator() };
}

int main()
{
    std::string ss = "hola:mundo:de:codigo";
    std::string sep = ":";
   
    auto v = split(ss, sep);
    for (auto vi : v)
        std::cout << vi <<std::endl;
}

Desde que el soporte de expresiones regulares es nativo. C++ se ha convertido en un lenguaje adulto. Es cierto que antes podíamos hacer multitud de cosas, y que seguro que muchos aprovechamos ese soporte de expresiones regulares para complicarnos la vida y hacer cosas menos eficientes, pero se agradece mucho en ocasiones y seguro que simplifica nuestros códigos.

…separadas con un carácter

Otro ejemplo más, puede ser este:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <string>
#include <vector>

const std::vector<std::string> split(const std::string& haystack, const char& needle)
{
    std::string buff{""};
    std::vector<std::string> v;
   
    for(auto c:haystack)
    {
        if(c != needle)
            buff.push_back(c);
        else if ( (c == needle) && (!buff.empty()))
            {
                v.push_back(buff);
                buff = "";
            }
    }
   
    if (!buff.empty())
        v.push_back(buff);
   
    return v;
}

int main()
{
    std::string ss = "hola:mundo:de:codigo";
    std::string sep = ":";

    auto v = split(ss, ':');
    for (auto vi : v)
        std::cout << vi <<std::endl;
}

Aunque si queremos utilizar las nuevas herramientas que nos da C++ como lambdas o iteradores, que pueden llegar a ser muy interesantes, podemos utilizar esto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>

std::vector<std::string> split(const std::string& str, char delimiter)
{
    std::vector<std::string> ret;
    std::string::const_iterator i = str.begin();

    while (i != str.end())
        {
            i = find_if(i, str.end(), [delimiter](char c)   {
                    return (c!=delimiter);
                });
            std::string::const_iterator j = find_if(i, str.end(), [delimiter](char c)   {
                    return (c==delimiter);
                });
            if (i != str.end())        
                ret.push_back(std::string(i, j)); i = j;
        }
    return ret;
}

int main()
{
    std::string ss = "hola     mundo   de codigo";
    std::vector<std::string> v = split(ss, ' ');

    for (auto vi : v)
        std::cout << vi <<std::endl;
}

En este punto, dependiendo de nuestras necesidades podríamos utilizar find() y derivados. Si os apetece también podemos recurrir a strtok() de C (aunque si no tenemos que hacer varios millones de operaciones como esta por segundo, mejor nos quedamos con las herramientas de C++ que serán algo más seguras y podríamos implementarlas fácilmente con wstring. (Si os queréis aventurar con strtok(), mejor utilizar strtok_r(), porque en un mundo multihilo sería normal que dos hilos llamasen a strtok() al mismo tiempo, y eso puede causarnos problemas.

Reemplazar subcadenas dentro de cadenas

Varios ejemplos de esto los encontramos en estos posts: Reemplazar cadenas de texto en C++ (string y Glib::ustring) y Reemplazar cadenas en C++, esta vez desde un map, para múltiples sustituciones.

Rot13

Una manera sencilla de hacer cifrados, pero no voy a eso. Es un cifrado tremensamente fácil de romper, pero transform() puede dar mucho juego, entre otras cosas, hacer un rot13 se vuelve muy sencillo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

std::string rot13(std::string text) {
    std::transform(
        begin(text), end(text), begin(text),
        [] (char c) -> char {
            if (not std::isalpha(c))
                return c;

            char const pivot = std::isupper(c) ? 'A' : 'a';
            return (c - pivot + 13) % 26 + pivot;
        });
    return text;
}

int main()
{
    std::string ss = "hola mundo desde Poesía Binaria";

    std::cout << rot13(ss)<<std::endl;
}

Eliminar espacios de una cadena

¡En una sola línea de código, con posibilidad de eliminar no sólo los espacios sino cualquier carácter que cumpla una determinada condición. Con remove_if():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>

std::string strip(std::string s)
{
    s.erase( std::remove_if(s.begin(), s.end(), ::isspace), s.end() );
    return s;
}

int main()
{
    std::cout << strip("¿ Qué pasa si a una cadena le quito los espacios ?") << std::endl;
}

Y, en lugar de ::isspace (de cctype) podemos utilizar un lambda con una función que determine qué carácter eliminamos (no removemos), en dicha función podrán entrar otros delimitadores.

Y ya puestos, probemos con palíndromos

Este es un típico ejercicio de programación que se hace en todas las escuelas y universidades (bueno, seguro que en todas no, pero en la mayoría). Se utiliza para enseñar a los alumnos a utilizar bucles y cadenas de caracteres. Aunque aquí daremos un paso más en la abstracción y como antes, con una línea comprobaremos si la cadena es palindrómica o no. O, lo que es lo mismo, que podemos leerla tanto de izquierda a derecha como de derecha a izquierda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>

void strip(std::string &s)
{
    s.erase( std::remove_if(s.begin(), s.end(), ::isspace), s.end() );
}

bool palindromo(std::string s)
{
    strip(s);
    return std::equal(s.begin(), s.end(), s.rbegin());
}

int main()
{
    std::string ss = "reconocer";
    std::cout << ss<< ( (!palindromo(ss))?" no":"")<<" es palíndromo."<<std::endl;

    ss = "murciélago";
    std::cout << ss<< ( (!palindromo(ss))?" no":"")<<" es palíndromo."<<std::endl;

    ss = "anita la gorda lagartona no traga la droga latina";
    std::cout << ss<< ( (!palindromo(ss))?" no":"")<<" es palíndromo."<<std::endl;

    ss = "la ruta nos aporto otro paso natural";
    std::cout << ss<< ( (!palindromo(ss))?" no":"")<<" es palíndromo."<<std::endl;
}

Esto sólo es el principio

Estas son unas pocas operaciones con cadenas en C++. ¿Se te ocurren algunas más? Seguro que sí, y podrán dar para otro post con muchos más ejemplos. Déjame un comentario con tus sugerencias.

Foto principal: Katie Chase

También podría interesarte...

Leave a Reply