1. Información general
En este artículo, vamos a explorar la clase DecimalFormat junto con sus usos prácticos.
Esta es una subclase de NumberFormat , que permite formatear la representación de cadenas de números decimales utilizando patrones predefinidos.
También se puede usar a la inversa, para convertir cadenas en números.
2. ¿Cómo funciona?
Para formatear un número, tenemos que definir un patrón, que es una secuencia de caracteres especiales potencialmente mezclados con texto.
Hay 11 caracteres de patrones especiales, pero los más importantes son:
- 0: imprime un dígito si se proporciona, 0 en caso contrario
- #: Imprime un dígito si se proporciona, nada de lo contrario
- . - indicar dónde poner el separador decimal
- , - indicar dónde colocar el separador de agrupación
Cuando el patrón se aplica a un número, se ejecutan sus reglas de formato y el resultado se imprime de acuerdo con DecimalFormatSymbol de nuestra JVM's Locale a menos que se especifique una Locale específica.
Los resultados de los siguientes ejemplos son de una JVM que se ejecuta en una configuración regional en inglés .
3. Formateo básico
Veamos ahora qué salidas se producen al formatear el mismo número con los siguientes patrones.
3.1. Decimales simples
double d = 1234567.89; assertThat( new DecimalFormat("#.##").format(d)).isEqualTo("1234567.89"); assertThat( new DecimalFormat("0.00").format(d)).isEqualTo("1234567.89");
Como podemos ver, la parte entera nunca se descarta, no importa si el patrón es menor que el número.
assertThat(new DecimalFormat("#########.###").format(d)) .isEqualTo("1234567.89"); assertThat(new DecimalFormat("000000000.000").format(d)) .isEqualTo("001234567.890");
Si, en cambio, el patrón es más grande que el número, se agregan ceros, mientras que se eliminan los hash, tanto en la parte entera como en la decimal.
3.2. Redondeo
Si la parte decimal del patrón no puede contener toda la precisión del número de entrada, se redondea.
Aquí, la parte de .89 se ha redondeado a .90, luego se ha eliminado el 0:
assertThat(new DecimalFormat("#.#").format(d)) .isEqualTo("1234567.9");
Aquí, la parte de .89 se ha redondeado a 1.00, luego se ha eliminado el .00 y el 1 se ha sumado al 7:
assertThat(new DecimalFormat("#").format(d)) .isEqualTo("1234568");
El modo de redondeo predeterminado es HALF_EVEN, pero se puede personalizar mediante el método setRoundingMode .
3.3. Agrupamiento
El separador de agrupación se utiliza para especificar un subpatrón que se repite automáticamente:
assertThat(new DecimalFormat("#,###.#").format(d)) .isEqualTo("1,234,567.9"); assertThat(new DecimalFormat("#,###").format(d)) .isEqualTo("1,234,568");
3.4. Múltiples patrones de agrupación
Algunos países tienen un número variable de patrones de agrupación en sus sistemas de numeración.
El sistema de numeración indio utiliza el formato #, ##, ###. ##, en el que solo el primer separador de agrupación contiene tres números, mientras que todos los demás tienen dos números.
Esto no es posible de lograr usando la clase DecimalFormat , que mantiene solo el último patrón encontrado de izquierda a derecha y lo aplica al número entero, ignorando los patrones de agrupación anteriores.
Un intento de usar el patrón #, ##, ##, ##, ### resultaría en un reagrupamiento a #######, ### y terminaría en una redistribución a #, ###, # ##, ###.
Para lograr una coincidencia de patrones de agrupación múltiple, es necesario escribir nuestro propio código de manipulación de cadenas o, alternativamente, probar DecimalFormat de Icu4J , que lo permite.
3.5. Mezcla de literales de cadena
Es posible mezclar literales de cadena dentro del patrón:
assertThat(new DecimalFormat("The # number") .format(d)) .isEqualTo("The 1234568 number");
También es posible utilizar caracteres especiales como literales de cadena , mediante el escape:
assertThat(new DecimalFormat("The '#' # number") .format(d)) .isEqualTo("The # 1234568 number");
4. Formato localizado
Muchos países no usan símbolos en inglés y usan la coma como separador decimal y el punto como separador de grupos.
La ejecución del patrón #, ###. ## en una JVM con una configuración regional italiana , por ejemplo, daría como resultado 1.234.567,89.
Si bien esta podría ser una característica útil de i18n en algunos casos, en otros es posible que deseemos aplicar un formato específico independiente de la JVM.
Así es como podemos hacer eso:
assertThat(new DecimalFormat("#,###.##", new DecimalFormatSymbols(Locale.ENGLISH)).format(d)) .isEqualTo("1,234,567.89"); assertThat(new DecimalFormat("#,###.##", new DecimalFormatSymbols(Locale.ITALIAN)).format(d)) .isEqualTo("1.234.567,89");
Si el Locale que nos interesa no está entre los cubiertos por el constructor DecimalFormatSymbols , podemos especificarlo con el método getInstance :
Locale customLocale = new Locale("it", "IT"); assertThat(new DecimalFormat( "#,###.##", DecimalFormatSymbols.getInstance(customLocale)).format(d)) .isEqualTo("1.234.567,89");
5. Notaciones científicas
The Scientific Notation represents the product of a Mantissa and an exponent of ten. The number 1234567.89 can also be represented as 12.3456789 * 10^5 (the dot is shifted by 5 positions).
5.1. E-Notation
It's possible to express a number in Scientific Notation using the E pattern character representing the exponent of ten:
assertThat(new DecimalFormat("00.#######E0").format(d)) .isEqualTo("12.3456789E5"); assertThat(new DecimalFormat("000.000000E0").format(d)) .isEqualTo("123.456789E4");
We should keep in mind that the number of characters after the exponent is relevant, so if we need to express 10^12, we need E00 and not E0.
5.2. Engineering Notation
It's common to use a particular form of Scientific Notation called Engineering Notation, which adjusts results in order to be expressed as multiple of three, for example when using measuring units like Kilo (10^3), Mega (10^6), Giga (10^9), and so on.
We can enforce this kind of notation by adjusting the maximum number of integer digits (the characters expressed with the # and on the left of the decimal separator) so that it's higher than the minimum number (the one expressed with the 0) and higher than 1.
This forces the exponent to be a multiple of the maximum number, so for this use-case we want the maximum number to be three:
assertThat(new DecimalFormat("##0.######E0") .format(d)).isEqualTo("1.23456789E6"); assertThat(new DecimalFormat("###.000000E0") .format(d)).isEqualTo("1.23456789E6");
6. Parsing
Let's see how is possible to parse a String into a Number with the parse method:
assertThat(new DecimalFormat("", new DecimalFormatSymbols(Locale.ENGLISH)) .parse("1234567.89")) .isEqualTo(1234567.89); assertThat(new DecimalFormat("", new DecimalFormatSymbols(Locale.ITALIAN)) .parse("1.234.567,89")) .isEqualTo(1234567.89);
Dado que el valor devuelto no se infiere por la presencia de un separador decimal, podemos usar métodos como .doubleValue () , .longValue () del objeto Number devuelto para hacer cumplir una primitiva específica en la salida.
También podemos obtener un BigDecimal de la siguiente manera:
NumberFormat nf = new DecimalFormat( "", new DecimalFormatSymbols(Locale.ENGLISH)); ((DecimalFormat) nf).setParseBigDecimal(true); assertThat(nf.parse("1234567.89")) .isEqualTo(BigDecimal.valueOf(1234567.89));
7. Seguridad del hilo
DecimalFormat no es seguro para subprocesos , por lo que debemos prestar especial atención al compartir la misma instancia entre subprocesos.
8. Conclusión
Hemos visto los usos principales de la clase DecimalFormat , junto con sus fortalezas y debilidades .
Como siempre, el código fuente completo está disponible en Github.