Programming Ruby

The Pragmatic Programmer's Guide

Precedente < Indice ^
Prossimo >

I Tipi Standard



Finora ci siamo divertiti ad implementare pezzi di codice del nostro jukebox, ma siamo stati negligenti. Abbiamo osservato gli array, gli hash ed i processi, ma non abbiamo realmente affrontato gli altri tipi basilari di Ruby: i numeri, le stringhe,i range e le espressioni regolari. Utilizziamo ora alcune pagine su questi costrutti di base.

Numeri

Ruby supporta i numeri interi e a virgola mobile. Gli interi possono avere qualsiasi dimensione (fino ad un massimo determinato dall' ammontare della memoria libera nel vostro sistema). Gli interi all' interno di un certo valore (normalmente -230 fino a 230-1 oppure -262 fino a 262-1) sono mantenuti internamente in forma binaria, e sono oggetti della classe Fixnum. Gli interi al di fuori di questi valori sono salvati in oggetti della classe Bignum (correntemente implementati come configurazione di una lunghezza-variabile di un piccolo intero). Questo processo è trasparente, e Ruby gestisce automaticamente la conversione avanti e indietro.

num = 8
7.times do
  print num.type, " ", num, "\n"
  num *= num
end

produce:

Fixnum 8
Fixnum 64
Fixnum 4096
Fixnum 16777216
Bignum 281474976710656
Bignum 79228162514264337593543950336
Bignum 6277101735386680763835789423207666416102355444464034512896

Potrete scrivere i numeri interi usando un segno opzionale al primo posto, un indicatore opzionale di base (0 per gli ottali, 0x per gli esadecimali, oppure 0b per i binari), seguiti da una stringa di cifre nella corretta base di appartenenza. I caratteri di underscore sono ignorati nella stringa di cifre.

123456                    # Fixnum
123_456                   # Fixnum (underscore ignorato)
-543                      # Fixnum Negativo
123_456_789_123_345_789   # Bignum
0xaabb                    # eaadecimale
0377                      # Ottale
-0b101_010                # Binario (negativo)

Si possono anche prendere il valore intero di un corrispondente carattere ASCII o di una sequenza di escape semplicemente precedendolo con un punto interrogativo. Le combinazioni di controllo ed i metacaratteri possono anche essere generati utilizzando ?\C-x, ?\M-x, e ?\M-\C-x. La versione di controllo di un valore è la stessa di ``value & 0x9f''. La versione meta di un valore è ``value | 0x80''. Alla fine la sequenza ?\C-? genera un valore ASCII di delete, 0177.

?a                        # codice carattere
?\n                       # codice di nuova linea(0x0a)
?\C-a                     # control a = ?A & 0x9f = 0x01
?\M-a                     # meta pone il bit a 7
?\M-\C-a                  # meta e control a
?\C-?                     # cancella carattere

Un numero letterale con un punto decimale e/o un esponente è trasformato in in un oggetto Float , corrispondente al tipo di dati double dell' architettura nativa. Si deve seguire il punto decimale con una cifra, come 1.e3 tenta di invocare il metodo e3 nella classe Fixnum.

Tutti i numeri sono oggetti, e rispondono ad una varietà di messaggi (elencati completamente alle pagine 294, 318, 319, 328, e 354). Così, differentemente ( dico ) dal C++, trovate il valore assoluto di un numero semplicemente scrivendo aNumber.abs, e non abs(aNumber).

Gli interi supportano anche numerosi utili iteratori. Ne abbiamo già visto uno---7.times nel codice d' esempio a pagina 49. Altri includono upto e downto, per interagire su e giù tra due interi, e step, che assomiglia ad un tradizionale ciclo 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, un avviso per gli utenti del Perl. Le stringhe che contengono numeri non sono automaticamente convertite in numeri quando usate in espressioni. Questo tende ad essere ingannati quando si leggono numeri da un file. Il codice seguente (probabilmente) non fà quello che si vorrebbe.

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

Dategli in pasto un file che contiene

3 4
5 6
7 8

e riceverete come risultato ``34 56 78.'' Che cosa è accaduto?

Il problema stà nel fatto che l' input è stato letto come una stringa, e non come un numero. L' operatore di somma concatena le stringhe, questo è il motivo per cui abbiamo quel risultato. Per risolverlo, usiamo il metodo String#to_i per convertire la stringa in un intero.

DATA.each do |line|
  vals = line.split
  print vals[0].to_i + vals[1].to_i, " "
end

produce:

7 11 15

Le Stringhe

Le stringhe di Ruby sono semplicemente una sequenza di byte ad 8-bit. Normalmente mantengono i caratteri stampabili, ma non è una necessità. Una stringa può anche mantenere i dati in forma binaria. Le stringhe sono oggetti della classe String.

Le stringhe sono spesso create utilizzando valori letterali--- sequenze di caratteri tra delimitatori. Poichè i dati binari sono evidentemente difficoltosi da rappresentare all'interno del sorgente del programma, si possono posizionare varie sequenze di escape in una stringa letterale. Ognuno è sostituito con il corrispondente valore binario non appena il programma è compilato. I tipi di delimitatore di stringhe determinano il grado di esecuzione della sostituzione. All' interno delle stringhe con un solo apice, due consecutivi backslashe ( \\ ) sono sostituiti da uno solo, e un backslash seguito da un apice rimane l' apice stesso.

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

Le stringhe con i doppi apici support a boatload more escape sequences. Il più comune è probabilmente ``\n'', il carattere di nuova riga. La tavola 18.2 a pagina 205 fornisce la lista completa. Ulteriormente, si può sostituire il valore di un espressione di Ruby in una stringa utilizzando la sequenza #{ expr }. Se l' espressione è una variabile globale, una variabile di classe, oppure una variabile istanza, si possono omettere le graffe.

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

Esistono tre ulteriori modi per costruire stringhe letterali: %q, %Q, e ``here documents.''

%q e %Q iniziano stringhe con uno o due apici.

%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

Il carattere che segue la ``q'' o la ``Q'' è il delimitatore. Se esiste una parentesi quadra, graffa, tonda, o il segno di meno aperti la stringa verrà letta fino al segno di chiusura. Diversamente la stringa verrà letta fino alla successiva occorrenza con lo stesso delimitatore.

Finalmente, si può costruire una stringa utilizzando il costrutto 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 in un insieme di righe fino, ma non incluse, alla fine della stringa che specificate dopo il carattere << . Normalmente, il carattere di chiusura deve iniziare nella prima colonna. Comunque, se inserite un segno di meno dopo il carattere << , potrete fare andare a capo il carattere di chiusura.

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

produce:

     Concat
        enate

Lavorare con le Stringhe

String è probabilmente la classe più grande precostituita in Ruby, con oltre 75 metodi standard. La nostra intenzione non è quella di approfondirli tutti in questo momento; la biblioteca di consultazione dispone di una lista completa. Invece, osserveremo alcune stringhe di espressioni comuni---cose che sono facilmente implementabili durante la programmazione quotidiana.

Ritorniamo al nostro jukebox. Sebbene sia stato disegnato per la connessione ad internet, mantiene localmente in un hard disk alcune copie delle canzoni. In questo modo, se la connessione dovesse venir meno saremo sempre in grado di intrattenere i clienti.

Per ragioni di storico (non ne esistono di altre?), la lista delle canzoni è mantenuta come una serie di righe in un file inattivo. Ogni riga conserva il nome del file che contiene la canzone, la durata, l' artista ed il titolo, il tutto in campi separati da una barra verticale. Un esempio di file potrebbe incominciare così:

/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
         :                  :           :                   :

Osservando i dati, è chiaro che useremo alcune classi dei numerosi metodi di String per estrarre e pulire i campi prima di creare gli oggetto Song basati su di essi. Come minimo avremo bisogno di:

In nostro primo lavoro sarà di scindere ogni riga nei campi: String#split svolgerà il compito felicemente. In questo caso, passeremo a split un' espressione regolare, /\s*\|\s*/, la quale seziona le linee in segni ovunque split trova una barra verticale, opzionalmente circondati da spazi. E, poichè la linea letta dal file si trascina una nuova riga, useremo String#chomp per toglierla prima di iniziare la divisione della stessa..

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)

Sfortunatamente, chiunque abbia creato il file originale ha inserito i nomi degli artisti in colonne, alcune di esse contengono degli spazi ulteriori. Ciò si presenta male sul nostro pannello supermoderno, così sarebbe meglio rimuovere questi spazi prima di andare troppo lontano. Esistono molti modi per realizzarlo, ma probabilmente il più semplice è String#squeeze, che regola il corso dei caratteri ripetuti. Useremo la formula squeeze! del metodo, che modifica la stringa adatta.

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)

Alla fine affrontiamo il problema minore ,il formato del tempo: il file segna 2:58 e noi vogliamo il valore in secondi, 178. Possiamo usare nuovamente split, questa volta dividendo il campo time attorno al carattere dei due punti.

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

Invece, useremo un metodo connesso. String#scan è simile a split in quanto interrompe una stringa in grosse sezioni in base al modello. Comunque, sfortunatamente per split, con scan specificate il modello di stringa che volete che venga trovato. Nel nostro caso vogliamo cercare uno o più numeri per entrambi i componenti dei minuti e dei secondi. Il modello per una o più cifre è /\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)

Il nostro jukebox ha la capacità di ricerca. Fornita una parola per un titolo di una canzone o per il nome di un artista, elencherà tutte le corrispondenze. Digitando ``fats,'' potrebbe restituire canzoni tipo Fats Domino, Fats Navarro, e Fats Waller, per esempio. Implementeremo la cosa creando una classe indice. Fornendogli un oggetto ed alcune stringhe, creerà l' indice dell' oggetto sotto ogni parola ( di due o più caratteri) che risultano in quelle stringhe. Questo illustrerà un poco meglio i molti metodi della classe String.

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

Il metodo String#scan estrae gli elementi da una stringa che corrispondono ad un' espressione regolare. In questo caso, il modello ``\w[-\w']+'' confronta tutti i caratteri che appaiono in una parola, seguiti da una o più cose specificate tra le parentesi quadre (un trattino, un altro carattere della parola, oppure un apice). Parliamo più approfonditamente delle espressioni regolari a partire da pagina 58. Per rendere le nostre ricerche indipendenti dal 'case sensitive', segnamo entrambe le parole da estrarre e le parole usate come chiavi durante la ricerca del valore minuscolo. Notate il punto esclamativo alla fine del nome del primo metodo downcase!. Come con il metodo squeeze! utilizzato precedentemente, questo è un indicatore che il metodo modificherà il destinatario dove si trova, nel nostro caso convertendo la stringa in formato minuscolo[Esiste un bug di lieve entità nell' esempio: la canzone ``Gone, Gone, Gone'' sarebbe indicizzata tre volte. Potete sistemarlo?]

Estenderemo la nostra classe SongList per indicizzare le canzoni così come sono state aggiunte, ed un metodo per cercare una canzone data la parola.

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 testeremo nella sua integrità.

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)

Potremo passare le prossime 50 pagine ad osservare tutti i metodi nella classe String. Comunque, andremo oltre per approfondire un datatype più semplice: i range.

I Range

I range sono ovunque: da Gennaio a Dicembre, da 0 a 9, da superlativo a ben fatto, le linee da 50 fino alla 67, e così via. Se Ruby ci aiuta con la realtà dei modelli, sembrerebbe naturale che supporti questi range. Infatti Ruby fà ancora meglio: usa i range per implementare tre differenti caratteristiche: le sequenze, le condizioni e gli intervalli.

I Range come Sequenze

Il primo, e forse il più naturale uso dei range, è l' espressione di una sequenza. Le sequenze hanno un punto di partenza, un punto di arrivo, ed un modo di produrre una sequenza di valori successivi. In Ruby, queste sequenze sono create utilizzando gli operatori range ``..'' e ``...''. La forma con i due punti crea un range inclusivo, mentre quella con tre punti ne crea uno con i due estremi esclusi.

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

In Ruby, sfortunatamente come nelle prime versioni del Perl, i range non sono rappresentati internamente come una lista: la sequenza 1..100000 è contenuta in un oggetto Range, che contiene i riferimenti a due oggetti Fixnum. Nel momento del bisogno, si potranno convertire il range in una lista utilizzando il metodo to_a.

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

I range implementano i metodi che vi permettono di iterarli e di testare i loro contenuti in una varietà di modi.

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

Fin' ora vi abbiamo mostrato i range di numeri e stringhe. Comunque, come vi aspettereste da un linguaggio object-oriented, Ruby è in grado di creare range basandosi sugli oggetti che voi definite. L' unica restrizione è che gli oggetti devono reagire a succ restituendo il prossimo oggetto nella sequenza e che l' oggetto deve essere comparabile utilizzando <=>, l' operatore di comparazione generale. Talvolta denominato operatore di spazio, <=> compara due valori, restituendo -1, 0, oppure +1 a seconda se il primo valore sia minore, uguale o maggiore del secondo.

Questa è una semplice classe che rappresenta le righe del segno ``#''. Potremo utilizzarlo come troncone testuale, quando testeremo il controllo del volume del jukebox.

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

Lo testeremo creando una classe di oggetti VU.

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

I Range come Condizioni

Per rappresentare al meglio le sequenze, i range possono anche essere usati come espressioni condizionali. Per esempio, il seguente frammento di codice stampa una serie di linee dallo standard input, dove la prima riga di ogni serie contiene la parola ``start'' e l' ultima riga la parola ``end.''

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

Dietro le quinte, il range tiene traccia dello stato di ognuno dei testi. Mostreremo alcuni esempi di ciò nella descrizione dei loop che inizia a pagina 84.

I Range come Intervalli

L' uso finale della versatilità dei range è come test di intervallo: verificando se alcuni valori cadono all'interno dell' intervallo rappresentato dal range. Ciò è fatto utilizzando ===, il caso dell' operatore di uguaglianza.

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

L' esempio di un espressione di un caso a pagina 83 mostra questo test in azione, mentre determina lo stile del jazz in base all'anno.

Le Espressioni Regolari

Indietro a pagina 53, quando stavamo creando una lista di canzoni da un file, abbiamo utilizzato un' espressione regolare per determinare il delimitatore del campo all'interno del file di input. Asserivamo che l' espressione line.split(/\s*\|\s*/) selezionava una barra verticale circondata da opzionali spazi vuoti. Approfondiamo le espressioni regolari per vedere perchè quella asserzione sia vera.

Le espressioni regolari sono utilizzate per selezionare dei modelli all'interno delle stringhe. Ruby fornisce un supporto che esegue la selezione del modello ed una sostituzione comoda e breve. In questa sezione lavoreremo attraverso tutte le principali caratteristiche delle espressioni regolari. Ci saranno dettagli di cui non ci occuperemo: osservate pagina 207 per maggiori informazioni.

Le espressioni regolari sono oggetti del tipo Regexp. Possono essere create chiamando il costruttore esplicitamente oppure utilizzando le forme letterali /pattern/ e %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 volta che disponete di un oggetto di un espressione regolare, potrete selezionarlo all'interno di una stringa utilizzando Regexp#match(aString) oppure gli operatori di selezione =~ (positivo) e !~ (negativo). Gli operatori di selezione sono definiti per entrambi gli oggetti String e Regexp. Se entrambi gli operatori delle selezione sono Strings, quello sulla destra sarà convertito in una espressione regolare.

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

L' operatore di selezione restituisce la posizione del carattere corrispondente al valore trovato. They also have the side effect of setting a whole load of Ruby variables. $& receives the part of the string that was matched by the pattern, $` receives the part of the string that preceded the match, and $' receives the string after the match. We can use this to write a method, showRE, which illustrates where a particular pattern matches.

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 selezione setta anche le variabili $~ e $1 fino a $9. La variabile $~ è un oggetto MatchData (descritta a pagina 340) che mantiene ogni cosa potreste voler conoscere al riguardo del confronto. $1e così avanti mantengono i valori di parti del confronto. Approfondiremo la cosa più avanti. E per quelle persone che si inchinano quando vedono questi nomi di variabili sul modello del Perl, resiste in armonia. Ci saranno buone nuove alla fine del paragrafo.

I modelli

Ogni espressione regolare contiene un modello, che è utilizzata per confrontare l' espressione regolare con la stringa.

All'interno di un modello, tutti i caratteri ad eccezione di ., |, (, ), [, {, +, \, ^, $, *, e ? confrontano se stessi.

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

Se volete confrontare uno di questi caratteri in modo letterale, precedetelo da backslash ( \ ). Ciò spiega parte del modello che abbiamo usato per dividere la linea della canzone, /\s*\|\s*/. I simboli \| significano `confronta una barra verticale.'' Senza il backslash, il segno ``|''avrebbe significato avvicendamento (che descriveremo in seguito).

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

Un backslash seguito da un carattere alfanumerico è utilizzato per introdurre un particolare costrutto di confronto, che scopriremo più avanti. In aggiunta, un' espressione regolare può contenere un' espressione di sostituzione#{...}.

Le Ancore

In generale, una espressione regolare cercherà il primo valore del modello in una stringa. Confronta /iss/ con la stringa ``Mississippi,'' e troverà la sottostringa ``iss'' che inizia alla posizione uno. Ma come fare se vorrete forzare un modello per confrontare solo l' inizio o la fine di una stringa?

I modelli ^ e $ confrontano rispettivamente, l' inizio e la fine di una linea. Questi sono spesso usati per ancorare un modello di confronto: per esempio, /^option/ confronta la parola ``option'' solamente se incomincia all'inizio di una riga. La sequenza \A confronta l' inizio di una stringa, e \z e \Z confrontano la fine di una stringa. (Attualmente, \Z confronta la fine di una stringa a meno che la stringa finisca con un ``\n'', nel qual caso il confronto si limita a prima del ``\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

Similmente, i modelli \b e \B confrontano rispettivamente una parola attigua e non attigua. I caratteri della parola sono lettere, numeri e underscore.

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

Le classi carattere

Una classe di caratteri è un insieme di caratteri racchiusi tra parentesi quadre: [caratteri] confronta ogni singolo carattere tra le parentesi quadre. [aeiou] confronterà una vocale, [,.:;!?] confronterà la punteggiatura, e così avanti. Il significato dell' espressione regolare di caratteri speciali ---.|()[{+^$*?--- è chiuso all'interno delle parentesi quadre. Comunque, la sostituzione di stringhe normali accade ancora, così (per esempio) \b rappresenta un carattere di backspace e \n uno di nuova linea (osservate la tavola 18.2 a pagina 205). Oltre a ciò, potrete usare l' abbreviazione mostrata nella Tavola 5.1 a pagina 62, così (per esempio) \s confronta tutti i caratteri matches any whitespace character, not just a literal space.

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

All' interno delle parentesi quadre, la sequenza c1-c2 rappresenta tutti i caratteri compresi tra c1 e c2, compresi.

Se vorrete includere il carattere letterale ] e - all'interno di una classe, esso dovrà apparire all'inizio.

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]

Inserite il carattere ^ immediatamente dopo la parentesi quadra di apertura per negare una classe di caratteri: [^a-z] confronta qualsiasi carattere che non sia in minuscolo.

Alcune classi di caratteri sono utilizzate così frequentemente che Ruby ne fornisce abbreviazioni. Queste abbreviazioni sono fornite nella Tavola 5.1 a pagina 62---possono essere utilizzate indifferentemente all'interno delle parentesi quadre e del corpo del modello.

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 (``.'') che appare all'esterno di una parentesi quadra rappresenta qualsiasi carattere ad eccezione della nuova linea (e nella configurazione multiline ne cerca anche una nuova linea).

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

Ripetizione

Una volta specificato il modello che scompone la linea della lista della canzone, /\s*\|\s*/, abbiamo detto che volevamo confrontare una linea verticale circondata da una certa quantità di spazi liberi (caratteri non scritti). Ora sappiamo che la sequenza \s confronta un singolo carattere di spazio libero, così da assomigliare al significato dell' asterisco che in un qualche modo rappresenta ``un certo quantitativo''. Infatti, l' asterisco rappresenta un numero di modificazioni che permettono di confrontare multiple occorrenze di un modello.

Se r rappresenta ciò che stà prima in una espressione regolare all'interno di una modello, allora:

r* confronta zero o più occorrenze di r.
r+ confronta una o più occorrenze di r.
r? confronta nessuna o una occorrenze di r.
r{m,n} confronta almeno ``m'' e al massimo ``n'' occorrenze di r.
r{m,} confronta almeno ``m'' occorrenze di r.

Questi costrutti di ripetizione hanno una grande precedenza---essi sono tenuti solamente a precedere un' espressione regolare nel modello. /ab+/ confronta una ``a'' seguita da una o più ``b'', non una sequenza di ``ab''. Dovete stare attenti anche con i costrutti * ---il modello /a*/ confronterà qualsiasi stringa; ogni stringa ha nessun o più ``a''.

Questi modelli sono chiamati greedy, perchè di default confronteranno tutto quello che potranno della stringa. Potrete modificare questa caratteristica, e dovendo confrontarle il minimo, aggiungendo un segno di punto esclamativo.

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

Alternanza

Sappiamo che la barra verticale è speciale, poichè la nostra linea divide il modello because our line splitting pattern had to escape it with a backslash. Questo perchè That's because an unescaped vertical bar ``|'' confronta sia le espressioni regolari che precedono e sia quelle che seguono.

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

C' è una trappola per l' imprudente (programmatore), poichè ``|'' possiede una bassa precedenza. L' ultimo esempio sopra confronta ``red ball'' o ``angry sky'', non ``red ball sky'' o ``red angry sky''. Per confrontare ``red ball sky'' o ``red angry sky'', avrete bisogno di sovrascrivere la precedenza di default utilizzando il grouping.

Grouping

Potrete usare le parentesi per raggruppare termini all'interno di un' espressione regolare. Ogni cosa all'interno di un gruppo è trattata come una singola espressione regolare.

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>>

Le parentesi sono usate anche per raccogliere i risultati di un modello di ricerca. Ruby conta le parentesi aperte, e per ognuno salva il risultato della ricerca parziale tra esso ed la corrispondente chiusura di parentesi. Potrete usare questa parziale ricerca sia all'interno del resto del modello, sia nel vostro programma in Ruby. All'interno del modello, la sequenza \1 si riferisce alla ricerca del primo gruppo, \2 al secondo, e così via. All'esterno del modello, le speciali variabili $1, $2, e così via, servono allo stesso intento.

"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"

L' abilità di usare parte del confronto corrente in seguito nel ricerca, vi permette di cercare varie forme di ripetizione.

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

Potrete usare anche riferimenti precedenti per confrontare i delimitatori.

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

Sostituzione dei modelli base

Talvolta cercare un modello all'interno di una stringa è abbastanza buono. Se un amico vi sfida a cercare una parola che contiene le lettere a, b, c, d, e in ordine, potreste cercare una lista di parole con il modello /a.*b.*c.*d.*e/ e trovare ``absconded (latitanza)'' e ``ambuscade (imboscata)''. Questo deve valere qualcosa.

In qualche modo, esistono volte in cui vorreste cambiare le cose basate su un modello di ricerca. Ritorniamo al nostro file della lista di canzoni. Chiunque lo abbia creato ha inserito tutti i nomi degli artisti in minuscolo. Nel momento in cui li mostreremo sul display del jukebox, starebbero meglio in una forma mista. Come possiamo cambiare la prima lettera di ogni parola in maiuscolo?

Il metodo String#sub e String#gsub cerca una porzione di una stringa che confronti il loro primo argomento e lo sostituisca con il secondo. String#sub esegue una sostituzione, finchè String#gsub rimpiazzi ogni occorrenza del confronto. Entrambe le routine restituiscono una nuova copia di String che contiene le sostituzioni. Le versioni mutate String#sub! e String#gsub! modificano la stringa originale.

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"

Il secondo argomento di entrambe le funzioni può essere o String oppure un blocco. Se viene usato un blocco, il valore verrà sostituito in String.

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

Così, questo assomiglia alla risposta per convertire il nome dell' artista. Il modello che confronta il primo carattere di una parola è \b\w---cerca una parola di confine seguita da un carattere di una parola . Combinatelo con gsub e potremo modificare il nome dell' artista.

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"

Sequenza di Backslash nella Sostituzione

Precedentemente abbiamo notato che la sequenza \1, \2, e così avanti, sono disponibili nel modello, rappresentando il gruppo confrontato precedentemente. Le stesse sequenze sono disponibili nel secondo argomento di sub e gsub.

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

esistono sequenze addizionali di backslash che lavorano nella sostituzione delle stringhe: \& (ultimo confronto), \+ (ultimo gruppo confrontato), \` (stringa precedente da confrontare), \' (stringa dopo il confronto), e \\ (un backslash nel senso letterale). Cio vi confonde se vorrete includere un backslash in una sostituzione. La cosa ovvia è scrivere:

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

Chiaramente, questo codice stà tentando di sostituire ogni backslash in str con due. Il programmatore ha raddoppiato il backslashes nel testo della sostituzione, sapendo che sarebbero stati convertiti in ``\\'' nell'analisi sintattica. Comunque, quando la sostituzione viene eseguita, il motore dell' espressione regolare esegue un altro passaggio attraverso la stringa, convertendo ``\\'' in ``\'', così l' effetto risultante è quello di sostituire ogni singolo backslash con un altro backslash. Avrete bisogno di scrivere gsub(/\\/, '\\\\\\\\')!

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

Comunque, sapendo che \& è sostituito con la stringa di confronto, potrete anche scrivere

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

Se utilizzate il blocco formale gsub, la stringa per la sostituzione è analizzata solo una volta (durante il passaggio sintattico) ed il risultato è ciò che desiderate.

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

Alla fine, come esempio della meravigliosa espressività di combinare le espressione regolari con i blocchi di codice, considerate il seguente frammento di codice del modulo di libreria CGI, scritto da Wakou Aoyama. Il codice prende una stringa contenente sequenze di escape in HTML e lo converte in normale codice ASCII. Poichè è stato scritto per i giapponesi, usa il modificatore ``n'' nell'espressione regolare, che taglia fuori which turns off wide-character processing. Illustra anche un caso di un espressione di Ruby, di cui abbiamo discusso all'inizio di pagina 83.

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

Espressioni Regolari Object-Oriented

Dobbiamo ammettere che mentre tutte queste misteriose variabili sono molto utili da usare, esse non sono molto object oriented, e sono oltretutto criptiche. Mentre non avevamo detto che ogni cosa in Ruby è un oggetto? Che cosa c' è di sbagliato?

In realtà nulla. E' solamente che quando Matz ha disegnato Ruby, ha prodotto un sistema di manipolazione delle espressioni regolari completamente object-oriented. Poi lo rese familiare ai programmatori in Perl avviluppandolo completamente di tutte queste $-variabili sopra di esso. Gli oggetti e le classi sono ancora lì, sotto la superficie. Così passiamo un pò di tempo per mostrarle.

Siamo già passati attraverso una classe: le lettere di espressioni regolari creano istanze di classe Regexp (documentate a partire da pagina 366).

re = /cat/
re.type » Regexp

Il metodo Regexp#match confronta un' espressione regolare con una stringa. Se non esistono valori, il metodo restituisce nil. Al contrario, restituisce un instanza della classe MatchData, documentata a partire da pagina 340. E che l' oggetto MatchData vi fornisce l' accesso a tutte le informazioni disponibili sul confronto. Tutte queste buone cose che ottenete dalle $-variabili è raccolto in un pratico piccolo oggetto.

.

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"

Poichè i dati del confronto sono conservati nel suo proprio oggetto, potrete ottenere il risultato di due o più modelli di confronto allo stesso tempo, cosa che non potreste usando le $-variabili. Nel prossimo esempio, lo constatiamo confrontando il medesimo oggetto Regexp con due stringhe. Ogni confronto restituisce un unico oggetto MatchData, che verifichiamo esaminando i due campi dei sottomodelli.

.

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"]

Così come fanno le $-variabili a sistemarlo? Bè, dopo ogni modello di confronto, Ruby salva un riferimento al risultato (nil o un oggetto MatchData) in una variabile thread-local (accessibile usando $~). Tutte le altre variabili di espressioni regolari sono poi derivate da questo oggetto. Nonostante ciò non possiamo realmente pensare di usare il seguente codice, esso dimostra che tutte le altre MatchData-related $-variabili sono in realtà schiavizzate al valore in $~.

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"]

Avendo detto tutto ciò, dobbiamo 'fess up. Andy e Dave normalmente usano le $-variabili piuttosto che preoccuparsi degli oggetti MatchData. Per l' uso quotidiano, lo ritengono più conveniente. Talvolta non riusciamo ad essere pragmatici.


Precedente < Indice ^
Prossimo >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0.
This reference is available for download.