Mise en forme de décimales en chaînes pour la performance
-
07-07-2019 - |
Question
J'écris une application qui doit générer des décimales de différentes longueurs et échelles variables pour les chaînes sans décimale pour l'écriture dans un fichier plat en tant qu'entrée vers un autre système. par exemple
12345 -> Length:10, Scale:2 -> 0001234500
123.45 -> Length:10, Scale:2 -> 0000012345
123.45 -> Length:10, Scale:3 -> 0000123450
-123.45 -> Length:10, Scale:3, signed:true -> -000123450
123.45 -> Length:10, Scale:3, signed:true -> +000123450
La fonction que j'ai écrite pour gérer ceci est ci-dessous et sera appelée des centaines de milliers de fois. Je veux donc m'assurer qu'il n'y a pas de meilleure façon, plus efficace, de faire cela. J'ai cherché des moyens de faire en sorte que DecimalFormat en fasse plus pour moi, mais je ne le vois pas gérer mon besoin de formater avec des décimales, mais sans séparateur décimal.
protected String getFormattedDecimal( String value, int len, int scale, Boolean signed ) throws Exception{
StringBuffer retVal = new StringBuffer();
//Need a BigDecimal to facilitiate correct formatting
BigDecimal bd = new BigDecimal( value );
//set the scale to ensure that the correct number of zeroes
//at the end in case of rounding
bd = bd.setScale( scale );
//taking it that if its supposed to have negative it'll be part of the num
if ( ( bd.compareTo( BigDecimal.ZERO ) >= 0 ) && signed ){
retVal.append( "+" );
}
StringBuffer sbFormat = new StringBuffer();
for (int i = 0; i < len; i++)
{
sbFormat.append('0');
}
DecimalFormat df = new DecimalFormat( sbFormat.toString() );
retVal.append( df.format( bd.unscaledValue() ) );
return retVal.toString();
}
La solution
Ma mise en œuvre améliorée en termes de performances est ci-dessous. Elle est environ 4,5 fois plus rapide que la solution basée sur DecimalFormatter: en cours d’exécution sur ma machine, en utilisant Eclipse avec un bon harnais de test préparé à la maison, les résultats sont les suivants:
- Il fallait 5421 ms pour passer 600 000 appels (en moyenne 0,009035 ms par appel).
- Il a fallu 1219 ms à la nouvelle manière pour passer 600 000 appels (en moyenne 0,002032 ms par appel).
Quelques notes:
- Ma solution utilise un bloc de zéros de taille fixe pour le remplissage. Si vous prévoyez avoir besoin de plus de rembourrage de chaque côté que la trentaine que j'ai utilisée, vous devrez augmenter la taille ... clairement, vous pouvez l'augmenter dynamiquement si nécessaire.
- Vos commentaires ci-dessus ne correspondaient pas tout à fait au code. En particulier, si un caractère de signe a été renvoyé, la longueur renvoyée est supérieure à celle demandée (vos commentaires indiquent le contraire). J'ai choisi de croire le code plutôt que les commentaires.
- J'ai rendu ma méthode statique, car elle ne nécessite aucun état d'instance. C'est une affaire de goût personnel - ymmv.
Aussi: pour imiter le comportement de l'original (mais pas dans les commentaires), ceci:
- Cependant : si len est négatif, l'original renvoie une chaîne délimitée par des virgules. Ceci jette une exception IllegalARgumentException
package com.pragmaticsoftwaredevelopment.stackoverflow;
...
final static String formatterZeroes="00000000000000000000000000000000000000000000000000000000000";
protected static String getFormattedDecimal ( String value, int len, int scale, Boolean signed ) throws IllegalArgumentException {
if (value.length() == 0) throw new IllegalArgumentException ("Cannot format a zero-length value");
if (len <= 0) throw new IllegalArgumentException ("Illegal length (" + len + ")");
StringBuffer retVal = new StringBuffer();
String sign=null;
int numStartIdx;
if ("+-".indexOf(value.charAt(0)) < 0) {
numStartIdx=0;
} else {
numStartIdx=1;
if (value.charAt(0) == '-')
sign = "-";
}
if (signed && (value.charAt(0) != '-'))
sign = "+";
if (sign==null)
sign="";
retVal.append(sign);
int dotIdx = value.indexOf('.');
int requestedWholePartLength = (len-scale);
if (dotIdx < 0) {
int wholePartPadLength = (requestedWholePartLength - ((value.length()-numStartIdx)));
if (wholePartPadLength > 0)
retVal.append(formatterZeroes.substring(0, wholePartPadLength));
retVal.append (value.substring(numStartIdx));
if (scale > 0)
retVal.append(formatterZeroes.substring(0, scale));
}
else {
int wholePartPadLength = (requestedWholePartLength - (dotIdx - numStartIdx));
if (wholePartPadLength > 0)
retVal.append(formatterZeroes.substring(0, wholePartPadLength));
retVal.append (value.substring(numStartIdx, dotIdx));
retVal.append (value.substring (dotIdx+1));
int fractionalPartPadLength = (scale - (value.length() - 1 - dotIdx));
if (fractionalPartPadLength > 0)
retVal.append(formatterZeroes.substring(0, fractionalPartPadLength));
}
return retVal.toString();
}
Autres conseils
Si vous obtenez votre entrée sous forme de chaîne pour commencer, pourquoi devez-vous la convertir en BigDecimal et inversement?
Il semble qu’il serait beaucoup plus rapide de trouver la position du point décimal, de le comparer à la longueur / l’échelle et d’enfiler la chaîne en conséquence.
Je suis d’accord avec ChssPly76 en ce qui concerne la manipulation manuelle des chaînes.
Cependant, si vous voulez descendre la route BigDecimal
/ DecimalFormat
, vous pouvez envisager de partager vos DecimalFormat
au lieu de créer un nouveau à chaque itération. Notez que ces classes ne sont pas thread-safe, donc si vous utilisez plusieurs threads pour effectuer votre traitement, vous souhaiterez utiliser quelque chose comme stockage ThreadLocal
pour maintenir un formateur par Thread
.
btw, avez-vous testé cela et trouvé que les performances n'étaient pas acceptables ou êtes-vous simplement à la recherche de la solution la plus efficace possible? Notez ce que Donald Knuth a dit au sujet de l'optimisation initiale .