如何将 double 转换为 unsigned 货币?

How do I convert double to unsigned for currency?

我知道双打的准确性存在问题,但这种情况让我感到惊讶。

我从文件中读取了一些双打。这些是值:

90.720000 33.800000 43.150000 37.970000 46.810000 48.770000 81.800000 19.360000 6.760000

由于它们表示货币,因此它们始终具有最多 2 位小数的精度。我希望将这些值存储为无符号值。所以,我将它们全部乘以 100.0,并将它们转换为无符号数组。

当我打印无符号值时,我得到了这个:

9071 3379 4314 3796 4681 4877 8179 1935 675

为什么这些数字有的有错误有的没有?还有另一种解决方法吗?为什么代码会告诉我它的值为 90.72,如果它真的有 90.71?

应要求,相关代码如下:

unsigned itemprices[MAXITEMS];
unsigned u_itemweights[MAXITEMS];
double itemweights[MAXITEMS];

numberofobjects=0;
do{
    fscanf(fp, " (%*u,%lf,$%u)", &itemweights[numberofobjects], &itemprices[numberofobjects]);
    numberofobjects++;
    if(fgetc(fp)==' ') continue;
    else break;
}while(1);

puts("\nitemweights before convert:");
for(i=0; i<numberofobjects; i++) printf("%f ", itemweights[i]);

//  convert itemweights to unsigned.
for(i=0; i<numberofobjects; i++) u_itemweights[i] = itemweights[i] * 100.0;

puts("\nitemweights after convert:");
for(i=0; i<numberofobjects; i++) printf("%u ", u_itemweights[i]);

这是一个输出示例:

itemweights before convert:
90.720000 33.800000 43.150000 37.970000 46.810000 48.770000 81.800000 19.360000 6.760000
itemweights after convert:
9071 3379 4314 3796 4681 4877 8179 1935 675

实际值为90.719999999999998863131622783839702606201171875。如果乘以 100 结果为 9071.9999999999998863131622783839702606201171875 并转换为整数导致 9071.

示例:http://ideone.com/QQ7Ddm

在将乘法结果转换为整数之前,您可以将 0.5(或任何其他小于 1 的小数)加到乘法结果中。

另一种选择是使用 round 函数。

如果您从文件中读取的那些双打是文本格式(您没有指定),那么您可以将其作为文本读取并手动解析文本,而不是将它们读入双打然后进行转换。 (例如删除句点并跳过零,然后转换为 uint

给定一个包含您引用的值的文件,这将以美分打印它们,并以美分表示相同的 2 位小数。

#include <stdio.h>

int main(void)
{
    FILE *inf;
    unsigned cents;
    double money;
    if((inf = fopen("test.txt", "r")) == NULL)
        return 1;
    while (fscanf(inf, "%lf", &money) == 1) {
        cents = (unsigned)(money * 100.0 + 0.1);
        printf("File %f, cents %u\n", money, cents);
    }
    fclose(inf);
    return 0;
}

程序输出:

File 90.720000, cents 9072
File 33.800000, cents 3380
File 43.150000, cents 4315
File 37.970000, cents 3797
File 46.810000, cents 4681
File 48.770000, cents 4877
File 81.800000, cents 8180
File 19.360000, cents 1936
File 6.760000, cents 676

编辑 给评论的非信徒。这需要最大 32 位 unsigned 美分,转换为 double 美元,然后再无损失地返回美分。 double尾数有53位,比int多21位。

#include <stdio.h>
#include <limits.h>

int main()
{
    double money;
    unsigned cents = UINT_MAX;
    printf("cents = %u\n", cents);
    money = ((double)cents) / 100;
    printf("money = %.2f\n", money);
    cents = (unsigned)(money * 100.0 + 0.1);
    printf("cents = %u\n", cents);
    return 0;
}

程序输出:

cents = 4294967295
money = 42949672.95
cents = 4294967295

看到人们鼓励将值解析为浮点数并对这些值进行任何数学运算,我真的有点失望。

有两个问题。首先是十进制值不能用固定精度浮点数表示。例如,0.01 不能用浮点数表示。

下一题是乘法和除法的基础。两者都更改小数点后的位数。从根本上说,任何有限精度数据类型(如 doubleuint32_t.

都无法获得无限精度

可以使用定点运算处理小数值(如货币),但在计算中仍会失去准确性。

例如,0.50 美元的 1% 为 0.005 美元,四舍五入为 0.01 美元。但是,使用两位精度的定点运算...

 0.50
x0.01
----- 
=0.00

这里,结果是$0,但实际值应该是$0.01。并且,如果将其乘以 25(例如复利),则结果现在相差 2500%。

为了后代,这里是不使用浮点数读取值的代码。

#include <stdio.h>

int main(int argc, char* argv[argc]) {

  unsigned dollars = 0;
  char dimes = 0;
  char pennies = 0;

  unsigned fixed = 0;

  FILE* values;

  values = fopen("values", "r");

  while (fscanf(values, "%u.%c%c%*i\n", &dollars, &dimes, &pennies) != EOF) {

    dimes -= '0';
    pennies -= '0';

    fixed = (dollars * 100) + (dimes * 10) + pennies;
    printf("$%u.%u%u -> %u (cents)\n", dollars, dimes, pennies, fixed);
  }

  return 0;
}

输出...

.72 -> 9072 (cents)
.80 -> 3380 (cents)
.15 -> 4315 (cents)
.97 -> 3797 (cents)
.81 -> 4681 (cents)
.77 -> 4877 (cents)
.80 -> 8180 (cents)
.36 -> 1936 (cents)
.76 -> 676 (cents)

Why do some of these numbers have errors and some don't? Is there another way around this? Why would the code tell me it has the value 90.72, if it really has 90.71?

您问题的答案与浮点数在内存中的存储方式有关。浮点值以 IEEE-754 单精度 (32-bit float) 或 IEEE-754 双精度 ( 64-bit double) 浮点格式。格式(二进制)由 3 部分编码数字组成,其中最高有效位(31 或 63)是 sign bit,接下来的 8 或 11 位(float/double)是指数多余的格式,最后接下来的 23 或 52 位(float/double)是归一化的 mantissa/significand。

例如,90.72double 值在内存中存储为:

 IEEE-754 dbl  : 0100000001010110101011100001010001111010111000010100011110101110
                |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
                |s|   exp    |                     mantissa                      |

该格式允许将各自范围内的所有浮点数或双精度数分别存储在内存中的 32 位或 64 位中。但是,由于可用位数有限,这些格式的准确性受到限制。

您可以通过考虑表示每个浮点值的二进制数来最好地理解此限制。在记忆中,无非是'1's'0's32-bit64-bit序列。由于每种格式(single/double 精度)占用的位数与 unsigned intunsigned long(或某些硬件上的 unsigned long long)使用的位数相同,对于每个浮点值,内存中都有一个 unsigned int 或 unsigned long 与完全相同二进制表示。

这将有助于揭示为什么有些数字有错误而有些没有。如果考虑 90.72 双精度值的等效无符号整数,您会发现 90.72 在内存中以 IEEE-754 双精度格式表示的方式存在限制。具体来说:

Actual Value as double, and unsigned long equivalent:

 double        : 90.7199999999999989
 long unsigned : 4636084269408667566
 binary        : 01000000-01010110-10101110-00010100-01111010-11100001-01000111-10101110

这是考虑 unsigned long 等价物的地方。下一个可以在内存中表示的更大的数字是多少? (答: 1unsigned long 等价的当前值多了)

Closest next larger value:

 double        : 90.7200000000000131
 long unsigned : 4636084269408667567
 binary        : 01000000-01010110-10101110-00010100-01111010-11100001-01000111-10101111

(注意: 1unsigned long 等价物中的这种变化(或 中的变化一位 在内存中)仅影响小数点后第 13 位附近的值,但如果您尝试乘以 100.0 并按照您发现的方式进行转换,可能会产生巨大的后果)

看看你当前的双倍 90.72 是如何存储在内存中的,以及下一个可能存储的更大的值,应该清楚地告诉你为什么 你的一些值有错误并且有些没有。

如果任何给定的双精度值在内存中用略小于货币值的值表示(例如 90.719... 而不是 90.720...),您将 产生舍入错误 通过使用您的 乘以 100.0 和 cast 方法。这就是为什么您最好使用其他答案中提供的不受此类错误影响的方案之一,也是为什么您想在处理金钱时避免(或正确管理)浮点不准确的原因。

小心从货币到整数的音乐会。

1) 规模。
2) 通过 round() 舍入。 不要 使用 +0.5 技巧 - 问题太多。
3) 在范围内投保。
4) 演员

unsigned convert_double_to_unsigned_scaled_rounded(double x, double scale) {
  double x_scaled = x*scale;
  double x_rounded = round(x_scaled);
  assert(x_rounded >= 0 && x_rounded <= UINT_MAX);
  return (unsigned) x_rounded;
}

// u_itemweights[i] = itemweights[i] * 100.0;
u_itemweights[i] = convert_double_to_unsigned_scaled_rounded(itemweights[i], 100.0);

货币编码的一个关键问题是需要准确 - 这涉及到舍入以纠正。正如您所见,一个简单的 (unsigned) x 经常会出现问题,因为它会截断分数。另一个例子:计算贷款的 7.3% 是一个问题,代码应该使用 double 或整数。