Home Rhyylen
Contatto
 
 
 
Crystal Language
Capitolo 11
Le stringhe

Eccoci ad un altro argomento tormentone dei linguaggo moderni. Le stringhe. Di fatto, per quanto nela mia esperienza, è una tipologia di dato che incontrerete con grande frequenza, seconda solo ai numeri, e la loro padronanza è un fattore di grande impotnaza. Dal punto di vista formale si tratta di sequenze indicizzate di caratteri UTF-8 (anche se come vedremo ci può essere qualche eccezione a questo insieme). Ci sono molti facili riferimenti sull'argomento liberamente accessibili pertanto non mi soffermerò su ulteriori dettagli riguardo a questa codifica. Per i nostri scopi non serve. In Crystal le stringhe sono delimitate da una coppia di doppi apici. Tutto ciò che cade all'interno fa parte della stringa e perde una sua eventuale diversa natura. Come ultimo, fondamentale punto, le stringhe in questo linguaggio sono immutabili. Cambiarne un elemento si può ma questo equivale a creare una nuova stringa. Vediamo ora qualche esempio,

"ciao"
"W la Juventus"
"1234"
"a + b * 123"
"......"

sono tutti esempi di stringa, anche se possono apparire altro. La codifica UTF-8 permette una ampia rappresentazione di caratteri il che estende le possibilità rappresentative anche a lingue diverse da quelle con alfabeto latino.
Anche in questo linguaggio sono ammesse le solite stringhe standard che permettono di rappresentare caratteri particolari:

"\"" # doppio apice
"\\" # backslash
"\e" # escape
"\f" # form feed
"\n" # a capo
"\r" # ritorno del carrello
"\t" # tabulazione orizzontale
"\v" # tabulazione verticale

e ovviamente all'interno delle stringhe stesse sono utilizzabili quei caratteri speciali che abbiamo visto nello studio del tipo char. La cosa in realtà può essere un po' più complessa ma possiamo fermarci qui.


Esempio 11.1
1
2
3
puts("\a\a\a")
puts("riga sopra\nriga sotto")
puts("a\tb")

Abbiamo altre notazioni possibili, analogamente a quanto visto per i caratteri:

"\u0041" # == "A"
oppure usando le parentesi graffe.
"\u{41}" # == "A"

Fin qui nulla di nuovo. Così come non è una novità il poter dividere una stringa su più righe tramite il carattere a capo o anche "fisicamente":

  Esempio 11.2
1
2
3
4
5
s01 = "ciao\nciao"
s02 = "ciao
ciao"
puts s01
puts s02

Le righe 2 e 3 compongono un'unica stringa, mentre alla riga 1 abbiamo l'uso di una sequenza speciale. Le possibilità rappresentative non finiscono qui:

  Esempio 11.3
1
2
3
4
puts "ciao"\
" mondo"
puts "ciao"\
     " mondo"

Vi darà lo stesso output nonostante le differenze alla righe 2 e 4, in pratica gli spazi bianchi e i salti di riga vengono ignorati:

ciao mondo
ciao mondo

Questo sistema può venire utile ad esempio quando dovete scrivere righe particolarmente lunghe che volete, durante il processo di codifica, compattare a livello di visualizzazione in modo da poterle gestire meglio.
Crystal supporta anche altre notazioni per definire le stringhe, insieme a quella classica dei doppia apici. La prima notazione prevede l'uso del carattere % a cui devono seguire dei delimitatori che possono essere parentesi tonde, quadre, graffe e le coppie
<> e ||. A parte l'ultimo tutti gli altri possono essere innestati. Le stringhe che sono introdotte dal % possono gestire al loro interno il simbolo "

s01 = %(ciao ("mondo"))
puts s01
s02 = %(ciao ["mondo"])
puts s02

Al posto del simbolo % potete usare anche %q o %Q. La differenza è che %Q ha la stessa valenza del simbolo % mentre %q inibisce l'interpolazione e l'uso delle sequenze di escape. Potete dedicarvi ai vostri esperimenti. In questo ambito ci limiteremo ad usare stringhe senza particolarità concentrandoci maggiormente sui numerosi aspetti dell'oggetto stringa in sè. Va sottolineato che diversamente dal linguaggio Ruby, non sono supportate in Crystal le stringhe all'interno di una coppia di singoli apici, questo anche per non creare confusioni con la rappresentazione dei caratteri. Mi sembra una scelta corretta.
Per inciso, sempre rimanendo nell'ambito della stampa delle stringhe, attraverso i range è possibile stampare solo una parte della stringa:

s01 = "abcdefgh"
puts s01[1..3]
puts s01[0..5]

output:

bcd
abcdef

La costruzione di una stringa è abbastanza semplice, abbiamo già visto, una sequenza di caratteri all'interno di una coppia di doppi apici costituisce una stringa a tutti gli effetti, vedasi esempi a inizio paragrafo. E' interessante e molto comodo poter usare anche l'interpolazione per inserire non solo variabili ma anche vere e proprie espressioni:

x01 = 1
x02 = 2
x03 = "somma = #{x01 + x02}"
puts x03

che in output ci darà:

somma = 3

Tuttavia potete anche eliminare questa possibilità come segue, se volete che il risultato sia proprio la stringa così come è scritta; per fare questo basta premettere il carattere \ backslash prima del cancelletto:

x04 = "somma = \#{x01 + x02}"

e vi uscirà esattamente la stringa presente entro la coppia di doppi apici. Oppure, come visto, potete usare %q per questo scopo.

Se volete creare una stringa vuota potrete anche usare new in alternativa a s01 = ""

  Esempio 11.4
1
2
3
s01 = String.new
puts typeof(s01)
puts s01.size

Come vedete size vi indica la lunghezza, ovvero il numero di caratteri di una stringa. Il metodo empty (s01.empty usando la stringa dell'esempio) è un metodo invece che ritorna true se la stringa è vuota. All'interno di una stringa i caratteri sono, come avevamo detto ad inzio paragrafo, indicizzati, esattamente allo stesso modo visto per gli elementi di un array. Gli indici, anche qui, sono interi sequenziali che iniziano da 0. Quindi, da un punto di vista visuale, la stringa "abcdef" può essere rappresentata così:

a b c d e f
0 1 2 3 4 5
-6 -5 -4 -3 -2 -1

dove la seconda riga rappresenta gli indici e la terza gli indici negativi, presenti anche qui come negli array.
L'accesso ai singoli elementi di una stringa avviene attraverso il consueto operatore []. Attraverso il seguente esempio vediamo l'operatore al lavoro e impariamo un altro concetto:

  Esempio 11.5
1
2
3
4
5
6
7
s01 = "abcde"
c01 = s01[2]
puts c01
puts typeof(c01)
s02 = "12"
c02 = s02[1]
puts typeof(c02)

che presenta questo output:

c
Char
Char

quindi:
  • la riga 3 ci conferma il lavoro svolto dall'operatore []
  • la riga4 e la 7 ci icono che i componenti di una stringa sono caratteri, indipendentemente dalla natura dei singoli elementi.
L'ultimo punto è molto importante in quanto ci conferma, una volta di più, la natura delle stringhe come sequenza di caratteri. Da notare che l'operatore [] funziona a destra, non può essere usato per sostituire un carattere nella stringa, operazione per la quale c'è un metodo specifico.

Abbiamo visto come creare una stringa. Come possiamo aggiungere caratteri / stringhe a questa o comunque ad una stringa esistente? La questione non è banale, anche perchè in programmi di grossa portata l'impatto di un simile procedimento puà essere pesante. Un primo sistema per legare più stringhe è utilizzare un overload dell'operatore + che, oltre che ai numeri, possiamo applicare alle stringhe:

  Esempio 11.6
1
2
3
4
5
6
s01 = "aa"
s02 = "bb"
s01 = s01 + s02
puts s01
s01 = s01 + "cc" + "dd"
puts s01

output:

aabb
aabbccdd

Questo funziona ovviamente anche con una stringa vuota in partenza, ad esempio creata con String.new. E funziona anche per aggiungere un singolo carattere alla stringa:

s01 = String.new
s01 = s01 + 'a'
puts s01
s01 = s01 + 'a'
puts s01

Al momento in cui scrivo questo mi pare l'unico metodo semplice e contemporaneamente abbastanza efficiente per effettuare questa concatenazione. Interessante notare che potete usare, sia pure con altra finalità, l'operatore * che, come nel caso numerico, effettua una moltiplicazione:

aa * 3 -> aaaaaa

Per cercare una sottostringa all'interno di una stringa esiste index che restituisce la prima occorrenza della sottostringa cercata sia di una o più lettere oppure nil se nulla viene trovato.

  Esempio 11.7
1
2
3
4
5
6
s01 = "Oggi sto proprio bene"
puts s01.index("gg")
puts s01.index("pr")
puts s01.index("bene")
nihil = s01.index("zz")
puts typeof(nihil)

che presenta questo output:

1
9
17
(Int32 | Nil)

sono evidenziati gli indici iniziali delle 3 sottostringhe cercate mentre l'ultimo elemento, cercato alla riga 5, sarà nil in quanto la stringa "zz" non esiste in s01. 

Se invece volete sapere qual è il carattere ad un certo indice, avete char_at(indice) che restituisce proprio il carattere che si trova nella posizione dell'indice selezionato:

puts "abcde".char_at(3)   -> d
puts "abcde"[3]           -> d

come è evidente questo metodo ha lo stesso esito dell'uso di [], è una questione di gusti. Tuttavia char_at ha una interessante variante. Se infatti usando [] selezionate un indice fuori range ne risulta un errore. Con char_at potete invece dire al metodo di esporre un carattere di allarme che vi dice che qualcsa è andato storto usando il formato char_at(indice){carattere}:

puts "abcde".char_at(3){'?'}   -> d
puts "abcde".char_at(9){'?'}   -> ?

nel secondo caso, siccome 9 è oltre il range di indice validi per la stringa esce fuori il nostro carattere sentinella. Va detto, pur se ovvio, che se usate un carattere sentinella che è presente nella stringa questo può dar luogo ad ambiguità. Attenzione, quindi.

Il metodo delete è molto potente ed ha vari utilizzi.
  • delete(char) cancella tutte le occorrenze del carattere indicato dalla stringa

    s01 = "aabbccddeeff"
    s01 = s01.delete('a')
    puts s01

    output: bbccddeeff

  • delete(stringa) cancella tutte le occorrenze dei caratteri che compongono la stringa

    s01 = "aabbccddeeffab"
    s01 = s01.delete("ab")
    puts s01

    output: ccddeeff

  • delete(set) cancella un insieme di elementi:

    s01 = "aabbccddee"
    s01 = s01.delete("a-d")
    puts s01

    output: ee
Analogamente, sub permette di effettuare una sostituzione di caratteri all'interno di una stringa in varie modalità, anche in questo caso ne indichiamo alcune a mio avviso tra le più utili:
  • sub(stringa1, stringa2) - cambia la prima occorrenza di stringa1 in stringa2

    s01 = "aabbccddbbb"
    s01 = s01.sub("bb", "TT")
    puts s01

    output: aaTTccddbbb

  • sub(carattere, stringa) -  sostituisce la prima occorrenza del carattere con la stringa

    s01 = "aabbccddbbb"
    s01 = s01.sub('b', "TT")
    puts s01

    output: aaTTbccddbbb

  • sub (carattere, carattere) - sostituisce la prima occorrenza di un carattere con un altro

    s01 = "aabcde"
    s01 = s01.sub('a', 'j')
    puts s01

    output: jabcde
  • sub(indice, stringa) - sostituisce il carattere alla posizione indice con la stringa oppure con un carattere nel formato
    sub(indice, carattere)

    s01 = "abcde"
    s01 = s01.sub(1,"TT")
    puts s01

    output: aTTcde

  • sub(range, stringa) - sostituisce gli elementi aventi indici compresi nel range con la stringa

    s01 = "0123456789"
    s01 = s01.sub(2..5, "abcdef")
    puts s01

    output: 01abcdef6789

 

Come abbiamo visto sub sostituisce la prima occorrenza di una stringa o di un carattere con un'altra stringa. Ma se volessimo sostituire tutte le occorrenze? Allora possiamo usare gsub (global sub, in pratica).


  Esempio 11.8
1
2
3
4
5
s01 = "abcdabcdabcd"
s02 = s01.gsub('a','T')
s03 = s01.gsub('a', "JUVE")
puts s02
puts s03

con questo output:

TbcdTbcdTbcd
JUVEbcdJUVEbcdJUVEbcd

Ora, l'elenco e le possibilità sono davvero tante e in questo paragrafo non intendo certo coprirle tutte. Gli esempi sono solo una traccia di quello che si può e come. la documentazione ufficiale vi indhcerà tutto quanto sia possibile fare, ovviamente. Per intanto vediamo alcuni altri metodi interessanti, sempre nell'ottica di garantire un uso immediato anche di questo strumento.

includes? - restituisce true se la stringa contiene un certo carattere / stringa:

s01 = "abcde"
puts s01.includes?('a')     -> true
puts s01.includes?("bc") -> true
puts s01.includes?('z')    -> false

index - restituisce l'indice della prima occorrenza di un carattere o di una stringa, se non è presente il risultato è nil.

s01 = "ciao come stai amico"
puts s01.index('a')      -> 2
puts s01.index("stai")     -> 10
puts s01.index('3')    -> nil

insert - inserisce a partire dall'indice specificato un carattere o una stringa:

s01 = "aaaa"
s02 = s01.insert(0, 'c')
s03 = s01.insert(0, "xx")
puts s02
puts s03

output:

caaaa
xxaaaa

reverse - rovescia la stringa

puts "abc".reverse   -> cba

split è un interessante metodo e molto utile che permette di costruire un array in cui ogni elemento è una stringa ottenuta dalla suddivisione della stringa originaria eliminando gli spazi bianchi.

s01 = "prima parola seconda parola"
ar1 = s01.split
puts typeof(ar1)
puts ar1

output:

Array(String)
["prima", "parola", "seconda", "parola"]

L'istruzione split può essere basata su altri separatori. Ne parleremo nella sezione degli esempi.

Sempre nell'ambito della correlazione tra array e stringhe non possiamo non parlare dell'utile metodo chars che partendo da una stringa crea un array di caratteri in cui ogni singolo elemento è un carattere della stringa:

s01 = "ciao"
ar1 = s01.chars
puts ar1

questo codice genera quindi un array di caratteri.

strip di default elimina gli spazi iniziali e finali ma, diciamo a richiesta, anche altri caratteri specificandoli quando necessario:

puts " abcd ".strip      -> abcd
puts "aabbaa".strip('a')  -> bb

Esistono anche lstrip e rstrip che effettuano la stessa operazione di strip ma rispettivamente a sinistra e destra della stringa su cui lavorate.

Se invece dovete eliminare il cosiddetto "a capo" potete usare stringa.chomp

Abbiamo poi il solito insieme di metodi che manipolano maiuscole e miniscole in vari modi, vediamo con esempio:

  Esempio 11.9
1
2
3
4
puts "ABCD".downcase
puts "qui stiamo bene".titleize
puts "abcd".capitalize
puts "abcd".upcase

Output:
abcd
Qui Stiamo Bene
Abcd
ABCD

La riga 1 rende tutta una stringa minuscola, la riga 2 pone in maiuscolo le inizali dopo uno spazio, la riga 3 rende maiuscola l'iniziale, la 4, fa il contrario della 1 e rende maiuscola tutta una stringa. Su caratteri che non appartengono alle lettere questi metodi non hanno alcun effetto e nemmeno vanno in errore.Non fanno nulla.

Se vi chiedete quali problematiche esistano per copiare una stringa potete tirare un sospiro di sollievo. Le stringhe sono  immutabili, come abbiamo detto all'inizio e quindi copiare una stringa in un'altra vuole dire crearne una nuova indipendente. Per creare un copia di una stringa potete usare il metodo dup o il semplice =, non cambia nulla e il risultato è lo stesso.  L'esempio che segue illustra le due modalità ed anche il fatto che le modifiche sulla stringa origine non impattano sulle copie. Potete facilmente verificare da soli anche l'opposto.

  Esempio 11.10
1
2
3
4
5
6
7
s01 = "abcd"
s02 = s01
s03 = s01.dup
s01 = s01.delete('a')
puts s01
puts s02
puts s03

e quindi il risultato sarà:

bcd
abcd
abcd

Ricorderete che dalle stringhe, non fosse che per il fatto che lo standard input viene trattato in forma, appunto, di stringa, si può passare ad  altre tipologie di dati, in particolare numerici, Esiste un numero grande di metodi che compiono questa traslazione, alcuni li abbiamo già visti e sul sito ufficiale li trovate proprio tutti.  to_ito_u8to_f64to_i32, ecc... sono tutti lì e il loro uso e significato sono abbastanza evidenti. Come abbiamo evidenziato è ovvio che la stringa deve essere nel formato corretto perchè la conversione abbia successo. Interessanti a questo proposito sono i metodi di conversione vero tipi numerici che terminano con un punto interrogativo. Questi metodi restituiscono in output un tipo unione, ne parleremo in apposito paragrafo, aiutandoci ad evitare crash indesiderati. L'esempio seguente mostra la differenza nel caso di uso di un metodo "normale" e di uno diciamo "esteso"

s01 = "a".to_i
puts typeof(s01)

Questo codice non può funzionare e dà anzi origine ad un erroraccio. Il motivo è, ovviamente, che la stringa "a" non può essere convertita in un numero. Se invece modifichiamo il nostro piccolo programma così:

s01 = "a".to_i?
puts typeof(s01)

ci esce fuori:

(Int32 | Nil)

ovvero un tipo unione. Tenete presente questa possibilità.

Concludiamo questo paragrafo... tornando all'inizio. Abbiamo detto che String.new crea una stringa vuota. Ruby permette di scrivere espressioni come:

s01 = String.new("ciao")

ma in Crystal questo è un errore.
Error: expected argument #1 to 'String.new' to be Pointer(UInt8) or Slice(UInt8), not String
Un possibile uso di new quindi è il seguente:


  Esempio 11.11
1
2
3
4
s01 = String.new(Bytes[98,99,100,101])
puts s01
s02 = String.new(Bytes[1255])
puts s02

Qui l'output è:

bcde    

ovvero una stringa costituita dai caratteri e da un "vuoto" che dipende dal fatto che abbiamo inserito un carattere che non è UTF-8 (come si può constatare con s02.valid_encoding? che restituisce false. Questo è un problema in quanto non ci sono avvisi di questa errata codifica della stringa.

Il trattamento delle stringhe è veramente uno degli argomenti fondamentali in qualsiasi linguaggio e quanto abbiamo visto non è certamente completo, anzi. Ad esempio essendo così tanti i metodi di manipolazione è impossibile essere esaustivi e vi consiglio vivamente di andare sul sito ufficiale e provarli un po'. Troverete tante cose interessanti e utili e sarà un'esperienza formativa districarsi nei vari formati.
Inoltre, riparleremo delle stringhe in congiunzione con l'importante capitolo delle funzioni.