samedi 3 décembre 2011
Ce matin, je vais vous entretenir à propos d'un bogue que j'ai trouvé cette semaine dans un système qui calcule du temps. Pour faire court, le temps saisi au formulaire était correct, il était ensuite stocké dans une base de données sous forme de nombre à virgule flotante et lorsqu'il était récupéré puis affiché à l'écran, une minute était parfois perdue par rapport à l'heure initiale. Concrètement, 8h25 devenait soudain 8h24! Chaque enregistrement modifiant l'heure décrémentait les minutes. Multiplié par des centaines de manipulations, ça cumulait du temps perdu.
Je soupçonnais la fonction de formatage de l'heure à l'écran.
// simplifiée pour la démonstrationJe l'ai donc isolée et j'ai lancé un jeu de tests couvrant toutes les minutes d'une heure donnée (BR est une constante définie pour imprimer une balise de saut de ligne) :
function format($decimalTime){
$hour = (int)$decimalTime;
$minute = (int)(($decimalTime - $hour) * 60);
return $hour . 'h' . str_pad($minute, 2, '0', STR_PAD_LEFT);
}
echo "Heure\tMinutes\tFormaté\tReprésentation interne" . BR;Selon les résultats retournés, pratiquement 1 affichage sur 2 étaient erronés!
$hour = 8;
foreach( range(0,59) as $minute ){
$time = $hour + ($minute/60);
echo $hour . "\t" . $minute . "\t" . format($time) . "\t" . $time . BR;
}
Comme la partie des minutes est stockée en décimales, l'hypothèse d'une mauvaise conversion se confirmait. Dans certains cas, ça fonctionnait bien pour les fractions simples (8h15 = 8.25, 8h30 = 8.50, etc), alors que pour d'autres valeurs comme 8h12 (8.2 en décimal, 12 min = 1/5 d'une heure, donc 0.2), la fonction retourne la valeur formatée de 8h11 avec une minute en moins. Ce n'est pas le genre d'erreur qu'on peut tolérer (surtout quand le temps, c'est de l'argent).
J'ai ensuite fait quelques tests supplémentaires :
//Pour la portion minutes, si :À cause de la conversion (cast) float à int, la représentation interne de 4.9999999999999 ne conserve que le 4 plutôt que de l'arrondir à 5. Ce qu'il aurait fallu faire :
var_dump(5/1); // int(5)
var_dump(5/60); // float(0.083333333333333)
// remettre les minutes décimales sur une base 60
var_dump((5/60)*60); // devient un float(5)
// exemple
$time = 8.083333333333333; // représentation interne de 8h05
$hour = (int)$time;
$minute = ($time - $hour) * 60;
var_dump( $minute ); // float(4.9999999999999)
var_dump( (int)$minute ); // int(4) et non 5 !
var_dump( (int)round($minute, 0) ); // 5Pour corriger le bogue, je n'ai eu qu'à modifier la fonction en remplaçant une ligne comme ceci :
function format($decimalTime){Ce fût ni long, ni difficile à régler. Mais la cause était incompréhensible pour le client et les conséquences étaient fâcheuses puisqu'il fallait ensuite faire rebalancer les données.
$hour = (int)$decimalTime;
//$minute = (int)(($decimalTime - $hour) * 60);
$minute = (int)round(($decimalTime - $hour) * 60, 0);
return $hour . 'h' . str_pad($minute, 2, '0', STR_PAD_LEFT);
}
Après réflexion, je me demande ce que ça aurait donné si, au lieu d'avoir choisit initialement de stocker les heures en float, on avait plutôt opté pour les conserver en un seul nombre entier représentant le total des minutes plutôt que des fractions d'heures (8h05 = 8h*60 minutes + 5 minutes = 485 minutes) ? Nous n'aurions probablement pas eu de problème de conversion, mais ça aurait certainement occasionné d'autres genres de problèmes.
Stocker du temps sous forme de nombre à virgule flottante, c'est criminel :D
Quelle est la sentence, bourreau ? :)
À la limite, un champ "time" d'une base de données aurait pu faire l'affaire si le temps à stocker était inférieur à 24h (à vrai dire, ça fait référence à du temps à l'intérieur d'une journée). Mais comme l'objectif est de cumuler un nombre d'heures travaillées, par exemple une semaine de travail de 40h, ou un cumulatif annuel, le float n'est pas l'idéal pour les raisons que j'ai donné.
Si j'avais eu à modéliser le stockage interne, j'aurais probablement opté pour que le total des heures soit converti en minutes (integer = (heures*60)+minutes).
Vous avez une meilleure solution ?
J'utilise le format time sans aucun problème, même pour des durées supérieur a 24h. Cela dépend de la base de donnée, dans mon cas il s'agit de MySql.
Je cite : 'Les valeurs de TIME vont de '-838:59:59' à '838:59:59'.'
cf: doc mysql
Merci, c'est une particularité utile à savoir pour MySQL.
Sous Posgres, les valeurs du type time vont de 00:00:00 à 24:00:00. Si on veut développer une application portable et compatible sur différents types de base de données, il vaut mieux se limiter à des valeurs de 24h maximum ou stocker le temps autrement.