Archive

Posts Tagged ‘Texto’

C# 101: cadenas de texto


Las cadenas de texto representan al tipo de dato más importante en un lenguaje, tras el tipo de dato número entero. En efecto, toda la información (al final el propósito de un programa informático es manipular información) consiste principalmente en cadenas de texto. Una gran parte al menos. Así, manipular las cadenas de texto se vuelve esencial en cualquier lenguaje.

Si uno compara plataformas antiguas con plataformas modernas como Java y .NET, podemos encontrar diferencias significativas. Probablemente la mayor de éstas sea que hoy en día las cadenas de texto son inmutables. Esto quiere decir que una cadena de texto que se crea ya no puede cambiarse. Puede, en cambio, crear nuevas cadenas a partir de una. En fin, poco a poco.

¿Qué es una cadena de texto?

Una cadena de texto es una colección secuencial de caracteres, los cuales forman palabras y demás tipo de información. Cada carácter es en realidad un carácter en formato Unicode.

La clase que representa a una cadena de texto es System.String, y la que representa un carácter, es System.Char. El operador " " representa una cadena de texto constante, y por tanto puede asignarse directamente a una cadena de texto. El operador ‘ ‘ representa un carácter, y por tanto puede asignarse a una variable apropiada.

string str = "Hola mundo";
char ch = 'H';
char[] chs = new char[] { ch, 'o', 'l', 'a', ' ', 'm', 'u', 'n', 'd', 'o' };

La clase string tiene un método, llamado Empty, que representa una cadena de texto vacía. Equivale a colocar la constante "".

string str = string.Empty;
str = "";

La clase string puede inicializarse de varias formas:

// operador " "
string s1 = "Hola mundo";

// mediante el constructor
string s2 = new strnig("Hola mundo");

// a partir de un array de caracteres
string s3 = new string(
new char[] { 'H', 'o', 'l', 'a', ' ', 'm', 'u', 'n', 'd', 'o' });

// a partir de un carácter que se repite n veces
string s4 = new string('*', 10); // **********

// a partir de un grupo de bytes con signo
sbyte[] bytes = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x00 };

unsafe {
    sbyte* p = bytes;
    string s5 = new string(p);
}

Los caracteres son una estructura, por tanto son tipos valor (ValueType). Las cadenas de texto son clases, y por tanto son tipos referencia.

¿Qué es eso de Unicode?

Unicode es el nombre del formato en el que se presenta una cadena de texto en .NET. Un byte con signo (8 bits) puede almacenar números del -127 al 128. Los 128 números son suficientes para representar toda la tabla de caracteres ASCII. Así, por ejemplo, el número 64 equivale a una A, el 65 a una B, etc. Así nació la primera tabla de caracteres.

Después a alguien se le ocurrió que podía utilizarse un byte sin signo (8 bits), el cual puede almacenar números del 0 al 255. Con esto podemos almacenar más caracteres, y se creó la tabla ASCII extendida. 255 caracteres son muchos. Las letras acentuadas, por ejemplo, forman de ASCII extendido. Prácticamente se incluyeron todas las letras, caracteres y símbolos del alfabeto latino.

Sin embargo, a pesar de ello, ASCII+ es insuficiente todavía para guardar todos los caracteres de otros lenguajes, como el chino mandarín, la escritura hebrea, griega, cirílica, etc. Así, en los 90s se propuso un formato, llamado Unicode, el cual considera caracteres de dos bytes sin signo (16 bits), los cuales pueden almacenar números del 0 al 65536. Ahora, con 65536 caracteres sí que caben los chinos, los sánscritos y hasta el élfico antiguo de Lord of the Rings. Hay otro formato, Unicode 32 (de 4 bytes sin signo), pero la verdad es que no se utiliza mucho. El bueno, estándar, es el Unicode, al menos para .NET.

¿Eso quiere decir que cada carácter me cuesta 2 bytes de memoria?

Así es. La cadena de texto "Hola mundo" te cuesta 20 bytes. Esto implica, por supuesto, que mucho texto puede salirte caro en términos de memoria. El tamaño máximo de una cadena de texto en .NET es de 2 GB, o 1,073,741,824 caracteres.

Hay que tener cuidado, al final las cadenas de texto pueden ser potencialmente grandes, así que ten cuidado nada más.

¿Y qué es eso de que son inmutables?

Ah, eso quiere decir que una cadena de texto no cambia, una vez que le has asignado un valor. Es como si el valor en memoria fuera de solo lectura. Por supuesto que internamente no es así, pero la clase string no expone ni un solo método o propiedad que modifique el contenido. Todos los métodos operan sobre el valor del objeto actual, pero construyen uno nuevo, que es lo que devuelven.

La decisión de hacerlo de esta forma viene por razones históricas. Si uno programa para Win32 con C o C++, recordará que en estas plataformas las cadenas de texto pueden o no ser mutables. Por un lado, tenemos las cadenas estáticas o array de caracteres que no cambian su tamaño. Pueden ser declaradas en la pila de memoria o en el montículo de memoria. Si se declara en el montículo, entonces la cadena puede cambiar de tamaño. Desde el punto de vista de una función, el parámetro que recibe no sabe si es una cadena fija o dinámica.

void SetWindowText(HWND hWnd, LPCTSTR lpszText) // LPCTSTR == const TCHAR* 
{ 
… 
} 

TCHAR sz[20] = L"Hola mundo!"; // cadena fija 
TCHAR* psz = new TCHAR[20]; 
wcscpy(psz, L"Hola mundo!"); 

// indistinguible para SetWindowText 
SetWindowText(sz); 
SetWindowText(psz); 

delete [] psz; 

Y peor aún es cuando una cadena de texto tiene que ser creada y devuelta por una función. En ese caso pides la cadena en referencia más el tamaño de la misma. O bien, regresas una cadena dinámicamente creada y esperas que el programador no se olvide de liberar la memoria antes de salir. Ah, y si tu cadena permite tener nulos intermedios, entonces tienes que usar otros métodos.

Por esta misma complicación, salieron muchas muchas muchas clases y opciones para paliar el dolor. De entrada, Win32 define CHAR, WCHAR y TCHAR, con sus equivalentes punteros: LPSTR, LPWSTR y LPTSTR, más sus constantes LPCSTR, LPCWSTR, LPCTSTR. El Microsoft Foundation Classes, MFC, tiene la clase CString, y el Active Template Library, ATL, (junto con Windows Template Library, WTL) define su propia clase CStringT. Ah, y el estándar de C++ define std::string y std::wstring, que heredan de std::basic_string<T>. Por ahí el Component Object Model define BSTR, con varios métodos como SysAllocString y SysFreeString, más la clase _bstr_t y CComStr.

En fin, con tanto relajo, la gente que ideó C# y .NET debía tomar una decisión sobre cómo manejar cadenas en .NET, para evitar caer en el relajo que se tenía. Tanto cadenas estáticas como dinámicas presentan ventajas y desventajas. Las estáticas provocan algoritmos complejos para crear nuevas cadenas de texto, pero son seguras. Las dinámicas son inherentemente inseguras pero pueden crecer a conveniencia. Al final, la decisión fue que las cadenas iban a ser dinámicas (i.e. referencias) PERO sólo internamente: la clase que las representa no expondría ningún método para que un programador pudiera cambiar su valor interno. De esta forma se aseguraban lo mejor de los dos mundos: dinamismo pero a su vez, inmutabilidad. Incluso gracias a eso pudieron diseñar algunos algoritmos para optimizar su manejo.

Ok, entiendo el dilema, pero si son inmutables, ¿cómo podemos realizar operaciones para crear nuevas cadenas?

Las operaciones que pueden realizarse sobre cadenas de texto son variadas: concatenación, substitución de bloques de texto, búsquedas, indización, reemplazo de caracteres, formateo, conversiones, etc. Todas estas operaciones en realidad devuelven una nueva cadena de texto, basada en la cadena actual más la operación en cuestión. Pero la cadena actual nunca se ve afectada.

Pongamos como ejemplo el reemplazo de un bloque de texto. El método String.Replace toma dos parámetros: una cadena que representa el valor que queremos buscar, y otra cadena que representa el valor con el que reemplazaremos a la primera. El método opera sobre una cadena determinada, y regresa otra cadena con el valor modificado.

string strorg = "Todos los perros van al cielo"; 
string strmod = strorg.Replace("perros", "pollos"); 

Console.WriteLine("Cadena original: {0}", strorg); 
Console.WriteLine("Cadena nueva: {0}", strmod); 

/* imprime: 
Todos los perros van al cielo 
Todos los pollos van al cielo 
*/ 

Vemos que aunque aplicamos el método Replace sobre strorg, éste no modifico su valor, sino que creó una nueva cadena con la operación aplicada, la cual regresó.

Momento: ¿no significa eso que entonces que estamos creando muchas instancias de cadenas?

En efecto. Hay que tener mucho cuidado, es muy importante estar conscientes de esto. En particular, la concatenación de cadenas de texto es particularmente cara en cuanto a recursos.

int i = 42; 
int n = 9; 
int m = 6; 

string s = "Según la guía interestelar, " + i + " es la respuesta última a la " + 
"vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es " + 
n + " por " + m + "? "; 

El operador + está sobrecargado para precisamente concatenar dos valores. Si uno de los valores no es una cadena de texto, se le convierte en cadena en automático, bien mediante una conversión directa, bien mediante el método ToString. Así las cosas, veamos las cadenas que se han creado.

 

Expresión

Cadena

Estática

Según la guía interestelar,

i.ToString()

42

Concatenación

Según la guía interestelar, 42

Estática

es la respuesta última a la

Concatenación

Según la guía interestelar, 42 es la respuesta última a la

Estática

vida, el universo yo todo lo demás, siendo la pregunta: ¿cuánto es

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es

n.ToString()

9

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9

Estática

 por

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por

m.ToString()

6

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por 6

Estática

?

Concatenación

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por 6?

 

Perdonen la expresión, pero ¡en la madre! Quince cadenas se han creado, y se duplican muchísimos valores. ¡De entrada todas las cadenas "Concatenación" deberían ahorrarse, no hacen sentido! Y ahora se me caen los calzones: 1738 bytes por 869 caracteres, es decir 1.7 KB. La cadena original que queríamos formar, al final, fue de 266 bytes por 133 caracteres. Es decir que para formar una cadena de 266 bytes tuvimos que gastarnos 1472 bytes: 553% más de consumo de memoria.

Desafortunadamente, es la desventaja (o "trade-off") que tenemos que dar por poder manejar este modelo de cadenas de texto inmutables. La lógica detrás de esto es que es mejor tener un sistema más ineficiente a propiciar excepciones y errores de programación en las aplicaciones. Pero eso no nos exime de problemas ulteriores. He visto sistemas (sobre todo aplicaciones web hechas con ASP.NET) donde la aplicación está muy lenta, consume muchos recursos o de plano muere por falta de memoria, precisamente por tener un muy mal manejo de cadenas de texto.

¿Entonces no hay esperanza? ¿No podremos escribir aplicaciones intensivas en texto?

Depende mucho del programador. Una concatenación como la anterior causará muchos problemas, sobre todo cuando ésta se repite mucho. Y eso depende del programador, depende de ti.

Sin embargo, vale la pena aclarar, por si hubo alguna confusión: las cadenas son inmutables para los clientes de la clase String, es decir, para nosotros simples programadores mortales. Otras clases amigas (internas en C#) sí que pueden tener acceso a los miembros que permitan modificar la cadena de texto.

¿Eso quiere decir que hay clases especiales para ayudarnos?

Precisamente. .NET pone a nuestra disposición varias clases que nos ayudan muchísimo con la gestión de recursos de texto. Estas clases, en particular, hacen uso de estos principios:

1.- uso dinámico de la cadena de texto, ampliando y reduciendo su tamaño, pero protegido.

2.- creación de búferes lo suficientemente grandes para no estar redimensionando la cadena en cada momento, pero no tanto que consuma demasiada memoria.

3.- preferir el movimiento de los bytes de una cadena en lugar de copiarlos.

Esto disminuye significativamente la creación de las cadenas y por tanto el consumo de memoria. Un método de la clase String: String.Format, nos ayuda también con esto. Veamos:

int i = 42; 
int n = 9; 
int m = 6; 

string s = string.Format("Según la guía interestelar, {0} es la respuesta última 
a la vida, el universo y todo lo demás, siendo la 
pregunta: ¿cuánto es {1} por {2}?", i, n, m); 

Este código genera las siguientes cadenas:

Expresión

Cadena

Estática

Según la guía interestelar, {0} es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es {1} por {2}?

i.ToString()

42

n.ToString()

9

m.ToString()

6

Formateo

Según la guía interestelar, 42 es la respuesta última a la vida, el universo y todo lo demás, siendo la pregunta: ¿cuánto es 9 por 6?

String.Format hace uso internamente de otra clase, que veremos más adelante, y que se adhiere a los tres principios expuestos anteriormente. En este caso, creamos 275 caracteres, lo cual representa 550 bytes. Esto representa un 37% de los bytes empleados al usar la concatenación del ejemplo anterior. Es decir, nos ahorramos un 63% de memoria. Una diferencia significativa.

String.Format parece adecuado para cadenas pequeñas, pero ¿qué pasa con las grandes cadenas?

En estos casos tienes unas cuantas opciones. La primera, más fácil y más obvia, es utilizar la clase StringBuilder. Esta clase construye cadenas de forma eficiente, y de hecho el método String.Format utiliza internamente un StringBuilder.

Básicamente, StringBuilder mantiene su propio búfer, y cuando se hace alguna operación sobre el texto que cambie la longitud de la cadena, la clase revisa si tiene suficiente tamaño en su búfer, y si no, el tamaño del búfer es incrementado en un tamaño determinado. Durante todo este tiempo, ¡StringBuilder NO utiliza ningún string! Sino que guarda todo a nivel de bytes. Sólo hasta que se invoca al método StringBuilder.ToString es que la clase construye el string.

Nuestro ejemplo anterior se vería así, utilizando StringBuilder.

StringBuilder text = new StringBuilder() 
.Append("Según la guía interestelar, ") 
.Append(i) 
.Append(" es la respuesta última a la vida, el universo y todo lo demás, 
siendo la pregunta: ¿cuánto es ") 
.AppendFormat("{0} por {1}?", n, m); 

string s = text.ToString(); 

Este código puede resultar extraño, así que expliquémoslo punto por punto. Primero, vemos que existe un método Append, el cuál añade al búfer una cadena de texto. De hecho, el método se encuentra sobrecargado para incorporar como parámetros todos los tipos base de .NET: bytes, ints, floats, decimales, DateTimes, etc.

Otro método es AppendFormat. Éste funciona igual que Append, con la diferencia que reemplaza en el texto las llaves {0}, {1}, {2}, etc., por el parámetro correspondiente en la posición 0, 1, 2, etc. De hecho, si pudieras ver el código de String.Format, verías algo así:

public static string Format(string text, params object[] objs) 
{ 
    StringBuilder b = new StringBuilder(); 
    b.AppendFormat(text, objs); 
    
    return b.ToString(); 
} 

Ahora bien, tanto Append como AppendFormat regresan una instancia a StringBuilder. De hecho, la instancia que regresan es la propia instancia. Es decir, hacen un return this. Si pudieras ver el código de Append, verías algo así:

public StringBuilder Append(string text) 
{ 
    // hacer algo con text 

    return this; 
} 

¿Por qué hacer esto? Ah, pues para que precisamente puedes encadenar las llamadas a Append y AppendFormat de la forma en que hicimos en el ejemplo. Este es un patrón de diseño que comúnmente se aplica en las clases que implementan el patrón Builder.

¿Se puede escoger las opciones de formato de un texto?

¡En efecto! Una de las sobrecargas de AppendFormat incluye un IFormatProvider como parámetro, así que para formatear el texto con una cultura especial puedes usar dicha sobrecarga.

float f = 1983.42; 
CultureInfo esmx = new CultureInfo("es-MX"); 
CultureInfo eses = new CultureInfo("es-ES"); 
StringBuilder b = new StringBuilder() 
    .AppendFormat(eses, "Número español: {0}\n", f) 
    .AppendFormat(esmx, "Número mejicano: {1}\n", f); 
Console.WriteLine(b); 

/* Imprime: 
Número español: 1.983,42 
Número mejicano: 1,983.42 
*/ 

Aparte de un CultureInfo, puedes pasar un NumberFormatInfo para el formateo de números, un DateTimeFormatInfo para el formateo de fechas, o bien puedes implementar tu propio IFormatProvider / ICustomFormatter.

¿Qué operaciones podemos hacer sobre el texto con el StringBuilder?

Evidentemente Append y AppendFormat, junto con ToString, son los métodos más utilizados. Sin embargo, también puedes realizar las siguientes operaciones.

1.- Eliminar todo el contenido del búfer, mediante el método Clear.

2.- Añadir un texto seguido de una nueva línea, mediante AppendLine.

3.- Copiar un bloque de caracteres por posición, a un array de caracteres, mediante CopyTo.

4.- Insertar caracteres en una posición determinada. El método Insert, funciona similar a Append, sólo que se hace en una posición del búfer determinada.

5.- Eliminar un rango de caracteres del búfer, mediante Remove.

6.- Asegurar la capacidad de un búfer, mediante EnsureCapacity.

7.- Remplazar una sub-cadena de texto por otra, mediante Replace.

StringBuilder text = 
    new StringBuilder("Tres tristes tigres tragaban trigo en un trigal"); 
text.EnsureCapacity(200); 

// text.Capacity >= 200 
text.AppendLine(); 

// añade un carácter \n al final 
char[] chars = new char[10]; 
text.CopyTo(13, chars, 0, 6); 
// chars == { t, i, g, r, e, s } 
text.Insert(12, 42); 
// text == "Tres tristes42 tigres tragaban trigo en un trigal" 
text.Remove(5, 10); 
// text == "Tres tigres tragaban trigo en un trigal" 
text.Replace("tigres", "pollos"); 
// text == "Tres pollos tragaban trigo en un trigal"; 
text.Clear(); 
// text == "" 
// text.Capacity >= 200, ésta propiedad no se afecta 

¿Qué otros métodos para manipular texto existen?

Básicamente hay dos categorías: la manipulación de texto directa y la búsqueda de cadenas avanzada. Esta última utiliza expresiones regulares, lo cual queda fuera del alcance de esta entrada.

Para la manipulación de texto directa, la clase String expone diferentes métodos que nos ayudan con esto. Básicamente puedes realizar lo siguiente.

1.- Comparar cadenas de texto contra otras cadenas, otros valores o sub-cadenas.

2.- Concatenar cadenas, valores o sub-cadenas.

3.- Buscar valores, sub-cadenas de texto, etc.

4.- Formateo de cadenas de texto.

5.- Normalizar cadenas de texto (i.e. cambiar caracteres Unicode combinados por un solo carácter).

6.- Unir y separar cadenas de texto a partir de arrays, cadenas u objetos.

7.- Remover y remplazar caracteres y valores.

8.- Transformar los valores internos de la cadena de texto.

9.- Convertir cadena de texto a otros tipos de datos.

¿Cómo se pueden comparar cadenas de texto entre sí?

Hay diferentes tipos de comparaciones. La primera es la de igualdad, y el método en cuestión es Equals. Este método viene en muchos sabores. En primer lugar, permite comparar contra cualquier objeto, dado que sobrescribe Object.Equals. Sin embargo, esta comparación regresará false si el parámetro no es un string.

El segundo sabor es el Equals con una cadena de texto como parámetro. Este método hace una comparación carácter a carácter, se acuerdo a la cultura actual. String tiene un método estático homónimo que compara dos cadenas. Ambos métodos hacen lo mismo.

string s1 = "Pollo"; 
string s2 = "Pollo"; 
string s3 = "pollo"; 

bool val; 
val = s1.Equals(s2); // val == true 
val = s2.Equals(s3); // val == false 
val = string.Equals(s1, s3); // val == false 
val = string.Equals(s2, s1); // val == true 

El tercer sabor es similar al anterior: dos métodos Equals, uno de instancia y otro estático, que adicional a la cadena de texto, toma como parámetro una enumeración que indica el tipo de comparación a realizar: cultura actual o invariante, ignorar capitalización (mayúsculas y minúsculas) para la cultura actual o cultura invariante, o bien comparar de forma ordinal tomando en cuenta e ignorando la capitalización.

string s1 = "Pollo"; 
string s2 = "Pollo"; 
string s3 = "pollo"; 

bool val; 
// val == true 
val = s2.Equals(s3, StringComparison.CurrentCultureIgnoreCase); 
// val == true 
val = string.Equals(s1, s3, StringComparison.InvariantCultureIgnoreCase); 

¿Y cómo comparo cadenas alfabéticamente?

Con el siguiente tipo de comparación de relación: mayor qué y menor qué. Cuando se comparan dos caracteres, se dice que uno es mayor que el otro, si el primero está después en la tabla de posiciones Unicode. Una forma fácil de ver esto es en el programa Mapa de Caracteres, de Windows.

clip_image001

Aquí vemos que N es menor que Q, y que Q es menor que n minúscula. Por otra parte, también podemos comparar cualquier tipo de símbolos, según dicha tabla.

Así pues, decimos que una cadena de texto es menor que otra cuando conforme se van comparando carácter a carácter, se encuentra conque un carácter es menor que el de la otra en el mismo índice.

azul < carro, porque a < c.

auto < azul porque a == a, y u < z.

ampolla < amputar porque a == a, m == m, p == p, o < u.

Y así sucesivamente. Aquí vale la pena aclarar que las comparaciones pueden hacerse de dos formas: invariantes o por cultura. Una comparación por cultura toma en cuenta el idioma, y ciertas reglas explícitas de comparación pueden ser verdaderas para una cultura, pero falsas para otra. Por ejemplo:

ß == ss para de-DE (alemán de Alemania) y falso en español.

eßen == essen // de-DE

eßen > essen // es-ES

á == a para español, pero á > a para alemán.

ámpula == ampula // es-ES

ámpula > ampula // en-US

La comparación invariante no distingue entre culturas.

El primer método de comparación que veremos, según las reglas que hemos visto, es Compare. Éste es un método estático que nos permite comparar entre cadenas de texto. Prácticamente todos los métodos de comparación que veremos regresan un valor entero (Int32): menor a cero cuando la primera cadena es menor que la segunda, mayor a cero cuando la primera cadena es mayor a la segunda, o cero si son iguales (en cuyo caso, Equals regresaría true).

El método Compare tiene muchas sobrecargas, que permiten indicar cultura, si se ignora capitalización o no, si se comparan sub-cadenas, si se compara con opciones (ignorar capitalización, símbolos, espacios en blanco, etc.) y así sucesivamente. Algunos ejemplos a continuación (suponiendo que mi cultura actual es español de México).

int ret; 

CultureInfo dede = new CultureInfo("de-DE"); 
ret = string.Compare("Das Maedchen", "Das Mädchen"); 
// ret < 0 porque a < ä 
ret = string.Compare("Das Maedchen", "Das Mädchen", true, dede); 
// ret == 0 porque ae == ä en alemán 
ret = string.Compare("das maedchen", "DAS MÄDCHEN", 
    false, dede); 
// ret == 0 porque se ignora capitalización y ae == Ä en alemán 
string url = "https://fermasmas.wordpress.com"; 
ret = string.Compare(url, 0, "http:", 0, 5, 
StringComparison.InvariantCultureIgnoreCase); 
// ret == 0 porque se compara url con http: en los primeros cinco caracteres. 

Etcétera. El método Compare es estático, pero la clase String tiene un método a nivel de instancia, llamada CompareTo. Este método funciona prácticamente igual que Compare, aunque sólo tiene dos sobrecargas: una que compara contra un objeto, el cual se evalúa a string (i.e. ToString()); y otra que compara contra otro string, tomando en cuenta capitalización y cultura actual.

int ret; 
string str = "Das Maedchen"; 
ret = str.CompareTo("Das Mädchen"); // ret < 0 porque a < ä 
ret = str.CompareTo("Das maedchen"); // ret > 0 porque M > m 
str = "42"; 
ret = str.CompareTo(42); // ret == 0 porque "42" == (42).ToString() 

Otro asunto: existe un método estático llamado CompareOrdinal. Este método hace la comparación evaluando los valores numéricos de cada carácter. Así, es totalmente agnóstico de la cultura empleada.

Ya por último, si en algún momento quieres comparar una cadena de texto vacía o nula, en lugar de hacer esto:

if (str != null || str != "") { … } 

Mejor hacer esto:

if (!string.IsNullOrEmpty(str)) { … } 

El método IsNullOrEmpty regresa verdadero si el parámetro es nulo o está vacío. También existe isNullOrWhiteSpace, si el parámetro es nulo, está vacío o sólo contiene espacios blancos.

¿Qué hay respecto a la concatenación de cadenas?

La forma más fácil es usar los operadores + y +=. Pero ya vimos que no es muy eficiente, sobre todo cuando concatenamos muchas cadenas y valores. Asimismo, ya hablamos sobre String.Format y StringBuilder. Podemos usar ambas para concatenar cadenas.

Adicional a esto, la clase String cuenta con un método estático, llamado Concat. Este método, con todas sus sobrecargas, permite concatenar dos o más cadenas, objetos y valores. Al final, regresa una cadena de texto con la cadena final.

object[] vals = { "Dos y dos son ", 
    2 + 2, 
    ", cuatro y dos son ", 
    4 + 2 }; 

string s = string.Concat(vals); // "Dos y dos son 4, cuatro y dos son 6"

 

¿Y cómo podemos realizar búsquedas de cadenas de texto?

Hay muchos métodos encaminados a buscar cadenas de texto de forma directa (es decir, sin utilizar expresiones regulares). Se me ocurre clasificarlos en tres grupos: aquellos que indican concordancia, aquellos que regresan un índice, y aquellos que regresan una porción de texto.

Para el primer grupo, tenemos los siguientes métodos que presentamos a continuación. Todos regresan true o false, dependiendo de si se cumple la condición de búsqueda. Nota: consideremos la siguiente cadena para los ejemplos.

string s = "Cuántos pollos comen melón, cuántos sandía"; 

1.- El método Contains regresa true si una cadena de texto está contenida dentro de otra.

s.Contains(""); // true, una cadena vacía siempre está contenida. 
s.Contains("pollo"); // true; 
s.Contains("chabacano"); // false 

2.- El método EndsWith regresa true si una cadena de texto tiene una terminación determinada. Toma en cuenta cultura y comparación de cadenas (i.e. ordinal, invariante o sensible a la cultura actual).

s.EndsWith("sandía"); // true 
s.EndsWith("sandia"); // false 
s.EndsWith("SANDÍA", StringComparison.CurrentCultureIgnoreCase); // true 
s.EndsWith("SANDIA", true, new CultureInfo("es-MX")); // true 

3.- La contraparte al método anterior es StartsWith, que regresa true si una cadena de texto tiene un comienzo determinado.

s.StartsWith("Cuántos"); // true 
s.StartsWith("Cuantos"); // false 
s.StartsWith("CUÁNTOS", StringComparison.CurrentCultureIgnoreCase); // true 
s.StartsWith("CUANTOS", true, new CultureInfo("es-MX")); // true 

El segundo grupo cuenta también con varios métodos, que básicamente regresan un número. Éste es el índice a partir del cual se encuentra la cadena a ser buscada. Si el valor es menor a cero, es que la cadena no fue encontrada.

1.- El método directo para encontrar el índice es IndexOf. Este método busca la primera coincidencia de la cadena de texto pasada como parámetro. IndexOf busca también carateres, y permite especificar opciones de formato, como cultura o capitalización.

int index; 

index = s.IndexOf("pollo"); // index == 8 
index = s.IndexOf("Cuántos"); // index == 0 
index = s.IndexOf("cuántos"); // index == 28 
index = s.IndexOf("cuántos" 
    StringComparison.CurrentCultureIgnoreCase); // index == 0 
index = s.IndexOf("Melón"); // index == -1 
index = s.IndexOf("Melón", 
    StringComparison.CurrentCultureIgnoreCase); // index == 21 

2.- Un converso de IndexOf es LastIndexOf. Hace lo mismo, sólo que busca la última coincidencia, a diferencia de IndexOf que busca la primera coincidencia.

int index; 

index = s.LastIndexOf("Cuántos"); // index == 0 
index = s.LastIndexOf("cuántos"); // index == 28 
index = s.LastIndexOf("cuántos", 
    StringComparison.CurrentCultureIgnoreCase); // index == 28 

3.- Existe un método que permite buscar un conjunto de caracteres, en un array, y regresa el índice del primer carácter encontrado. Éste es IndexOfAny.

int index; 
char[] chars = new char[] { 'n', 'o', 's' }; 
index = s.IndexOfAny(chars); // index == 3, en "Cuántos" 

4.- El converso es LastIndexOfAny, que busca caracteres pero comenzando por el último.

int index; 
char[] chars = new char[] { 'o', 'n', 's' }; 
index = s.IndexOfAny(chars); // index == 38, en "sandía" 

El último grupo es un solo método, y es similar a la familia IndexOf, pero al revés: le pasas un índice y te regresa una sub-cadena. Este método es Substring, el cual tiene dos sobrecargas: la primera obtiene la cadena a partir de un índice, y la segunda obtiene la cadena a partir del índice y de una longitud determinada.

string str = s.Substring(8, 6); // str == "pollos" 

// Este método funciona particularmente bien en conjunto con IndexOf. 

string s = "Cuántos {pollos} comen melón, cuántos sandía"; 
string io = s.IndexOf('{'); 
string if = s.IndexOf('}'); 
string ss = s.Substring(io, if - io); 
Console.WriteLine(ss); // pollos 

Ok, basta de búsquedas. ¿Cómo se puede formatear cadenas de texto?

Ya hemos analizado tanto string.Format como StringBuilder.AppendFormat, métodos que nos ayudan a generar formato. Pero podemos ahondar un poquito más. El método string.Format le pasa el control directamente a StringBuilder, por lo que hablaremos de ambos métodos de forma indistinta, bajo este entendido.

En fin, String.Format siempre interactúa con respecto a la cultura local. Normalmente esto suele ser suficiente: si la computadora está en español de México, pues tendrás puntos decimales, comillas de miles, etc. Si estás en español de España, pues es al revés. Hay ocasiones, sin embargo, en las que necesitamos forzar el uso de un idioma o cultura en particular. Para ello, String.Format (y de hecho prácticamente cualquier método que implique conversión entre un tipo de dato y texto) tiene una sobrecarga que permite pasar por parámetro un IFormatProvider.

El IFormatProvider por excelencia es CultureInfo. Esta clase representa una cultura, y por lo tanto el objeto de formato que implementa formatea las cadenas de texto conforme a las reglas de esa cultura.

CultureInfo japones = new CultureInfo("ja-JP"); 
CultureInfo aleman = new CultureInfo("de-DE"); 
CultureInfo espanol = new CultureInfo("es-MX");
 
float c = "42.06"; 
string s = string.Format("{0:C}", japones); 
Console.WriteLine(s); 
string s = string.Format("{0:C}", aleman); 
Console.WriteLine(s); 
string s = string.Format("{0:C}", espanol); 
Console.WriteLine(s); 

/* 
* ¥ 42.06 
* 42,06 € 
* $ 42.06 
*/ 

Veamos un par de cosas que pasaron. En primer lugar creamos tres objetos de cultura: japonés de Japón, alemán de Alemania, y español de México. Luego, usamos string.Format para realizar un formato. Vemos que en la cadena de formato, pasamos {0:C}. Normalmente pasamos {0}. Ese :C de más quiere decir que formatearemos la entrada como una moneda, en este caso el yen japonés, el euro alemán y el peso mexicano. La C determina el tipo de formato, y la cultura, la forma de llevarlo a cabo.

Otros ejemplos son: D para decimal, E para notación exponencial o científica, F para punto decimal fijo (i.e. 4 => 4.0), G para la versión general, es decir, la más compacta posible; N para un número con separador de miles, decimales, etc.; P para porcentaje, R para redondear, X para hexadecimal.

s = string.Format("{0:D}", 99901); // s == 99901 
s = string.Format("{0:D10}", 99901); // s == 0000099901 
s = string.Format("{0:E}", 1983.42"); // s == 1.98342E+003 
s = string.Format("{0:F2}", 1983"); // s == 1983.00 
s = string.Format("{0:F3}", 1983.4266"); // s == 1983.427 
s = string.Format(japones, "{0:N}", 1983.55); // s == 1.983,55 
s = string.Format("{0:P}", 0.42); // s == 42 % 
s = string.Format("{0:X4}", 255); // s == 00FF 

Si ninguna de estas nos satisface, podemos crear nuestro propio formato mediante el formato de números especializado. Para hacer esto, podemos colocar en lugar del D, E, C, etc., un 0 para representar un dígito, si existe, o un cero si no; un # para representar un dígito si existe, si no, no muestra nada; El punto . para separar decimales y la coma para separar grupos. Asimismo, tenemos % para porcentaje, ‰ para por-miles, E0 para notación exponencial, ‘cadena’ para poner una cadena de texto fija.

s = string.Format("{0:0,00.00#:D}", 99.1); // s == 0,99.10 
s = string.Format(aleman, "{0:0,00.00#:D}", 99.1); // s == 0.99,10 
s = string.Format("{0:000,000.000}", 9991); // s == 009,991.000 
s = string.Format("{0:#,#00}", 99.1); // s == 99 
s = string.Format("{0:#,#00}", 9.1); // s == 09 
s = string.Format("{0:#,#00}", 9999.1); // s == 9,999 
s = string.Format("{0:##-###-##}", 99942); // s == 999-42 
s = string.Format("{0:##-###-##}", 4299942); // s == 42-999-42 

El formato también podemos aplicarlo a las fechas, de forma muy similar a como se aplica para números. Existen muchos formatos, pero los más utilizados son los siguientes: se usa la d y D para una fecha corta y una fecha larga, respectivamente; f y F para fecha con tiempo corto y largo, u y U para formato universal corto y largo.

CultureInfo enUS = new CultureInfo("en-US"); 
CultureInfo deDE = new CultureInfo("de-DE"); 
CultureInfo esMX = new CultureInfo("es-MX"); 

DateTime tm = new DateTime(2012, 12, 04, 11, 05, 42); 
s = string.Format(esMX, "{0:d}", tm); // s == 12/04/2012 
s = string.Format(esMX, "{0:d}", tm); // s == 04/12/2012 
s = string.Format(deDE, "{0:D}", tm); // s == Dienstag, 4. Dezember 2012 
s = string.Format(enUS, "{0:f}", tm); // s == Tuesday, 4 Dic, 2012 11:05 

Etcétera. También podemos utilizar formato especial usando y, M, d, m, h, M, s para representar año, mes, día, minuto hora, segundo, etc. Por ejemplo, cuatro yes suponen año en formato de cuatro dígitos, como 1983. Dos yes y tendríamos 83.

s = string.Format(esMX, "{0:ddd d MMM}", tm); 
// s == mar 4 dic 
s = string.Format(esMX, "{0:dddd d MMMM}", tm); 
// s == martes 4 diciembre 
s = string.Format(esMX, "{0:H mm ss}", tm); 
// s == 11 05 42 
s = string.Format(esMX, "{0:y yy yyyy yyyyy}", tm); 
// s == 2 12 2012 02012 

¿Y qué significa eso de ‘normalizar’?

Cuando hablamos de normalización, en general, queremos decir que estandarizamos algo, es decir, que lo dejamos en su forma más sencilla. Haciendo una comparación un poco simple, pero ilustrativa, consideremos el siguiente número.

clip_image002

A primera instancia luce complicado. Sin embargo, si resuelves, te darás cuenta que el número es 42. Pues bien, decimos que 42 es la forma normalizada del número anterior.

Ahora bien, cuando hablamos de texto, la normalización también significa simplificar la cadena conservando el mismo valor. En este caso, la simplificación se da a nivel binario.

Para entender lo anterior, necesitamos recordar que las cadenas de texto son una secuencia de caracteres Unicode. Cada carácter Unicode está formado por dos bytes. Ahora bien, a nivel textual, existen algunos caracteres que pueden representarse por dos o más pares de bytes totalmente diferentes. Es decir, a nivel binario los valores son diferentes, pero a nivel de texto son iguales.

Por ejemplo, el carácter ắ podemos representarlo de tres formas:

s1 = 0x1EAF

s2 = 0x0103 + 0x0301

s3 = 0x0061 + 0x0306 + 0x0301

Cada uno de los números significa representa un byte. Así, podemos crear las siguientes cadenas de texto.

string a1 = new string( new char[] { '\u1EAF' }); 
string a2 = new string( new char[] { '\u0103', '\u0301' }); 
string a3 = new string( new char[] { '\u0061', '\u0306', '\u0301' }); 

Las tres cadenas imprimirían el mismo carácter. Pero al compararlas entre sí, especialmente usando la forma de comparación con String.CompareOrdinal, ninguna de las tres será igual a las demás. Estas divergencias pueden complicar ciertos algoritmos, como búsquedas de cadenas de texto.

Como quizás ya hayas adivinado, la normalización de una cadena de texto consiste, precisamente, en simplificar la representación binaria de un carácter o secuencia de caracteres. En el ejemplo anterior, significaría convertir a2 y a3 en a1. Esto es posible debido a que el estándar de Unicode define algunos algoritmos para hacer esto, llamados imaginativamente C, D, KC, y KD. Cada uno define ciertas reglas a seguir. El más utilizado, y el estándar en .NET, es el C.

La clase String, así, define un método llamado Normalize con dos sobrecargas, una que utiliza el algoritmo C por default, y una que permite seleccionar el tipo de algoritmo. Asimismo, define un método IsNormalized, que nos permite saber si una cadena está normalizada o no.

string a1 = new string( new char[] { '\u1EAF' }); 
string a2 = new string( new char[] { '\u0103', '\u0301' }); 
string a3 = new string( new char[] { '\u0061', '\u0306', '\u0301' }); 

if (!a1.IsNormalized()) 
    a1 = a1.Normalize(); 
if (!a2.IsNormalized(NormalizationForm.FormKD)); 
    a2 = a2.Noramlize(NormalizationForm.FormKD); 
if (!a3.IsNormalized()); 
    a3 = a3.Noramlize(); 

Cuando trabajamos con archivos o bases de datos y tenemos campos sobre los que haremos búsquedas (por ejemplo, cláusula "where" en una consulta "select" de SQL), es muy recomendable que normalices el texto antes de guardarlo en el archivo o base de datos.

¿Cómo puedo unir y separar cadenas de texto?

Ésta es útil y facilita. Para unir cadenas de texto, necesitamos ponerlas todas en un array o cualquier colección que implemente IEnumerable. Luego, invocamos al método estático Join. Este método toma como primer parámetro el separador. Es decir, cuando las unas puedes querer separarlas por algún carácter o cadena. Esta es tu oportunidad. El segundo parámetro es la colección.

string[] cylons { 
    "John", 
    "Leoben", 
    "D'Anna", 
    "Simon", 
    "Caprica Six", 
}; 

string text1 = string.Join("\nCylon: ", cylons); 
Console.WriteLine(text1); 

/* imprime: 
Cylon: John 
Cylon: Leoben 
Cylon: D'Anna 
Cylon: Simon 
Cylon: Caprica Six 
*/ 

string text2 = string.Join(", ", cylons); 
Console.WriteLine(text2); 

/* imprime: 
John, Leoben, D'Anna, Simon, Caprica Six 
*/ 

Para separar la cadena, hay que usar el método Split. Este método no estático toma como parámetro una cadena que representa un separador. Cada que se encuentre un separador, se corta la cadena y el bloque se añade a un array. Si el separador nunca es encontrado, entonces el array tendrá una sola cadena: la cadena original.

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 
string[] cylons = text.Split('-'); 

foreach (string cylon in cylons) { 
    Console.WriteLine(cylon); 
} 

/* imprime: 
John 
Leoben 
D'Anna 
Simon 
Caprica Six 
*/ 

Ok, ¿y ahora cómo remuevo o reemplazo caracteres?

Tenemos un par de métodos para ello. En primer lugar, Remove se encarga de remover los caracteres a partir de un índice y una longitud determinada.

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 
text = text.Remove(5, 7); 
// text == "John-D'Anna-Simon-Caprica Six" 
text = text.Remove(0, 5); 
// text == "D'Anna-Simon-Caprica Six" 
text = text.Remove(6); // del 6to carácter en adelante 
// text == "D'Anna" 

Y así es que nos quedamos con Lucy Lawless.

También podemos usar Replace. Este método substituye un carácter por otro, o una cadena de texto por otra.

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 

text = text.Replace("John", "Saul") 
    .Replace("Leoben", "Galen") 
    .Replace("Simon", "Ellen") 
    .Replace("D'Anna", "Anders"); 
// text == "Saul-Galen-Anders-Ellen-Caprica Six" 
text = text.Replace('a', '*') 
    .Replace('l', '1'); 
// text == "S*u1-G*1en-Anders-E11en-C*prica Six" 

Dado que cada Replace regresa una cadena regresa de texto, es posible encadenarlas de esta forma. Por cierto, que Replace puede actuar también como Remove, si somos imaginativos:

string text = "John-Leoben-D'Anna-Simon-Caprica Six"; 

text.Replace("John-", string.Empty) 
    .Replace("D'Anna-", string.Empty) 
    .Replace("-Caprica Six", string.Empty); 
// text == "Leoben-Simon 

¿Y de qué forma podemos transformar valores de una cadena?

Existen varias transformaciones que podemos hacer. La primera es que podemos insertar caracteres dentro de una cadena, mediante el método Insert. A este método le pasamos un índice y la cadena a insertar.

En segundo lugar, podemos alinear texto al margen izquierdo o margen derecho mediante las funciones PadLeft y PadRight, respectivamente. El primer parámetro es el número de caracteres totales a alinear. El segundo, el carácter con el que queremos llenar la alineación.

Convertir entre mayúsculas y minúsculas también entran en acción. Tenemos a ToLower y ToUpper, por supuesto, para convertir la cadena a puras minúsculas o puras mayúsculas. Una sobrecarga de estos métodos toma como parámetro un objeto CultureInfo, que nos permite especificar el idioma. Si no pasamos cultura, entonces se toma la cultura por default. Si queremos convertir sin importar la cultura, usamos ToLowerInvariant y ToUpperInvariant.

Y ya por último, a veces querremos eliminar espacios en blanco. Esto es muy útil sobre todo cuando trabajamos con bases de datos, que nos llega con espacios en blanco antes y después. Entonces podemos usar el método TrimStart y TrimEnd, para eliminar los espacios al inicio y al final de la cadena, respectivamente. O Trim, que lo hace en las dos al mismo tiempo. Por cierto, si a algún método le pasamos un array de caracteres, dichos métodos los eliminarán antes o después. Básicamente estos tres métodos son el converso de PadLeft y PadRight.

string str = "Todos los pollos van al cielo."; 

str = str.Insert(17, "siempre "); 
// str == "Todos los pollos siempre van al cielo." 
str = str.PadLeft(45); 
// str == " Todos los pollos siempre van al cielo." 
str = str.PadRight(50); 
// str == " Todos los pollos siempre van al cielo. " 
str = str.PadRight(55, '*'); 
// str == " Todos los pollos siempre van al cielo. *****" 
str = str.ToUpper(); 
// str == " TODOS LOS POLLOS SIEMPRE VAN AL CIELO. *****" 
str = str.ToLower(); 
// str == " todos los pollos siempre van al cielo. *****" 
str = str.TrimLeft(); 
// str == "todos los pollos siempre van al cielo. *****" 
str = str.TrimRight('*'); 
// str == "todos los pollos siempre van al cielo. " 
str = str.Trim(); 
// str == "todos los pollos siempre van al cielo."

Y ya para concluir la lista (¡uf!), ¿cómo se convierten cadenas de texto en otros tipos de dato?

La verdad es que String implementa explícitamente la interfaz IConvertible. Esta interfaz permite convertir a otros tipos de datos. Por lo que a partir de una cadena, sólo tienes que obtener la referencia directa a IConvertible y usar el método que apetezcas. Nota que para todos los tipos de datos numéricos aplican cuestiones de formato.

CultureInfo ci = new CultureInfo("es-ES"); 
string str "42,06"; 
IConvertible cv = str as IConvertible; 
double d = cv.ToDouble(ci); 
str = "True"; 
cv = str as IConvertible; 
bool b = cb.ToBoolean(); 
str = "04/12/2012"; 

ci = new CultureInfo("es-MX"); 
cv = str as IConvertible; 

DateTime tm = cv.ToDateTime(ci); 

Esto es quizás un poquito engorroso. Una alternativa quizás un poco más sencilla es utilizar los métodos de conversión (usualmente estáticos) que proveen las clases de los tipos básicos. Por ejemplo, casi todas estas clases tienen dos métodos sobrecargados: Parse y TryParse. Ambas intentan convertir una cadena de texto en su tipo base (por ejemplo, Int32.Parse convierte a int, Double.Parse convierte a double, etc.). La diferencia es que Parse lanza una excepción si no puede realizarse la conversión, y TryParse regresa un valor por default si no puede realizarla.

CultureInfo ci = new CultureInfo("es-ES"); 
string str = "42,06"; 
double d = double.Parse(str, ci); 
int i; 
bool ret = int.TryParse("NoEsNúmero", out i); 
// ret == false y i == 0 
str = "True"; 
bool b = bool.Parse(str 
str = "04/12/2012"; 
ci = new CultureInfo("es-MX"); 
DateTime tm; 
ret = DateTime.TryParse(str, ci); 
// ret == true y tm == 4/12/2012 

Por último, la plataforma también contiene la clase Convert. Esta clase estática tiene muchos métodos para convertir de cualquier tipo de dato a cualquier otro tipo de dato.

CultureInfo ci = new CultureInfo("es-ES"); 

string str = "42,06"; 
double d = Convert.ToDouble(str, ci); // d == 42.06 

int i = Convert.ToInt32("1.983", ci); // i == 1983 
i = Convert.ToInt32("0xFF", 16); // i == 255 
i = Convert.ToInt32("100110000011111001", 2) // i == 155897 

str = "True"; 
bool b = Convert.ToBoolean(str); // b == true; 

str = "04/12/2012"; 
ci = new CultureInfo("es-MX"); 
DateTime tm = Convert.ToDateTime(str, ci); // tm == 4/12/2012

¿Qué nos falta?

Bueno, hasta ahora hemos visto cómo manipular cadenas de texto, y hemos visto mejores prácticas, en particular, utilizando a StringBuilder. Nos falta un tema que es importante, aunque sale un poquito del alcance de esta entrada: los flujos de datos.

¿Qué tienen que ver los flujos de datos?

Primero, un flujo de dato es una secuencia ordenada de bytes. En .NET, la clase que representa a los flujos se llama Stream, ubicada en el espacio de nombres System.IO. De momento no ahondaremos más en el tema de los flujos (puedes consultar la entrada Todo sobre flujo de datos). Pero retomaremos el concepto de leer y escribir datos en los flujos.

¿Qué la clase Stream no tiene métodos para leer y escribir?

Sí, pero de hecho esta clase no escribe sobre un Stream, sino que…

¿Y entonces?

Calma. Estas clases heredan de un par: TextReader y TextWriter. Estas clases base son las usadas para leer y escribir en flujos, y las clases derivadas: StreamReader y StreamWriter nos permiten hacerlo en un flujo. Sin embargo, hay veces en la que nos interesará reutilizar los algoritmos escritos para TextReader y TextWriter, pero para crear cadenas de texto. Ese es el propósito de estas dos clases: StringReader y StringWriter.

Ambas clases guardan todo el flujo dentro de un… ajam… StringBuilder. Así, podemos usarlas de esta forma.

StringWriter w = new StringWriter(); 
w.WriteLine("Todos los pollos van al cielo."); 
w.Write("Un número: "); 
w.WriteLine(42); 
w.Write("Un booleano: "); 
w.WriteLine(false); 
str = w.ToString(); // construye la cadena de texto 
w.Close(); 

StringReader r = new StringReader(str); 

do 

{ 
    str = r.ReadLine(); 
    Console.WriteLine("Ln: {0}", str ?? "EOF"); 
} 
while (str != null); 

r.Close(); 

/* imprime: 
Ln: Todos los pollos van al cielo. 
Ln: Un número: 42 
Ln: Un booleano: false 
*/ 

Realmente usarlas así no aporta mucho, pues podemos usar StringBuilder directamente. Sin embargo, puede servirnos para reutilizar algoritmos de lectura / escritura de datos, sin importar la fuente. Y ahí StringBuilder no nos puede ayudar, me temo.

void CreateHtml(TextWriter w) 

{ 
    w.WriteLine("<html>"); 
    w.WriteLine("<body>"); 
    w.WriteLine("<h1>Todos los pollos van al cielo.</h1>"); 
    w.WriteLine("<p>Lorem ipsum dolor sit amet, qua micas la pater "
        + "santis sempire damore denarii.</p>"); 
    w.WriteLine("</body>"); 
    w.WriteLine("</html>"); 
} 

StreamWriter file = new StreamWriter("C:\\miarchivo.txt"); 
CreateHtml(file); 
file.Close(); 
StringWriter text = new StringWriter(); 
CreateHtml(text); 
Console.WriteLine(text.ToString()); 
text.Close();

Aquí vemos cómo podemos escribir el texto a un archivo o a la consola, reutilizando el mismo método CreateHtml.

¿Y ahora sí ya estuvo?

Ya estuvo. Las cadenas de texto son el alma de todos los programas, pues es como mejor representamos la información. Cuídalas para que no sufras por memoria. Siempre prefiere usar StringReader y StringWriter sobre string, y StringBuilder sobre todas. También revisa las expresiones regulares, otra forma de buscar cadenas de texto, más avanzada.

Nos vemos pronto con más de esta serie: C# 101.

Categorías:.NET Framework, Apunte, C# Etiquetas: , ,

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Algunas colecciones para trabajar con texto


A lo largo de esta serie “Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar” hemos tratado con implementaciones de colecciones y diccionarios que vieron la luz del día en la primera versión del .NET Framework, cuando todavía no había clases genéricas. En aquella época, la norma era que cuando se necesitaba una colección fuertemente tipada el programador creaba una clase que implementara ICollection o IDictionary, internamente utilizara un ArrayList o un Hashtable, y actuara como envoltorio sobre ésta. Por supuesto, con la llegada de Generics en .NET 2.0, ésta práctica comenzó a caer en desuso (por ejemplo, ahora es más común derivar de List<T>, de Collection<T> o Dictionary<TKey, TValue>, pero todavía falta para que veamos Generics en esta serie).

Sin embargo, los propios programadores de .NET crearon una serie de clases para este escenario, sobre todo que utilizaran mucho. Por ejemplo, la clase System.Web.HttpRequest tiene una propiedad llamada QueryString, que se utiliza para acceder a un diccionario en donde la llave y el valor son ambos cadenas de texto.

Así, en este apunte vamos a explorar algunas de las clases que podemos utilizar, en particular, para trabajar con cadenas de texto: StringCollection, StringDictionary y NameValueCollection, todas dentro del espacio de nombres System.Collections.Specialized.

La primera clase es StringCollection. Como seguramente habrás inferido, ésta representa una colección de cadenas de texto, y está particularmente optimizada para trabajar con cadenas de texto. Recordemos que en .NET las cadenas de texto son inmutables, por lo que el empleo de éstas debe realizarse de forma cuidadosa para evitar caer en problemas de rendimiento.

Como cabría esperar, la colección cuenta con varias propiedades estándares, como SyncRoot e IsSynchronized para temas de concurrencia y sincronización en el acceso a elementos; Count para obtener el número de elementos que tiene la colección; IsReadOnly para saber si podemos agregar o eliminar elementos, y finalmente un indexador, a través del cuál podemos acceder a las cadenas de texto dado un índice determinado.

Además, también cuenta con los métodos tradicionales: Add y Remove para añadir o eliminar una cadena determinada; Insert para añadir una cadena en una determinada posición; AddRange para añadir un array de cadenas de texto de un solo golpe, y Clear para remover todas las cadenas existentes; CopyTo para copiar el contenido de la colección a un array unidimensional e IndexOf para obtener el índice de un elemento determinado (o –1 si no encuentra algo).

Realmente es una colección sencilla, y su razón de ser es precisamente ser una colección fuertemente tipada. El siguiente programita muestra un sencillo ejemplo sobre el uso de StringCollection.

using System;
using System.Collections.Specialized;
using System.Linq;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            StringCollection strings = new StringCollection();
            strings.Add("Jetzt laden die Vampire zum Tanz");

            string[] strs = new string[] {
                "Steckt den Himmel im Brand",
                "und streut Luzifer Rosen!",
                "die Welt gehört den Lüngern",
                "und den Rücksichtlosen"
            };
            strings.AddRange(strs);
            strings.Insert(1, "Wir wollen alles und Ganz!");

            if (strings.Contains("Jetzt laden die Vampire zum Tanz"))
            {
                int index = strings.IndexOf("Jetzt laden die Vampire zum Tanz");
                Console.WriteLine(strings[index]);
            }

            foreach (string str in strings)
                Console.WriteLine(str);
            Console.WriteLine();

            var query = from string str in strings
                        where str.Contains("und")
                        select str;
            foreach (string str in query)
                Console.WriteLine(str);

            strings.Clear();

            Console.ReadKey(true);
        }
    }
}

StringCollection no cuenta con métodos sofisticados para realizar búsquedas. Sin embargo, nada nos detiene de utilizar algún método en particular, digamos LINQ, como se muestra en las líneas 33 a 37 del ejemplo anterior. A continuación, la salida del programa.

Jetzt laden die Vampire zum Tanz
Jetzt laden die Vampire zum Tanz
Wir wollen alles und Ganz!
Steckt den Himmel im Brand
und streut Luzifer Rosen!
die Welt gehört den Lüngern
und den Rücksichtlosen

Wir wollen alles und Ganz!
und streut Luzifer Rosen!
und den Rücksichtlosen

La segunda clase que me gustaría mostrar es StringDictionary. Así como StringCollection es una implementación similar a ArrayList pero fuertemente tipada, StringDictionary implementa un Hashtable pero fuertemente tipado para emplear cadenas de texto.

Por ende, podemos encontrar métodos similares a los que tenemos en StringCollection: Add y Remove para añadir y quitar pares llave-valor del diccionario, ContainsKey y ContainsValue para saber si una llave o un valor están determinadas, y las propiedades Count, Keys y Values para contar el número de elementos, obtener todas las llaves y todos los valores, respectivamente.

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            StringDictionary dic = new StringDictionary();
            dic.Add("Chorus 1", "Steckt den Himmel in Brand");
            dic.Add("Chorus 2", "und streut Luzifer Rosen!");
            dic.Add("Chorus 3", "die Welt gehört den Lüngern");
            dic.Add("Chorus 4", "und den Rücksichtlosen");

            Console.WriteLine("Chorus 1: {0}", dic["Chorus 1"]);
            Console.WriteLine();

            foreach (DictionaryEntry entry in dic)
                Console.WriteLine("{0}: {1}", entry.Key, entry.Value);

            foreach (string key in dic.Keys)
                Console.WriteLine(key);

            Console.ReadKey(true);
        }
    }
}

El programa anterior genera esta salida.

Chorus 1: Steckt den Himmel in Brand

chorus 3: die Welt gehört den Lüngern
chorus 2: und streut Luzifer Rosen!
chorus 4: und den Rücksichtlosen
chorus 1: Steckt den Himmel in Brand
chorus 3
chorus 2
chorus 4
chorus 1

Nota que en la salida, cuando pintamos las llaves, nos regresa un valor en minúsculas, cuando nosotros las añadimos en mayúsculas. Esto es importante, dado que internamente StringDictionary no distingue entre mayúsculas y minúsculas para las llaves, una característica a tener en cuenta.

Finalmente, tenemos la colección NameValueCollection. En la práctica, ésta se comporta de forma muy similar a StringDictionary: nos permite agrupar pares llave-valor. Pero esta clase es una colección, no un diccionario, y ahí radica la principal diferencia. Por principio, NameValueCollection nos permite acceder a los elementos vía un índice, además de la llave. Además cuenta con constructores un poco más versátiles que nos permiten especificar el comparador (si es sensible a mayúsculas y minúsculas, por ejemplo).

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            NameValueCollection col = new NameValueCollection();
            col.Add("Chorus 1", "Steckt den Himmel in Brand");
            col.Add("Chorus 2", "und streut Luzifer Rosen!");
            col.Add("Chorus 3", "die Welt gehört den Lüngern");
            col.Add("Chorus 4", "und den Rücksichtlosen");

            Console.WriteLine("Chorus 1: {0}", col["Chorus 1"]);
            Console.WriteLine();

            foreach (string key in col.AllKeys)
                Console.WriteLine(key);
            Console.WriteLine();

            for (int i = 0; i < col.Count; i++)
                Console.WriteLine("{0}: {1}", col.GetKey(i), col.Get(i));

            Console.ReadKey(true);
        }
    }
}

Su salida:

Chorus 1: Steckt den Himmel in Brand

Chorus 1
Chorus 2
Chorus 3
Chorus 4

Chorus 1: Steckt den Himmel in Brand
Chorus 2: und streut Luzifer Rosen!
Chorus 3: die Welt gehört den Lüngern
Chorus 4: und den Rücksichtlosen

Notarás que la forma de emplear esta clase difiere un poquito de StringDictionary, pero se obtiene prácticamente el mismo resultado.

NameValueCollection se utiliza en muchos lugares del .NET Framework, como en los parámetros de consulta de una página web o bien para leer archivos de configuración. De ahí la importancia que tiene.

¿Cómo saber cuándo usar NameValueCollection y StringDictionary? En esencia, la respuesta se base en si necesitas tener una colección (ICollection) o un diccionario (IDictionary) respectivamente. Aunque se ha de reconocer que NameValueCollection es un poquitín más versátil de StringDictionary.

Ya para terminar, me gustaría que leyeras este pequeño artículo sobre cómo leer la configuración de la aplicación en un archivo web.config, que se encuentra en MSDN, para que veas una aplicación más de NameValueCollection.

Jetzt laden die Vampire zum Tanz, wir wollen alles und ganz!

Categorías:.NET Framework, C#, Tutorial Etiquetas: , ,

Codificación de texto en .NET


Dado que últimamente, con eso de los flujos de datos y las expresiones regulares, he hablado mucho de texto, creo que valdría la pena hacer una entrada sobre la forma en la que podemos codificar nuestras cadenas de texto. Comencemos.

Cualquier tipo de dato en cualquier lenguaje no es más que un conjunto de bytes que se representan de una u otra forma. Así, cuando hablamos de caracteres hacemos referencia a un número natural que representa cada caracter.

La codificación hace referencia a qué número corresponde a qué caracter. Obviamente el conjunto de caracteres disponibles implica el tamaño en bytes. Por ejemplo, siete bits nos permite asignar hasta 128 caracteres, mientras que un byte nos permite asignar 256. Entre más caracteres queramos representar, más bytes ocuparemos por caracter.

Una de las primeras codificaciones que existieron es la famosa ASCII (American Standard Code for Information Interchange). ASCII asigna un caracter  a bytes de siete bits desde el 0 al 127. Estos caracteres incluyen el alfabeto inglés, números, signos de puntuación (inglesa;  no esperes encontrar caracteres como “¿”) y alguno que otro caracter especial.

Sin embargo, ASCII es una codificación que no sirve si quieres escribir texto en otro idioma que no sea el inglés. Para subsanar este problema, se creó la codificación ASCII extendida, que utiliza bytes de ocho bits, con posibles valores que varían del 0 al 256. De estos 256 caracteres, los primeros 128 (es decir, del 0 al 127) quedaron exactamente igual que el ASCII original, y los 128 restantes se utilizaron para representar caracteres de otros idiomas.

Para ello, se creó el término de código de página. Éste define los caracteres 128 al 255 que se utilizan, y por supuesto, existen diferentes según la región del planeta en la que uno se encuentre. Por ejemplo, el código de página 28591 Western European (ISO) contiene los caracteres (del 128 al 255) más comunes utilizados en los alfabetos europeos, que se basan en el alfabeto romano. En contraparte, el código de página 28597 Greek (ISO) contiene caracteres del alfabeto griego. Y así sucesivamente: existen códigos de página para el chino, el japonés, el árabe, el hebreo, el alfabeto cirílico y otros.

El problema con los códigos de página es que aunque tenemos más caracteres disponibles todavía no se abarcan todos. Así, un texto creado en China (osea, con código de página chino) es muy probable que no se vea en sistemas operativos que solo tengan instalado el Western European. Más aún, para algunos lenguajes como el chino o el árabe, los 128 caracteres que provee ASCII extendido son insuficientes.

En aras de solventar este problema fue que se creó otro tipo de codificación, llamado Unicode. Unicode es, en esencia, un código de página masivo que contiene decenas de miles de caracteres que soportan la mayoría de los lenguajes y alfabetos: latino, griego, cirílico, hebreo, árabe, chino y japonés.

Para ello, Unicode define varios tipos de codificación, llamados UTF (Unicode Transformation Format):

  • UTF-32. Esta codificación representa caracteres como números enteros de 32 bits, lo cual nos da un rango de hasta 4,294,967,296 posibles caracteres. Evidentemente, UTF-32 es el más amplio y abarca prácticamente cualquier caracter y signo utilizado por la humanidad. El problema es que ocupa cuatro bytes por caracter, y por ende cuatro veces más memoria que ASCII.
  • UTF-16. Aunque no abarca tantos caracteres como UTF-32, ésta codificación soporta hasta 65536 caracteres, suficientes para abarcar la mayoría de los caracteres y símbolos de los lenguajes más utilizados.
  • UTF-8. Es la codificación más sencilla, y equivale al ASCII extendido de 256 caracteres.
  • Para trabajar con estas codificaciones, .NET pone a nuestra disposición las clases equivalentes a los UTFs mencionados más la codificación ASCII. Por defecto, el .NET Framework utiliza UTF-16 (aunque a veces utiliza UTF-8 de forma interna), de tal suerte que en la mayoría de las ocasiones no es necesario especificar el tipo de codificación a emplear para una cadena de texto. Aún así, si quieres trabajar con diferentes codificaciones, puedes utilizar las clases contenidas en el espacio de nombres System.Text de la siguiente forma.

  • Para UTF-32, utiliza la clase UTF32Encoding.
  • Para UTF-16, utiliza la clase UnicodeEncoding.
  • Para UTF-8, utiliza la clase UTF8Encoding.
  • Para ASCII, utiliza la clase ASCIIEncoding.

Todas estas clases derivan de la clase base Encoding. Ésta, por cierto, contiene algunas propiedades que nos devuelven un objeto que representa las codificaciones mostradas en la lista anterior. Así, Encoding.UTF32, Encoding.UTF8, encoding.UTF7, Encoding.Unicode y Encoding.ASCII se usan para UTF32, UTF8, UTF7, UTF-16 y ASCII, respectivamente. Raramente necesitarás algo más que éstas.

Aún así, uno puede utilizar Encoding.GetEncoding si necesitas codificaciones extra, o cierto código de página específico. Por ejemplo, Encoding.GetEncoding(“Korean”) regresa una codificación que incluye código de página para caracteres coreanos. En fin, a menos que tengas una razón muy poderosa, no deberías necesitar más que los objetos que devuelven las propiedades estáticas de la clase Encoding.

Ahora bien, ¿para qué querría utilizar la codificación de texto? En principio el .NET Framework se encarga de la mayoría de problemas de texto de forma interna. De hecho la clase String utiliza UTF-16 como codificación por defecto. Hay, sin embargo, razones para las cuales necesitamos utilizar la codificación.

La primera es cuando realizamos una conversión de texto a bytes y viceversa. En efecto, cuando queremos obtener los bytes de una cadena necesitamos saber la codificación de ésta, ya que el tamaño en bytes de cada caracter varía (puede ser uno, dos o cuatro bytes por caracter, por ejemplo). De forma similar, al convertir bytes a texto se necesita la codificación, para saber cuántos bytes ocupa cada caracter. El siguiente ejemplo muestra cómo convertir texto a bytes y viceversa, utilizando UTF-16, y vemos lo que pasa al convertir a UTF-8.

using System;
using System.Text;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static void Main(string[] args)
    {
      string original = "Meine Schwester geht auf die Straße Spandauer";
      Console.WriteLine("Texto original: {0}", original);

      int count = Encoding.Unicode.GetByteCount(original);
      Console.WriteLine("El texto mide {0} bytes en UTF-16", count);
      count = Encoding.UTF8.GetByteCount(original);
      Console.WriteLine("El texto mide {0} bytes en UTF-8", count);

      byte[] buffer = Encoding.Unicode.GetBytes(original);
      string utf16 = Encoding.Unicode.GetString(buffer, 0, buffer.Length);
      string utf8  = Encoding.UTF8.GetString(buffer, 0, buffer.Length);

      Console.WriteLine("Decodificado en UTF-16: {0}", utf16);
      Console.WriteLine("Decodificado en UTF-8: {0}", utf8);

      Console.ReadKey(true);
    }
  }
}

Al ejecutarse, este código muestra lo siguiente en la consola:

Texto original: Meine Schwester geht auf die Straße Spandauer
El texto mide 90 bytes en UTF-16
El texto mide 46 bytes en UTF-8
Decodificado en UTF-16: Meine Schwester geht auf die Straße Spandauer
Decodificado en UTF-8: M e i n e   S c h w e s t e r   g e h t   a u f
d i e   S t r a ? e   S p a n d a u e r

Notarás, en primera instancia, que el tamaño en bytes de UTF-16 duplica al de UTF-8, lo cual era de esperarse. Nota también cómo al escribir el texto decodificado en UTF-8 tenemos la palabra Stra?e en lugar de Straße, lo cual también es obvio porque UTF-8 no reconoce ese valor. Y finalmente, notarás que la decodificación UTF-8 pone espacios en blanco entre cada letra. Esto es así porque el texto original utiliza dos bytes por cada caracter. Sin embargo, al decodificar en UTF-8, asume un caracter por un byte, por lo que un caracter UTF-16 lo interpreta como dos caracteres UTF-8.

Aparte de la conversión entre texto y bytes, otro uso importante es al momento de leer y escribir en archivos. Cuando escribimos binario, no nos importa mucho, pero cuando queremos escribir puro texto, la codificación es importante. Por defecto, la codificación utilizada es UTF-8 (ya que es la codificación estándar de Windows). Si en lugar de utilizar FileInfo utilizamos StreamReader y StreamWriter directamente, sus constructores nos permiten especificar el tipo de codificación, aunque StreamReader por defecto checa el texto y determina la codificación del archivo.

En el siguiente ejemplo escribimos a un archivo utilizando UTF-16 y lo leemos utilizando UTF-8.

using System;
using System.IO;
using System.Text;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static void Main(string[] args)
    {
      string original = "Meine Schwester geht auf die Straße Spandauer";
      string file = @"C:\users\fgomez\utf16test.txt";

      StreamWriter writer = new StreamWriter(file, false, Encoding.Unicode);
      writer.Write(original);
      writer.Close();

      StreamReader reader = new StreamReader(file, Encoding.UTF8, false);
      string text = reader.ReadToEnd();
      reader.Close();

      Console.WriteLine(original);
      Console.WriteLine(text);

      Console.ReadKey(true);
    }
  }
}

Al ejecutar este programa obtenemos el siguiente resultado.

Meine Schwester geht auf die Straße Spandauer
??M e i n e   S c h w e s t e r   g e h t   a u f   d i e   S t r a ? e
S p a n d a u e r

Al escribir en UTF-16, el archivo de texto coloca un par de bytes (llamados “preámbulo”) que indica la codificación que se utilizó (y por ello StreamReader puede determinar que se usó UTF-16), y por ello se muestra ?? cuando lo leemos con UTF-8, ya que esta codificación no supo cómo interpretar dichos bytes.

Bueno, ya para finalizar este apunte, y para poder irme a dormir (porque para variar ya casi son las tres de la mañana), me gustaría mencionar algunos miembros importantes de la clase Encoding. Ya vimos en los ejemplos que Encoding.GetByteCount nos regresa el número de bytes dada una cadena de texto, para la respectiva codificación. Por otra parte, Encoding.GetBytes hace la conversión propiamente dicha de un texto a bytes. En contraparte, GetString te convierte los bytes en cadena de texto. Una propiedad estática importante es Convert, que convierte un array de bytes de una codificación a otra (especificadas en el primer y segundo parámetro, respectivamente). Por otra parte, tenemos a Encoding.GetPreamble, que nos devuelve la marca que identifica un archivo de texto para esa codificación (recuerda las ?? del ejemplo anterior).

Algunas propiedades útiles son Encoding.EncodingName, que nos devuelve el nombre (Unicode, ASCII, UTF-32, etc.) del codificador empleado. Encoding.BodyName nos devuelve el nombre de la codificación en formato entendible para navegadores web. IsBrowserDisplay e IsMailNewsDisplay nos indican si una codificación puede ser desplegada por un navegador web, y por los clientes de correo y noticias. Y finalmente, Encoding.WindowsCodePage nos devuelve el equivalente código de página de la codificación en cuestión.

Categorías:.NET Framework, Apunte, C# Etiquetas:

Cuestionario de expresiones regulares: 2. creación de expresiones


En la primera parte de este cuestionario revisamos como manipular expresiones regulares; es decir, ya que contamos con el texto de una expresión, cómo usamos .NET Framework para manipular los resultados. Pero then again, no hemos visto cómo crear las expresiones que .NET evaluará.

Crear estas expresiones es un tema complicado y muy extenso, pero trataremos de ver los símbolos más utilizados. Prosigamos.

¿Cómo hago concordar texto simple?

El patrón para el texto simple es… simple. Si quiero encontrar todas las cadenas que contengan “fer”, el patrón es ese tal cual: “fer”.

string pattern = @"fer";

Regex regex = new Regex(pattern);
string text = "fernando";
Match match = regex.Match(text);
if (match.Success)
  Console.WriteLine(match.Value);
else
  Console.WriteLine("No hay concordancias.");

Aquí, “fernando” contiene el patrón “fer”, por lo que concuerda con el texto fuente.

¿Y que tal si quiero buscar al inicio o al final del texto?

Para concordar texto al inicio, se utiliza el símbolo ^, mientras que $ se utiliza para concordar al final. Así, “^fer” concordará con “fernando”, pero “fer$” no lo hará. Para que concuerde al inicio y al final, pues combinamos ambos: “^fer$” buscará cadenas que concuerden únicamente con “fer”.

¿Cómo puedo concordar con el inicio o fin de una palabra?

Los símbolos ^ y $ sirven para concordar con el inicio o fin del texto, pero si queremos buscar en inicio o fin de palabras intermedias no nos sirve. Supongamos que dado un texto quiero buscar palabras que comiencen con “fer”. En este tenor, “mi nombre es fernando gómez” y “es un día feriado” concordarían, mientras que “una categoría inferior”.

Para ello, utilizamos \b (así con la diagonal invertida) antes del patrón, para hacer la concordancia al inicio de la palabra, o después del patrón para concordar al final. El patrón para el ejemplo anterior sería “\bfer”.

string pattern = @"\bfer";
Regex regex = new Regex(pattern);
string text = "mi nombre es fernando gómez";
Match match = regex.Match(text);
if (match.Success)
  Console.WriteLine(match.Value);

Lo contrario a lo anterior, es decir buscar una concordancia que no esté al inicio o al final de una palabra, se logra utilizando de forma similar \B. Así, “\Bfer” concuerda con “categoría inferior”, pero no con “mi nombre es fernando”.

¿Cómo puedo concordar puros dígitos?

Si queremos hacer una búsqueda por puros números (es decir, del 0 al 9) utilizamos \d. En contraparte, podemos utilizar \D si queremos buscar cualquier caracter menos un dígito. Así, el patrón “\d\d” (dos dígitos consecutivos) concuerda con “tengo 27 años” (y la concordancia regresa “27”, obviamente).

¿Y qué pasa con los caracteres en blanco?

Similar a lo anterior, tenemos a \s para concordar con espacios blancos (espacios, tabs, salto de líneas, etc.), mientras que \S concuerda con cualquier caracter que no sea espacio blanco.

¿Cómo se identifican las palabras?

Una palabra, en el contexto de expresiones regulares, se define como un conjunto de caracteres consecutivos que puede ser cualquier combinación de letras mayúsculas (de la A a la Z), minúsculas (de la a a la z), dígitos (del 0 al 9) y el caracter de guión bajo _. Si queremos concordar un caracter de una palabra (es decir, A-Z, a-z, 0-9 ó _ ) utilizamos \w y en contraparte, \W para cualquier caracter que no pertenezca a una palabra.

¿Y qué pasa si quiero concordar varios dígitos, caracteres en blanco o de palabras?

Por ejemplo, ¿qué pasa si quiero concordar cinco dígitos? Por supuesto, podría escribir “\d\d\d\d\d”, pero esto es poco práctico (¿qué pasaría si en lugar de cinco fueran veinte?). Para solventar este problema existe la expresión {n}, donde n es un número natural que repite n veces la expresión inmediata anterior. Así, \d{5} concordaría cinco dígitos, \w{3} concordaría 3 caracteres de palabra y \s{7} serían siete espacios en blanco.

Más aún, si agregamos una coma después del número, esto es {n,}, indicamos que queremos concordar al menos n caracteres. Así, \d{5,} concordaría con al menos cinco dígitos (por ejemplo, “12345678”).

Y si incluimos algún número después de la coma, es decir {n,m} (con n <= m) indicamos que queremos concordar al menos n y a lo más m caracteres. Así, \w{3,10} concordaría con caracteres de palabra (recordemos: A-Z, a-z, 0-9 y _ ) que midan tres y hasta diez caracteres de longitud.

Por cierto, existe el símbolo especial ? que concuerda la subexpresión anterior cero o una vez, y evidentemente es equivalente a {0,1}. Por ejemplo, \d? concuerda con cero o un dígito, y es equivalente a \d{0,1}. Es solo que {0,1} es tan empleado que se le asignó un símbolo especial.

De igual forma, podemos representar {0,} (es decir, cero concordancias en adelante) con el símbolo *. Así, f\w*r concuerda con fr, fer y fabricar.

Y ya entrados en gastos, también existe abreviación para {1,}, es decir, una o más concordancias, y se utiliza el símbolo +. Así, f\w+r concuerda con fer y fabricar, pero no con fr.

¿Cómo podría concordar con algún conjunto de caracteres?

Supongamos que queremos buscar un conjunto de caracteres explícitamente especificado. Por ejemplo, supongamos que queremos concordar las vocales a, e, i. ¿Cómo se puede indicar?

Los conjuntos se forman con corchetes y poniendo adentro los caracteres que nos interesan. Por ejemplo, [aei] busca una a, una e o una i, pero no busca ni la o ni la u. Igual podemos mezclarlo con otros caracteres. Por ejemplo, [cpv]elo concuerda con celo, pelo y velo, pero no concuerda con lelo.

¿No hay otra forma de especificar un conjunto de caracteres?

Digo, si queremos concordar, por ejemplo, que una cadena tenga puras letras minúsculas, ni modo de escribir [abcdefghijklmnñopqrstuvwxyz]. Digo, es válido, pero qué hueva. En lugar de eso, podemos especificar rango de caracteres poniendo un guión entre los caracteres (o dígitos) que funjan como cotas. El ejemplo anterior quedaría reducido a [a-z]. Por supuesto, [A-Z] concordaría un texto con puras mayúsculas.

Y aún podemos especificar varios rangos en el mismo conjunto. Por ejemplo, [A-Za-z] concordaría cualquier caracter con mayúsculas y minúsculas, pero sin dígitos.

En base a esta definición, podríamos decir que \w equivale a [A-Za-z0-9_].

¿De qué forma especifico que se pueda concordar una opción u otra?

Por ejemplo, puedo querer concordar todos los caracteres que sean 0 o 1. Es decir, dar una serie de opciones sobre las que elegir.

Para lograr lo anterior, utilizamos una barra vertical | para separar cada una de las opciones. Por ejemplo, dado [0|1]+ y buscando 99901, obtendría como concordancia 01 y se desecharía el 999.

¿Hay alguna forma de concordar cualquier caracter especial o no especial?

A veces podemos estar haciendo validaciones sobre texto que está escrito en otro idioma que no es nuestro español. Y a veces no disponemos en nuestro teclado de caracteres especiales de otros idiomas. Por ejemplo, si el texto está en alemán quizás queramos concordar el caracter especial “scharfes s”, ß, que seguramente estará incluido en teclados alemanes pero no en español (ni siquiera en inglés).

Por ejemplo, si queremos buscar en un texto palabras que contengan la scharfes s, podríamos recurrir al mapa de caracteres de Windows y escribir la expresión regular \w*ß\w*. Así, “essen” (comer) no concordaría, pero “Straße” (calle) sí que lo haría.

Sin embargo, podemos representar cualquier caracter a través de su código Unicode. Para ello, empleamos \uNNNN, donde NNNN es el código Unicode del caracter. Así, dado que ß tiene como valor Unicode 00DF (en hexadecimal), podemos reescribir la expresión regular como \w*\u00DF\w* y listo, “Straße” concordaría sin problemas.

Esta técnica es especialmente útil cuando uno trata con caracteres especiales. Por ejemplo, \u00A9 concordaría el símbolo de copyright © mientras que \u20AC concordaría con el símbolo del euro €.

¿De qué forma podemos obtener el conjunto de caracteres contrario a una expresión dada?

Supongamos que tenemos la expresión [0-9], que quiere decir cualquier dígito entre el cero y el nueve. Y supongamos que queremos expresar: cualquier caracter que no esté entre el 0 y el nueve. Para lograr esto, utilizamos el símbolo ^ dentro de los corchetes. Si lo especificamos fuera, indicamos que la expresión tiene que concordar al inicio de la cadena. Así, nuestro ejemplo quedaría como [^0-9], lo cual, dicho sea de paso, es equivalente a \D. De igual forma, \W es equivalente a [^A-Za-z0-9_].

¿Qué es un grupo?

Un grupo es una construcción que nos permite capturar subexpresiones dentro de una expresión regular. Por ejemplo, podemos crear una expresión regular sobre la cual queramos buscar los diferentes componentes que contenga, o bien aplicar algún modificador (como * o ? o +) sobre una subexpresión.

Por ejemplo, pensemos que queremos que una cadena de texto termine en “.com” o “.net”. tendríamos que especificar \w+ seguido de \. para indicar al punto, y com|net para especificar las opciones com ó net. Pero queremos asegurarnos que .com o .net aparezcan forzosamente. Esto lo logramos con ?, pero ¿cómo aplicarlo tanto para el punto como para el com|net? La solución es agrupar. Para hacerlo, simplemente ponemos la subexpresión entre paréntesis. Así, la expresión regular para nuestro ejemplo anterior sería \w+\.(com|net) la cual concordaría con “gmail.com” y “gmail.net”, pero no con “gmail.mx”. Nota que ya no fue necesario el ? porque con agrupar com|net fue más que suficiente.

La otra opción para los grupos es identificar los componentes de las subexpresiones. Pensemos una versión sencilla (y ciertamente no muy rigurosa) de una expresión regular que valide un correo electrónico estándar. Este estará formado por un conjunto de caracteres de palabra, seguido de una arroba, seguido de otro conjunto de caracteres palabra, seguido de un punto y una palabra de dos o tres letras. Algo como: fernando@gmail.com. Una expresión regular que cumpla con esto podría ser: \w+@\w+\.[A-Za-z]{2,3} donde:

  • \w+ – una palabra de por lo menos un caracter de longitud.
  • @ – concuerda exactamente con la arroba.
  • \w+ – otra palabra de por lo menos un caracter de longitud.
  • \. – el caracter punto (nota la diagonal invertida: tenemos que escapar el caracter).
  • [A-Za-z]{2,3} – representa cualquier palabra de dos o tres caracteres con mayúsculas o minúsculas: por ejemplo, “com” o “mx”.

Dado este sencillo ejemplo, fernando@gmail.com y fernando@unam.mx concuerdan con nuestro patrón. De esta forma, ya podríamos hacer una validación sencilla de un correo electrónico.

Sin embargo, supón que tu jefe te dice: bien, ahora quiero que me digas cuál es el nombre de usuario y cuál es el dominio. De pronto, el mundo se viene encima. ¿Cómo podríamos obtener el dominio y el usuario? Con grupos.

.NET Framework provee una forma de acceder a los grupos, ya sea por nombre o por ubicación (contando de izquierda a derecha, siendo el primer grupo siempre la expresión completa). Así, podríamos escribir nuestra expresión como (\w+)@(\w+\.[A-Za-z]{2,3}). Nota que creamos dos grupos: (\w+) nos da el grupo correspondiente al nombre de usuario (es decir, lo que va antes de la arroba) mientras que el grupo (\w+\.[A-Za-z]{2,3}) nos da el dominio, así sin más. De esta forma, tenemos que el primer grupo es la expresión completa (siempre el primer grupo es toda la expresión regular, siempre siempre siempre), el segundo grupo nos da el usuario y el tercero el dominio. Ahora solo tenemos que usar las clases de .NET para obtener el segundo y tercer grupo, y listo: problema resuelto.

¿Y cómo obtengo los grupos usando las clases en .NET?

Muy fácil. La clase Match contiene la propiedad Groups, de tipo GroupCollection. Basta con obtener, en el ejemplo de la pregunta anterior, el segundo y tercer elemento de la colección.

string pattern = @"(\w+)@(\w+\.[A-Za-z]{2,3})";

Regex regex = new Regex(pattern);
string text = "fernando@gmail.com";
Match match = regex.Match(text);
if (match.Success)
{
  Console.WriteLine(match.Value);
  string user = match.Groups[1].Value;
  string domain = match.Groups[2].Value;
  Console.WriteLine("Usuario: {0}", user);
  Console.WriteLine("Dominio: {0}", domain);
}
else
  Console.WriteLine("No hay concordancias.");

Este ejemplo muestra lo siguiente en pantalla:

fernando@gmail.com
Usuario: fernando
Dominio: gmail.com

¿Ves qué sencillo?

¿Por qué no hablaste de los grupos en la primera parte del cuestionario, cuando vimos las clases de .NET para las expresiones regulares?

Bueno, cierto que se suponía que la primera parte del cuestionario trataría código y la segunda, las expresiones como tal. Sin embargo, el problema es que los grupos son, a final de cuentas, parte de las expresiones, y no tenía mucho sentido hablar de la clase Group y la propiedad Match.Groups si no sabías antes qué es un grupo y para qué sirve. Y eso solo lo podía explicar cuando tratara las expresiones. De hecho, dada su importancia, me vi tentado a crear una tercera parte del cuestionario para tratar el tema de los grupos, pero la verdad es que, fuera de lo expuesto en las dos preguntas anteriores, no hay mucho más que tratar. Bueno, quizás solo el hecho de que puedes asignar un nombre a un grupo…

¿Uh? ¿Cómo está eso de que puedes nombrar un grupo?

Y sí, es posible. Imagínate una expresión con muchos grupos. Sería un tedio buscar los grupos por su índice. Una forma más fácil sería hacer:

string user = match.Groups["Usuario"].Value;

Para ello, evidentemente habríamos de asignar al grupo 1 el nombre de “Usuario”…

¿Y cómo diantres hago eso?

Ah, sencillo. Después del paréntesis, pones ?<nombre>, donde nombre es… ahm… el nombre del grupo…

Solo que aguas. El nombre no debe contener ningún signo de puntuación ni empezar con algún número. Así, la expresión (\w+)@(\w+\.[A-Za-z]{2,3}) la podríamos reescribir como:

(?<Usuario>\w+)@(?<Dominio>\w+\.[A-Za-z]{2,3})

y ahora sí, match.Groups contendrá tres grupos: el primero, sin nombre, que contiene toda la expresión regular. El segundo será llamado “Usuario” y el tercero, “Dominio”. Podemos reescribir el código de ejemplo de la siguiente forma:

string pattern = @"(?<Usuario>\w+)@(?<Dominio>\w+\.[A-Za-z]{2,3})";

Regex regex = new Regex(pattern);
string text = "fernando@gmail.com";
Match match = regex.Match(text);
if (match.Success)
{
  Console.WriteLine(match.Value);
  string user = match.Groups["Usuario"].Value;
  string domain = match.Groups["Dominio"].Value;
  Console.WriteLine("Usuario: {0}", user);
  Console.WriteLine("Dominio: {0}", domain);
}
else
  Console.WriteLine("No hay concordancias.");

y obtendríamos el mismo resultado. Y eso es todo lo que tengo que decir de los grupos.

¿Seguro es todo o nada más te haces güey?

Osh, bueno (don|doña) perfect(o|a), por supuesto que no es todo, hay muchas cosas más que decir. Pero no puedo decirlo todo en un simple cuestionario, y menos aún en un blog. Como he mencionado, se pueden escribir libros enteros sobre expresiones regulares. Pero a final de cuentas, esto que mencioné ha sido lo más importante.

Si tienes más dudas sobre grupos, te recomiendo que leas el artículo construcciones de agrupamiento en la documentación de MSDN.

Y colorín colorado…

…este cuestionario ha terminado.

Espero que este cuestionario de dos partes te sirva, por lo menos, para introducirte al fascinante, potente y complejo mundo de las expresiones regulares. Por supuesto, hay varios temas que dejé fuera, por lo que te recomiendo ampliamente que no dejes de echarle un vistazo a la documentación de MSDN: elementos del lenguaje de expresiones regulares.

Lo que sigue depende de tí. Evidentemente hay que practicar mucho y repasar estos temas seguido, ya que siendo un tema complejo luego se nos puede olvidar algún aspecto. Bueno, al menos yo sí tengo que regresar a la documentación de forma seguida para refrescar la memoria.

Y bueno, ahora sí a dormir, que nuevamente termino una entrada en el blog a las tres de la mañana. Con suerte, si esto te sirve de algo, habrá valido la pena.

Auf wiedersehen!

Categorías:.NET Framework, C#, Cuestionario Etiquetas:

Cómo ofuscar nuestras contraseñas


Hola queridos amantes de la programación. En estos momentos me encuentro escribiendo la segunda parte del cuestionario sobre expresiones regulares. Pero no quería dejar pasar otro día sin compartir este pequeño consejo.

Sucede que cuando trabajamos en procesos de autenticación para nuestras aplicaciones muchas veces contamos con una tabla en una base de datos donde se guarda la información del usuario, así como su contraseña. Es una muy mala idea dejar las contraseñas así tal cual ya que si un atacante accede a nuestra base de datos, tendrá las contraseñas de todos los usuarios para hacer y deshacer el sistema a su antojo. Si bien el método más seguro sería encriptarlas (simétrica o asimétricamente) también es cierto que ésto conlleva complejidad ya que se requieren claves de encriptación que los usuarios deben conocer, o bien que deben ser estáticas y guardarse en el sistema de alguna forma. Así, en ocasiones no es posible realizar encriptación de las contraseñas. Pero debemos hacer un intento por ofuscarlas, cuando menos, para que sea difícil para el atacante obtenerlas.

Una primera aproximación para ello es codificar texto a base 64. Pero seamos honestos, esto sería muy ingenuo. Cualquiera que vea un texto con caracteres ininteligibles reconocerá el base 64 si ve caracteres = ó == al final de la cadena.

Pero podemos utilizar un algoritmo hash. .NET Framework, en su espacio de nombres System.Security.Cryptography nos provee de algunos algoritmos, como el MD 5 (clase MD5CryptoServiceProvider), el MD160 (RIPEMD160Managed), el SHA1 (SHA1CryptoServiceProvider) o el SHA256 (SHA256Managed). La forma de llevar a cabo esta ofuscación es la siguiente.

  1. Crear un objeto que represente algún algoritmo de hash.
  2. Guardar la contraseña como un array de bytes.
  3. Llamar al método ComputeHash del objeto del algoritmo y guardar el valor de retorno en lugar de la contraseña plana.
  4. Opcionalmente, la propiedad Hash de dicho objeto contendrá el hash utilizado, por si lo necesitas para algo más.

Como detalle adicional, dado que ComputeHash regresa un array de bytes, podemos convertirlo a su representación de texto en base 64. Ya no nos importa que sepan que es base 64 porque de todas formas estará hasheado, y el atacante no sabrá por dónde ir. Y si lo sabe, le tomará unos cuantos años averiguar la contraseña.

El siguiente ejemplo muestra lo anterior a través de un método que toma una contraseña, la ofusca y regresa su representación en base 64.

using System;
using System.Security.Cryptography;

static string ObfuscatePassword(string password)
{
  MD5 hash = new MD5CryptoServiceProvider();
  byte[] passwordBuffer = Encoding.UTF8.GetBytes(password);
  byte[] hashedPassword = hash.ComputeHash(passwordBuffer);
  string obfuscatedPassword = Convert.ToBase64String(hashedPassword);

  return obfuscatedPassword;
}

En la línea 6 creamos un objeto de algoritmo hash MD5, de los más básicos. Línea 7, codificamos la contraseña y la transformamos en bytes. Siguiente línea, aplicamos el hash sobre dichos bytes, y línea 9, convertimos dichos bytes a base 64.

Luego, cuando queramos validar al usuario solo tenemos que ofuscar la contraseña provista y compararla contra el valor previamente ofuscado: una simple comparación de texto bastará, dado que dos textos iguales regresan el mismo hash. Sirva el siguiente programa de ejemplo.

using System;
using System.Text;
using System.Security.Cryptography;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static string ObfuscatePassword(string password)
    {
      MD5 hash = new MD5CryptoServiceProvider();
      byte[] passwordBuffer = Encoding.UTF8.GetBytes(password);
      byte[] hashedPassword = hash.ComputeHash(passwordBuffer);
      string obfuscatedPassword = Convert.ToBase64String(hashedPassword);

      return obfuscatedPassword;
    }

    static void Main(string[] args)
    {
      string originalPassword = ObfuscatePassword("P@ssw0rd_123");
      Console.WriteLine("Hash de la contraseña original: {0}", 
          originalPassword);
      Console.WriteLine();

      ConsoleKey key = ConsoleKey.Q;
      do
      {
        Console.WriteLine();
        Console.WriteLine("Ingrese contraseña: ");
        
        string newPassword = ObfuscatePassword(Console.ReadLine());
        Console.WriteLine("Hash de la contraseña actual: {0}", newPassword);

        if (originalPassword == newPassword)
          Console.WriteLine("Acceso concedido...");
        else
          Console.WriteLine("Accesso denegado...");
        
        Console.WriteLine(
          "Presione cualquier tecla para continuar o 'Q' para terminar.");
        key = Console.ReadKey(true).Key;
      }
      while (key != ConsoleKey.Q);
    }
  }
}

Al correrlo, ingreso dos contraseñas inválidas y una tercera válida. Esta es la salida en la consola.

Hash de la contraseña original: lxCD4E8CcaDFA3M7017FwA==


Ingrese contraseña:
sorbetedelimon
Hash de la contraseña actual: 7vbXes/q8UMUylv34xop6A==
Accesso denegado...
Presione cualquier tecla para continuar o 'Q' para terminar.

Ingrese contraseña:
patatús
Hash de la contraseña actual: xpP+vt1fJz5OzX7DDyHyGA==
Accesso denegado...
Presione cualquier tecla para continuar o 'Q' para terminar.

Ingrese contraseña:
P@ssw0rd_123
Hash de la contraseña actual: lxCD4E8CcaDFA3M7017FwA==
Acceso concedido...
Presione cualquier tecla para continuar o 'Q' para terminar.

Eso es todo. Esta forma sencilla puede mejorar la seguridad de tu aplicación. Y también la puedes aplicar para cuando guardes información sensitiva en tus recursos o en tu web.config o app.config o cualquier otro archivo de configuración.

Categorías:.NET Framework, C#, Cómo hacer Etiquetas: ,

Cuestionario de expresiones regulares: 1. manipulación desde .NET


Qué ondas gente, heme aquí de nuevo, listo para comenzar a bloguear, pero ya no a la una de la mañana. Ahora que ya han pasado los festejos por el día de las madres, es momento de volver a nuestro mundo de programación. En estos días estuve pensando en las expresiones regulares. ¿Cuánto código no se encuentra uno, que hace un string.Replace o string.IndexOf para buscar texto dentro de otras cadenas? ¡Cuando una expresión regular simplificaría mucho el código!

La verdad es que las expresiones regulares son un tema difícil. Son difíciles de crear pero más difíciles de leer (si no fuiste el creador). Es un lenguaje críptico y muy propenso a errores, por lo que hay que tener muchísimo cuidado al emplearlo: un mal uso y podemos hasta generar brechas de seguridad en nuestra aplicación…

El tema ciertamente es muy extenso y he visto que hasta hay libros enteros al respecto. Y la verdad es que intentar abarcar todo en una entrada pues no se puede. Más bien habría que hacer un tutorial, pero como ahorita ando con mi tutorial de LINQ, habré de posponerlo un poco.

Sin embargo, no sufran mis estimados colegas, porque he preparado un cuestionario, una guía de preguntas y respuestas sobre expresiones regulares, su uso en .NET y el meta-lenguaje que utiliza. Este cuestionario está, por razones de espacio y tiempo, dividido en dos: el primero trata sobre cómo manipular expresiones regulares desde .NET, mientras que el segundo trata sobre cómo formar los patrones de las expresiones. Sin más preámbulos, comencemos.

 

¿Qué es una expresión regular?

Una expresión regular es un conjunto de caracteres que definen un patrón, el cual puede ser comparado contra una cadena de texto cualquiera en aras de determinar si dicha cadena cumple con los requerimientos de formato. Si la cadena cumple, se puede extraer la porción de texto que concuerda en uno o varios grupos.

¿Cuáles son los elementos de una expresión?

Al texto que funge como patrón se le denomina expresión. Una expresión puede estar constituida por los siguientes elementos.

  • Caracteres de escape. Ciertos caracteres son especiales y tienen un significado concreto. Por ejemplo, \n representa una nueva línea y \t representa un tab.
  • Clases de caracteres. Representa un conjunto de caracteres, como letras, palabras, espacio en blanco e incluso rangos entre letras.
  • Grupos. Es posible agrupar  una expresión en componentes más pequeños, a los que incluso se les puede dar un nombre para referenciarlo posteriormente.
  • Cuantificadores. Especifican las instancias de algún elemento previo (caracteres, grupos, clases de caracteres) que deben estar presentes para que una cadena concuerde con el patrón.
  • Referencias previas. Es posible que una subexpresión ya evaluada pueda ser referenciada subsecuentemente.
  • Construcciones alternas. Permite construir expresiones que permitan acertar una o más posibilidades, de tal suerte que los valores permitidos sean fijos. Por ejemplo, se puede crear un patrón que limite los valores a (sí | no).
  • Substituciones. Una vez que se encuentra un valor que concuerda con el patrón, se puede reemplazar por alguna otra cadena.
  • Misceláneos. Permite añadir comentarios y opciones de pre-procesamiento.
  •  

    ¿Cómo puedo evaluar una expresión regular en .NET Framework?

    Dentro del espacio de nombres System.Text.RegularExpressions existe la clase Regex. Ésta es la que se encarga de hacer las evaluaciones. Para ello, crea una nueva instancia de esta clase y pásale como parámetro tu expresión regular. Luego, utiliza el método IsMatch para evaluar el patrón contra la cadena que se pase como parámetro. Por ejemplo:

    Regex regex = new Regex(@"^\d{5}$");
    bool v1 = regex.IsMatch("fernando");
    bool v2 = regex.IsMatch("99901");
    

    La expresión que pasamos representa una cadena de texto con exactamente cinco caracteres que sean dígitos. Así, v1 será falso y v2 será verdadero.

     

    ¿Cómo puedo extraer la cadena evaluada de una expresión regular?

    Lo primero que hay que hacer es crear la expresión regular. Luego, ejecutamos el método Match de nuestro objeto Regex. Éste método nos devolverá un objeto tipo Match. Si se encontró alguna correspondencia entre la expresión y la cadena a evaluar (pasada como parámetro a Match) entonces la propiedad Match.Success será verdadera, y Match.Value contendrá el primer extracto de la cadena evaluada que concuerda con la expresión regular. Por ejemplo:

    Regex regex = new Regex(@"\d{5}");
    Match match = regex.Match("fer12345nando99901gomez");
    if (match.Success)
    {
      string val = match.Value;
      Console.WriteLine(val);
    }
    

La expresión regular que le pasamos intenta ubicar cualquier secuencia de cinco dígitos. Así, en el ejemplo anterior tenemos que la cadena a evaluar contiene dos secuencias: 12345 y 99901. El objeto Match que obtenemos hace referencia a la primera secuencia encontrada, imprimiendo por ende “12345” en la consola.

 

Pero ¿y si la cadena evaluada tiene más de una concordancia?

El objeto Match que nos regresa Regex.Match contiene un método llamado NextMatch, que lo que hará es regresar otro objeto Match, el cual hará referencia a la siguiente concordancia. Así, podemos llamar a NextMatch hasta que nos devuelva un objeto cuya propiedad Success nos devuelva un false. El siguiente código hace esto mismo a través de un bucle for.

Regex regex = new Regex(@"\d{5}");
string eval = "fer12345nando99901gomez54321flores";
for (Match match = regex.Match(eval); match.Success; match = match.NextMatch())
{
  Console.WriteLine(match.Value);
}

Al ejecutar este código, se imprime en consola “12345 99901 54321”.

 

¿Existe alguna forma de obtener todas las concordancias de una sola vez?

Pues sí, la hay. En lugar de ejecutar el método Match, puedes ejecutar el método Matches, que te regresará un MatchCollection, sobre el cual puedes iterar (digamos, usando un foreach).

Regex regex = new Regex(@"\d{5}");
string input = "fer12345nando99901gomez54321flores";
MatchCollection matches = regex.Matches(input);
foreach (Match match in matches)
{
  Console.Write("{0} ", match.Value);
}

 

¿Qué es un grupo?

Un grupo permite delinear subexpresiones de una expresión regular y capturar las subcadenas de una cadena a evaluar. Los grupos se utilizan para:

  • Hacer que una subexpresión que se repite, concuerde.
  • Aplicar cuantificadores a subexpresiones que tienen múltiples elementos de expresiones regulares.
  • Poder asignar un nombre a alguna subexpresión, y referenciarla posteriormente.
  • Obtener subexpresiones indifivuales.

Una subexpresión  es cualquier patrón contenido dentro de unos paréntesis. Por ejemplo, si a \d{5} lo escribimos como (\d{5}), entonces tendríamos una subexpresión. A cada una de estas se le asigna un número automáticamente, y la clase Match contiene la propiedad Groups, que devuelve un GroupCollection, donde se encuentra cada una de las subexpresiones encontradas, así como sus respectivos valores. Adicionalmente, es posible darle un nombre explícito a un grupo, poniendo el patrón entre “?<” y “>”. Por ejemplo, al grupo (<?CincoDigitos>\d{5}) le hemos dado el nombre “CincoDigitos”.

Regex regex = new Regex(@"(?<CincoDigitos>\d{5})");
string input = "fer12345nando99901gomez54321flores";
MatchCollection matches = regex.Matches(input);
foreach (Match match in matches)
{
  Group group = match.Groups["CincoDigitos"];
  Console.WriteLine(group.Value);
}

 

¿Y para qué cuernos quiero crear y nombrar grupos?

Bueno, dada una expresión regular sencilla como las que hemos manejado, quizás no te interese mucho. Pero son muy útiles cuando tratas con expresiones más complejas. Imagínate que tienes que validar que una cadena de texto tenga un formato de correo electrónico. Entonces tu código luciría de forma semejante al siguiente.

string[] emails = { 
  "fer@e-people.com.mx", 
  "fernando.a.gomez.f@gmail.com", 
  "fer(arroba)algo.com", 
  "fernando.gomez@matematicas.net", 
  "fer@otrodominio"
};
string mailPattern =
  @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)" +
  @"(?<=[0-9a-zA-Z])@))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)" +
  @"+[a-zA-Z]{2,6}))$";

Regex regex = new Regex(mailPattern);
foreach (string email in emails)
{
  if (regex.IsMatch(email))
    Console.WriteLine("{0} es un correo válido. ", email);
  else
    Console.WriteLine("{0} es inválido. ", email);
}

Esa sí que es una expresión enorme e ilegible, y eso que es para algo que nos parecería trivial, como un correo electrónico (¿cuántas aplicaciones no buscan simplemente la arroba y el punto? *sigh*).

Este código funciona (al ejecutarlo, la tercera y quinta dirección aparecen como inválidas), pero ahora imagínate que tu jefe te pide que identifiques cuál es el dominio del correo y cuál es el nombre de usuario. Menudo lío tío, ¿qué le modificarías? Por supuesto, la mejor opción es darle un nombre al grupo que represente a ambos y sanseacabó…

string[] emails = { 
  "fer@e-people.com.mx", 
  "fernando.a.gomez.f@gmail.com", 
  "fer(arroba)algo.com", 
  "fernando.gomez@matematicas.net", 
  "fer@otrodominio"
};
string mailPattern =
  @"^(?("")("".+?""@)|((?<Usuario>[0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)" +
  @"(?<=[0-9a-zA-Z])@))(?<Dominio>(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)" +
  @"+[a-zA-Z]{2,6}))$";

Regex regex = new Regex(mailPattern);
foreach (string email in emails)
{
  Match match = regex.Match(email);
  if (match.Success)
  {
    Console.WriteLine("{0} es válido...", email);

    Group userGroup = match.Groups["Usuario"];
    Group domainGroup = match.Groups["Dominio"];

    Console.WriteLine("+ El usuario es {0}", userGroup.Value);
    Console.WriteLine("+ El dominio es {0}", domainGroup.Value);
  }
  else
  {
    Console.WriteLine("{0} es inválido. ", email);
  }
  Console.WriteLine();
}

Al ejecutar este código, se imprime lo siguiente en la consola.

fer@e-people.com.mx es válido...
+ El usuario es fer
+ El dominio es e-people.com.mx

fernando.a.gomez.f@gmail.com es válido...
+ El usuario es fernando.a.gomez.f
+ El dominio es gmail.com

fer(arroba)algo.com es inválido.

fernando.gomez@matematicas.net es válido...
+ El usuario es fernando.gomez
+ El dominio es matematicas.net

fer@otrodominio es inválido.

Mucho mejor, ¿no?

¿Qué tan eficiente es usar expresiones regulares?

Ah, una pregunta sensata. Las expresiones regulares, a final de cuentas, hacen un análisis sintáctico-semántico del patrón y la cadena a evaluar. Tras bambalinas, se genera un árbol de expresiones, y para efectos prácticos se comporta igual que un compilador, solo que en tiempo de ejecución. Te imaginarás que hacer lo anterior es, efectivamente, costo.

Cuando hacemos una validación sobre una sola cadena el tiempo es despreciable. Pero puede haber ocasiones en las que tengamos que evaluar muchas cadenas (por ejemplo, unas mil direcciones de correo electrónico). En estos casos sí que sentiríamos el tiempo empleado por la expresión regular.

Por suerte, contamos con un mecanismo para optimizar un poco el asunto. Uno de los constructores de la clase Regex nos permite especificar algunas opciones de comportamiento a través de la enumeración RegexOptions. Una de estas opciones es RegexOptions.Compiled. Si especificamos este valor, cuando se cree por primera vez el Regex se compilará a un ensamblado temporal, que será llamado para cada evaluación subsiguiente. Esto, por supuesto, hace que no se genere el árbol de expresiones, reduciendo el costo en recursos. Sin embargo, la primera vez que se evalúe tardará un poco más de lo normal, ya que tendrá que generar el ensamblado. Por eso, esta opción solo deberá emplearse en caso de que una expresión sea evaluada en un número considerable de ocasiones.

Un ejemplo a continuación.

Regex regex = new Regex(@"\d{5}", RegexOptions.Compiled);
string input = "fer12345nando99901gomez54321flores";
MatchCollection matches = regex.Matches(input);
foreach (Match match in matches)
{
  Console.Write("{0} ", match.Value);
}

 

Esta historia continuará…

Bueno, hemos llegado a la primera parte de este cuestionario. Hasta ahorita hemos visto como utilizar expresiones regulares, pero no hemos visto como crearlas. En la segunda parte de este cuestionario trataré cómo formar las expresiones básicas.

Pero ahora ya me tengo que poner a trabajar, así que nos vemos al rato o mañana. Auf wiedersehen!

Categorías:.NET Framework, C#, Cuestionario Etiquetas:

Crear nuestra propia etiqueta [url]


Hoy me vi en la necesidad de crear una columna personalizada para un sitio en SharePoint que estoy desarrollando. SharePoint cuenta con una columna, Hyperlink, que permite guardar enlaces a una dirección web, en un par URL-título. Pero no permite guardar en una sola columna varios de estos pares, por lo que si requiero que mi lista tenga un número variable de enlaces, tengo que crear varias columnas Hyperlink, y eso no está chidito.

Así las cosas, me di a la tarea de crear mi propia columna. Sin embargo, solo puedo guardar texto, así que decidí que lo mejor era guardar los diferentes enlaces en pares URL-título de la siguiente forma:

[url=https://fermasmas.wordpress.com]Blog de Fer++[/url]

 

Este tipo de notación no es nueva, y de hecho se puede encontrar en diversas wikis y blogs. Lo que yo necesito, pues, es poder guardar varios de estos valores, y el control que muestra la columna personalizada parsea las diferentes etiquetas y las muestra en forma de lista.

Si te encuentras en una situación similar, sigue leyendo.

Para resolver este problema, me cree una clase hace precisamente eso. Lo interesante de ésta, por supuesto, es el método Parse, que utiliza una expresión regular para obtener la URl y el título.

La clase en cuestión es la siguiente.

using System;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;

namespace Fermasmas.Wordpress.Com
{
  public class Url : ISerializable
  {
    private string _url;
    private string _title;
    private string _description;
    private static Regex _regex;

    public static readonly Url Empty;

    static Url()
    {
      _regex = new Regex(
           @"\[url=(http|https)://{1}([a-zA-Z0-9/%@?:#&+._=-]*)\](.*?)\[/url\]",
           RegexOptions.Compiled);
      Empty = new Url("http://");
    }

    public Url()
      : this(string.Empty, string.Empty)
    {
    }

    public Url(string url)
      : this(url, string.Empty)
    {
    }

    public Url(string url, string title)
    {
      if (url == null)
        throw new ArgumentNullException("url");
      if (title == null)
        throw new ArgumentNullException("title");

      _url = url;
      _title = title;
      _description = string.Empty;
    }

    protected Url(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");

      _url = info.GetString("UrlPath") ?? string.Empty;
      _title = info.GetString("Title") ?? string.Empty;
      _description = info.GetString("Description") ?? string.Empty;
    }

    public string UrlPath
    {
      get { return _url; }
      set
      {
        if (value == null)
          throw new ArgumentNullException("value");
        _url = value;
      }
    }

    public string Title
    {
      get { return _title; }
      set
      {
        if (value == null)
          throw new ArgumentNullException("value");
        _title = value;
      }
    }

    public string Description
    {
      get { return _description; }
      set
      {
        if (value == null)
          throw new ArgumentNullException("value");
        _description = value;
      }
    }

    public string CodedUrl
    {
      get { return string.Format("[url={0}]{1}[/url]", _url, _title); }
    }

    public void Parse(string input)
    {
      _url = string.Empty;
      _title = string.Empty;

      Match match = _regex.Match(input ?? string.Empty);
      if (match.Success)
      {
        string protocol = string.Empty;
        if (match.Groups.Count >= 2)
          protocol = match.Groups[1].Value;
        string url = string.Empty;
        if (match.Groups.Count >= 3)
          url = match.Groups[2].Value;
        string text = string.Empty;
        if (match.Groups.Count >= 4)
          text = match.Groups[3].Value;
        _url = string.Format("{0}://{1}", protocol, url);
        _title = text;
      }
    }

    public Uri ToUri()
    {
      return ToUri(true);
    }

    public Uri ToUri(bool fail)
    {
      Uri uri;
      bool created = Uri.TryCreate(UrlPath, UriKind.RelativeOrAbsolute, out uri);
      if (!created && fail)
        throw new UriFormatException(
            string.Format("La url '{0}' está mal formada. ", UrlPath));
      
      return uri;
    }

    public override string ToString()
    {
      return CodedUrl;
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");

      info.AddValue("UrlPath", _url);
      info.AddValue("Title", _title);
      info.AddValue("Description", _description);
    }
  }
}

Creo que la clase no es muy difícil de entender. Lo interesante ocurre en el método Parse (línea 94). Este método utiliza una expresión regular (estática, definida en el constructor estático, línea 18), la cual obtiene hasta cuatro grupos:

  • El primer grupo obtiene la etiqueta completa. Es decir: [url=https://fermasmas.wordpress.com]Blog de Fer++[/url].
  • El segundo grupo obtiene el protocolo, limitado a http y https.
  • El tercer grupo obtiene la URL sin el protocolo, en nuestro ejemplo, fermasmas.wordpress.com.
  • El cuarto grupo obtiene el título, osea Blog de Fer++.

La expresión regular fuerza a que ésta sea de la siguiente forma:

  • [url=http://url]título[/url]
  • [url=https://url]título[/url]

Donde URL es cualquier URL válida formada por caracteres alfanuméricos y cualesquiera signos “%@?:#&+._=-“. Nada fuera del otro mundo.

La clase implementa ISerializable porque necesito poder serializarla a binario o XML, y para ello implementamos el constructor protegido, que se encarga de de-serializar, y el método GetObjectData. Nada de interés para el tema en cuestión.

Fuera de eso todo lo demás es fácil de comprender.

Ahora bien, también tuve que crear una clase UrlCollection que me permita almacenar una colección de URLs. Esta clase también implementa un método Parse, pero a diferencia del de la clase Url donde el método obtiene la primera concordancia de la expresión regular, aquí se obtienen todas las concordancias, y para cada una agrega un nuevo objeto Url a la colección. He aquí el código.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;

namespace ePeople.VirtualStore
{
  public class UrlCollection : Collection<Url>, ISerializable
  {
    private static Regex _regex;
    private static readonly string _serializationName;

    static UrlCollection()
    {
      _regex = new Regex(
           @"\[url=(http|https)://{1}([a-zA-Z0-9/%@?:#&+._=-]*)\](.*?)\[/url\]", 
           RegexOptions.Compiled);
      _serializationName = "Values";
    }

    public UrlCollection()
      : base()
    {
    }

    public UrlCollection(IList<Url> list)
      : base(list)
    {
    }

    protected UrlCollection(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");

      string values = info.GetString(_serializationName);
      if (!string.IsNullOrEmpty(values))
        Parse(values);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      if (info == null)
        throw new ArgumentNullException("info");
      info.AddValue(_serializationName, ToString());

    }

    public Url Add(string url, string title)
    {
      Url newUrl = new Url(url, title);
      Add(newUrl);
      return newUrl;
    }

    public Url Add(string url)
    {
      return Add(url, string.Empty);
    }

    public void Parse(string input)
    {
      Parse(input, false);
    }

    public void Parse(string input, bool clear)
    {
      if (clear)
        Clear();

      MatchCollection matches = _regex.Matches(input ?? string.Empty);
      foreach (Match match in matches)
      {
        string protocol = string.Empty;
        if (match.Groups.Count >= 2)
          protocol = match.Groups[1].Value;
        string url = string.Empty;
        if (match.Groups.Count >= 3)
          url = match.Groups[2].Value;
        string text = string.Empty;
        if (match.Groups.Count >= 4)
          text = match.Groups[3].Value;
        url = string.Format("{0}://{1}", protocol, url);
        Add(url, text);
      }
    }

    public override string ToString()
    {
      StringBuilder value = new StringBuilder();
      foreach (Url url in this)
        value.Append(url.ToString());

      return value.ToString();
    }
  }
}

 

Ahora sí, el siguiente programa muestra cómo se puede utilizar.

class Program
{
  static void Main(string[] args)
  {
    string singleInput = 
            @"[url=https://fermasmas.wordpress.com]Blog de Fer++[/url]";
    string multipleInput = 
            @"[url=https://fermasmas.wordpress.com]Blog de Fer++[/url]
                 [url=http://www.tiburones-rojos.com]Tiburones Rojos[/url]
                 [url=http://www.codeproject.com]Code Project[/url]
                 [url=http://msdn.microsoft.com]MSDN[/url]";

    Url url = new Url();
    url.Parse(singleInput);
    Console.WriteLine("URL sencilla\n=====\n ");
    Console.WriteLine("Original: {0}", url.CodedUrl);
    Console.WriteLine("URL: {0}", url.UrlPath);
    Console.WriteLine("Título: {0}", url.Title);
    Console.WriteLine();

    UrlCollection urls = new UrlCollection();
    urls.Parse(multipleInput);
    Console.WriteLine("URL múltiples\n=====\n ");
    foreach (Url thisUrl in urls)
    {
      Console.WriteLine("Original: {0}", thisUrl.CodedUrl);
      Console.WriteLine("URL: {0}", thisUrl.UrlPath);
      Console.WriteLine("Título: {0}", thisUrl.Title);
      Console.WriteLine();
    }

    Console.ReadKey(true);
  }
}

Y al ejecutarse, obtenemos la siguiente salida en la consola.

URL sencilla
=====

Original: [url=https://fermasmas.wordpress.com]Blog de Fer++[/url]
URL: https://fermasmas.wordpress.com
Título: Blog de Fer++

URL múltiples
=====

Original: [url=https://fermasmas.wordpress.com]Blog de Fer++[/url]
URL: https://fermasmas.wordpress.com
Título: Blog de Fer++

Original: [url=http://www.tiburones-rojos.com]Tiburones Rojos[/url]
URL: http://www.tiburones-rojos.com
Título: Tiburones Rojos

Original: [url=http://www.codeproject.com]Code Project[/url]
URL: http://www.codeproject.com
Título: Code Project

Original: [url=http://msdn.microsoft.com]MSDN[/url]
URL: http://msdn.microsoft.com
Título: MSDN


Y eso es todo. Sirva este código para que lo utilices directamente o bien para que lo adaptes para crear tus propias etiquetas. Nos vemos la próxima.

Post Scriptum: cuando termine la columna personalizada de SharePoint, pondré aquí el código para que veas cómo quedó.

Categorías:.NET Framework, C#, Código y ejemplos Etiquetas: