Programando en Ruby

La Guía de los "Programadores Pragmáticos"

Standard Types



Hasta ahora nos hemos estado divirtiendo implementando partes de nuestra jukebox, pero hemos sido negligentes. Hemos visto arrays, tablas hash, y procs, pero no hemos cubierto realmente los otros tipos básicos en Ruby: numeros, cadenas de texto, rangos y expresiones regulares. Vamos a gastar un par de páginas en esos bloques de construcción básicos.

Números

Ruby soporta números enteros y en coma flotante. Los enteros pueden ser de cualquier longitud (hasta un máximo determinado por el total de memoria libre disponible en tu sistema). Los enteros dentro de un cierto rango (normalmente -230 hasta 230-1 o -262 hasta 262-1) son tratados internamente en formato binario, y son objetos de la clase Fixnum. Los enteros fuera de este rango son almacenados en objetos de la clase Bignum (actualmente implementados como como un conjunto de longitud variable de pequeños enteros). Este proceso es transparente, y Ruby gestiona automáticamente la conversión en un sentido y otro.

num = 8
7.times do
  print num.type, " ", num, "\n"
  num *= num
end
produces:
Fixnum 8
Fixnum 64
Fixnum 4096
Fixnum 16777216
Bignum 281474976710656
Bignum 79228162514264337593543950336
Bignum 6277101735386680763835789423207666416102355444464034512896

Tu escribes enteros utilizando un simbolo inicial opcional, un indicador de base también opcional (0 para octal, 0x para hexadecimal, o 0b para binario), seguido por una cadena de dígitos en la base apropiada. Los caracteres de subrayado son ignorados en la cadena de dígitos.

123456                    # Fixnum
123_456                   # Fixnum (underscore ignored)
-543                      # Negative Fixnum
123_456_789_123_345_789   # Bignum
0xaabb                    # Hexadecimal
0377                      # Octal
-0b101_010                # Binary (negated)

También puedes obtener el valor entero correspondiente a un caracter ASCII o secuencia de escape precediendolo con el signo de interrogación. Combinaciones de control y meta pueden ser también generadas utilizando ?\C-x, ?\M-x, y ?\M-\C-x. La versión de control de un valor es la misma que ``value & 0x9f''. La meta versión de un valor es ``value | 0x80''. Finalmente, la secuencia ?\C-? genera un borrado ASCII, 0177.

?a                        # character code
?\n                       # code for a newline (0x0a)
?\C-a                     # control a = ?A & 0x9f = 0x01
?\M-a                     # meta sets bit 7
?\M-\C-a                  # meta and control a
?\C-?                     # delete character

Un literal numérico con un punto decimal y/o un exponente es convertido en un objeto Float, correspondiendo a tipo de datos double correspondiente a la arquitectura nativa. Debes continuar el punto decimal con un dígito, ya que 1.e3 intentan invocar el método e3 en la clase Fixnum.

Todos los números son objetos, y responden a una variedad de mensajes (listados de manera completa en las páginas 290, 313, 315, 323 y 349). Así que, al reves que C++, puedes encontrar el valor absoluto de un número escribiendo aNumber.abs, no abs(aNumber).

Los enteros también soportan varios interadores útiles. Hemos visto ya uno ---7.times en el ejemplo de código de la página 47. Otros incluyen por ejemplo upto y downto, para iterar arriba y abajo entre dos enteros, y step que se parece más al tradicional bucle for.

3.times        { print "X " }
1.upto(5)      { |i| print i, " " }
99.downto(95)  { |i| print i, " " }
50.step(80, 5) { |i| print i, " " }
produce:
X X X 1 2 3 4 5 99 98 97 96 95 50 55 60 65 70 75 80

Finalmente, una advertencia para usuarios de Perl. Las cadenas que contienen números no son automáticamente convertidas en números cuando son utilizadas en expresiones. Esto se hace presente de manera más frecuente cuando leemos numeros de un fichero. El siguiente código (probablemente) no hace lo que se pretendía.

DATA.each do |line|
  vals = line.split    # split line, storing tokens in val
  print vals[0] + vals[1], " "
end

Feed es un fichero que contiene

3 4
5 6
7 8

y tu obtienes la salida ``34 56 78.'' ¿Qué ha sucedido?

El problema es que la entrada fue leida como una cadena, no como un número. El operador mas concatena cadenas, así que eso es lo que vemos en la salida. Para arreglar eso, utilizamos método String#to_i para convertir la cadena en un entero.

DATA.each do |line|
  vals = line.split
  print vals[0].to_i + vals[1].to_i, " "
end
produce:
7 11 15

Cadenas

Las cadenas en Ruby son simples secuencias de bytes de 8 bits. Normalmente almacenan caracteres imprimibles, pero eso no es un requerimiento; una cadena también puede almacenar datos binarios. Las cadenas son objetos de la clase String.

Las cadenas son frecuentemente creadas utilizando literales de cadena ---secuencias de caracteres entre delimitadores. Debido a que los datos en binario son dificiles de representar dentro del código fuente del programa, puedes utilizar varias secuencias de escape en un literal de cadena. Cada uno es reemplazado con el valor binario correspondiente cuando el programa es compilado. El tipo de delimitador de cadena determina el grado de sustitución realizado. Con comillas simples, dos barras inversas son reemplazadas con una única barra invertida, y una barra invertida seguida por una comilla simple se convierte en una comilla simple.

'escape using "\\"' » escape using "\"
'That\'s right' » That's right

Las cadenas con comillas dobles soportan una mayor capacidad de secuencias de escape. El más común probablmente sea ``\n'', el caracter de nueva línea.. La tabla 18.2 en la página 203 te da la lita completa. Adicionalmente, puedes sustituir el valor de cualquier expresión Ruby dentro de la cadena utilizando la secuencia #{ expr }. Si las expresión es una variable global, una variable de clase o una variable de instancia, puedes omitir las llaves.

"Seconds/day: #{24*60*60}" » Seconds/day: 86400
"#{'Ho! '*3}Merry Christmas" » Ho! Ho! Ho! Merry Christmas
"This is line #$." » This is line 3

There are three more ways to construct string literals: %q, %Q, and ``here documents.''

%q and %Q start delimited single- and double-quoted strings.

%q/general single-quoted string/ » general single-quoted string
%Q!general double-quoted string! » general double-quoted string
%Q{Seconds/day: #{24*60*60}} » Seconds/day: 86400

El caracter que sigue a ``q'' o ``Q'' es el delimitador. Si es un corchete, llave, parentesis de apertura o un signo de menor, la cadena es leida hasta el siguiente que el simbolo correspondiente de cierre es encontrado. En otro caso la cadena es leida hasta la siguiente ocurrencia del mismo delimitador.

Finalmente, puedes construir una cadena utilizando un here document.

aString = <<END_OF_STRING
    The body of the string
    is the input lines up to
    one ending with the same
    text that followed the '<<'
END_OF_STRING

Un here document consiste en lineas en el código fuente, hasta pero no incluyendo, la cadena de terminación que especificaste después de los caracteres<<. Normalmente, este terminador debe comenzar en la primera columna. Sin embargo, si pones un signos menos despues de los caracteres <<, entonces puedes identar el terminador.

print <<-STRING1, <<-STRING2
   Concat
   STRING1
      enate
      STRING2
produces:
     Concat
        enate

Working with Strings

String es probablemente la clase incluida en ruby más extensa, con más de 75 métodos estandar. No los veremos todos aquí; la referencia de la libreria tiene la lista completa. En cambio, veremos algunos de los más comunes que nos facilitaran la programación del día a día.

Volvamos a nuestra jukebox. Aunque está diseñada para conectarse a Internet, también almacena copias de algunas canciones populares en un disco duro local. De este modo, aunque una ardilla mordisquee nuestra conexión, todavía podremos entretener a los clientes.

Por razones historicas (¿existe alguna de otro tipo?), la lista de canciones está almacenada en filas en un fichero de texto plano. Cada fila almacena el nombre del fichero que contiene la canción, la duración, el artista y el título, todos en campos separados por barras verticales. Un fichero típico podría comenzar:

/jazz/j00132.mp3  | 3:45 | Fats     Waller     | Ain't Misbehavin'
/jazz/j00319.mp3  | 2:58 | Louis    Armstrong  | Wonderful World
/bgrass/bg0732.mp3| 4:09 | Strength in Numbers | Texas Red
         :                  :           :                   :

Observando estos datos, está claro que utilizaremos algunos de los métodos de la clase String para extraer y limpiar los campos antes de crear objetos Song basados en ellos. Al menos, necesitaremos:

Nuestra primera tarea es dividir cada línea en campos, y String#split hará el trabajo amablemente. En este caso, pasaremos a split una expresión regular, /\s*\|\s*/, la cual divide la línea en trozos donde split encuentre una barra vertical, opcionalmente rodeada por espacios. Y, como la línea del fichero tiene un caracter de nueva línea, utilizaremos String#chomp para quitarselo antes de aplicar split.

songs = SongList.new

songFile.each do |line|   file, length, name, title = line.chomp.split(/\s*\|\s*/)   songs.append Song.new(title, name, length) end puts songs[1]
produce:
Song: Wonderful World--Louis    Armstrong (2:58)

Desafortunadamente, quien creo el fichero original puso el nombre de los artistas en columnas, así que algunos contienen espacios extra. Esto quedaría feo en nuestro panel, así que mejor borremos esos espacios extra antes de ir mucho más allá. Existen varias maneras de hacer eso, pero problemente la más sencilla es String#squeeze , el cual recurta los caracteres repetidos. Utilizaremos el formato squeeze! de este método para alterar el valor de la cadena.

songs = SongList.new

songFile.each do |line|   file, length, name, title = line.chomp.split(/\s*\|\s*/)   name.squeeze!(" ")   songs.append Song.new(title, name, length) end puts songs[1]
produce:
Song: Wonderful World--Louis Armstrong (2:58)

Finalmente, está el pequeño problema del formato de tiempo: el fichero dice 2:35, y nosotros queremos el número de segundos, 178. Podríamos utilizar el método split de nuevo, esta vez dividiendo el campo tiempo alrededor del símbolo de dos puntos.

mins, secs = length.split(/:/)

En vez de eso, utilizaremos un método relacionado. String#scan es similar a split en el sentido de que rompe una cadena en varios trozos basandose en un patrón. Sin embargo, al contrario que con split, con scan tu especificas el patron que quieres que cumplan los trozos. En este caso, nosotros queremos que coincidan uno o más dígitos para ambos componentes: minutos y segundos. El patrón para uno o más dígitos es: /\d+/.

songs = SongList.new
songFile.each do |line|
  file, length, name, title = line.chomp.split(/\s*\|\s*/)
  name.squeeze!(" ")
  mins, secs = length.scan(/\d+/)
  songs.append Song.new(title, name, mins.to_i*60+secs.to_i)
end
puts songs[1]
produce:
Song: Wonderful World--Louis Armstrong (178)

Nuestra jukebox tiene capacidad de busqueda por palabra clave. Dando una palabra del título de una canción o el nombre de un artista, devolverá una lista de todas las canciones coincidentes. Teclea ``fats,'' y nos devolvería canciones de Fats Domino, Fats Navarro, y Fats Waller, por ejemplo. Nosotros implementaremos esto creado una clase indexadora. Alimentada con un objeto y algunas cadenas, indexara ese objeto bajo cada palabra (o dos o más caracteres) que aparezcan en esas cadenas. Esto ilustrará un poco más varios métodos de la clase String.

class WordIndex
  def initialize
    @index = Hash.new(nil)
  end
  def index(anObject, *phrases)
    phrases.each do |aPhrase|
      aPhrase.scan /\w[-\w']+/ do |aWord|   # extract each word
        aWord.downcase!
        @index[aWord] = [] if @index[aWord].nil?
        @index[aWord].push(anObject)
      end
    end
  end
  def lookup(aWord)
    @index[aWord.downcase]
  end
end

El método String#scan extrae los elementos de una cadena que coincidan con una expresión regular. En este caso, el patrón ``\w[-\w']+'' coincide con cualquier caracter que puede aparecer en una palabra, seguido por una o más de las cosas especificadas en los corchetes (un guión, otro caracter de palabra, o una comilla simple). Hablaremos más sobre expresiones regulares, comenzando en la página 56. Para hacer nuestras búsquedas insensibles a las mayúsculas, convertimos ambos, las palabras que extraemos y las palabras que utilizamos como clave durante la búsqueda. Atención al símbolo de exclamación al final del primer metodo downcase!. Como con el método squeeze! que hemos utilizado previamente, esta es una indicación de que el método modificará al receptor, en este caso conviertiendo una cadena a minúsculas..[Hay un pequeño error en este ejemplo de código: la canción ``Gone, Gone, Gone'' podría ser indexada tres veces. ¿Puedes descubrir alguna solución?]

Extenderemos nuestra clase SongList para indexar canciones tal como hemos añadido, y añadir un método para buscar una canción dando una palabra.

class SongList
  def initialize
    @songs = Array.new
    @index = WordIndex.new
  end
  def append(aSong)
    @songs.push(aSong)
    @index.index(aSong, aSong.name, aSong.artist)
    self
  end
  def lookup(aWord)
    @index.lookup(aWord)
  end
end

Finalmente, lo probamos todo.

songs = SongList.new
songFile.each do |line|
  file, length, name, title = line.chomp.split(/\s*\|\s*/)
  name.squeeze!(" ")
  mins, secs = length.scan(/\d+/)
  songs.append Song.new(title, name, mins.to_i*60+secs.to_i)
end
puts songs.lookup("Fats")
puts songs.lookup("ain't")
puts songs.lookup("RED")
puts songs.lookup("WoRlD")
produce:
Song: Ain't Misbehavin'--Fats Waller (225)
Song: Ain't Misbehavin'--Fats Waller (225)
Song: Texas Red--Strength in Numbers (249)
Song: Wonderful World--Louis Armstrong (178)

Podríamos gastar las siguientes 50 páginas mirando todos los métodos de la clase String. En cambio, vamos a continuar estudiando un tipo de datos simple: rangos.

Rangos

Los rangos están por todas partes: Enero a Diciembre, 0 a 9, mal a bien hecho, lineas de la 50 hasta la 67 y todo así. Si Ruby nos va a ayudar, parece natural que soporte rangos. De hecho, Ruby hace algo mejor: realmente utiliza rangos para implementar tres características separadas: secuencias, condiciones e intervalos.

Rangos y Secuencias

El primero y quizás el uso más natural de los rangos es expresar una secuencia. Las secuencias tienen un punto de inicio, un punto final y un modo de producir sucesivos valores de la secuencia. En Ruby, estas secuencias son creadas utlizando los operadores ``..'' y ``...''. El formato de dos puntos crea un rango inclusivo, mientras que el formato de tres puntos crea un rango que excluye el valor más alto expecificado.

1..10
'a'..'z'
0...anArray.length

En Ruby, al contrario que versiones anteriores de Perl, los rangos no son representados internamente como listas: las secuencia 1..100000 is tratada como un objeto Range contiendo referencias a dos objetos de la claseFixnum. Si lo necesitas, puedes convertir un rango a una lista utlizando el método to_a.

(1..10).to_a » [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
('bar'..'bat').to_a » ["bar", "bas", "bat"]

Los ramgos implementa métodos que permite iterar sobre ellos y comprobar sus contenidos de varios modos.

digits = 0..9
digits.include?(5) » true
digits.min » 0
digits.max » 9
digits.reject {|i| i < 5 } » [5, 6, 7, 8, 9]
digits.each do |digit|
  dial(digit)
end

Hasta ahora hemos mostrado rangos de números y cadenas. Sin embargo, como esperarías de un lenguaje orientado a objetos, ruby puede crear rangos basados en objetos que tu definas. La únicas restricciones son que los objetos deben responder al método succ retornando el siguiente objeto en la secuencia y el objeto debe ser comparable utilizando el operador general de comparación <=>. El operador <=> compara dos valores, devolviendo -1, 0 o +1 dependiendo de si el primero es menor, igual o mayor que el segundo.

Aquí tenemos una clase simple que representa filas de signos ``#''. Nosotros podríamos utilizarlo para comprobar el control de volumen de la jukebox en modo texto.

class VU

  include Comparable

  attr :volume

  def initialize(volume)  # 0..9     @volume = volume   end

  def inspect     '#' * @volume   end

  # Support for ranges

  def <=>(other)     self.volume <=> other.volume   end

  def succ     raise(IndexError, "Volume too big") if @volume >= 9     VU.new(@volume.succ)   end end

Podemos probarlo creando un rango de objetos de la clase VU.

medium = VU.new(4)..VU.new(7)
medium.to_a » [####, #####, ######, #######]
medium.include?(VU.new(3)) » false

Rangos y Condiciones

Además de para representar secuencias, los rangos también pueden ser utilizados como expresiones condicionales. Por ejemplo, el siguiente fragmento de código imprime conjuntos de líneas desde la entrada estandar, donde la primera linea en cada conjunto contiene la palabra ``start'' y la última línea la palabra ``end.''

while gets
  print if /start/../end/
end

Entre bastidores, el rango mantiene la pista sobre el estado de cada uno de las pruebas. Mostraremos algunos ejemplos de esto en la descripción de bucles que comienza en la página 82.

Rangos como Intervalos

Un uso de la versatilidad de los rangos es como una prueba de intervalo: viendo si algún valor cae dentro del intervalo representado por el rango. Esto se hace utilizando ===, el operador case equality.

(1..10)    === 5 » true
(1..10)    === 15 » false
(1..10)    === 3.14159 » true
('a'..'j') === 'c' » true
('a'..'j') === 'z' » false

El ejemplo de una expresion case de la página 81 muestra este test en acción, determinando un estilo de jazz dado un año.

Expresiones Regulares

De vuelta a la página 50 cuando estuvimos creado una lista de canciones desde un fichero, utilizamos una expresión regular para buscar coincidencias del campo delimitador en el fichero de entrada. Nosotros afirmamos que la expresión line.split(/\s*\|\s*/) buscaba coincidencias con una barra vertical rodeada por espacios en blanco opcionales. Vamos a explorar las expresiones regulares en más detalle para ver por qué esta afirmación es cierta.

Las expresiones regulares son utilizadas para buscar coincidencias de patrones con cadenas. Ruby proporciona soporte incorporado que hace la coincidencia de patrones y la sustitución cómoda y concisa. In esta sección trabajaremos sobre las carácterísticas fundamentales de las expresiones regulares. Existen algunos detalles que no cubriremos: hay que echar un vistazo a la página 205 para más información.

Las expresiones regulares son objetos del tipo Regexp. Puden ser creadas llamando al constructor de forma explicita o utilizan el formato literal /pattern/ y %r\pattern\.

a = Regexp.new('^\s*[a-z]') » /^\s*[a-z]/
b = /^\s*[a-z]/ » /^\s*[a-z]/
c = %r{^\s*[a-z]} » /^\s*[a-z]/

Una vez que tienes el objeto expresión regular, puedes compararlo contra una cadena utilizando Regexp#match(aString) o el operador de coincidencia =~ (coincidencia positiva) and !~ (coincidencia negativa). Los operadores de coincidencia están definidos para ambos, los objetos String y Regexp. Si ambos operandos del operador de coincidencia son Strings, el de la derecha será convertido en una expresión regular.

a = "Fats Waller"
a =~ /a/ » 1
a =~ /z/ » nil
a =~ "ll" » 7

Los operadores de coincidencia devuelve la posición del caracter con el que la coincidencia ha sucedido. También tienen el efecto añadido de establecer el valor de mucha variables de Ruby. $& recibe la parte d e la cadena que coincidió con el patrón, $` recibe la parte de la cadena que precede a la coincidencia, y $' recibe la cadena detrás de la coincidencia. Podemos utilizar esto para escribir un método, showRE, el cual ilustra donde un patrón particular coincide.

def showRE(a,re)
  if a =~ re
    "#{$`}<<#{$&}>>#{$'}"
  else
    "no match"
  end
end
showRE('very interesting', /t/) » very in<<t>>eresting
showRE('Fats Waller', /ll/) » Fats Wa<<ll>>er

La coincidencia también establece las variables globales $~ y $1 hasta $9. La variable $~ es un objetode la clase MatchData (descrito al comienzo de la página 336) que almacena todo lo que deberías saber sobre la coincidencia. Hablaremos osbre eso más tarde. Y para la gente que se acobarda cuando ven esos nombres de variable que suenan a Perl, estad atentos. Hay buenas noticas al final de capítulo.

Patrones

Cada expresión regular contiene un patrón, el cual es utilizado para buscar coincidencias de la expresión regular contra la cadena.

Dentro del patrón, todos los caracteres excepto ., |, (, ), [, {, +, \, ^, $, *, and ? coinciden consigo mismos.

showRE('kangaroo', /angar/) » k<<angar>>oo
showRE('!@%&-_=+', /%&/) » !@<<%&>>-_=+

Si quieres buscar coincidencias de uno de estos caracteres especiales de forma literal, precedelos de una barra inversa. Esto explica parte del patrón que utilizamos para dividir la línea de la canción, /\s*\|\s*/. El \| significa ``coincidir con una barra vertical.'' Sin la barra invertida, el ``|'' podría significarw alternacia (la cual describiremos más tarde).

showRE('yes | no', /\|/) » yes <<|>> no
showRE('yes (no)', /\(no\)/) » yes <<(no)>>
showRE('are you sure?', /e\?/) » are you sur<<e?>>

Una barra inversa seguida de un carácter alfanumérico es utilizado para introducir un constructor especial de coincidencia, el cual cubriremos más tarde. Adicionalmente, una expresión regular podría contener #{...}, una expresión de sustitución.

Anchors

Por defecto, una expresión regular intentará encontrar la primera coincidencia del patrón en una cadena. compra /iss/ contra la cadena ``Mississippi,'' y encontrarás la subcadena ``iss'' comenzando en la posición uno. ¿Pero qué sucede si quieres forzar un patrón para ser comparado al comienzo o al final de la cadena?

Los patrones ^ y $ casan con el principio y el fin de la linea, respectivamente. Son utilizados frecuentemente para amarrar un patrón: por ejemplo, /^option/ concuerda con la palabra ``option'' sólo si aparece al comienzo de la línea. La secuencia \A encaja con el final de la cadena. (Realmente, \Z encaja con el final de una cadena a no ser que la cadena termine con un ``\n'', en cuyo caso coincide justo antes de ``\n''.)

showRE("this is\nthe time", /^the/) » this is\n<<the>> time
showRE("this is\nthe time", /is$/) » this <<is>>\nthe time
showRE("this is\nthe time", /\Athis/) » <<this>> is\nthe time
showRE("this is\nthe time", /\Athe/) » no match

Similarly, the patterns \b and \B match word boundaries and nonword boundaries, respectively. Word characters are letters, numbers, and underscore.

showRE("this is\nthe time", /\bis/) » this <<is>>\nthe time
showRE("this is\nthe time", /\Bis/) » th<<is>> is\nthe time

La Clase Caracteres

Una clase caracter es un conjunto de caracteres entre corchetes: [ characters ] encaja con cualquier caracter entre los corchetes. [aeiou] encajará con cualquier vocal, [,.:;!?] encaja con signos de puntuación y así podríamos continuar. El significado de la expresión regular especial caracteres---.|()[{+^$*?---es repelida dentro de los corchetes. Sin embargo, la sustitución normal de cadena continúa ocurriendo, así que (por ejemplo) \b representa un caracter de retroceso y \n una nueva línea (ver tabla 18.2 en page 203). Adicionalmente, puedes utilizar las abreviaturas mostradas en la tabla 5.1 de la página 59, así que (por ejemplo) \s encaja con cualquier caracter de espacio en blanco, no sólo un espacio literal.

showRE('It costs $12.', /[aeiou]/) » It c<<o>>sts $12.
showRE('It costs $12.', /[\s]/) » It<< >>costs $12.

Dentro de los corchetes, la secuencia c1-c2 representa todos los caracteres entre c1 y c2, ambos inclusive.

Si tu quieres incluir el caracter literal ] y - dentro de una clase caracter, deben aparecer al comienzo.

a = 'Gamma [Design Patterns-page 123]'
showRE(a, /[]]/) » Gamma [Design Patterns-page 123<<]>>
showRE(a, /[B-F]/) » Gamma [<<D>>esign Patterns-page 123]
showRE(a, /[-]/) » Gamma [Design Patterns<<->>page 123]
showRE(a, /[0-9]/) » Gamma [Design Patterns-page <<1>>23]

Pon un ^ inmediatamente después de un corchetes abierto para negar una clase caracter: [^a-z] encaja con cualquier caracter que no es una letra minúscula.

Algunas clases caracter son utilizadas tan frecuentemente que Ruby proporciona abreviaturas para ellas. Esas abreviaturas son listadas en la tabla 5.1 de la página 59---deberían ser utilizadas dentro de corchetes y en el cuerpo de un patrón.

showRE('It costs $12.', /\s/) » It<< >>costs $12.
showRE('It costs $12.', /\d/) » It costs $<<1>>2.

Character class abbreviations
Sequence As [ ... ] Meaning
\d [0-9] Digit character
\D [^0-9] Nondigit
\s [\s\t\r\n\f] Whitespace character
\S [^\s\t\r\n\f] Nonwhitespace character
\w [A-Za-z0-9_] Word character
\W [^A-Za-z0-9_] Nonword character

Finalmente, un punto (``.'') fuera de los corchetes representa cualquier caracter excepto una nueva línea (y en modo multilínea coincide con una nueva línea, también).

a = 'It costs $12.'
showRE(a, /c.s/) » It <<cos>>ts $12.
showRE(a, /./) » <<I>>t costs $12.
showRE(a, /\./) » It costs $12<<.>>

Repetición

Cuando especificamos el patrón que divide la linea de la lista de canciones, /\s*\|\s*/, dijimos que queriamos encajar una barra verticial rodeada por un conjunto arbitrario de espacios en blanco. Ahora sabemos que la secuencia \s encaja un único caracter de espacio en blanco, así que parece como si los asteriscos de algún modo significasen ``un número arbitrario.'' De hecho, el asterisco es uno de los varios modificadores que permiten encajar múltiples ocurrencias de un patrón.

Si r se encuentra precediendo inmediatamente una expresión regualar dentro de un patrón, entonces:

r * encaja cero o más ocurrencias de r.
r + encaja una o más ocurrencias de r.
r ? encaja cero o una ocurrencia de r.
r {m,n} encaja al menos ``m'' y como máximo ``n'' ocurrencias de r.
r {m,} encaja al menos ``m'' ocurrencias de r.

Esos constructores de repetición tienen una alta precedencia---se enlazan solamente a la expresión regular inmediatamente precedente en el patrón. /ab+/ encaja una ``a'' seguida por una o más ``b'', uno una secuencia de ``ab''. Tienes que ser cuidadoso con el constructor también *---el patrón /a*/ encajará con cualquier cadena; cadena tiene cero o más ``a''.

Esos patrones son llamados avariciosos, porque por defecto encajarán la mayor parte de la cadena que puedan. Puedes alterar este comportamiento, y entonces encajarán lo mínimo, añadiendo una interrogación como sufijo.

a = "The moon is made of cheese"
showRE(a, /\w+/) » <<The>> moon is made of cheese
showRE(a, /\s.*\s/) » The<< moon is made of >>cheese
showRE(a, /\s.*?\s/) » The<< moon >>is made of cheese
showRE(a, /[aeiou]{2,99}/) » The m<<oo>>n is made of cheese
showRE(a, /mo?o/) » The <<moo>>n is made of cheese

Alternancia

Ya sabemos que la barra vertical es especial porque nuestro patron de división de líneas tiene que anteponer una barra invertida. Esto es porque en caso contrario la barra vertical encaja con la expresión regular que la precede o con la expresión regular que la sigue.

a = "red ball blue sky"
showRE(a, /d|e/) » r<<e>>d ball blue sky
showRE(a, /al|lu/) » red b<<al>>l blue sky
showRE(a, /red ball|angry sky/) » <<red ball>> blue sky

Existe aquí una trampa para el incauto, ya que ``|'' tiene muy baja precedencia. El último ejemplo de arriba encaja ``red ball'' o ``angry sky'', no ``red ball sky'' o ``red angry sky''. Para encajar ``red ball sky'' o ``red angry sky'', necesitarías sobreescribir la precedencia por defecto utilizando agrupación.

Agrupación

Puedes utilizar parentesis para agrupar terminos dentro de una expresión regular. Todo dentro del grupo es tratado como una única expresión regular.

showRE('banana', /an*/) » b<<an>>ana
showRE('banana', /(an)*/) » <<>>banana
showRE('banana', /(an)+/) » b<<anan>>a

a = 'red ball blue sky'
showRE(a, /blue|red/) » <<red>> ball blue sky
showRE(a, /(blue|red) \w+/) » <<red ball>> blue sky
showRE(a, /(red|blue) \w+/) » <<red ball>> blue sky
showRE(a, /red|blue \w+/) » <<red>> ball blue sky

showRE(a, /red (ball|angry) sky/) » no match
a = 'the red angry sky'
showRE(a, /red (ball|angry) sky/) » the <<red angry sky>>

Los paréntesis con también utilizados paa recoger los resultados del encaje de un patrón. Ruby cuenta los paréntesis de apertura, y por cada uno almacena el resultado de el encaje parcial entre el y correspondiente paréntesis de cierre. Puedes utilizar este encaje parcial tanto dentro del patrón restante y en tu programa Ruby. Dentro del patrón, la secuencia \1 se refiere al encaje del primer grupo, \2 el segundo grupo y así. Fuera del patrón, Fuera del patrón, las variables especiales $1, $2, y demás, sirven para el mismo propósito.

"12:50am" =~ /(\d\d):(\d\d)(..)/ » 0
"Hour is #$1, minute #$2" » "Hour is 12, minute 50"
"12:50am" =~ /((\d\d):(\d\d))(..)/ » 0
"Time is #$1" » "Time is 12:50"
"Hour is #$2, minute #$3" » "Hour is 12, minute 50"
"AM/PM is #$4" » "AM/PM is am"

La habilidad para utilizar parte del encaje actual con postioridad en el encaje permite buscar varias formas de repetición.

# match duplicated letter
showRE('He said "Hello"', /(\w)\1/) » He said "He<<ll>>o"
# match duplicated substrings
showRE('Mississippi', /(\w+)\1/) » M<<ississ>>ippi

También puedes utilizar referencias hacia atrás para encontrar delimitadores.

showRE('He said "Hello"', /(["']).*?\1/) » He said <<"Hello">>
showRE("He said 'Hello'", /(["']).*?\1/) » He said <<'Hello'>>

Sustitución basada en Patrones

A veces encontrar un patrón en una cadena es suficiente. Si un amigo nos reta a encontra una palabra que contiene las letras a, b, c, d, y e en orden, podrías buscar una lista de palabras con el patrón /a.*b.*c.*d.*e/ and encontrar ``absconded'' y ``ambuscade.'' Eso tendría que ser suficiente.

Sin embargo, hay veces en las que necesitas cambiar cosas basandoste en un patrón de encaje. Volvamos a nuestro fichero con la lista de canciones. Quienquiera que lo creo introdujo todos los nombres de artistas en minúsculas. Cuando los mostramos en la pantalla de la jukebox, tendrían mejor aspecto en mayusculas y minúsculas. ¿Cómo podemos cambiar el primer carácter de cada palabra a mayúsculas?

Los métodos String#sub y String#gsub buscan una porción de una cadena que encaje con su primeros argumentos y lo reemplaza con los segundos. String#sub realiza un reemplazo, mientras String#gsub reemplaza cada ocurrencia del encaje. Ambas rutinas devuelven una nueva copia de String conteniendo las sustituciones. Versiones mutadas String#sub! y String#gsub! modifican la cadena original.

a = "the quick brown fox"
a.sub(/[aeiou]/,  '*') » "th* quick brown fox"
a.gsub(/[aeiou]/, '*') » "th* q**ck br*wn f*x"
a.sub(/\s\S+/,  '') » "the brown fox"
a.gsub(/\s\S+/, '') » "the"

El segundo argumento de ambas funciones puede ser tanto un String o un bloque. Si se utiliza un bloque, el valor del bloque es sustituido dentro de String.

a = "the quick brown fox"
a.sub(/^./) { $&.upcase } » "The quick brown fox"
a.gsub(/[aeiou]/) { $&.upcase } » "thE qUIck brOwn fOx"

Así que esta parece la respuesta para convertir nuestros nombres de artistas. El patrón que encaja el primer caracter de una palabra es \b\w---busca un límite de una palabra seguido por un caracter de palabra. Combina esto con gsub y ya podemos solucionar los nombres de artistas.

def mixedCase(aName)
  aName.gsub(/\b\w/) { $&.upcase }
end
mixedCase("fats waller") » "Fats Waller"
mixedCase("louis armstrong") » "Louis Armstrong"
mixedCase("strength in numbers") » "Strength In Numbers"

Secuencias de barras invertidas en la sustitición

Anteriormente ya vimos que las sencuencias \1, \2, y demás estan disponibles en el patrón, available in the pattern, representando el enesimo grupo encajado hasta ahí. Las mismas secuencias están disponibles en el segundo argumento de sub y gsub.

"fred:smith".sub(/(\w+):(\w+)/, '\2, \1') » "smith, fred"
"nercpyitno".gsub(/(.)(.)/, '\2\1') » "encryption"

Existen secuencias de barras invertidas que se utilizan en cadenas de sustitución: \& (ultima coincidencia), \+ (ultima coincidencia de grupo), \` (cadena previamente encajada), \' (cadena tras el encaje), y \\ (barra invertida literal). Se vuelve confuso si quieres incluir una barra invertida litera en una sustitución. La manera obvia es escribir

str.gsub(/\\/, '\\\\')

Claramente, este código está intentando reemplazar cada barra invertida en str por dos. El programador duplica las barras inverdiad en el texto de reemplazo, sabiendo que ellas serán convertidas en ``\\'' en el analisis de sintaxis. Sin embargo, cuando la sustitución sucede, el motor de expresión regular realiza otra pasada por la cadena, convirtiendo ``\\'' en ``\'', así que el efecto red es reemplazar cada barra invertida con otra barra invertida. Tienes que escribir gsub(/\\/, '\\\\\\\\')!

str = 'a\b\c' » "a\b\c"
str.gsub(/\\/, '\\\\\\\\') » "a\\b\\c"

Sin embargo, utilizando el hecho de que \& es reemplazado con la cadena encajada, podrías escribir

str = 'a\b\c' » "a\b\c"
str.gsub(/\\/, '\&\&') » "a\\b\\c"

Si utilizas el formato bloque de gsub, la cadena para sustitución es analizada una única vez (durante la pasada de sintaxis) y el restuado es el que tu pretendías.

str = 'a\b\c' » "a\b\c"
str.gsub(/\\/) { '\\\\' } » "a\\b\\c"

Finalmente, como un ejemplo de la maravillosa expresividad de combinar expresiones regulares con bloques de código, considera el siguiente fragmento de código del modulo de librería CGI, escrito por Wakou Aoyama. El código toma una cadena conteniendo la secuencia de escapeHTML y la convierte en ASCII normal. Como fue escrito para la audiencia japonesa, utiliza el modificador ``n'' en la expresión regular, deshabilita el procesado de wide-character. Tambien ilustra la expresion case de Ruby, que discutimos en la página 81.

def unescapeHTML(string)
  str = string.dup
  str.gsub!(/&(.*?);/n) {
    match = $1.dup
    case match
    when /\Aamp\z/ni           then '&'
    when /\Aquot\z/ni          then '"'
    when /\Agt\z/ni            then '>'
    when /\Alt\z/ni            then '<'
    when /\A#(\d+)\z/n         then Integer($1).chr
    when /\A#x([0-9a-f]+)\z/ni then $1.hex.chr
    end
  }
  str
end

puts unescapeHTML("1&lt;2 &amp;&amp; 4&gt;3") puts unescapeHTML("&quot;A&quot; = &#65; = &#x41;")
produce:
1<2 && 4>3
"A" = A = A

Expresiones Regulares Orientadas a Objetos

Tenemos que admitir que aunque esas extrañas variables son muy cómodas, no son muy orientadas a objetos, y son ciertamente crípticas. ¿Y no habíamos dicho que todo en Ruby era un objeto? ¿Qué esta mal aquí?

Realmente, nada. Es sólo que cuando Matz diseñó Ruby, el produjo un sistema de manejo de expresiones regulares totalmente orientado a objetyos. Entonces lo hizo familiar a los programadores de Perl poniendo todas esas variables-$ encima. Los objetos y las clases están ahí, debajo de la superficie. Así que vamos a gastar un rato desenterrandolos.

Ya nos hemos topado con una clase: las expresiones regulares literales crean instancias de la clase Regexp (documentado comenzando en la página 361).

re = /cat/
re.type » Regexp

El método Regexp#match encaja una expresión regular con una cadena. Si no tiene éxito, devuelve nil. Si tiene éxito, devuelve una instancia de la clase MatchData, cuya documentación comienza en la página 336. Y ese objeto MatchData te da acceso a toda la información disponible sobre el encaje. Todo lo interesante que puedes coger de las variables-$ está empaquetado en este manejable objeto.

re = /(\d+):(\d+)/     # match a time hh:mm
md = re.match("Time: 12:34am")
md.type » MatchData
md[0]         # == $& » "12:34"
md[1]         # == $1 » "12"
md[2]         # == $2 » "34"
md.pre_match  # == $` » "Time: "
md.post_match # == $' » "am"

Como el dato de encaje es almacenando en su propio objeto, pudes mantener el resultado de dos o más encajes de patrones disponible al mismo tiempo, algo que no podías hacer utilizando las variables-$. En el siguiente ejemplo, estamos encajando el mismo objeto Regexp contra dos cadenas. Cada encaje devuelve un único objeto MatchData, el cual nosotros verificamos examinando los dos campos subpatrón.

re = /(\d+):(\d+)/     # match a time hh:mm
md1 = re.match("Time: 12:34am")
md2 = re.match("Time: 10:30pm")
md1[1, 2] » ["12", "34"]
md2[1, 2] » ["10", "30"]

¿Así que cómo encajan las variables-$? Bien, después de cada encaje de patrón, Ruby almacena una referencia al resultado (nil o un objeto MatchData) en una variable de hebra local (accesible utilizando $~). Todas las otras variables de expresion regular son entonces derivados de este objeto. Aunque no podemos realmente pensar en un uso para el siguiente código, demuestra que todos las otras variables-$ MatchData son de hecho esclavas del valor en $~.

re = /(\d+):(\d+)/
md1 = re.match("Time: 12:34am")
md2 = re.match("Time: 10:30pm")
[ $1, $2 ]   # last successful match » ["10", "30"]
$~ = md1
[ $1, $2 ]   # previous successful match » ["12", "34"]

Andy y Dave normalmente utilizan las variables-$ preferiblemente que preocuparse sobre objetos MatchData. Para el uso diario, ellos las encuentra más convenientes. Algunas veces no podemos ayudar siendo pragmáticos.


Extraído del libro "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. liberado bajo los términos de la Open Publication License V1.0.
La referencia está disponible en: download.