Problemas da própria linguagem para resolver desafio de programação

Enunciado:

Leia um valor de ponto flutuante com duas casas decimais. Este valor representa um valor monetário. A seguir, calcule o menor número de notas e moedas possíveis no qual o valor pode ser decomposto. As notas consideradas são de 100, 50, 20, 10, 5, 2. As moedas possíveis são de 1, 0.50, 0.25, 0.10, 0.05 e 0.01. A seguir mostre a relação de notas necessárias.

Acredito que meu código esteja certo, porém acho que estou esbarrando naquele velho problema da programação da aritmética com ponto flutuante, onde os resultados das operações não são exatos, o que compremete no meu algoritmo. Tentei usar o BigDecimal, porém ainda sim o problema persiste.

package principal;

import java.math.BigDecimal;
import java.util.Scanner;

public class CalculoDasNotas2 {
	
	

	public static void main(String[] args) {

		Scanner scan = new Scanner(System.in);

		int numeroNotasDe100 = 0;
		int numeroNotasDe50 = 0;
		int numeroNotasDe20 = 0;
		int numeroNotasDe10 = 0;
		int numeroNotasDe5 = 0;
		int numeroNotasDe2 = 0;
		int numeroNotasDe1 = 0;

		int numeroMoedasDe50 = 0;
		int numeroMoedasDe25 = 0;
		int numeroMoedasDe10 = 0;
		int numeroMoedasDe5 = 0;
		int numeroMoedasDe1 = 0;

		BigDecimal valor = new BigDecimal(scan.nextDouble());

		for(int i = (int) valor.doubleValue(); i >= 100; i -= 100) {
			valor = valor.subtract(new BigDecimal(100));
			numeroNotasDe100++;
			
		}
		
		for(int i = (int) valor.doubleValue(); i >= 50; i -= 50) {
			valor = valor.subtract(new BigDecimal(50));
			numeroNotasDe50++;
		}
		
		for(int i = (int) valor.doubleValue(); i >= 20; i -= 20) {
			valor = valor.subtract(new BigDecimal(20));
			numeroNotasDe20++;
		}
		
		for(int i = (int) valor.doubleValue(); i >= 10; i -= 10) {
			valor = valor.subtract(new BigDecimal(10));
			numeroNotasDe10++;
		}
		
		for(int i = (int) valor.doubleValue(); i >= 5; i -= 5) {
			valor = valor.subtract(new BigDecimal(5));
			numeroNotasDe5++;
		}
		
		for(int i = (int) valor.doubleValue(); i >= 2; i -= 2) {
			valor = valor.subtract(new BigDecimal(2));
			numeroNotasDe2++;
		}
		
		for(int i = (int) valor.doubleValue(); i >= 1; i--) {
			valor = valor.subtract(new BigDecimal(1));
			numeroNotasDe1++;
		}
		
		for(BigDecimal i = valor; i.doubleValue() >= 0.5; i = i.subtract(new BigDecimal(0.5))) {
			valor = valor.subtract(new BigDecimal(0.5));
			numeroMoedasDe50++;
		}
		
		for(BigDecimal i = valor; i.doubleValue() >= 0.25; i = i.subtract(new BigDecimal(0.25))) {
			valor = valor.subtract(new BigDecimal(0.25));
			numeroMoedasDe25++;
		}
		
		for(double i = valor.doubleValue(); i >= 0.1; i -= 0.1) {
			valor = valor.subtract(new BigDecimal(0.1));
			numeroMoedasDe10++;
		}
		
		for(BigDecimal i = valor; i.doubleValue() >= 0.05; i = i.subtract(new BigDecimal(0.05))) {
			valor = valor.subtract(new BigDecimal(0.05));
			numeroMoedasDe5++;
		}
		
		for(BigDecimal i = valor; i.doubleValue() >= 0.01; i = i.subtract(new BigDecimal(0.01))) {
			System.out.println(valor);
			valor = valor.subtract(new BigDecimal(0.01));
			numeroMoedasDe1++;
		}
		
		System.out.println("NOTAS:");

		System.out.println(numeroNotasDe100 + " nota(s) de R$ 100.00");
		System.out.println(numeroNotasDe50 + " nota(s) de R$ 50.00");
		System.out.println(numeroNotasDe20 + " nota(s) de R$ 20.00");
		System.out.println(numeroNotasDe10 + " nota(s) de R$ 10.00");
		System.out.println(numeroNotasDe5 + " nota(s) de R$ 5.00");
		System.out.println(numeroNotasDe2 + " nota(s) de R$ 2.00");

		System.out.println("MOEDAS:");

		System.out.println(numeroNotasDe1 + " moeda(s) de R$ 1.00");
		System.out.println(numeroMoedasDe50 + " moeda(s) de R$ 0.50");
		System.out.println(numeroMoedasDe25 + " moeda(s) de R$ 0.25");
		System.out.println(numeroMoedasDe10 + " moeda(s) de R$ 0.10");
		System.out.println(numeroMoedasDe5 + " moeda(s) de R$ 0.05");
		System.out.println(numeroMoedasDe1 + " moeda(s) de R$ 0.01");


		scan.close();

	}

}

Quando a entrada é 0,97 ao invés dele me retornar que são 2 moedas de 1 centavo, ele me retorna que é apenas 1. Então, gostaria de destacar a seguinte parte do código:

    for(BigDecimal i = valor; i.doubleValue() >= 0.01; i = i.subtract(new BigDecimal(0.01))) {
			System.out.println(valor);
			valor = valor.subtract(new BigDecimal(0.01));
			numeroMoedasDe1++;
		}

Na parte do “System.out.println(valor);” ele deveria imprimir na tela um 0.2, mas ao invés disso sai o 0.0199999999999999622524171627446776255965232849121093750, então o for só executado uma vez (já que depois de executado uma vez, o valor será menor que 0.1) o que faz com que a variável “numeroMoedasDe1” seja incrementada apenas uma vez, e não duas.

Tendo tudo isso em vista, como faço para resolver esse problema?

Só pra constar, isso é um “problema” (eu diria mais que é uma característica) da norma IEEE 754 (que define o funcionamento dos números de ponto flutuante). A grande maioria das linguagens mainstream suportam este padrão, por isso a culpa não é exatamente da linguagem.

Enfim, se você já sabe que float e double tem esses problemas de exatidão e você precisa de exatidão, então não use esses tipos :slight_smile: Até porque eles não são recomendados para trabalhar com valores monetários, justamente por causa desses problemas de imprecisão.

E não adianta usar BigDecimal se você passa um número de ponto flutuante no construtor, pois ele acaba levando a sua imprecisão para o BigDecimal. Neste caso, é melhor usar strings, veja a diferença:

// usando o double 0.1, a imprecisão é "transferida" para o BigDecimal
System.out.printf("%.20f\n", new BigDecimal(0.1).add(new BigDecimal(0.1)).add(new BigDecimal(0.1)));

// usando strings, não tem mais imprecisão, pois agora BigDecimal tem o valor exato
System.out.printf("%.20f\n", new BigDecimal("0.1").add(new BigDecimal("0.1")).add(new BigDecimal("0.1")));

Usando new BigDecimal(0.1), a imprecisão do 0.1 acaba sendo “transportada” para o BigDecimal. Já usando uma string (new BigDecimal("0.1")), o problema é eliminado e agora ele tem o valor exato. O código acima imprime:

0.30000000000000001665
0.30000000000000000000

Enfim, isso já bastaria para corrigir seu código. Mas eu acho que tem uma solução melhor.


Pra que tudo isso?

Você faz um loop para cada valor, mas repare que eles são praticamente iguais (só muda o valor da nota/moeda e o contador que é atualizado). Sempre que você tem código repetitivo, pode generalizar para um loop.

Além disso, em vez de ficar subtraindo várias vezes um valor, pode usar simplemente divisão e resto.

Por exemplo, se o valor for 250, basta dividir por 100 (ignorando as casas decimais), que o resultado será 2 (ou seja, preciso de 2 notas de 100). Depois pegue o resto da divisão para ter o valor restante (que no caso é 50). Aí continue o cálculo com a próxima nota, que será 50, e ao dividir dará 1 (ou seja, 1 nota de 50). Já o resto da divisão dá zero, ou seja, terminou o cálculo.

E para evitar os problemas de float e também evitar BigDecimal (que apesar de útil, é mais lento), você pode trabalhar com os valores em centavos (ou seja, para notas de 100, use o valor 10000, que é a quantidade de centavos). Assim você só trabalha com int, que não tem problemas de imprecisão e é mais rápido que BigDecimal:

Scanner scan = new Scanner(System.in);

// todas as notas e moedas com o valor em centavos
int[] valores = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 25, 10, 5, 1};

// quantidades de cada nota/moeda (começa tudo com zero)
// usar LinkedHashMap para manter a ordem em que as notas foram inseridas
Map<Integer, Integer> quantidades = new LinkedHashMap<>();
for (int valor : valores) {
    quantidades.put(valor, 0);
}

int valorLido = (int) (scan.nextDouble() * 100);
for (int valor : valores) {
    if (valorLido >= valor) {
        // divide para saber a quantidade
        int qtd = valorLido / valor;
        // resto da divisão para obter o valor restante
        valorLido %= valor;
        quantidades.put(valor, qtd);
        if (valorLido == 0) { // se valor zerou, sai do for, porque não tem mais o que verificar
            break;
        }
    }
}

System.out.println("NOTAS:");
for (Map.Entry<Integer, Integer> entry : quantidades.entrySet()) {
    int valor = entry.getKey();
    int qtd = entry.getValue();
    String tipo = valor > 100 ? "nota" : "moeda";
    if (valor == 100) {
        System.out.println("MOEDAS:");
    }
    System.out.printf("%d %s(s) de R$ %.2f\n", qtd, tipo, valor / 100.0);
}

Eu usei um Map para guardar as quantidades de cada nota/moeda. Mas se não precisa guardar essa informação, e só quer imprimir e pronto, aí nem precisa do Map:

Scanner scan = new Scanner(System.in);

// todas as notas e moedas com o valor em centavos
int[] valores = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 25, 10, 5, 1};

int valorLido = (int) (scan.nextDouble() * 100);
System.out.println("NOTAS:");
String tipo = "nota";
for (int valor : valores) {
    if (valor == 100) {
        System.out.println("MOEDAS:");
        tipo = "moeda";
    }
    int qtd = 0;
    if (valorLido >= valor) {
        // divide para saber a quantidade
        qtd = valorLido / valor;
        // resto da divisão para obter o valor restante
        valorLido %= valor;
    }
    System.out.printf("%d %s(s) de R$ %.2f\n", qtd, tipo, valor / 100.0);
}