Ir para conteúdo

Lidando com valores monetários corretamente...


fredericopissarra

Posts Recomendados

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).

Link para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...