Formattazione dei decimali in stringhe per prestazioni
-
07-07-2019 - |
Domanda
Sto scrivendo un'app che deve generare decimali di varie lunghezze e scala variabile in stringhe senza il punto decimale per scrivere su un file piatto come input su un altro sistema. per es.
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 funzione che ho scritto per gestirlo è sotto e verrà chiamata centinaia di migliaia di volte, quindi voglio assicurarmi che non ci sia un modo migliore e più efficiente per farlo. Ho esaminato i modi in cui DecimalFormat può fare di più per me, ma non riesco a vederlo gestendo il mio bisogno di formattare con posizioni decimali ma senza virgola.
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();
}
Soluzione
La mia implementazione migliorata per le prestazioni è di seguito. È circa 4,5 volte più veloce della soluzione basata su DecimalFormatter: in esecuzione sulla mia macchina, utilizzando Eclipse con un discreto cablaggio di prova preparato in casa, i risultati sono:
- Il vecchio modo impiegava 5421 ms per effettuare 600.000 chiamate (in media 0,009035 ms per chiamata)
- Il nuovo modo ha richiesto 1219 ms per effettuare 600.000 chiamate (in media 0,002032 ms per chiamata)
Alcune note:
- La mia soluzione utilizza un blocco di zero di dimensioni fisse per il riempimento. Se prevedi di aver bisogno di più imbottitura su entrambi i lati rispetto ai circa trenta che ho usato, dovresti aumentare le dimensioni ... chiaramente potresti aumentarle dinamicamente se necessario.
- I tuoi commenti sopra non corrispondono del tutto al codice. In particolare, se è stato restituito un carattere di segno, la lunghezza restituita è maggiore di quella richiesta (i tuoi commenti dicono diversamente). Ho scelto di credere al codice piuttosto che ai commenti.
- Ho reso statico il mio metodo, poiché non richiede uno stato di istanza. Questa è una cosa di gusto personale - ymmv.
Inoltre: al fine di imitare il comportamento dell'originale (ma non indicato nei commenti), questo:
- Tuttavia : se len è negativo, l'originale restituisce una stringa delimitata da virgole. Ciò genera un'eccezione 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();
}
Altri suggerimenti
Se stai ricevendo il tuo input come String per cominciare, perché devi convertirlo in BigDecimal e viceversa?
Sembra che sarebbe molto più veloce trovare la posizione del punto decimale, confrontarlo con lunghezza / scala e riempire la stringa di conseguenza.
Sono d'accordo con la manipolazione manuale delle stringhe di ChssPly76 wrt.
Tuttavia, se stai andando a scendere la rotta BigDecimal
/ DecimalFormat
, potresti prendere in considerazione l'idea di condividere i tuoi DecimalFormat
invece di crearne uno nuovo ad ogni iterazione. Nota che queste classi non sono thread-safe, quindi se stai usando più thread per eseguire la tua elaborazione, vorrai usare qualcosa come l'archiviazione ThreadLocal
per mantenere un formattatore per Thread
.
a proposito, hai provato questo e hai trovato le prestazioni non accettabili o stai solo cercando la soluzione più efficiente possibile? Nota cosa dice Donald Knuth sull'argomento dell'ottimizzazione precoce .