miércoles, 27 de febrero de 2013

Tokenizer

Ahora que sabemos cómo representar los elementos de un documento HTML, es momento de ir a por lo realmente interesante: cómo analizar el documento e ir extrayendo elemento a elemento, separando unas cosas de otras, decidiendo dónde empieza y dónde acaba cada cosa.

Afortunadamente, Ruby es, además de un lenguaje de programación moderno, una mezcla de características de otros lenguajes. De aquellas características que más le gustaban a su creador.

Y entre ellas estaban las expresiones regulares.

Expresiones regulares

 Hay veces que necesitas saber si una cadena sigue o no un determinado patrón. Que si comienza así, que si termina de aquel modo, que si contiene tal cosa... Pues bien, para representar los patrones se utilizan las expresiones regulares.

Mira este ejemplo:
irb(main):001:0> /a.*b/.match('123a11b456')
=> #<MatchData "a11b">

Vamos por partes. Para empezar, eso de /a.*b/ es la expresión regular. Las expresiones regulares se encierran entre barras “/” pero, como ocurre con las almejas, lo que nos interesa es lo de dentro. Y ahí pone “a.*b”.

¿Qué significa eso? Bueno, la letra “a” inicial no tiene demasiado misterio. Quiere decir que la expresión regular, al compararse con la cadena, trata primero de encontrar una letra “a”.

Lo de “.*” merece mención aparte. El punto “.” es un comodín que significa “cualquier carácter”. Pero no queda ahí la cosa: al punto le sigue un asterisco, *, que, en la jerga de las expresiones regulares, quiere decir “repite esto último tantas veces como haga falta. Cero o más veces”.

En definitiva, “.*” podría traducirse como “repite cualquier carácter cero o más veces”. O, en otras palabras “aquí me vale cualquier cosa, o incluso nada”.

Y despúes de “.*” va la “b”.

Resumiendo: /a.*b/ significa que primero va la “a”, después cualquier cosa de la longitud que sea y, finalmente, una “b”.

En la cadena que se le pasó como argumento a “match” en el ejemplo anterior, “123a11b456”, se puede encontrar ese patrón. Exactamente, a partir del cuarto carácter tenemos “a11b”. El método “match” lo encuentra y retorna un objeto de tipo MatchData con la información acerca de qué coincidencia ha encontrado y dónde la ha hecho.

Sobre expresiones regulares, Ruby y cómo elegir un buen burro

A veces hay más de un posible acierto.

Por ejemplo, imaginemos la misma expresión regular de antes y la cadena “123a11b456b”. Ahora, la expresión /a.*b/ tiene dos posibles soluciones: “a11b” y “a11b456b”. ¿Cuál elige Ruby?

Bueno, la cosa depende (ya se verá más adelante), pero siguiendo la famosa regla empírica de “mejor el burro grande, ande o no ande”, lo normal es que se elija la coincidencia más larga. Y, como no quiero que te creas nada sin probarlo, vete al irb e inténtalo:
irb(main):002:0> /a.*b/.match('123a11b456b')
=> #<MatchData "a11b456b">

Observa que el valor devuelto no es una cadena. Es un objeto de tipo MatchData. Si deseas saber qué texto fue el encontrado y obtenerlo como cadena, puedes utilizar el método “to_s”
irb(main):004:0> /a.*b/.match('123a11b456b').to_s
=> "a11b456b"


Personalmente, creo que no es momento aún de entrar en más profundidades acerca de cómo funcionan las expresiones regulares, pero si tú tienes otra opinión, te dejo un rato para ir a leer el siguiente documento:
http://www.ruby-doc.org/core/Regexp.html

Eso sí, vuelve cuando termines. Aquí me quedo esperando.

El tokenizador

Bueno, pues aquí estamos de nuevo. ¿A qué vino todo ese rollo de las expresiones regulares?

Cuando queremos analizar un documento, lo primero es ir extrayendo los elementos básicos que lo componen. Que si una palabra, que si un número,... Esto se llama análisis lexicográfico y, por hoy, a estos elementos básicos les vamos a llamar “tokens”.

Después hay que determinar cómo se relacionan entre sí los elementos para componer estructuras más complejas. Eso es el análisis sintáctico.

Y, finalmente, hay que ver qué significan las estructuras que hemos encontrado. El análisis semántico, vamos.

Por ahora, nos centraremos en el análisis lexicográfico. Y un poquito del sintáctico. A este nivel, se trata de hacer cosas como tomar una expresión como:
contador = contador + 5

Y extraer de ella sus tokens e identificarlos. En el caso anterior, si se pueden ignorar los espacios, se tendrían cinco tokens:
Primero hay un nombre de variable, “contador”
Después, un operador de asignación “=”
Después, un nombre de variable, “contador”
Después, un operador aritmético, “+”
Después, un número “5”

Como puedes comprobar, es importante saber tanto la cadena que representa el token como de qué tipo de token se trata.
Pues bien, para esto, las expresiones regulares son muy, muy, muy, muy útiles. Para demostrarlo, vamos a crear una clase que extraiga los tokens que forman una cadena.

Los objetos de nuestra clase, a la que llamaremos Tokenizer (así soy de original), necesitarán dos cosas:
  • una lista de expresiones regulares, cada una de las cuales tendrá asociado un tipo de token. Por ejemplo, podremos definir una expresión regular consistente en una sucesión de digitos y decir que eso se corresponde con un número entero.
  • Una cadena de texto a analizar

Y, una vez les hayamos dado estos datos, podremos decirle “saca un token y dime qué encuentras y de qué tipo es”.

Como la mejor forma de demostrar el movimiento es andando, veamos cómo nos gustaría poder utilizar la clase. Utilizando irb podríamos empezar por cargar el fichero en que se define la clase y a crear un objeto Tokenizer
irb(main):001:0> require './tokenizer.rb'
=> true
irb(main):002:0> t = Tokenizer.new('ab28dgadf83a3')
=> #<Tokenizer:0x8cd7a14 @rules=[], @str="ab28dgadf83a3">



Al crearlo, le hemos dicho que queremos analizar la cadena 'ab28dgadf83a3'. Ahora le vamos a añadir dos reglas para reconocer los tokens de esta cadena. Supongamos que queremos reconocer dos tipos de cosas: secuencias de letras y secuencias de números.

Tendremos, pues, dos expresiones regulares. La primera de ellas, que nos permitirá encontrar los números, vendrá dada por un hash:
{ :regexp => /[0-9]+/ , :type => :number }

La expresión regular es /[0-9]+/ . Cuando en una expresión se encuentra un corchete de apertura y otro de cierre, “[“ y “]”, eso representa un carácter cualquiera de los que haya dentro. Así “[abc]” representa “a” o “b” o “c”. Uno de ellos.

Pero imagina ahora que quieres indicar que quieres una letra minúscula cualquiera. Podrías poner
[abcdefghijklmnopqrstuvwxyz]

¿Qué te parece? ¿No es demasiado largo? Aparte de que uno puede equivocarse cuando escribe tanto. Por eso hay una forma alternativa de escribirlo:
[a-z]

Con “a-z” se indica “cualquier carácter desde la “a” hasta la “z”.

De igual forma,
[0-9]
… representa cuanquier carácter del “0” al “9”. Cualquier dígito.

En nuestra expresión regular, después va un signo más, “+”. Este carácter quiere decir “repite lo anterior una o más veces”. O sea, y en definitiva, que “[0-9]+” quiere decir “uno o más dígitos”. Y a eso le asignamos el tipo :number.

De igual forma, a “[a-z]+”, una secuencia de letras de longitud mayor que cero, le asignamos el tipo :letter con
{ :regexp => /[a-z]+/ , :type => :letter }

Podemos usar un método llamado addRules para añadir ambas reglas de una sola vez:


irb(main):003:0> t.addRules([{:regexp=>/[0-9]+/, :type=>:number},{:regexp=>/[a-z]+/, :type=>:letter}])
=> [{:regexp=>/[0-9]+/, :type=>:number}, {:regexp=>/[a-z]+/, :type=>:letter}]


Y ya sólo quedaría decirle al objeto que nos proporcione los tokens que hay en la cadena y hacer algo con ellos. Habrá un método, getTokens, que lo haga:
irb(main):004:0> t.getTokens.each {|x| print x.inspect ; print "\n"}
{:type=>:letter, :string=>"ab"}
{:type=>:number, :string=>"28"}
{:type=>:letter, :string=>"dgadf"}
{:type=>:number, :string=>"83"}
{:type=>:letter, :string=>"a"}
{:type=>:number, :string=>"3"}
=> [{:type=>:letter, :string=>"ab"}, {:type=>:number, :string=>"28"}, {:type=>:letter, :string=>"dgadf"}, {:type=>:number, :string=>"83"}, {:type=>:letter, :string=>"a"}, {:type=>:number, :string=>"3"}]



Observa que el bloque de código contiene dos instrucciones y que, como están en la misma línea, hay que buscar una forma de separarlas. En Ruby, para eso, podemos usar un punto y coma “;”.
getTokens nos extrae todos los tokens de una sola vez. También existirá un método, getToken (en singular) que obtiene sólo un token.

A ambos, getTokens y getToken, le vamos a añadir una característica más. Puede que haya ocasiones y situaciones en que no todos los tipos de tokens sean sintáctica o semánticamente correctos. En que no queramos obtener un token de un tipo inadecuado. Para esos casos, los dos métodos admitirán un parámetro que les indique qué tipos de token queremos reconocer.

Esto nos permitirá tambén utilizar un mismo objeto Tokenizer para distintos análisis. Algunas veces puede venir bien tener este grado de flexibilidad. Y, como veremos, es algo que no cuesta demasiado.

La creación del objeto

Comencemos con los métodos que preparan el objeto para hacer su trabajo. Ahí van los primeros:
# Object creation. Initial text may be provided / Creación del objeto. Se puede dar texto inicial
def initialize(str='')
    @rules = []
    loadStr(str)
end


# Load initial text / Cargar el texto inicial
def loadStr(str='')
    @str = str
end


Poco nuevo hay aquí. El objeto tendrá dos variables de instancia, @rules y @str, que contendrán las reglas (lo de las expresiones regulares y sus correspondientes tipos) y la cadena a analizar, respectivamente. Pasemos ahora a la carga de los patrones a buscar:



# Add a rule for token recognition / Añadir regla para reconocimiento de tokien
# Parameters / Parámetros:
# regexp: String or RegExp with the regular expression. If it doesn't start with \A, \A will be
#appended at the start
# Cadena o Expresión Regular con la expresión regular. Si no empieza por \A, se le añade al
#principio
# type: Symbol recommended. A type for the regexp. Several regexp may share a common type
# Recomendable que sea Symbol. El tipo asociado a regexp. Varias regexp pueden compartir tipo
# opts: Options when applying the regexp
# Opciones para aplicar regexp

def addRule(regexp, type, opts = Regexp::MULTILINE + Regexp::IGNORECASE)
    str = case regexp.class.name
        when 'Regexp' then regexp.source
        when 'String' then regexp
        else raise 'Unsupported type for regexp'
    end


    if str[0..1] != '\A'
        str = '\A' + str
    end


     myregexp = Regexp.new(str, opts)

    @rules.push({:regexp=>myregexp, :type=>type})
end



El método sirve para que le digamos al objeto “cuando encuentres algo que siga este patrón, es que tienes un token de tal tipo. Y recibe tres parámetros.

Para no empezar por el principio, el segundo de los parámetros indica qué tipo de token estamos definiendo. A Ruby no le preocupa demasiado qué Yo recomendaría usar aquí valores de tipo Symbol, salvo que se tenga una razón fundada para hacer otra cosa. Ya veremos por qué.

El primero es una expresión regular o bien una cadena que contiene el texto correspondiente a una expresión regular. Igual me da que pongas una cadena como 'a.*b' que una expresión regular como /a.*b/ . En ambos casos, eso va a indicar el patrón que hay que encontrar.

Y el tercero son unas opciones a utilizar cuando se utilice la expresión regular. Ruby tiene unos cuantos. Por defecto, le diremos que las expresiones regulares pueden ocupar más de una línea y que no nos preocupa si las cosas están en mayúsculas o la minúsculas.

El cuerpo del método comienza con una estructura rara:
str = case regexp.class.name
    when 'Regexp' then regexp.source
    when 'String' then regexp
    else raise 'Unsupported type for regexp'
end



Aquí “str” es una variable local al método. Las variables locales se crean al asignarles el valor. Y esta vez el valor que se le asigna es el resultado de evaluar
case regexp.class.name
    when 'Regexp' then regexp.source
    when 'String' then regexp
    else raise 'Unsupported type for regexp'
end



Los bloques “case” como éste, toman una expresión (en este caso “regexp.class.name”, una cadena con el nombre de la clase a que pertenece el parámetro “regexp”) y la evalúan. Después miran, una a una, las reglas “when opcion then valor” que siguen. En el caso anterior tenemos
when 'Regexp' then regexp.source
when 'String' then regexp



.. lo que quiere decir: si es “Regexp”, entonces el valor a devolver es “regexp.source”. El método “source” de la clase “Regexp” convierte la expresión regular en una cadena. Por otro lado, si ya es “String”, entonces retorna directamente “regexp”.

En definitiva, ¿recuerdas que regexp podía ser una expresión regular o una cadena? Pues estamos asegurándonos de que a partir de ahora vamos a trabajar con una cadena.

Lo de
else raise 'Unsupported type for regexp'

… quiere decir que si no se cumple ninguna de las condiciones de las reglas “when … then ...” (eso es el “else”), se genere una excepción de esas que suelen hacer cascar los programas, con el mensaje de error “Unsupported type for regexp” (“Tipo de datos no soportado para regexp”). La instrucción “raise” se encarga de provocar el error.

Quizá no te parezca buena idea esa de hacer reventar tú mism@ tus programas. Que no queda bien eso de que tus aplicaciones terminen de forma rara y con mensajes de error. Pues te aseguro que te equivocas: me sé de una compañía de software cuyos productos no paran de hacerlo y se venden como rosquillas.

Aparte de eso, ocasión habrá de ver como lidiar con las excepciones y utilizarlas en beneficio propio.

Dejando atrás estas disquisiciones y volviendo al grano, como es de esperar, un “end” marca el final de la expresión.


A continuación aparece:
if str[0..1] != '\A'
    str = '\A' + str
end



La instrucción “if” permite ejecutar unas instrucciones si y solo si se cumple una condición. En este caso, se comprueba si “str[0..1] != '\A'”.

Recuerda que “str” era una cadena. Con str[0..1] se obtienen los caracteres de str que ocupan las posiciones a partir de la 0 (el primer carácter) hasta la 1 (el segundo). En definitiva, estamos hablando de los dos primeros caracteres de str. Después va “!=” que significa “es distinto de”.

Tenemos encontces que si los dos primeros caracteres de str no son “\A”, se le pone “\A” al principio. De ese modo nos aseguramos que str va a comenzar por “\A” ¿Y a qué viene este afán?

Bueno, queremos ir obteniendo los tokens en orden, empezando por el principio, y, como vimos, el método “match” de las expresiones regulares busca en cualquier posición de la cadena... a no ser que le digamos lo contrario. Y “\A” es un código especial de las expresiones regulares que indica, precisamente, el inicio del texto.


Seguimos con
myregexp = Regexp.new(str, opts)



… que crea una expresión regular a partir de la cadena str y con las opciones que le pasamos al método (recuerda, el tercer parámetro) para después asignársela a la variable local myregexp.

Y acabamos con
@rules.push({:regexp=>myregexp, :type=>type})

… que usa el método “push” para añadir un elemento al final del array @rules . Dicho elemento es un hash cuyos índices :regexp y :type apuntan a la expresión regular y su correspondiente tipo, respectivamente.



Ya tenemos como añadir las reglas de una en una. Si queremos poder hacerlo en modo batch (aquí lo llamamos “a puñaos”), podemos definir otro método con unas cuantas líneas de código más:

# Utility function to add several rules at once from an array of hashes with indexes :regexp and :type
# Función de ayuda para añadir varias reglas de una vez a partrir de un array de hashes con índices :regexp y :type
# Parameters / Parámetros:
# array: the array / el array
# opts: options for regexp / opciones para aplicar las regexp

def addRules(array, opts = Regexp::MULTILINE + Regexp::IGNORECASE)
    array.each do |r|
        addRule(r[:regexp], r[:type], opts)
    end
end



Usando el objeto

Con lo anterior tenemos suficiente para crear y configurar el objeto. Pero, claro, si no añadimos algunos métodos más, no nos va a servir para nada. Comencemos por determinar si queda o no texto por procesar:
# True if there is nothing more to read / True si no queda nada por leer
def endOfString?
    return @str == ''
end



Fíjate que le hemos puesto un interrogante al final del nombre del método. Y recuerda que eso era habitual (que no obligatorio) en los métodos que retornaban true o false.

Una sóla línea de instrucciones: “ return @str == '' ”.

Con “@str == '' “ se comprueba si @str es igual a la cadena vacía. El valor resultante de evaluar esta condición es retornado por el método. De hecho, aquí no sería obligatorio poner el “return”, puesto que los métodos retornan, si no se le dice otra cosa, el resultado de evaluar la última instrucción ejecutada. De modo que habría bastado con:
# True if there is nothing more to read / True si no queda nada por leer
def endOfString?
    @str == ''
end



Y ahora al meollo de la cuestión: cómo reconocer el primer elemento de la cadena:



def getToken(types=nil)
    if endOfString?
        return nil
    end


     # Select applicable rules
    myrules = @rules.select{|r| types == nil or types.include?(r[:type])}


    # Get longest match
    match, type = myrules.collect{|r| [r[:regexp].match(@str), r[:type]]}.max{|x,y| x[0].to_s.length <=> y[0].to_s.length}


    string = match.to_s


    # If found, process output. Else return nil
    if string != ''
        # Removed matched text from the input
        @str = match.post_match

        # Subclasses may define methods to process each type
        if private_methods.include?(type)
            return send(type.to_s, match[0])
        else
            return {:type=>type, :string=>string}
        end
    else
        return nil
    end
end




Lo primero que hacemos es comprobar si nos queda algo que analizar. En caso contrario, el método retorna “nil” (recuerda: nil significa nada). Lógico: si no hay cadena a procesar, no hay tokens.

 Después seleccionamos qué reglas vamos a comprobar. Porque, recuerda, nuestra clase es capaz de comprobar sólo una o varias reglas si se lo decimos. Y para decírselo tenemos el parámetro opcional “types” al que podemos asignar un array de tipos:
myrules = @rules.select{|r| types == nil or types.include?(r[:type])}



Observa que se aplica a @rules, un array, el método “select” que permite seleccionar aquellos elementos que cumplen una condición. La condición viene dada por el bloque
{|r| types == nil or types.include?(r[:type])}



O lo que es lo mismo, si no se ha pasado ningún array de tipos permitidos (types == nil), se considera que todas las reglas son aplicables. Y si se ha pasado una lista de tipos, se seleccionan sólo aquellas reglas cuyo tipo asociado figure en esa lista.

Observa el uso del operador “or”. Una expresión del tipo “a or b” retorna valor true si a es cierto o, caso de que a sea falso, si b es cierto. Y en otro caso retorna false.

Ahora myrules contiene las reglas aplicables. Vamos a la instrucción más extraña de todas:
match, type = myrules.collect{|r| [r[:regexp].match(@str), r[:type]]}.max{|x,y| x[0].to_s.length <=> y[0].to_s.length}


Rara por varias cosas. En primer lugar porque es una asignación, pero en el lado de la izquierda hay dos variables separadas por comas en lugar de sólo una. Ruby permite asignar los elementos de un array a varias variables a la vez.
irb(main):012:0> v1, v2, v3 = [1, 3, 5]
=> [1, 3, 5]
irb(main):013:0> v1
=> 1
irb(main):014:0> v2
=> 3
irb(main):015:0> v3
=> 5


Luego, deducirás, lo que hay a la derecha del operador de asignación debe retornar un array de al menos dos elementos. ¡Bien! ¡Se ve que te vas haciendo con la filosofía de Ruby!

Y... ¿qué contiene ese array?

Vamos por partes. En primer lugar, aplicamos el método “collect”. Para cada elemento de myrules, se compara su expresión regular con @str, nuestra cadena de texto a analizar. Después se crea un array de dos elementos: la coincidencia encontrada (que bien podría ser de longitud cero si no se ha encontrado nada) y el tipo de token asociado a la regla aplicada.

Y con todos ellos se crea un nuevo array. Eso significa lo de:
myrules.collect{|r| [   r[:regexp].match(@str),    r[:type]  ]    }


Lo ideal sería que, como mucho, existiera una única coincidencia de longitud mayor que cero. Pero... ¿y si hay más de una? En ese caso, nos quedaremos con la que se corresponda a una cadena más larga (otra vez la regla de oro del burro grande). Para eso utilizamos el método “max”, que permite obtener el máximo valor de un array.

Comparando lo incomparable

Si el array contuviera números, está claro cuál sería su máximo: el mayor de todos ellos. Si contuviera cadenas, es de esperar que se eligiera la que álfabéticamente queda en último lugar. Pero estamos procesando un array de arrays... ¿cómo se comparan unos arrays formados por dos elementos: un MatchData y, previsiblemente, un Symbol?

Bueno, Ruby quizá no lo sepa, pero sabe que nosotros sí. Que tenemos un criterio en mente. Por eso se le puede pasar a “max” un bloque de código que sirva para evaluar si un elemento es mayor o menor que otro. El valor retornado por el bloque debe usar la misma convención que el operador “<=>”: positivo si el primer valor a comparar se considera mayor que el segundo, negativo si menor y cero si son iguales.

El criterio en este caso es la longitud de la coincidencia encontrada. O sea:
{|x,y| x[0].to_s.length <=> y[0].to_s.length}

Donde x e y son arrays de dos elementos. El primer elemento es una coincidencia MatchData y, el segundo, el tipo asociado. 

De modo que x[0] e y[0] son las coincidencias. Comparamos sus longitudes... y la mayor de ellas gana y es la seleccionada.

Y ahora que lo tenemos, vamos a ver qué hacemos con ello. Lo primero es comprobar si la cadena que se ha encontrado es vacía o no. Si no es vacía la procesaremos y si sí lo es retornaremos un valor nil, puesto que no hemos encontrado nada.

Para realizar este tipo de cosas son útiles las sentencias “if … else”, cuyo formato es:
if condición
    acciones a realizar si la condición es cierta
else
    acciones a realizar si la condición es falsa
end



Volviendo a nuestro método tenemos:
string = match.to_s


if string != ''
    # Removed matched text from the input
    @str = match.post_match

    # Subclasses may define methods to process each type
    if private_methods.include?(type)
        return send(type.to_s, match)
    else
        return {:type=>type, :string=>string}
    end
else
    return nil
end



De haber encontrado algo, lo primero es quitarlo de @str, de modo que no se lo vuelva a reconocer la próxima vez que se llame a getToken. Para ello usamos el método “post_match” que devuelve el texto de la cadena que va detrás de la coincidencia.

La pregunta del millón

Y después hacemos una pregunta curiosa: ¿Incluye private_methods, la lista de los métodos privados de nuestra clase, uno cuyo nombre sea igual al valor representado por el tipo de la regla seleccionada? O sea, y por poner un ejemplo, si la regla tuviera asignado el tipo :entero... ¿Tiene la clase un método privado llamado “entero”?

Pero ¿acaso no sabemos nosotros, que estamos haciendo la clase, si tenemos o no algún método privado y cómo se llama? ¿a qué viene esta pregunta?

Bueno, de hecho esta clase no va a tener ningún método privado. Así que la cosa estaría clara. Pero recuerda lo de la herencia y las subclases. Quién sabe qué van a tener las subclases que podamos crear a partir de ahora.

Si el método privado existe, ejecutamos otra instrucción llamativa:
send(type.to_s, match[0])



El método send es una forma de invocar los métodos aplicables a un objeto. Es lo mismo poner
3 + 1



… que
3.+(1)



… que
3.send('+',1)



… que
3.send(:+,1)


Fíjate: al usar “send”, en los parámetros primero se pone el identificador del método a invocar, como cadena o como Symbol, y después, si es necesario, los parámetros que hay que pasarle.

Nosotros usamos “send” para que futuras subclases puedan definir qué hacen cada vez que encuentran algo. Y, por si no lo hacen, prevemos que por defecto vamos a retornar un hash de dos elementos, de índices :type y :string, que contienen el tipo de token encontrado y la cadena reconocida.


Pues ya hemos casi acabado. Para simplificar las cosas podemos definir un método que saque todos los tokens de una cadena, uno trás otra, y cree un array con ellos:
def getTokens(types=nil, str=nil)
    if str != nil
        loadStr(str)
    end


    return_value = []

    while (r = getToken(types)) != nil
        return_value.push(r)
    end

    return return_value
end



Lo único que llama aquí la atención es eso del while:
while (r = getToken(types)) != nil
    return_value.push(r)
end



La construcción while realiza una comprobación y, si es falsa, no hace nada. Si es cierta, ejecuta un conjunto de instrucciones y, cuando las termina, vuelve a empezar con la comprobación.

La condición consiste en comparar dos valores y ver si son distintos. El primer valor es el resultado de evaluar
(r = getToken(types))



Una asignación. Asignamos a “r” el resultado de obtener un token. Recuerda que una asignación se evalúa al valor asignado. En nuestro caso, el token.

Si eso es distinto de nil es que algún token encontramos. Así que cogemos ese token y lo añadimos al final del array return_value:
return_value.push(r)



Para, cuando todo acabe, retornar este array:
return return_value



Ya está todo listo...

La clase completa

Y esto es lo que tenemos:
# Class to parse a text and extract tokens from it
class Tokenizer
    # Object creation. The initial text may be provided / Creación del objeto. Se le puede dar el texto inicial
    def initialize(str='')
        @rules = []
        loadStr(str)
    end


     # Load initial text / Cargar el texto inicial
    def loadStr(str='')
        @str = str
    end


    # Add a rule for token recognition / Añadir regla para reconocimiento de tokien
    # Parameters / Parámetros:
    # regexp: String or RegExp with the regular expression. If it doesn't start with \A, \A will be appended at the start
    # Cadena o Expresión Regular con la expresión regular. Si no empieza por \A, se le añade al principio
    # type: Symbol recommended. A type for the regexp. Several regexp may share a common type
    # Recomendable que sea Symbol. El tipo asociado a regexp. Varias regexp pueden compartir tipo
    # opts: Options when applying the regexp
    # Opciones para aplicar regexp
    def addRule(regexp, type, opts = Regexp::MULTILINE + Regexp::IGNORECASE)
        str = case regexp.class.name
            when 'Regexp' then regexp.source
            when 'String' then regexp
            else raise 'Unsupported type for regexp'
        end


         if str[0..1] != '\A'
            str = '\A' + str
        end


        myregexp = Regexp.new(str, opts)

        @rules.push({:regexp=>myregexp, :type=>type})
    end



     # Utility function to add several rules at once from an array of hashes with indexes :regexp and :type
    # Función de ayuda para añadir varias reglas de una vez a partrir de un array de hashes con     índices :regexp y :type
   # Parameters / Parámetros:
    # array: the array / el array
    # opts: options for regexp / opciones para aplicar las regexp
    def addRules(array, opts = Regexp::MULTILINE + Regexp::IGNORECASE)
        array.each do |r|
                addRule(r[:regexp], r[:type], opts)
        end
    end


     # True if there is nothing more to read
    def endOfString?
        return @str == ''
    end


     # Get the next token. An array with the allowed types may be provided
    # Returns nil if no match was found
    def getToken(types=nil)
        if endOfString?
            return nil
        end


         # Select applicable rules
        myrules = @rules.select{|r| types == nil or types.include?(r[:type])}


        # Get longest match
        match, type = myrules.collect{|r| [r[:regexp].match(@str), r[:type]]}.max{|x,y| x[0].to_s.length <=> y[0].to_s.length}


        string = match.to_s

        # If found, process output. Else return nil
        if string != ''
            # Removed matched text from the input
            @str = match.post_match

            # Subclasses may define methods to process each type
            if private_methods.include?(type)
                return send(type.to_s, match)
            else
                return {:type=>type, :string=>string}
            end
        else
            return nil
        end
end


    # Get tokens from a String. A list of applicable types and a string may be provided
    def getTokens(types=nil, str=nil)
        if str != nil
            loadStr(str)
       end

        return_value = []
        while (r = getToken(types)) != nil
            return_value.push(r)
        end
       return return_value
    end
end #class Tokenizer

Guárdalo en un fichero llamado “tokenizer.rb” y... experimenta un poco.

No hay comentarios:

Publicar un comentario