Jump to content
Sign in to follow this  
fredericopissarra

Lidando com valores monetários corretamente...

Recommended Posts

Posted (edited)

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

Edited by fredericopissarra
  • Curtir 1

Share this post


Link to post
Share on other sites

Join the conversation

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

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...