Ir para conteúdo
  • Cadastre-se

dev botao

Problemas com arredondamento de valor


Ver Solução Respondido por Daniel Simoes,
  • Este tópico foi criado há 1550 dias atrás.
  • Talvez seja melhor você criar um NOVO TÓPICO do que postar uma resposta aqui.

Recommended Posts

Postado

Boa tarde, hoje passei por um problema de arredondamento em um cálculo de ISS, consegui resolver com uma "gambiarra" que encontrei na internet, rs, mas gostaria de entender o motivo do problema, vou explicar melhor.

Um cliente estava emitindo uma nota de serviço onde o valor do serviço prestado era de R$ 11.000,00 e a alíquota do ISS era de 2,2215%. Calculando o ISS ficaria da seguinte maneira:

11000,00 * (2,2215 / 100) = 244,365

Até aí tudo certo, o problema está na hora de arredondar o valor o ISS para 2 casas decimais, o correto nesse caso seria ficar 244,37, porém as funções do Delphi arredondam para 244,36 e essa diferença no arredondamento gerou rejeição na hora da emissão da nota. Utilizava a função SimpleRoundTo para arredondar, e até mesmo tentando fazer algo "na mão" não resolvia o problema, consegui o resultado esperado com a seguinte função que encontrei na internet (que em partes é parecida com a SimpleRoundTo:

function ArredondaNumero(Valor:Extended;Decimais:Integer):Extended;
var
  Factor, Fraction: Extended;

begin
     Factor := IntPower(10, Decimais);
     Valor := StrToFloat(FloatToStr(Valor * Factor));
     Result := Int(Valor);
     Fraction := Frac(Valor);

     if Fraction >= 0.5
     then Result := Result + 1
     else if Fraction <= -0.5
     then Result := Result - 1;

     Result := Result / Factor;
end;

A "gambiarra" que faz essa função retornar o resultado esperado está na linha "Valor := StrToFloat(FloatToStr(Valor * Factor));", onde o valor é convertido para String e depois retornado para Float, se retiro as conversões e atribuo o cálculo diretamente na variável "Valor" o arredondamento fica errado.

Debugando é possível perceber que na linha em que é extraída a fração do valor "Fraction := Frac(Valor);" a fração não vem a correta que seria "0,5",  e sim "0,49999999999789", por isso o problema no arredondamento.

Fiz testes usando variáveis dos tipos Double e Extended e em ambas o problema ocorre;

Alguém já passou por isso ou saberia me dizer o motivo do problema? Utilizo o Delphi Seattle.

  • Consultores
Postado
1 hora atrás, Renato Chiari disse:

a fração não vem a correta que seria "0,5",  e sim "0,49999999999789", por isso o problema no arredondamento.

É impossível representar corretamente os valores ponto flutuante numa memória finita. Pense bem, existem infinitos números entre 0,5 e 0,6. Existem infinitos números entre 0,51 e 0,52. E assim por diante.

Para que seja possível representar os números é feito uma aproximação. Então quando você vê o número, na verdade está vendo uma aproximação. Assim 0,5 na verdade é 0,49999999999789.

Isso não é um problema do Delphi em si. Toda linguagem de programação tem essa limitação de uma forma ou de outra.

1 hora atrás, Renato Chiari disse:

Alguém já passou por isso ou saberia me dizer o motivo do problema?

Uma explicação mais detalhada está no artigo científico:

"What Every Computer Scientist Should Know About Floating-Point Arithmetic" disponível nesse link abaixo

https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

  • Curtir 3

[]'s

Consultor SAC ACBr

Elton
Profissionalize o ACBr na sua empresa, conheça o ACBr Pro.

Projeto ACBr     Telefone:(15) 2105-0750 WhatsApp(15)99790-2976.

Um engenheiro de Controle de Qualidade(QA) entra num bar. Pede uma cerveja. Pede zero cervejas.
Pede 99999999 cervejas. Pede -1 cervejas. Pede um jacaré. Pede asdfdhklçkh.
Postado
3 horas atrás, Daniel Simoes disse:

use a RoundABNT, da ACBrUtil.pas

Boa tarde Daniel, não conhecia a RoundABNT.

Porém testando com ela houve o mesmo problema, mas consegui o arredondamento correto utilizando o SimpleRoundTo fazendo uma alteração no meu código, antes o cálculo era feito da seguinte maneira:

Servico.Valores.ValorIss := SimpleRoundTo(Servico.Valores.BaseCalculo * (Servico.Valores.Aliquota / 100), -2);

Notei que existe uma diferença de tipos de dados entre as propriedades BaseCalculo e Aliquota, a BaseCalculo é do tipo Currency, já a Aliquota é do tipo Double, resolvi testar atribuindo o valor da Aliquota a uma variável do tipo Currency e realizar o cálculo usando a variável criada, ficando da seguinte maneira:

aliqIss := Servico.Valores.Aliquota;
Servico.Valores.ValorIss := SimpleRoundTo(Servico.Valores.BaseCalculo * (aliqIss / 100), -2);

Desta forma o arredondamento ficou correto, porém usando a função RoundABNT com esse mesmo procedimento da variável o arredondamento insistiu em ficar errado, debugando ela vi que o problema ocorre no mesmo momento em que a fração é separada do valor.

Acredito que o problema ocorreu por existir essa diferença de tipos de valores reais onde cada um dos tipos (Currency e Double) possuem precisões diferentes. Esse é um daqueles probleminhas chatos de se resolver e que não entendemos ao certo o motivo, mas fica aqui a maneira que consegui contornar caso alguém passe pelo mesmo problema.

1 hora atrás, EMBarbosa disse:

É impossível representar corretamente os valores ponto flutuante numa memória finita. Pense bem, existem infinitos números entre 0,5 e 0,6. Existem infinitos números entre 0,51 e 0,52. E assim por diante.

Para que seja possível representar os números é feito uma aproximação. Então quando você vê o número, na verdade está vendo uma aproximação. Assim 0,5 na verdade é 0,49999999999789.

Isso não é um problema do Delphi em si. Toda linguagem de programação tem essa limitação de uma forma ou de outra.

Uma explicação mais detalhada está no artigo científico:

"What Every Computer Scientist Should Know About Floating-Point Arithmetic" disponível nesse link abaixo

https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Obrigado pelo retorno EMBarbosa, vou dar uma olhada no artigo que você passou.

Abraço!

  • Fundadores
  • Solution
Postado
27 minutos atrás, Renato Chiari disse:

Desta forma o arredondamento ficou correto, porém usando a função RoundABNT com esse mesmo procedimento da variável o arredondamento insistiu em ficar errado, debugando ela vi que o problema ocorre no mesmo momento em que a fração é separada do valor.

Não há Bug na rotina... ela apenas segue as normas de arredondamento da ABNT...

http://svn.code.sf.net/p/acbr/code/tools/Diversos/ABNT NBR-5891 Regras de arredondamento e numeração decimal.pdf

RoundTo e SimplRoundTo, também usam regras diferentes entre si...

RoundTo uses "Banker's Rounding" - http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/Math_RoundTo.html

SimpleRoundTo uses asymmetric arithmetic rounding - http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/Math_SimpleRoundTo.html

 

O seu provedor de API, deveria usar o arredondamento da ABNT, pois afinal, esse é o arredondamento usado em todos os documentos eletrônicos Brasileiros

  • Curtir 3
Consultor SAC ACBr

Daniel Simões de Almeida
O melhor TEF, é com o Projeto ACBr - Clique e Conheça
Ajude o Projeto ACBr crescer - Assine o SAC

Projeto ACBr     Telefone:(15) 2105-0750 WhatsApp(15)99790-2976.

Postado

A divisão decimal sempre deve ser evitada, não importa a linguagem! E quando possível, primeiro multiplique, depois divida, veja o exemplo teórico:
 

3*(10/3) = 3* (3.33333333333)  = 9.99999999999
3*10/3 = 30 / 3 = 10


creio que você resolveria o problema sem gambiarras se removesse os parêntese e apenas multiplicasse:

11000,00 * 0.022215

É importante também você converter para o formato correto do número, preferencialmente usando Currency ao invés de Extended:
Lembre-se que a estrutura do Currency é basicamente um Int64 com quatro casas decimais

  • Curtir 4
Postado

É exatamente isso que você disse @Juliomar Marchetti olhando a norma da ABNT no link que o @Daniel Simoes postou, o valor correto deve ser mesmo o 244,36, confesso que não sabia da regra que o 5 só pode ser arredondado "pra cima" caso o número seguinte a ele for >= 0, ou se o número anterior a ele (casa que será mantida) for ímpar, sendo assim 244,365 de acordo com a ABNT deve ficar 244,36, pra mim que o 5 sempre era arredondado "pra cima", rs.

Sendo assim, meu sistema estava arredondando da forma correta desde o início...🤦‍♂️ E realmente o erro está na API da prefeitura que nesse caso exigia o arredondamento "pra cima".

Muito obrigado pela atenção de vocês!

 

@bylaardt muito boa a sua dica de sempre primeiro multiplicar e depois dividir!

 

  • Curtir 3
  • Este tópico foi criado há 1550 dias atrás.
  • Talvez seja melhor você criar um NOVO TÓPICO do que postar uma resposta aqui.
Visitante
Este tópico está agora fechado para novas respostas
×
×
  • Criar Novo...

Informação Importante

Colocamos cookies em seu dispositivo para ajudar a tornar este site melhor. Você pode ajustar suas configurações de cookies, caso contrário, assumiremos que você está bem para continuar.