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.

martes, 26 de febrero de 2013

Representar cosas de HTML

Ya es hora de hacer algo útil

Bueno, cuatro capítulos dando rodeos es, creo, suficiente. Vamos ya con las primeras clases “interesantes” para nuestro analizador de código HTML: aquellas que representan los distintos elementos que podemos encontrar en este tipo de documentos.

A los efectos que nos ocupan, es conveniente distinguir cinco tipo de elementos:
  • Las etiquetas de apertura, como “<b>”, “<hr />” o “<a href='http://418iamateapot.blogspot.com'>”. 
    En ellas se pueden reconocer tres elementos fundamentales: el nombre, la opcional lista de atributos y, a veces, el carácter “/” antes del “>” para indicar que la etiqueta no va a tener un cierre más adelante.
  • Las etiquetas de cierre como “</a>” que indican dónde acaban los “efectos” de una etiqueta abierta anteriormente.
  • Los textos independientes
  • Los comentarios, como “<!-- Esto es un comentario -->”
  • Las definiciones, construcciones incorrectas y “otras cosas” que HTML admite sin que sean parte del lenguaje.

Así que abre tu editor de textos y crea un fichero llamado “htmlthings.rb” en la misma carpeta en la que guardaste anteriormente “attributes.rb” y “nocase.rb”.


Eso de las rutas

Vamos a ver, paso por paso, qué hay que escribir para que “htmlthings.rb” sirva para algo. Lo primero es incluir las clases Attributes y Nocase que ya tenemos definidas. Algo que, como ya vimos, se puede conseguir con unas líneas como las siguientes:
require './attributes.rb'
require './nocase.rb'

¡Pero no escribas nada todavía! Piensa un poco. ¿Qué significa eso de './attributes.rb'? El fichero se busca en el directorio actual. Bueno, eso funciona... siempre que nuestro directorio actual sea el mismo en el que están las cosas. Pero... ¿y si el usuario ni siquiera sabe dónde se alojan los scripts? ¿y si los puede ejecutar porque se encuentran en uno de los directorios donde su sistema operativo busca los programas (eso que llaman "el PATH")?

Lo que necesitamos es alguna forma de determinar en tiempo de ejecución la ruta en que se encuentran los scripts de Ruby. Y hay forma de hacerlo. Para empezar, con __FILE__ se obtiene una String con la ruta del script que se está ejecutando.

Ahora sólo hace falta determinar el directorio correspondiente a dicha ruta. Y la clase File, que está disponible “de fábrica” tiene un método llamado dirname que hace eso. Así que, uniendo ambos queda:

require File.dirname(__FILE__) + '/attributes.rb'
require File.dirname(__FILE__) + '/nocase.rb'

Ahora sí. Copia estas dos líneas a tu fichero.

Varias clases que no hacen nada

Y ahora comencemos a especificar cómo funcionan nuestros objetos. Para tres de los tipos enumerados al principio, textos, comentarios y lo que antes denominamos “otras cosas”, no vamos a necesitar demasiados detalles. Por eso, sus correspondientes clases se especificarán como subclases de String.

No hay problema en que juntemos más de una clase en un mismo fichero. Empecemos con estas tres:

# HTML Text, comments and other things / Textos, comentarios y otras cosas de HTML


class HTMLText < String
end


class HTMLComment < String
end


class HTMLOther < String
end

Copia. Copia sin miedo. Ya después, si es que nos hace falta, podremos cambiarlo.


La clase de la etiqueta de cierre

Para las etiquetas de cierre, sólo nos interesa saber el nombre. Añade esta clase
# HTML Closing Tag / Etiqueta de Cierre HTML
class HTMLClose

# Closing Tag Data / Datos de la etiqueta de cierre
    attr_reader :name


# Object creation / Creación de objetos
    def initialize(name)
        @name = Nocase.new(name)
    end


# Convert to string / Convertir a cadena
    def to_s
        '</' + @name.downcase + '>'
    end
end

¿Has terminado ya de copiarlo? Pues expliquemos unas cuantas cosas.

Ya hemos visto algunos ejemplos de uso de variables y asignaciones de valores en los capítulos anteriores. Verdad que pasamos de puntillas, como quien no hace la cosa, pero es que es algo que, en principio, no precisa demasiadas explicaciones. Pero, por si acaso...

Variables de instancia

Una variable en Ruby es una referencia a un objeto. Una forma de acceder a él. Una referencia a la zona de memoria en que el objeto está ubicado.

O, en otras palabras, la variable NO CONTIENE EL VALOR O LOS VALORES DEL OBJETO, sino que nos dice “dónde vive”.

Y eso tiene sus repercusiones. Por ejemplo, si a una variable le asignamos otra, ambas referenciarán al mismo objeto. Y cualquier cambio que se haga a una afectará a la otra. En el próximo ejemplo (no vayas a copiarlo en el fichero, que es para que experimentes con irb), a y b señalan al mismo objeto:

irb(main):001:0> a = '1234'
=> "1234"
irb(main):002:0> b = a
=> "1234"
irb(main):003:0> a.insert(0,'abcd')
=> "abcd1234"
irb(main):004:0> a
=> "abcd1234"
irb(main):005:0> b
=> "abcd1234"

Observa cómo se define el método initialize (no lo copies, que ya lo hiciste antes ¡ayyy, que cruz!):
def initialize(name)
    @name = Nocase.new(name.strip)
end

Eso de @name es... una variable especial. Cuando una variable comienza por una arroba “@”, eso quiere decir que cada instancia de la clase, cada objeto que creemos, tendrá asociada una variable con dicho nombre.

En este caso, cada objeto de la clase HTMLClose tendrá una variable de instancia llamada @name. A efectos prácticos, la variable se crea en el momento en que se le asigna un valor por primera vez. Y lo que usamos como nombre es una de esas cadenas que creamos en el capítulo anterior y que no distinguen entre mayúsculas y minúsculas.

En cuanto a “strip”... nada que ver con eso que estás pensando. Aplicado a una String, strip retorna el resultado de eliminar de aquella todos los espacios iniciales y finales que pudiera tener.

De alguna forma habrá que poder acceder

En principio, la variable @name no estará accesible más que para el propio objeto al que pertenece. Si queremos que pueda referenciarse desde fuera, formas hay de hacerlo. Observa esto que ya copiaste al fichero “htmlthings.rb”

attr_reader :name

Con eso se indica que, desde fuera, se puede acceder a @name sólo para lectura. Sólo para consultar su valor.

Si se quisiera poder modificar su valor, pero no leerlo, en lugar de attr_reader se utilizaría attr_writer. Y si se deseara poder leer y modificar pondríamos attr_accessor.

En los tres casos, se puede indicar más de un atributo, separándolos con comas.

Por si necesitamos convertirlo en cadena...

La clase se completa con la definición del método to_s, que se suele usar para realizar la conversión a cadena
def to_s
    '</' + @name.downcase + '>'
end


Y otra clase para las etiquetas

La clase HTMLTags definirá una etiqueta de HTML, con su nombre, sus atributos y todo lo demás. Vamos a ir viendo poco a poco sus métodos y lo ponemos todo junto al final.

Como variables de instancia vamos a tener el nombre, el hash de atributos y un indicador de si la etiqueta termina en “/>” y, por tanto, se cierra ella misma solita.

Por un lado, el nombre no debe distinguir entre mayúsculas y minúsculas. Y, por otro, tampoco los nombres de los atributos, que para eso creamos antes la clase Attributes. Con todo ello en mente, podemos empezar por:
# Access to Tag Data / Acceso a los datos de la Etiqueta
attr_reader :name, :autoclosed, :attributes


# Object Creation / Creación de objetos
def initialize(name, autoclosed=false, attributes={})
    @name = Nocase.new(name.strip)
    @autoclosed = autoclosed
    @attributes = Attributes.new(attributes)
end

Ahora vamos a ver cómo convertir en una cadena un objeto de la clase HTMLTag. Para empezar, habrá que poner un “<” seguido del nombre de la etiqueta. Y, a continuación, los atributos con sus valores.

La lista de atributos es un hash en el que los índices son los nombres de sus atributos y sus correspodientes items sus valores. Para cada par “indice => valor” del hash, habría que poner un espacio separador, el índice, un signo igual “=” y el valor entre comillas. Así si tenemos un elemento como
'href' => 'http://418iamateapot.blogspot.com'

… nos quedaría
href="http://418iamateapot.blogspot.com"

Además, si el valor del atributo contuviera alguna comilla doble ("), ésta debe ser sustituida por “&quote;”. Bien ya sabemos qué queremos hacer.

Ahora, a ver cómo se consigue.

Reemplazando en cadenas

Lo de reemplazar una cadena por otra es cosa sencilla. String tiene un método llamado “gsub” que sustituye una cosa, todas las veces que aparezca, por otra. Más o menos así:
cadena.gsub(/"/, '&quote;')

Lo de /"/ te parecerá una forma rara de poner una cadena. Tan raro como que no lo es. En realidad se trata de una cosa muy interesante llamada “expresión regular”. Pero, por ahora, conténtate con saber que /"/ representa una comilla doble. En las próximas entregas tendremos expresiones regulares para hartarnos.

Existe otro método, “sub”, que hace lo mismo que “gsub” pero sólo sustituye las cosas la primera vez que las encuentra.

Otro detalle: ni “sub” ni “gsub” modifican la cadena a la que se aplican. Si quisiérmaos alterarlas existen otros métodos que sí lo hacen. Prueba esto en irb:
irb(main):013:0* a = 'a"b"c'
=> "a\"b\"c"
irb(main):014:0> a.gsub(/"/, '&quote;')
=> "a&quote;b&quote;c"
irb(main):015:0> a # Comprobemos que el valor de a no ha cambiado
=> "a\"b\"c"
irb(main):016:0> a.gsub!(/"/, '&quote;')
=> "a&quote;b&quote;c"
irb(main):017:0> a # Con gsub! Sí cambia
=> "a&quote;b&quote;c"

Eso de poner una exclamación “!” al final de las nombres de las versiones de métodos que modifican el valor del objeto es algo habitual. No es obligatorio, pero la gente suele hacerlo.

Procesar todo un hash de una vez

En cuanto a lo de ir recorriendo uno a uno los elementos del hash de atributos, generando la cadena correspondiente y todo lo demás, Ruby permite una notación bastante compacta gracias a uno de los métodos de la clase Hash, denominado “collect”.

collect recorre todos los elementos del hash, uno a uno. Y, para cada uno, genera un valor (tendremos que decirle cómo, claro). Finalmente, con todos esos valores que ha generado, forma un array y nos lo retorna.

Queda por saber cómo se le indica la forma de calcular los valores a partir de cada elemento. Veamos un ejemplo:

@attributes.collect {|x,y| '  ' + x + '="' + y.gsub(/"/,'&quote;') + '"'}

Lo que se pone a continuación de “collect” es un bloque de código. Ya vimos otra notación para los bloques de código antes. Algo como:
do |x,y|
instrucciones
end

Pues bien, eso es equivalente, más o menos, a
{|x,y| instrucciones }

El bloque de código que se le pasa a “collect” tiene dos “parámetros”. El primero es el índice del item y el segundo su valor asociado. Con ellos, el bloque genera el correspondiente texto siguiendo las directrices que nos marcamos antes.

Ya tenemos un array con las cadenas correspondientes a cada atributo. Ahora necesitaríamos unirlas. Y de nuevo hay un método, en este caso de la clase Array, que sirve precisamente para eso. Se llama join
irb(main):018:0> ['a', 'b', 'c'].join
=> "abc"

Con ello, la expresión para pasar los atributos a cadena quedaría
@attributes.collect {|x,y| ' ' + x + '="' + y.gsub(/"/,'&quote;') + '"'}.join

Después de los atributos viene el cierre de la etiqueta, que vendrá dado por un carácter “>” o, si la etiqueta se cierra ella misma, la secuencia “/>”. En definitiva, la barra “/” se pondrá sí y solo sí la etiqueta se “autocierra”.

Para que el valor de una expresión dependa de una expresión se puede usar un operador quizá conocido para quienes conozcan otros lenguajes de programación:
condicion ? valor_si_cierto : valor_si_falso

O, en nuestro caso, para que se ponga la barra si @autoclosed es cierto y nada si no lo es:
@autoclosed ? '/' : ''

Pongámoslo ahora todo junto y tendremos:
def to_s
    '<' + @name.downcase + @attributes.collect {|x,y| ' ' + x + '="' + y.gsub(/"/,'&quote;') + '"'}.join +
         (@autoclosed ? '/' : '') + '>'

end





En definitiva, la clase HTMLTag quedará:

Esto es lo que tienes que copiar

class HTMLTag
    # Access to Tag Data / Acceso a los datos de la Etiqueta
    attr_reader :name, :autoclosed, :attributes


     # Object Creation / Creación de objetos
    def initialize(name, autoclosed=false, attributes={})
        @name = Nocase.new(name.strip)
        @autoclosed = autoclosed
        @attributes = Attributes.new(attributes)
    end


    # Convert Tag to string / Convertir la etiqueta en cadena
    def to_s
       '<' + @name.downcase + @attributes.collect {|x,y| ' ' + x + '="' + y.sub(/"/,'&quote;') + '"'}.join
        + (@autoclosed ? '/' : '') + '>'
    end
end


Pongámoslo todo junto



El fichero “htmlthings.rb” quedará finalmente como sigue:
.
require File.dirname(__FILE__) + '/attributes.rb'
require File.dirname(__FILE__) + '/nocase.rb'


# HTML Tag / Etiqueta HTML
class HTMLTag
    # Access to Tag Data / Acceso a los datos de la Etiqueta
    attr_reader :name, :autoclosed, :attributes

    # Object Creation / Creación de objetos
    def initialize(name, autoclosed=false, attributes={})
        @name = Nocase.new(name.strip)
        @autoclosed = autoclosed
        @attributes = Attributes.new(attributes)
    end

    # Convert Tag to string / Convertir la etiqueta en cadena
    def to_s
        '<' + @name.downcase + @attributes.collect {|x,y| ' ' + x + '="' + y.sub(/"/,'&quote;') + '"'}.join
        + (@autoclosed ? '/' : '') + '>'
    end
end


# HTML Closing Tag / Etiqueta de Cierre HTML
class HTMLClose
    # Closing Tag Data / Datos de la etiqueta de cierre
    attr_reader :name

    # Object creation / Creación de objetos
    def initialize(name)
        @name = Nocase.new(name)
    end


    # Convert to string / Convertir a cadena
    def to_s
        '</' + @name.downcase + '>'
    end
end


# HTML Text, comments and other things / Textos, comentarios y otras cosas de HTML


class HTMLText < String
end

class HTMLComment < String
end


class HTMLOther < String
end



martes, 19 de febrero de 2013

Compare, compare (2 de 2)

Probando la clase Nocase

¡Eso! Ya la puedes probar con irb:

irb(main):001:0> require './nocase.rb'
=> true
irb(main):002:0> a = Nocase.new('abcde')
=> "abcde"
irb(main):003:0> 'abcde' <= 'ABCDE' # En String, las minúsculas tienen valor superior a las minúsculas
=> false
irb(main):004:0>
irb(main):005:0* a <= 'ABCDE'  # En Nocase, no
=> true
irb(main):006:0> a < 'AC'
=> true
irb(main):007:0> a == 'aBcDe'
=> true
irb(main):008:0>


Cosa curiosa


Cuando Ruby compara una String con un Nocase... ¿cómo decide si utiliza el método de la clase String o el de Nocase?

La regla a seguir es: se usa el método de la clase a la que pertenezca el primero de los operandos. Eso hace que a veces se produzcan situaciones extrañas. Sigamos probando cosas:

irb(main):009:0>  b = Nocase.new('a')
=> "a"
irb(main):010:0> 'A' == b
=> false
irb(main):011:0> b == 'A'
=> true


Una comparación puede ser cierta o falsa dependiendo de cómo se haga. 'A' no es igual que el valor de a y, sin embargo, el valor de a es igual que 'A'...

La explicación: en el primer caso ('A' == b), como 'A' es una String, se usa la comparación de esta clase, que distingue entre mayúsculas y minúsculas. Por su parte, en el segundo (b == 'A'), como la variable “a” referencia un objeto Nocase, se usa la comparación de Nocase, que no entiende de tamaños.

A mi, tal y como suelo hacer las comparaciones, esto no me supone un problema. Pero quizá a tí sí...

¿Tiene solución?

Sí, sí que la tiene.

El problema se produce cuando el primer operando es una String y, por tanto, se usa el método de esta clase. Por lo tanto, únicamente hace falta redefinir los métodos de la clase String.

¿Únicamente? ¿Puedo redefinir los métodos existentes de clases existentes?+

Sí. Sin ningún problema. Basta con volver a abrir la clase String y añadirle el código que queramos. Y es que en Ruby siempre vamos a poder modificar las cosas de las clases que tenemos.

En la misma carpeta donde pusiste “nocase.rb”, crea un fichero llamado “prueba.rb” con el siguiente contenido:

require './nocase.rb'
class String
private
 # Garantizando que se podrán referenciar los métodos originales

 # dentro de los nuevos
 alias_method :tmp_method_for_equal, :==


public 
 # Redefinición de ==
 # Si x es de clase Nocase, se realiza la comparación utilizando el

 # método de Nocase.
 # En otro caso, se usa el método original de la clase String
 def ==(x)
  if x.class.name.tmp_method_for_equal('Nocase')
   return Nocase.new(self) == x
  else
   return tmp_method_for_equal(x)
  end
 end
end


En “prueba.rb” se redefine el método “==” de la clase String de forma que, si el valor con el que se compara la cadena es de la clase Nocase, se utilice el método de Nocase mientras que para el resto de casos se use el método original de la clase String.

Pero ahora nos surge una nueva duda: Al redefinir  “==” perderemos el comportamiento original de este método y no podremos utilizarlo.

Una solución consistiría en hacerle un alias antes de la redefinición y utilizar posteriormente este alias. Lo cual resuelve el problema pero a costa de añadir un nuevo método, el alias, a la clase String.

Si este método no es necesario para el uso de la clase... cuanto menos se puede decir que esto queda feo. Menos mal que Ruby tiene formas de que las cosas no se vean. Observa que al principio de nuestra redefinición de String aparece la palabra “private”. Eso quiere decir que los métodos que se especifican a continuación no van a estar accesibles para los programas ni para otros objetos. Son  como cosas “secretas” que cada objeto de nuestra clase guarda para sí. Ni siquiera los pone a disposición de otros objetos de la misma clase.

Rincones bajo las alfombras donde se supone que nadie mira.

Más adelante, tras la definición del alias, nos encontramos con la palabra “public”. Lo que le sigue ya sí es otra vez “público”. O sea, visible para el resto de objetos y programas. De modo que éste es el sitio donde declarar los métodos que queramos que se puedan invocar desde fuera de nuestra clase.

Y lo que econtramos ahí es la redefinición de “==”. Algo que hay que hacer con cuidado de evitar comparar cadenas utilizando “==” o crearíamos una cadena de llamadas recursivas que acabaría con el espacio disponible para la pila y, de camino, con el funcionamiento normal del programa. En lugar de “==”, se utiliza el alias creado.

Y ya podemos probar
irb(main):001:0> require './prueba.rb'
=> true
irb(main):002:0> a = Nocase.new('abcde')
=> "abcde"
irb(main):003:0> 'abcde' == 'ABCDE' # Con dos cadenas,distingue
=> false
irb(main):004:0> a == 'ABCDE'       # Con un Nocase y una cadena no
=> true
irb(main):005:0> 'ABCDE' == a       # Con una cadena y un  Nocase no
=> true


Ya sabes cómo se hace. Ahora si quieres redefinir los otros métodos... ¡es tu turno!

Compare, compare (1 de 2)


El objetivo

Resuelto en el capítulo anterior el tema de los atributos, fijémonos ahora en los nombres de las etiquetas. Como ya se dijo, a HTML no le preocupa demasiado eso de las diferencias entre mayúsculas y minúsculas.

Para tenerlo en cuenta, vamos a crear ahora un subtipo de String que redefina los métodos de comparación de acuerdo con nuestros intereses. Pero... ¿cuáles son estos métodos? Y... ¿cómo funcionan?

Buenas preguntas. Porque el enfoque de Ruby es cuando menos... peculiar. Empezando porque una cosa es comprobar si dos objetos son iguales y otra determinar si son distintos.

Comencemos por el último caso.

Comparaciones de desigualdad

Las comprobaciones de desigualdad nos indican si un objeto es, o no, mayor o menor que otro. Y para realizarlas tenemos los conocidos operadores “>”, “>=”,  “<” y “<=”.

En Ruby, estos operadores se implementan como métodos de las clases sobre las que se realizan las comprobaciones. Y, por fortuna, el comportamiento de todos ellos  para una determinada clase se puede modificar redefiniendo un único método denominado “<=>”. De hecho, se pueden realizar comparaciones directamente con él, como en las siguientes pruebas con irb:

irb(main):001:0> 23 <=> 15
=> 1
irb(main):002:0> 23 <=> 1333
=> -1
irb(main):003:0> 23 <=> 23
=> 0
irb(main):004:0>


Si “<=>” retorna un valor positivo, eso quiere decir que el primer operando comparado es mayor que el segundo. Si es negativo, el mayor es el segundo. Y si son iguales, retorna cero.

Por ahora, la cosa va siendo sencillita. Pero tengo una mala noticia: al redefinir “<=>” sólo se modifica el comportamiento de las comparaciones de desigualdad. Las comparaciones de igualdad van por otro camino.

Y es un camino lleno de curvas y baches...

¿Iguales?

Si para las cuatro comparaciones de desigualdad basta con un método, para la de igualdad hay no menos de cuatro métodos. Cosas de la vida. Unos tanto y otros tan poco. Pero vayamos por partes.

El primer método es “==”. Como en:

irb(main):006:0> 23 == 23
=> true


Como se indica en la documentación de Ruby, cuando se trata la clase Object, en principio, “==” retorna un valor cierto si y solo si los dos objetos comparados son el mismo. Éste es el comportamiento por defecto de todos los operadores de comparación de igualdad a este nivel.

Lo que ocurre es que, después, cuando se definen clases concretas, lo normal es que algunos de estos métodos se redefinan para darle un significado más concreto y acorde con la clase concreta. Así, para números y cadenas, “==” retorna cierto si y solo sí los valores comparados son iguales, aunque se trate de objetos distintos.

Otro operador de comparación es “===” (tres signos "=" seguidos). Con él pasa lo mismo que con “==”. La principal diferencia es que “===” se usa en las comparaciones en las sentencias “case” (similares a las “switch” o las “case” de otros lenguajes de programación).

Y hay más: “eql?” tiene como objetivo comprobar si los valores representados por dos objetos son iguales o no. Y, aunque no sea demasiado frecuente, puede estar redefinida de forma distinta a “==”. Así ocurre en la clase Fixnum cuando la comparación se realiza con un número en coma flotante (clase Float): mientras “==” realiza las conversiones de tipo, “eql?” no lo hace, con lo que a veces los resultados de este último método son poco “intuitivos”:

irb(main):008:0> 2 == 2.0
=> true
irb(main):009:0> 2.eql?(2.0)
=> false


Sí, en el último caso, 2 no es lo mismo que 2.0 porque uno es un número entero y el otro uno en coma flotante.

Y, para acabar, está “equal?”. Este método no debería nunca ser redefinido y retorna un valor cierto sí y solo sí los dos objetos comparados son el mismo.

Para más detalles, puede consultarse:
http://ruby-doc.org/core-1.9.3/Object.html

Donde lo de 1.9.3 es la versión de Ruby sobre la que se consulta la documentación. Otras versiones tendrán otras URLs.

La clase vacía

Dicho todo esto, vamos a empezar a crear nuestra nueva clase. En ella, los métodos “==”, “===” y “eql?” se comportarán todos de la misma forma: comparando los valores de dos cadenas sin tener en cuenta mayúsculas y minúsculas.

El esqueleto de la clase sería
# Cadena con comparaciones no sensibles a mayúsculas y minúsculas
class Nocase < String
 # Compara dos valores
 def <=>(x)
 end

 # Comparar igualdades
 def ==(x)
 end
 
 alias_method :===, :==
 alias_method :eql?, :==
end



Aliases

La principal novedad está al final de la clase:

 alias_method :===, :==
 alias_method :eql?, :==


Con alias_method estamos creando un nuevo método que hace exactamente lo mismo que otro. Así, si nos fijamos en la primera de las dos líneas anteriores, para Nocase (y para sus subclases, en tanto no redefinan demasiadas cosas), el método “===” se comportará exactamente igual que “==”. Dos nombres distintos para la misma cosa.

A efectos prácticos, alias_method nos ahorra aquí el tener que repetir el mismo código varias veces. De ese modo, reducimos el riesgo de equivocarnos o de mantener incorrectamente la especificación de la clase. Y nos ahorramos de escribir, que para mí eso también es un aliciente.

Fíjate también: los nombres de los métodos se indican mediante Symbols. Eso suele ser frecuente, así que no lo pierdas de vista.

A lo que nos interesa

¡Vamos a rellenar esos métodos! Comencemos por “<=>”. Ya existe uno para la clase String, pero queremos que el nuestro no tenga en cuenta los “tamaños” de las letras. Si fuera una comparación de cadenas, y si nos atenemos a lo que ya sabemos, nos valdría con algo como:
cadena1.downcase <=> cadena2.downcase

Pero no podemos definir el método “<=>” de Nocase en función de sí mismo. Eso produciría una serie infinita de llamadas recursivas. Bueno, infinita no. Sólo hasta que nos carguemos la (algo reducida) pila que el entorno de ejecución de Ruby proporciona. Y, entonces, nuestro programa cascaría con un mensaje de error.

Afortunadamente, sí que podemos hacer uso del método “<=>” de la clase String.  Porque podemos convertir nuestro objeto Nocase en una cadena de las normalitas utilizando el método “to_s”. Así que podría escribirse:

 def <=>(x)
  return self.to_s.downcase <=> x.downcase
 end


Habría muchas otras formas de conseguir este mismo resultado. Lo bueno de usar programación orientada a objetos es que lo que importa son los resultados. Eso hace que podamos cambiar cómo funciona un método o una clase por dentro sin que nada se rompa, siempre y cuando los valores retornados y las acciones realizadas sean las mismas.

Una alternativa que funciona por lo general mucho más rápido que la anterior es usar el método “casecmp”, definido para la clase String. Y que realiza, mira por donde, la misma función que “<=>” pero sin distinguir mayúsculas de minúsculas:

irb(main):013:0> 'ABCDE'.casecmp('abcda')
=> 1


¡Justo lo que queríamos! Con ello nos podría quedar una definición de “<=>” muy sencillita:
 alias_method :<=>, :casecmp

Quédate con la implementación que más te guste. La que usa “downcase” es más lenta. La que establece “<=>” como un alias de “casecmp” podría tener problemas de recursividad si alguna vez “casecmp” estuviera definida a nivel interno usando “<=>”.

Yo, por ahora, me quedo con la segunda. Y mi clase queda así:

# Cadena con comparaciones no sensibles a mayúsculas y minúsculas
class Nocase < String
 # Compara dos valores
 alias_method :<=>, :casecmp


 # Comparar igualdades
 def ==(x)
  (casecmp(x) == 0)

 end
 
 alias_method :===, :==
 alias_method :eql?, :==
end