Home Rhyylen
Contatto
 
 
 
Crystal Language
Capitolo 5
Numeri: numeri con virgola

Anche questo è un argomento di grande importanza ed anche un po' critico, da sempre. Il problema è che i computer "parlano" con elementi interi in un sistema binario fatto di 0 e 1e in numeri con virgola o floating point hanno delle peculiarità concettuali difficili da riprodurre in un simile sistema. Infatti, tutto sommato, qualche particolarità c'è sempre ma possiamo per lo più ignorare questi aspetti.
I numeri con virgola si presentano nel consueto formato, niente di nuovo

x = 1.2
y = 0.34

vanno bene ma non

z = .22

la notazione accettata è quindi solo x.y. Un'altra notazione accettata è quella esponenziale:

x = 3.12345e3   equivale a
3.12345 * 10^3  quindi a
3123.45

Crystal ci propone due tipi di numeri con virgola:
  • float32
  • float64
evidentemente la differenza consiste nel numero di bit sui quali sono definiti che influenza anche la loro precisione ovvero la profondità di approssimazione alla quale si può arrivare. Anche in questo caso esistono dei massimi e dei minimi che possiamo scoprire attraverso questo semplice programma:

  Esempio 5.1
1
2
3
4
5
6
7

8
f32m = Float32::MIN
f32M = Float32::MAX
f64m = Float64::MIN
f64M = Float64::MAX
puts "Minimo per il tipo float32 = #{f32m}"
puts "Massimo per il tipo float32 = #{f32M}"
puts "Minimo per il tipo float64 = #{f64m}"
puts "Massimo per il tipo float64 = #{f64M}"

che ci dà il seguente output:

Minimo per il tipo float32 = -3.4028235e+38
Massimo per il tipo float32 = 3.4028235e+38
Minimo per il tipo float64 = -1.7976931348623157e+308
Massimo per il tipo float64 = 1.7976931348623157e+308

Anche le operazioni di base non creano particolari problemi:

  Esempio 5.2
1
2
3
4
5
6
7
8
9
10
x = 9.23
y = 7.557
puts typeof(x)
puts x + y
puts x - y
puts x * y
puts x / y
puts x % y
puts x ** y
puts x ** -y

l'output è:

Float64
16.787
1.673
69.75111000000001
1.2213841471483393
1.673
19680188.536044378
5.081252134188117e-8

Quindi, la riga 3 ci indica che il tipo attribuito di default è float64. Se vogliamo forzare l'appartenenza ai float32 dobbiamo ricorrere al solito metodo per forzare l'attribuzione del tipo da noi scelto, ovvero, come nel caso degli interi, si usa un suffisso:

x = 7.3_f32

tutto il resto del programma direi che non presenta difficoltà concettuali particolari, le operazioni sono quelle classiche viste nel paragrafo relativo agli interi. Vi sono alcune differenze, ad esempio non è possibile applicare tdiv per avere la parte intera della divisione. E' disponibile invece remainder che funziona come nel caso degli interi. Tuttavia potrete avere il quoziente di una divisione tra numeri con virgola usando un metodo proprio dei nimeri con virgola ovvero trunc, come dice il nome stesso, tronca un numero alla sua parte intera:

x = 3.2
y = 1.5
puts (x / y).trunc

che ci restituisce 2.0.  Ovviamente può essere applicato anche sul singolo numero. Un altro sistema fa uso del metodo to_i che permette il passaggio da float a intero eliminando la parte decimale ma anche modificando del tutto la natura del numero, originariamente nato come float mentre il risultato è un intero a tutti gli effetti. Vediamo le differenze in questo breve codice:

  Esempio 5.3
1
2
3
4
5
6
x = 2.17
y = 1.22
z = (x / y).trunc
t = (x / y).to_i
puts z
puts t

il cui output è:

1.0
1

Ovvero un float e un intero. Nello scorso paragrafo abbiamo visto to_f che fa il passaggio inverso da intero verso float.
Il passaggio inverso da intero verso float è molto semplice:

  Esempio 5.4
1
2
3
4
x = 3
y = 3.to_f
puts typeof(y)
puts y

il cui output è:

Float64
3.0

se volete forzare il passaggio verso un float32 dovrete usare to_f32. provare per credere.
Il passaggio da f64 a f32 è possibile con le medesime modalità ma attenzione alla perdita di informazione. Il passaggio inverso è possibile ma gli esiti potrebbero essere non sempre precisi per cui, personalmente, lo sconsiglio. L'esempio successivo vi illustra la cosa, analizzate con attenzione i risultati:

Esempio 5.5
1
2
3
4
5
6
7
8
9

10
11
x = 17 / 13
puts x
puts typeof(x)
x = x.to_f32
puts typeof(x)
puts x
z = (17 / 13).to_f32
puts z
z = z.to_f64
puts typeof(z)
puts z

e  il suo output:

1.3076923076923077
Float64
Float32
1.3076923
1.3076923
Float64
1.307692289352417

La convivenza tra interi e float è abbastanza pacifica. Le 4 operazioni di base che coinvolgano un numero di un tipo e un numero dell'altro danno un risultato di tipo float anche se intero come valore. Ovvero, ad esempio 4.0 * 2 dà 8.0 e non 8.

Un altro passaggio che potrebbe capitare, ad esempio importando i dati da e verso un file o un database, è quello della conversione da e verso il classico formato stringa. Il passaggio verso stringa è molto semplice grazie al metodo
to_s:

x = 2.333
y = x.to_s
puts y + 'a'

e vedrete che il carattere 'a' si aggiunge al vostro 2.333 convertito in sequenza di caratteri (tale è una stringa, come vedremo). Il passaggio inverso invece è un po' più delicato non tanto se convertite direttamente un numero quanto, come già visto nel caso degli interi, se gestite lo standard input:

print "Inserisci un numero: "
f1 = gets()
f2 = (f1.to_s).to_f
puts f2 + 1

Questo è il modo corretto, similmente a quanto visto per gli interi nel capitolo 2.
E' il momento per vedere un po' di metodi applicabili sui numeri in virgola. Abbiamo già visto to_s, to_i, trunc, nel seguente esempio aggiungiamo altri elementi utili:

  Esempio 5.6
1
2
3
4

5
x = 1.235
puts x.ceil
puts x.floor
puts x.round
puts 3.3.modulo(2)

ceil - arrotonda all'intero superiore più vicino - risultato sarà 2
floor - arrotonda all'intero inferiore più vicino - risultato sarà 1
round - arrotonda secondo alcune regole che vedremo nell'esempio successivo - in questo caso il risultato è 1
modulo - resituisce il resto della divisione tra il numero su cui è applicato e quello indicato nella parentesi. Il risulato potrebbe non essere preciso ma  solo una ottima approssimazione. Nell'esempio a me è saltato fuori 1.2999999999999998 invece di 1.3

Ovviamente, vi sono molti altri metodi applicabili, che trovate sul sito ufficiale.

Quanto a
round gli esempi successivi dovrebbero chiarire il suo comportamento:

puts 1.2.round     restituisce 1
puts 1.4.round     restituisce 1
puts 1.41.round   restituisce 1
puts 1.5.round     restituisce 2
puts 1.9.round     restituisce 2
puts 1.23456.round(3)  arrotonda secondo le regole precedenti ammettendo 3 cifre decimali.

Anche per i numeri con virgola può essere in qualche caso necessario spingersi un po' più in là con la profondità di calcolo ed in questo caso disponiamo di due tipi che fanno al caso nostro:

BigDecimal: numeri con virgola aventi con precisione arbitraria
BigFloat: numeri con virgola grandi a piacere

Anche in questo caso, si tratta di tipi pesanti che possono impattare negativamente (in caso di uso intenso, si capisce, non nei nostri semplici test) le prestazioni della vostra applicazione. Un breve esempio:


require "big"
x = BigDecimal.new("1.23445459485485043850943859430009866")
puts x + 1

Ne riparleremo nella sezione apposita dove approfondiremo anche il discorso dei BigInt e dei, finora non citati, BigRational.

Vediamo per chiudere i soliti casi critici che ricalcano pari pari quelli visti per gli interi:

puts 0.0 / 0.0
puts 1.0 / 0.0
puts -1.0 / 0.0

come risultato avremo rispettivamente Nan, Infinity e -Infinity mentre anche per questo tipo abbiamo il problema del resto per i dividendo negativi

puts -7.0 % 3.0
puts -7.0.remainder(3.0)
puts -7.0.modulo(3.0)

col seguente output:

2.0
-1.0
2.0

che ci ripresenta la solita ambiguità, almeno in apparenza, in realtà è proprio diversa la natura dell'operazione.

Con questo, abbiamo concluso il nostro rapido excursus sui numeri in virgola ed il loro uso. Non vi resta che fare un po' di prove.