Mover posiciones decimales en un doble

97

Entonces tengo un conjunto doble para igualar 1234, quiero mover un lugar decimal para que sea 12.34

Entonces, para hacer esto, multiplico .1 a 1234 dos veces, algo así

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.println(x);

Esto imprimirá el resultado, "12.340000000000002"

¿Hay alguna manera, sin simplemente formatearlo a dos lugares decimales, para que el doble almacene correctamente 12,34?

BlackCow
fuente
30
Aquí hay un enlace al artículo original "Lo que todo informático debe saber sobre la aritmética de punto flotante"
bsd
43
¿Hay alguna razón por la que no lo hizo x /= 100;?
Mark Ingram

Respuestas:

189

Si usa doubleo float, debe usar el redondeo o esperar ver algunos errores de redondeo. Si no puede hacer esto, use BigDecimal.

El problema que tiene es que 0,1 no es una representación exacta, y al realizar el cálculo dos veces, está agravando ese error.

Sin embargo, 100 se pueden representar con precisión, así que intente:

double x = 1234;
x /= 100;
System.out.println(x);

que imprime:

12.34

Esto funciona porque Double.toString(d)realiza una pequeña cantidad de redondeo en su nombre, pero no es mucho. Si se pregunta cómo se vería sin redondeo:

System.out.println(new BigDecimal(0.1));
System.out.println(new BigDecimal(x));

huellas dactilares:

0.100000000000000005551115123125782702118158340454101562
12.339999999999999857891452847979962825775146484375

En resumen, el redondeo es inevitable para respuestas sensatas en punto flotante, ya sea que lo esté haciendo explícitamente o no.


Nota: x / 100y x * 0.01no son exactamente iguales cuando se trata de error de redondeo. Esto se debe a que el error de redondeo de la primera expresión depende de los valores de x, mientras que el 0.01de la segunda tiene un error de redondeo fijo.

for(int i=0;i<200;i++) {
    double d1 = (double) i / 100;
    double d2 = i * 0.01;
    if (d1 != d2)
        System.out.println(d1 + " != "+d2);
}

huellas dactilares

0.35 != 0.35000000000000003
0.41 != 0.41000000000000003
0.47 != 0.47000000000000003
0.57 != 0.5700000000000001
0.69 != 0.6900000000000001
0.7 != 0.7000000000000001
0.82 != 0.8200000000000001
0.83 != 0.8300000000000001
0.94 != 0.9400000000000001
0.95 != 0.9500000000000001
1.13 != 1.1300000000000001
1.14 != 1.1400000000000001
1.15 != 1.1500000000000001
1.38 != 1.3800000000000001
1.39 != 1.3900000000000001
1.4 != 1.4000000000000001
1.63 != 1.6300000000000001
1.64 != 1.6400000000000001
1.65 != 1.6500000000000001
1.66 != 1.6600000000000001
1.88 != 1.8800000000000001
1.89 != 1.8900000000000001
1.9 != 1.9000000000000001
1.91 != 1.9100000000000001
Peter Lawrey
fuente
26
¡No puedo creer que no pensé en hacer eso en primer lugar! Gracias
:-P
6
Aunque 100 se puede representar exactamente en formato binario, la división por 100 no se puede representar exactamente. Por lo tanto, escribir 1234/100, como lo ha hecho, no hace nada sobre el problema subyacente; debería ser exactamente igual a escribir 1234 * 0.01.
Brooks Moses
1
@ Peter Lawrey: ¿Puede explicar más por qué si el número es par o impar afectaría el redondeo? Creo que / = 100 y * =. 01 serían lo mismo porque, aunque 100 es un int, se convertirá en 100.0 de todos modos como resultado de la coerción de tipo.
eremzeit
1
/100y *0.01son equivalentes entre sí, pero no a los OP *0.1*0.1.
Amadan
1
Todo lo que digo es que multiplicar por 0,1 dos veces introducirá en promedio un error mayor que multiplicar por 0,01 una vez; pero concederé felizmente el punto de @ JasperBekkers acerca de que 100 es diferente, siendo exactamente representable en binario.
Amadan
52

No, si desea almacenar valores decimales con precisión, utilice BigDecimal. doublesimplemente no puede representar un número como 0.1 exactamente, como tampoco puede escribir el valor de un tercio exactamente con un número finito de dígitos decimales.

Jon Skeet
fuente
46

si se acaba de formatear, printf

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.printf("%.2f",x);

salida

12.34
Augusto
fuente
8
Las respuestas mejor calificadas son técnicamente más perspicaces, pero esta es la respuesta correcta al problema de OP. Por lo general, no nos importa la leve inexactitud de double, por lo que BigDecimal es excesivo, pero al mostrar, a menudo queremos asegurarnos de que nuestra salida coincida con nuestra intuición, por lo que System.out.printf()es el camino correcto a seguir.
dimo414
28

En el software financiero, es común usar números enteros por monedas de un centavo. En la escuela, nos enseñaron cómo usar el punto fijo en lugar de flotante, pero eso suele ser potencias de dos. El almacenamiento de monedas de un centavo en números enteros también se puede llamar "punto fijo".

int i=1234;
printf("%d.%02d\r\n",i/100,i%100);

En clase, nos preguntaron en general qué números se pueden representar exactamente en una base.

Para base=p1^n1*p2^n2... puede representar cualquier N donde N = n * p1 ^ m1 * p2 ^ m2.

Deja base=14=2^1*7^1... puedes representar 1/7 1/14 1/28 1/49 pero no 1/3

Sé sobre software financiero: convertí los informes financieros de Ticketmaster de VAX asm a PASCAL. Tenían su propio formato () con códigos para monedas de un centavo. El motivo de la conversión fue que los números enteros de 32 bits ya no eran suficientes. +/- 2 mil millones de centavos son $ 20 millones y eso se desbordó para la Copa del Mundo o los Juegos Olímpicos, lo olvidé.

Juré guardar el secreto. Oh bien. En la academia, si es bueno publicas; en la industria, lo mantienes en secreto.

Terrence Andrew Davis
fuente
12

puedes probar la representación de números enteros

int i =1234;
int q = i /100;
int r = i % 100;

System.out.printf("%d.%02d",q, r);
Ángel Koh
fuente
5
@Dan: ¿Por qué? Este es el enfoque correcto para las aplicaciones financieras (o cualquier otra aplicación en la que incluso un pequeño error de redondeo sea inaceptable), mientras se mantiene la velocidad a nivel de hardware. (Por supuesto, estaría envuelto en una clase, normalmente, no escrito siempre)
Amadan
7
Hay un pequeño problema con esta solución: si el resto res menor que 10, no se produce ningún relleno de 0 y 1204 produciría un resultado de 12,4. La cadena de formato correcta es más similar a "% d.% 02d"
jakebman
10

Esto se debe a la forma en que las computadoras almacenan números de punto flotante. No lo hacen exactamente. Como programador, debe leer esta guía de punto flotante para familiarizarse con las pruebas y tribulaciones de manejar números de punto flotante.

CanSpice
fuente
Argh, solo estaba escribiendo una explicación que enlazaba exactamente con el mismo lugar. +1.
Pops
@ Señor Jaja, lo siento. De todos modos tengo Skeeted. :-)
CanSpice
Pensé que era por eso, pero me pregunto si hay alguna forma creativa de mover el decimal. Debido a que es posible almacenar 12.34 limpiamente en un doble, simplemente no le gusta multiplicar por .1
BlackCow
1
Si fuera posible almacenar 12.34 limpiamente en un doble, ¿no crees que Java lo habría hecho? No es. Tendrá que usar algún otro tipo de datos (como BigDecimal). Además, ¿por qué no divide por 100 en lugar de hacerlo en un bucle?
CanSpice
Do'h ... sí, dividirlo por 100 da como resultado un limpio 12.34 ... gracias
:-P
9

¿Es curioso que numerosas publicaciones mencionen el uso de BigDecimal pero nadie se moleste en dar la respuesta correcta basada en BigDecimal? Porque incluso con BigDecimal, aún puede salir mal, como lo demuestra este código

String numstr = "1234";
System.out.println(new BigDecimal(numstr).movePointLeft(2));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal(0.01)));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal("0.01")));

Da esta salida

12.34
12.34000000000000025687785232264559454051777720451354980468750
12.34

El constructor BigDecimal menciona específicamente que es mejor usar el constructor String que un constructor numérico. La precisión máxima también está influenciada por MathContext opcional.

De acuerdo con BigDecimal Javadoc , es posible crear un BigDecimal que sea exactamente igual a 0.1, siempre que use el constructor String.

Justin Rowe
fuente
5

Sí hay. Con cada operación doble puede perder precisión, pero la cantidad de precisión difiere para cada operación y puede minimizarse eligiendo la secuencia correcta de operaciones. Por ejemplo, al multiplicar un conjunto de números, es mejor ordenar el conjunto por exponente antes de multiplicar.

Cualquier libro decente sobre cálculo numérico describe esto. Por ejemplo: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Y para responder a tu pregunta:

Use dividir en lugar de multiplicar, de esta manera obtendrá el resultado correcto.

double x = 1234;
for(int i=1;i<=2;i++)
{
  x =  x / 10.0;
}
System.out.println(x);
Jan Kotek
fuente
3

No, ya que los tipos de coma flotante de Java (de hecho, todos los tipos de coma flotante) son un compromiso entre tamaño y precisión. Si bien son muy útiles para muchas tareas, si necesita precisión arbitraria, debe usar BigDecimal.

biziclop
fuente