Mostrando entradas con la etiqueta C#. Mostrar todas las entradas
Mostrando entradas con la etiqueta C#. Mostrar todas las entradas

martes, 9 de junio de 2015

Expresiones regulares en .NET

De todos es sabido que cuando tienes un problema e intentas solucionarlo con una expresión regular, acabas teniendo 2 problemas (el original y el añadido de tener que entender y mantener la expresión regular en tu código).

En cualquier caso, hay veces donde una expresión regular sí es la solución y es entonces cuando resulta necesario tener claros algunos conceptos. Inicialmente, mi escaso conocimiento sobre las expresiones regulares no era un tema que me preocupara (mas me vale por a día de hoy tampoco soy ningún experto), sabía lo que sabía y con ello sobrevivía, pero a medida que las he ido utilizando con más asiduidad (esto es en vez de “nunca” a “de pascuas a ramos”) he ido perfilando una guía de supervivencia que espero me permita enfrentarme a ellas con una mayor y “aparente” seguridad.

Para trabajar con expresiones regulares en .NET, utilizaremos la clase Regex del espacio de nombres System.Text.RegularExpressions.

Regex expone tanto métodos estáticos como métodos de instancia.

Los métodos estáticos crearán internamente una instancia de la propia clase Regex, pero cachearán la instancia creada para no tener que instanciarla de nuevo cuando se vuelva a llamar a un método estático con la misma expresión regular (el número de entradas de la caché es configurable según la propiedad CacheSize). De este modo, si sabemos de antemano que instanciaremos frecuentemente la misma expresión regular, utilizar los métodos estáticos nos reportará un mejor rendimiento.

Si trabajamos con métodos de instancia y relacionado con el rendimiento, otra opción sería compilar la expresión regular, que tardaría un poco más en instanciar el objeto pero a cambio sucesivas llamadas a la misma instancia no tendrían que interpretarla de nuevo cada vez. También podemos compilar expresiones regulares en un ensamblado con CompileToAssembly, pero lo cierto es que a día de hoy veo lejana esta opción :)

Para crear una instancia de Regex tendremos que suministrarle un patrón con la expresión regular y, opcionalmente, un opciones en un valor del tipo RegexOptions. Las opciones son modificadores para la expresión regular especificada en el patrón. Por ejemplo IgnoreCase (para no distinguir mayúsculas de minúsculas), Multiline (para cambiar el significado de ^ y $), etc. RegexOptions es un enumerado decorado con el atributo [Flags], luego acepta varias valores.

var pattern = "^sergio$";
// con RegexOptions.IgnoreCase | RegexOptions.Multiline se encontrarán 2 coincidencias, sin estas opciones no se encontrará ninguna
var regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
var input = "sergio\nSERGIO";
var match = regex.Match(input);
while (match.Success)
{
Console.WriteLine("Value {0} Index {1} Length {2}", match.Value, match.Index, match.Length);
match = match.NextMatch();
}
Si especificamos las opciones a través de RegexOptions se aplicarán a toda la expresión regular. Sin embargo, también es posible establecer estas opciones en línea en el texto de la propia expresión regular, que además permitirá entonces activar o desactivar la opción selectivamente. Por ejemplo, la anterior expresión regular podría rescribirse como sigue:
var pattern = "(?im)^sergio$";

Y aplicar las opciones de forma selectiva podemos verlo en el siguiente pantallazo que además muestra como ReSharper nos puede ayudar a depurar fácilmente nuestras expresiones regulares. Por ejemplo en esta expresión regular estamos diciendo que inicialmente sea case-insensitive y multiline pero que “gio” sea case-sensitive.

image

Otro gran problema de las expresiones regulares es que cuando vuelvas a ellas tiempo después ya no sabrás que querías buscar. En mi caso utilizo normalmente una herramienta como http://www.regexr.com/ para crear la expresión regular y, después de mucho método ensayo-error, doy con la expresión adecuada que finalmente copio y pego en mi código con algún que otro comentario más o menos afortunado…

// multiline
// al comienzo de cada línea y entre paréntesis un prefijo
// (+número) pudiendo haber espacios antes y después del/los número(s)
// opcionalmente espacios
// 9 números sin espacios para terminar la línea
var pattern = @"(?m)^\(\s*\+\s*(\d+)\s*\)\s+(\d{9})$";

Desde luego más vale ese comentario que nada, pero hay otra formas mejor de hacerlo que es incluir comentarios en la propia expresión regular. El primer tipo de comentario que podemos incluir es un comentario en línea con la sintaxis (?# comentario…)

var pattern = @"(?m)(?# multiline)^\(\s*\+\s*(\d+)\s*\)(?# prefijo)\s+(\d{9})(?# número)$";

Probablemente los comentarios en líneas sea útiles para expresiones regulares sencillas pero en el momento que sean complicadas o simplemente tengan un tamaño considerable, parece mejor usar los comentarios “hasta final de línea”. Para incluir este segundo tipo de comentarios tenemos que activar la opción/modificador IgnoreWhiteSpace que lo hace es que se ignoren los espacios en blanco en la expresión regular, habilitando de este modo que podamos incluir este nuevo tipo de comentarios con la sintaxis # comentario

var pattern = @"(?mx) # MultiLine, IgnoreWhiteSpace
^\(\s*\+\s*(\d+)\s*\) # Prefijo con paréntesis inicial y final obligatorio
# con espacios opcionales después del paréntesis inicial y antes del parántesis final
# en medio desde 1 a n dígitos contiguos
\s+ # espacios desde 1 a n
(\d{9})(?# número)$ # Teléfono de 9 dígitos contiguos
";

Otro concepto que me parece importante entender para poder avanzar y no usar sólo el método IsMatch (que está bien, evita overhead porque no captura nada, pero por contra nos estamos perdiendo todo el potencial de las expresiones regulares) es qué son las coincidencias, grupos y capturas.

Cuando buscamos coincidencias en un texto según una expresión regular, no sólo podemos comprobar que el texto coincide con nuestro patrón, sino también capturar ciertas porciones de la cadena para poder trabajar con ellas después. Esto es un grupo y es representado por el objeto Match. Para capturar un grupo utilizamos los paréntesis.

var pattern = @"(.+)@(.+)";
var regex = new Regex(pattern);
var input = "sergio @panicoenlaxbox\ncarmen @panicoenel20";
var match = regex.Match(input);
if (match.Success)
{
do
{
Console.WriteLine("Match - Value {0} Index {1} Length {2}", match.Value, match.Index, match.Length);
for (int i = 1; i < match.Groups.Count; i++)
{
Group group = match.Groups[i];
Console.WriteLine("Group - Value {0} Index {1} Length {2}", group.Value, group.Index, group.Length);
}
match = match.NextMatch();
} while (match.Success);
}

Imprime por consola lo siguiente:

Match - Value sergio @panicoenlaxbox Index 0 Length 22
Group - Value sergio Index 0 Length 6
Group - Value panicoenlaxbox Index 8 Length 14
Match - Value carmen @panicoenel20 Index 23 Length 20
Group - Value carmen Index 23 Length 6
Group - Value panicoenel20 Index 31 Length 12

Como vemos no hay en .NET un modificador g (global) sino que se accede a la siguiente coincidencia llamando a NextMatch() o directamente utilizando el método Matches en vez de Match.

var matches = regex.Matches(input);
foreach (Match match in matches)
{
//...
}

Además de coincidencias (Match) y grupos (Group) también aparecen las capturas (Capture). Una captura se produce cuando un grupo tiene un cuantificador y por ende se captura n veces. Por defecto, cuando preguntamos por el valor de un grupo nos devuelve el valor de su última captura, pero esto podría no servirnos si las capturas de un grupo tienen distintos valores. Por ejemplo:

image

Otra cosa acerca de los grupos es que además de poner acceder a ellos por un índice también podemos darles un nombre con la sintaxis ?<nombre>

var pattern = @"(?<name>.+)@(?<twitter>.+)";
var regex = new Regex(pattern);
var input = "sergio @panicoenlaxbox\ncarmen @panicoenel20";
var matches = regex.Matches(input);
foreach (Match match in matches)
{
Console.WriteLine("Match - Value {0} Index {1} Length {2}", match.Value, match.Index, match.Length);
Group group = match.Groups["name"];
Console.WriteLine("Group - Value {0} Index {1} Length {2}", group.Value, group.Index, group.Length);
group = match.Groups["twitter"];
Console.WriteLine("Group - Value {0} Index {1} Length {2}", group.Value, group.Index, group.Length);
}

Alrededor de los grupos gira mucho de las capacidades de la expresiones regulares, por ejemplo las backing references permiten hace mención a un grupo previamente capturado con \número_de_grupo. En el siguiente ejemplo, tanto el primer caracter como el último tienen que ser iguales:

image

Incluso sería más sencillo si le damos un nombre al grupo y después lo referenciamos con \k<nombre_de_grupo>

(?<delimiter>.{1}).+\k<delimiter>{1}

También existe el concepto de no capturar un grupo. Es decir, necesitamos paréntesis para aplicar un cuantificador o cualquier otro modificador pero no queremos que se produzca una captura del grupo. Esto lo conseguimos con lo que se denomina un “non-capturing group”

(?:pattern)

Con la opción ExplicitCaptures lo que conseguimos es que no se capture ningún grupo excepto aquellos que tengan nombre..

Algo que también crea mucha confusión es que un cuantificador es greedy por defecto (avaricioso). Esto significa que consumirá la mayor cantidad posible de texto para satisfacer al cuantificador. A veces justamente queremos lo contrario, que consuma sólo lo mínimo e imprescindible para que el cuantificador sea cumplido. Para indicar a un cuantificador que sea Lazy hay que utilizar ? a continuación del cuantificador.

El ejemplo canónico para ver el tema de greedy vs lazy es capturar en un grupo la etiqueta de apertura y cierre de un elemento HTML.

  • Con la expresión <.+> y para el texto <p>Sergio</p> habrá un match que será todo el texto “<p>Sergio</p>”. Esto es porque el cuantificador + del punto “encaja” con todo el texto y sólo se detiene en el último caracter para satisfacer el > final.
  • Sin embargo con <.+?> tendremos 2 matches. “<p>” y “</p>”. El operador lazy le dice la expresión regular que tiene que parar en el primer > que encontró (el del cierre de la apertura de la etiqueta).

Otra posibilidad que nos dan los grupos es la de utilizarlos para reemplazar texto con el método Replace. En este método podemos hacer mención a un grupo capturado con $número_de_grupo o ${nombre_de_grupo}. Por ejemplo (y utilizando ahora el plugin Regular Expression Tester Extension.

image

La verdad es que las expresiones regulares dan para muchos posts, pero como decía al comienzo, tampoco quiero excesivos problemas y me basta con saber lo justo y necesario :)

Un saludo!

jueves, 16 de octubre de 2014

Conversiones de usuario en C#

Las conversiones de usuario permiten definir conversiones personalizadas en C#, tanto explícitas como implícitas, desde un tipo origen a un tipo destino.

Lo cierto es que no suelo utilizarlas mucho (por no decir rara y extrañísima vez), pero como siempre estamos buscando una oportunidad para poner en juego características del lenguaje, recientemente me he encontrado con un caso que he resuelto con una conversión de usuario.

Para poner en situación, primero veremos un ejemplo de cómo son y qué hacen.
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var age = 38;
            Person person = age; // implícita de int a Person
            Console.WriteLine(person.Age); // 38 
            age = (int)person; // explícita, necesario cast, de Person a int            
            Console.WriteLine(age); // 38
            Console.ReadKey();
        }
    }

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public static implicit operator Person(int age)
        {
            return new Person() { Age = age };
        }

        public static explicit operator int(Person person)
        {
            return person.Age;
        }
    }
}
Como se puede ver en el ejemplo, la sintaxis de una conversión de usuario es:
public static {implicit|explicit} operator TipoDestino (TipoOrigen identificador) 
{
 // do something…
return TipoDestino;
}
Las restricciones que se aplican a una conversión de usuario son:

  • Sólo son válidas para clases y estructuras.
  • TipoDestino y TipoOrigen tienen que ser de distinto tipo.
  • No puede haber una relación de herencia entre TipoDestino y TipoOrigen
  • TipoDestino y TipoOrigen no pueden ser de un tipo interface ni object.
  • No se puede declarar la misma conversión tanto de forma implícita como explícita
  • No se puede usar el operador is
  • No se puede usar el operador as

Después de la teoría y ya sabiendo que es una conversión de usuario, el ejemplo donde he aplicado una conversión de usuario es en un método que devuelve si se puede o no enviar un pedido. El problema está en que si no se puede enviar el pedido, querría saber también el motivo. Es decir, el siguiente código no me servía.
    public class Order
    {
        public string CustomerId { get; set; }
        public DateTime? DeliveryDate { get; set; }

        public bool CanBeSent()
        {
            if (string.IsNullOrEmpty(CustomerId))
            {
                return false;
            }
            if (DeliveryDate == null)
            {
                return false;
            }
            return true;
        }
    }
Para solucionar el problema expuesto y haciendo uso de las conversiones de usuario, podemos hacer que el método devuelve un tipo complejo indicando si es o no posible enviar el pedido y, en caso negativo, también especificar el motivo. Además, el valor devuelto se puede trabajar tanto como un booleano como un tipo complejo cuando sea necesario.
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var order = new Order();
            var result = order.CanBeSent();
            if (result)
            {
                Console.WriteLine("Se puede enviar el pedido");
            }
            else
            {
                Console.WriteLine("No se puede enviar el pedido");
                Console.WriteLine(result.Reason);
            }
            Console.ReadKey();
        }
    }

    public class CanBeResult
    {
        public bool Value { get; set; }
        public string Reason { get; set; }

        public CanBeResult(bool value)
        {
            Value = value;
        }

        public CanBeResult(bool value, string reason)
            : this(value)
        {
            Reason = reason;
        }

        public static implicit operator bool(CanBeResult result)
        {
            return result.Value;
        }

        public static implicit operator CanBeResult(bool value)
        {
            return new CanBeResult(value);
        }
    }

    public class Order
    {
        public string CustomerId { get; set; }
        public DateTime? DeliveryDate { get; set; }

        public CanBeResult CanBeSent()
        {
            if (string.IsNullOrEmpty(CustomerId))
            {
                return new CanBeResult(false, "El pedido no tiene cliente");
            }
            if (DeliveryDate == null)
            {
                return new CanBeResult(false, "El pedido no tiene fecha de entrega");
            }
            return true;
        }
    }
}
Un saludo!