Ir para conteúdo
  • Cadastre-se
Entre para seguir isso  
fredericopissarra

Lidando com valores monetários corretamente...

Posts Recomendados

Postado (editado)

Nunca deixo de estressar isso: Se você estiver criando código que lida com cálculos envolvendo valores monetários não use ponto flutuante!

Ponto flutuante tem "precisão" fracionária, mas, geralmente, tem precisão binária menor que valores inteiros do mesmo tamanho (em bytes) e o valor quase nunca é exato, pelo simples fato que a base numérica para armazenamento fracionário é binária, não decimal.

Como já demonstrei antes, nenhum valor fracionário diferente de zero que termine em qualquer coisa diferente de 5 pode ser representado exatamente... Assim, 0,10 + 0,20 não resulta em 0,30 e isso é fácil demonstrar:

#include <stdio.h>

__attribute__ ( ( noinline ) )
static float sadd ( float a, float b )
{
  return a + b;
}

__attribute__ ( ( noinline ) )
static double dadd ( double a, double b )
{
  return a + b;
}

int main ( void )
{
  float f;
  double d;

  f = sadd ( 0.1f, 0.2f );
  d = dadd ( 0.1, 0.2 );

  printf ( "f = %.30f\n"
           "d = %.60f\n",
           f, d );
}

Você obterá:

Citar

f = 0.300000011920928955078125000000
d = 0.300000000000000044408920985006261616945266723632812500000000

Pior... se for comparar esse valor calculado com o valor literal 0.3, poderá nunca encontrar a igualdade, porque esses valores literais (para float e double, respecitivamente) são:

Citar

0.300000011920928955078125
0.299999999999999988897769753748434595763683319091796875

Por coincidência o valor literal, do tipo float, bateu com o cálculo de 0.1+0.2, mas isso não é comum.

Ao invés disso, podemos lidar com valores monetários em termos de "centavos", não em "reais"... Assim, todos os cálculos poderão ser feitos com inteiros que são sempre exatos... 10+20 sempre será 30, usando inteiros, não importa o que você faça. Daí, quando você for mostrar o valor na tela poderá fazer uma pequena conversão para string usando ponto flutuante, com precisão suficiente, delimitando a precisão em "casas depois da vírgula" via função printf. Por exemplo:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

// A função retorna 1 se tudo ocorreu bem ou 0 em caso de falha.
// Ela aloca um buffer para conter a string e assinala o endereço do
// buffer no ponteiro. Repare que o argumento 'cents' é inteiro,
// de 64 bits!
int cents2str( char **bufptr, int64_t cents )
{
  // Converte para R$.
  long double v = cents / 100.0L;

  // Aloca espaço para o buffer e o preenche com a string "R$ xxxx.xx"
  // de acordo com o locale... Note que estou limitando a string em 2
  // "casas depois da vírgula"...
  return asprintf( bufptr, "R$ %.2Lf", v ) > 0;
}

int main( void )
{
  char *str;

  // 30 centavos sempre serão 30 centavos, se for um
  // valor do tipo 'int'.
  if ( ! cents2str( &str, 30 ) )
  {
    fputs( "ERROR converting value to string.\n", stderr );
    return EXIT_FAILURE;
  }

  printf( "%s\n", str );
  free( str );
  
  return EXIT_SUCCESS;
}

Note que usar o tipo int64_t nos dá uma faixa de +/-9223372036854775807 centavos, mais que suficiente para qualquer cálculo, já que o tipo tem 63 bits significativos. O tipo long double tem 64 bits significativos e por isso não teremos perdas significativas na conversão (teríamos se usássemos double, que só tem 53 bits, mas, é claro, depende do valor).

Editado por fredericopissarra
  • Curtir 1

Compartilhar este post


Link para o post
Compartilhar em outros sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Visitante
Responder

×   Você colou conteúdo com formatação.   Remover formatação

  Apenas 75 emoticons no total são permitidos.

×   Seu link foi automaticamente incorporado.   Mostrar como link

×   Seu conteúdo anterior foi restaurado.   Limpar o editor

×   Não é possível colar imagens diretamente. Carregar ou inserir imagens do URL.

Entre para seguir isso  

  • Quem Está Navegando   0 membros estão online

    Nenhum usuário registrado visualizando esta página.

×
×
  • Criar Novo...