10 de septiembre de 2010

Cómo saber si un valor es numérico mediante una simple expresión regular

Hoy no estoy muy inspirado para escribir en el blog, así que he repasado mis librerías de subrutinas de perl para ver si encontraba algo curioso para publicar y me he encontrado con esto...
¿Quién no ha tenido nunca el problema de comprobar si una expresión es numérica o no? 
Perl no posee una función que lo haga automáticamente (por lo menos que yo conozca), sin embargo con una simple línea de código podemos salir de dudas:

 # Return TRUE if an expression is numerical  
 sub is_numeric {  
      my ($exp) = @_;  
      if ($exp =~ /^(-?[\d.\-]*e[\d.\-\+]+|-?[\d.\-]\^[\d.\-]+|-?[\d.\-]+)$/){  
      # Corregida siguiendo las indicaciones de Joaquín Ferrero:
      if ($exp =~ /^-?(?:[\d.-]+*e[\d.+-]+|\d[\d.-]*\^[\d.-]+|[\d.-]+)$/){
           return 1;  
      } else {  
           return 0;  
      }  
 }  

3 comentarios:

  1. Hay que tener cuidado con las expresiones regulares. Puede que se nos escapen algunas situaciones límite o frontera.

    Por ejemplo, la exp. reg. indicada en el artículo pasaría como expresión numérica correcta las expresiones '6.6.6' y 'e34.12'. Y en cambio, no daría por buena la expresión numérica '6.125e+32'.

    La forma 'académica' de reconocer números es usando las facilidades del módulo Regexp::Common:

    use Regexp::Common 'number';
    if ($var =~ /$RE{num}{real}/) { say 'es un número' }

    De esta manera, se puede reducir más la búsqueda (buscar por un número real), además de capturar el número identificado, en varios niveles de profundidad (el número entero, la mantisa, o, por ejemplo, solo el exponente).

    La forma 'ultrarápida' es haciendo una llamada a una función de la API del propio Perl. Es decir: es el propio Perl el que nos dice si la expresión "parece un número" o no. El módulo Scalar::Util contiene la función looks_like_number(), que llama a la función de la API de Perl del mismo nombre, devolviendo un valor verdadero si la expresión dada "se parece" a un número.

    use 5.010;
    use Scalar::Util 'looks_like_number';
    for ('6.6.6', 'e34.123', '23.45', '6.125e+32') {
        print "Expr: $_ ";
        if (looks_like_number($_)) {
            say "es un número";
        }
        else {
            say "no es un número";
        }
    }

    El módulo Scalar::Util ya viene incluido en los últimas distribuciones Perl (desde el 5.8, año 2002).

    ResponderEliminar
  2. Me he quedado impresionado... imaginaba que Perl tenía algo así en la API pero nunca lo encontré en Google.

    Gracias de nuevo por tus aportaciones, ahora ya conozco dos nuevos módulos a tener en cuenta en el futuro.

    PD: expresión regular corregida para reconocer '6.125e+32'

    ResponderEliminar
  3. Acabo de darme cuenta de que el caso intermedio (una base elevada a un exponente -es lo que creo entender-), solo permite que la base tenga un solo dígito o sea el carácter '.'.

    No es necesario "escapar" el guión (ni el signo más) si están, sobre todo, al final de la clase carácter. Así, [\d.\-\+] se puede dejar en [\d.+-] (el signo menos, en estos casos dudosos, siempre hay que dejarlo al final, para que no signifique "rango" de caracteres).

    Quizás sea un poco mejor así (no probado):

    if ($exp =~ /^-?(?:[\d.-]*e[\d.+-]+|\d[\d.-]*\^[\d.-]+|[\d.-]+)$/) {

    Lo dicho... a veces es mejor fiarse de exp. reg. creadas por otras personas que ya se han peleado con estos problemas, como es el caso de Regexp::Common.

    Hay otros módulos con una función is_numeric() implementada. En Data::Validate hay una, pero en realidad está llamando a la looks_like_number() ya comentada antes. Y en String::Numeric el autor se lo ha tomado muy en serio, ofertando más de una docena de funciones, usando expresiones regulares o llamando a código en C. Incluso hace un análisis comparativo con la looks_like_number(). Hay gente pa'to ;)

    Mira la exp. reg. que usa para ver si un número es decimal:
    /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/

    ResponderEliminar