La funzione round() in PHP 5.2 e precedenti

Pubblicato: 27 ottobre 2009 in Php
Tag:, ,

Tutti coloro che hanno studiato un po’ di fondamenti di informatica sanno che le funzioni di arrotondamento rappresentano la balaustra che ci protegge dal buco nero della rappresentazione binaria dei numeri in virgola mobile. Una balaustra invero piuttosto cedevole, visto che per sua natura questa rappresentazione non è in grado di rappresentare tutti i numeri reali, e questo è ovvio, ma nemmeno i numeri razionali, e questo ci piace dimenticarlo.
Che \sqrt 2 non si possa in nessun modo rappresentare esattamente con un numero finito di cifre decimali uno ci ha fatto un po’ il callo, però che nella rappresentazione binaria in virgola mobile nemmeno un numero come 0.1 possa essere rappresentato con precisione finita può essere dura da accettare.
Fa effetto, lo so, sapere che in double precision il numero 0.1 è scritto così:

0.1000000000000000055511151231257827021181583404541015625

mentre in extended precision è scritto così:

0.1000000000000000000013552527156068805425093160010874271392822265625

Ecco perché gli arrotondamenti sono un argomento fondamentale, l’unica possibilità di avere una rappresentazione decente di 0.1 è di arrotondarlo alla 14a cifra decimale (l’arrotondamento dovrebbe garantire le prime 15, ma non si sa mai).

Questo è quasi sempre perfettamente accettabile finché si lavora con numeri con poche cifre decimali, e diventa un incubo quando la precisione richiesta si accosta a quella che la macchina è in grado di gestire. Se dovete quindi lavorare con una precisione molto alta potete tranquillamente smettere di leggere, non parlerò dei metodi numerici né dei pacchetti software che cercano di ovviare a questo tipo di problemi.

Però anche nel caso che la precisione richiesta sia piccola non è difficile finire nei guai. Diamo un’occhiata a cosa dice il manuale di PHP 5.3 riguardo la funzione round():

float round ( float $val [, int $precision = 0 [, int $mode = PHP_ROUND_HALF_UP ]] )

Returns the rounded value of val to specified precision (number of digits after the decimal point). precision can also be negative or zero (default).

e poi, poco sotto nel changelog:

5.3.0 The mode parameter was introduced.

Già. Ma cosa si intende per “rounded value”? E prima della 5.3? E cosa indica questo mode?

Ci sono fondamentalmente solo cinque modi per arrotondare un numero (solo? direte voi un po’ delusi, no, sono molti di più stavo cercando di non spaventarvi):

– verso l’alto, cioè verso +\infty (ceil)
– verso il basso, cioè verso -\infty (floor)
– verso lo zero (truncate)
– allontanandosi dallo zero
– verso il più vicino

Un esempietto dovrebbe chiarire le idee:

il numero +42,42 può così essere arrotondato in precisione zero rispettivamente in:
+43
+42
+42
+43
+42

il numero +42,62 viene invece arrotondato:
+43
+42
+42
+43
+43

il numero -42,42:
-42
-43
-42
-43
-42

lascio gli arrotondamenti del numero -42,62 come esercizio.

Epperò “verso il più vicino” ci piace di più, è meno iniquo, solo che nasconde una trappola. Come arrotondo +42,5? ci sono solo quattro possibilità (bastardi! sono anche qui di più ma facciamo finta):

– per eccesso (arrotondamento aritmetico)
– allontanandosi dallo zero
– al numero pari (arrotondamento del banchiere)
– a caso

Sono di più perché i primi tre hanno un complementare (per difetto, verso lo zero, al numero dispari). Vediamoli agire sul nostro +42,5:
+43
+43
+42
??

mentre per -42,5 abbiamo:
-42
-43
-43
??

E lasciamo i complementari per esercizio.

Ora veniamo al PHP. Cosa intende il manuale con “rounded value”? prima della versione 5.3 a questa domanda in pratica non c’è risposta se non che l’arrotondamento che avviene è di tipo “verso il più vicino”, dalla 5.3 hanno introdotto questo benedetto parametro mode che può assumere i seguenti valori:

PHP_ROUND_HALF_UP (arrotondamento aritmetico)
PHP_ROUND_HALF_DOWN (suo complementare)
PHP_ROUND_HALF_EVEN (arrotondamento del banchiere)
PHP_ROUND_HALF_ODD (suo complementare)

Le prove che ho fatto io sono su una macchina Intel che monta un linux con PHP 5.2.4, e cambia, infatti il valore di d in:

// volatile to disable compile-time optimizations for this example
volatile double v = 2877.0;
double d = v / 1000000.0;

dipende dal compilatore C con il quale viene compilato. Che casino eh?

Le prove fanno pensare che di default il PHP 5.2 utilizzi PHP_ROUND_HALF_EVEN contrariamente a quanto succede per PHP 5.3 (anche se qui non ho verificato, ma direi che se nessuno s’incazza dovrebbe essere così).

E finalmente possiamo porre il problema dal quale tutto questo ha avuto origine. Abbiamo due sistemi, uno in intel/linux/PHP 5.2 ed uno da un’altra parte con altri defaults. Questi due sistemi però agli occhi del mondo si devono comportare nello stesso modo, giusto o sbagliato che sia. Ad esempio potrebbe esserci un e-commerce in PHP ed un programma di contabilità su AS/400. Se il programma di contabilità emette una fattura arrotondata al centesimo di euro che è diversa dall’ordine emesso dal PHP sempre arrotondato al centesimo di euro è un bel problema. Nel mio caso specifico il sistema di contabilità utilizza l’arrotondamento aritmetico, e così ho dovuto scrivere una funzione che correggesse il comportamento di default di round():

<?php
function myround($f, $p)
{
	// Sono numeri di precisione piccola, uso la manipolazione simbolica
	// che usare le potenze di dieci sono capaci tutti
	$s = "$f";
	$dot = strpos($s, ".");
	// Non ci sono cifre decimali
	if ($dot === false) {
		return round($f, $p);
	}
	else {
		$dec = strlen(substr($s, $dot)) - 1;
		// Se il numero di cifre decimali è uno in più della precisione
		// e se l'ultima cifra è 5 dobbiamo correggere
		if ($dec == $p + 1 && substr($s, -1, 1) == '5') {
			// Il vecchio trucco del "fuzz"
			return round($f + 0.0000001, $p);
		}
		else {
			return round($f, $p);
		}
	}
}

// esempi

$val = 16.275;
print "val: ".$val."\n";
print "myround: ".myround($val, 2)."\n";
print "round: ".round($val, 2)."\n";
print "================\n";
$val = 15.275;
print "val: ".$val."\n";
print "myround: ".myround($val, 2)."\n";
print "round: ".round($val, 2)."\n";
print "================\n";
$val = 16.2745;
print "val: ".$val."\n";
print "myround: ".myround($val, 2)."\n";
print "round: ".round($val, 2)."\n";
print "================\n";
$val = "16";
print "val: ".$val."\n";
print "myround: ".myround($val, 2)."\n";
print "round: ".round($val, 2)."\n";
print "================\n";
$val = "16.5";
print "val: ".$val."\n";
print "myround: ".myround($val, 2)."\n";
print "round: ".round($val, 2)."\n";
print "================\n";
$val = "16.2750000000001";
print "val: ".$val."\n";
print "myround: ".myround($val, 2)."\n";
print "round: ".round($val, 2)."\n";

Riferimenti:

Request for Comments: Rounding in PHP (bellissimo, da leggere!)
PHP Manual: round — Rounds a float
PHP Manual: number_format — Format a number with grouped thousands
Rounding – From Wikipedia, the free encyclopedia
754-2008 IEEE Standard for Floating-Point Arithmetic

Lascia un commento

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...