9

我以前主要使用整数,在需要将浮点数或双精度数截断为整数的情况下,我会使用以下内容:

(int) someValue

除非我发现以下内容:

NSLog(@"%i", (int) ((1.2 - 1) * 10));     // prints 1
NSLog(@"%i", (int) ((1.2f - 1) * 10));    // prints 2

(请参阅在 C# 中将浮点数转换为 int 时的奇怪行为以获取解释)。

简短的问题是:我们应该如何正确地将浮点数或双精度数截断为整数?(在这种情况下需要截断,而不是“四舍五入”)。或者,我们可以说,由于一个数字是 1.9999999999999,另一个是 2.00000000000001(粗略地说),因此截断实际上是正确完成的。所以问题是,我们应该如何转换浮点数或双精度数,以使结果是一个“截断”的数字,这使得通用意义?

(目的不是使用round,因为在这种情况下,对于1.8,我们确实想要1, 而不是 的结果2


更长的问题:

我用了

int truncateToInteger(double a) {
    return (int) (a + 0.000000000001);
}

-(void) someTest {
    NSLog(@"%i", truncateToInteger((1.2 - 1) * 10));
    NSLog(@"%i", truncateToInteger((1.2f - 1) * 10));
}

并且都打印为2,但它似乎太过分了,我们应该使用什么小数字来“消除不准确”?有没有更标准或研究的方法,而不是这样的任意黑客?

(请注意,我们希望在某些用法中截断,而不是四舍五入,例如,如果秒数是 90 或 118,当我们显示经过了多少分钟和多少秒时,分钟应该显示为1,但不应该四舍五入到2)

4

7 回答 7

12

当然,截断已正确执行,但中间值不准确。

一般来说,无法知道您的1.999999结果是稍微不准确2(因此截断后的精确数学结果是2),还是稍微不准确1.999998(因此截断后的精确数学结果是1)。

就此而言,对于某些计算,您可能会得到2.000001一个稍微不准确的1.999998. 几乎无论你做什么,你都会弄错那个。截断是一个非连续函数,所以无论你怎么做,它都会使你的整体计算在数值上不稳定。

无论如何,您都可以添加任意容差:(int)(x > 0 ? x + epsilon : x - epsilon). 它可能有帮助,也可能没有帮助,这取决于你在做什么,这就是为什么它是一个“黑客”。epsilon可以是常数,也可以根据 的大小进行缩放x

您第二个问题的最常见解决方案不是“消除不准确”,而是接受不准确的结果,就好像它是准确的一样。所以,如果你的浮点单元说(1.2-1)*10是 1.999999,好吧,它1.999999。如果该值表示分钟数,则将其截断为 1 分 59 秒。您最终显示的结果将与真实值相差 1 秒。如果您需要比这更准确的最终显示结果,那么您不应该使用浮点运算来计算它,或者您应该在截断为分钟之前四舍五入到最接近的秒数。

任何从浮点数中“消除”不准确性的尝试实际上只会改变不准确性 - 一些输入会给出更准确的结果,而另一些则不太准确。如果您很幸运能够将不准确性转移到您不关心的输入,或者可以在进行计算之前过滤掉,那么您就赢了。不过,总的来说,如果您必须接受任何输入,那么您将在某个地方迷路。您需要查看如何使您的计算更准确,而不是试图在最后的截断步骤中消除不准确性。

您的示例计算有一个简单的更正 - 使用以 10 为基数的小数点后一位的定点算术。我们知道 format 可以准确地表示 1.2。因此,(1.2 - 1) * 10您应该重新缩放计算以使用十分之一(写入(12 - 10) * 10)而不是 write ,然后将最终结果除以 10 以将其缩放回单位。

于 2012-06-28T12:44:29.870 回答
3

As you have modified your question, the problem now seems to be this: Given some inputs x, you calculate a value f'(x). f'(x) is the calculated approximation to an exact mathematical function f(x). You want to calculate trunc(f(x)), that is, the integer i that is farthest from zero without being farther from zero than f(x) is. Because f'(x) has some error, trunc(f'(x)) might not equal trunc(f(x)), such as when f(x) is 2 but f'(x) is 0x1.fffffffffffffp0. Given f'(x), how can you calculate trunc(f(x))?

This problem is impossible to solve. There is no solution that will work for all f.

The reason there is no solution is that, due to the error in f', f'(x) might be 0x1.fffffffffffffp0 because f(x) is 0x1.fffffffffffffp0, or f'(x) might be 0x1.fffffffffffffp0 because of calculation errors even though f(x) is 2. Therefore, given a particular value of f'(x), it is impossible to know what trunc(f(x)) is.

A solution is possible only given detailed information about f (and the actual operations used to approximate it with f'). You have not given that information, so your question cannot be answered.

Here is a hypothesis: Suppose the nature of f(x) is such that its results are always a non-negative multiple of q, for some q that divides 1. For example, q might be .01 (hundredths of a coordinate value) or 1/60 (represent units of seconds because f is in units of minutes). And suppose the values and operations used in calculating f' are such that the error in f' is always less than q/2.

In this very limited, and hypothetical, case, then trunc(f(x)) can be calculated by calculating trunc(f'(x)+q/2). Proof: Let i = trunc(f(x)). Suppose i > 0. Then i <= f(x) < i+1, so i <= f(x) <= i+1-q (because f(x) is quantized by q). Then i-q/2 < f'(x) < i+1-q+q/2 (because f'(x) is within q/2 of f(x)). Then i < f'(x)+q/2 < i+1. Then trunc(f'(x)+q/2) = i, so we have the desired result. In the case where i = 0, then -1 < f(x) < 1, so -1+q <= f(x) <= 1-q, so -1+q-q/2 < f'(x) < 1-q+q/2, so -1+q < f'(x)+q/2 < 1, so trunc(f'(x)+q/2) = 0.

(Note: If q/2 is not exactly representable in the floating-point precision used or cannot be easily added to f'(x) without error, then some adjustments have to be made in either the proof, its conditions, or the addition of q/2.)

If that case does not serve your purpose, then you cannot expect an answer expect by providing detailed information about f and the operations and values used to calculate f'.

于 2012-06-28T13:58:49.487 回答
1

我建议一般来说,您永远不要期望您的结果比您的输入具有更高的精度。所以在你的例子中,你的浮点数有一位小数,你不应该把你的结果比这更严重。

那么如何舍入到小数点后一位,然后转换为 int 呢?

float a = (1.2f - 1) * 10;
int b;

// multiply by 10 to "round to one decimal place"
a = round( a * 10. );

// now cast to integer first to avoid further decimal errors
b = (int) a;

// get rid of the factor 10 again by integer division
b = b / 10;

// now 'b' should hold the result you're expecting;
于 2012-06-28T14:03:30.310 回答
1
NSLog(@"%i", [[NSNumber numberWithFloat:((1.2 - 1) * 10)] intValue]); //2
NSLog(@"%i", [[NSNumber numberWithFloat:(((1.2f - 1) * 10))] intValue]); //2 
NSLog(@"%i", [[NSNumber numberWithFloat:1.8] intValue]); //1
NSLog(@"%i", [[NSNumber numberWithFloat:1.8f] intValue]); //1
NSLog(@"%i", [[NSNumber numberWithDouble:2.0000000000001 ] intValue]);//2
于 2012-06-28T14:14:01.710 回答
1

'hack'是正确的方法。浮点数的工作原理很简单,如果您想要更理智的十进制行为,NSDecimal(Number)可能就是您想要的。

于 2012-06-28T12:43:28.890 回答
0

正确的做法是:识别您执行的每个浮点运算。这包括将十进制数字转换为浮点数(例如源文本中的“1.2”产生浮点值 0x1.3333333333333p0 或“1.2f”产生 0x1.333334p0)。确定每个操作可能产生的错误限制。(对于 IEEE 754 定义的基本运算,例如简单算术,此限制是实际输入的数学精确结果的 1/2 ULP [最低精度单位]。对于从十进制数字到二进制浮点的转换,语言规范可能允许 1 个 ULP,但好的编译器会将其限制为 1/2 ULP。对于提供复杂函数(如正弦或对数)的库例程,商业库通常允许多个 ULP 错误,尽管它们通常在基本间隔内更好。您需要从库供应商处获得规范。)确定最终错误的界限,并使用数学证明。如果你能够证明,对于某个误差界 e,当精确的数学结果是某个整数 i 时,实际计算的结果是在半开区间 [即 i+1-e),那么你可以产生通过将 e 添加到计算结果并将该计算的结果截断为整数来获得精确的数学结果。(为了简洁,我省略了某些复杂性。一个是添加 e 可能导致四舍五入到 i+1 的问题。另一个是避免误报,即避免在结果不是 i 时产生 i,可能是因为最终错误时实际结果不是我可能会将计算结果放入 [ie, i+1-e).) ) 用数学证明确定最终误差的界限。如果你能够证明,对于某个误差界 e,当精确的数学结果是某个整数 i 时,实际计算的结果是在半开区间 [即 i+1-e),那么你可以产生通过将 e 添加到计算结果并将该计算的结果截断为整数来获得精确的数学结果。(为简洁起见,我省略了某些复杂性。一个是添加 e 可能导致舍入到 i+1 的问题。另一个是避免误报,即避免在结果不是 i 时产生 i,可能是因为最终错误时实际结果不是我可能会将计算结果放入 [ie, i+1-e).) ) 用数学证明确定最终误差的界限。如果你能够证明,对于某个误差界 e,当精确的数学结果是某个整数 i 时,实际计算的结果是在半开区间 [即 i+1-e),那么你可以产生通过将 e 添加到计算结果并将该计算的结果截断为整数来获得精确的数学结果。(为了简洁,我省略了某些复杂性。一个是添加 e 可能导致四舍五入到 i+1 的问题。另一个是避免误报,即避免在结果不是 i 时产生 i,可能是因为最终错误时实际结果不是我可能会将计算结果放入 [ie, i+1-e).) 当精确的数学结果是某个整数i时,实际计算的结果在半开区间[即i+1-e),那么你可以通过在计算结果上加上e并截断结果来产生精确的数学结果该计算的整数。(为简洁起见,我省略了某些复杂性。一个是添加 e 可能导致舍入到 i+1 的问题。另一个是避免误报,即避免在结果不是 i 时产生 i,可能是因为最终错误时实际结果不是我可能会将计算结果放入 [ie, i+1-e).) 当精确的数学结果是某个整数i时,实际计算的结果在半开区间[即i+1-e),那么你可以通过在计算结果上加上e并截断结果来产生精确的数学结果该计算的整数。(为简洁起见,我省略了某些复杂性。一个是添加 e 可能导致舍入到 i+1 的问题。另一个是避免误报,即避免在结果不是 i 时产生 i,可能是因为最终错误时实际结果不是我可能会将计算结果放入 [ie, i+1-e).)

如您所见,“正确”的方式通常非常困难。对于复杂的代码,证明仅在有限的高价值环境中产生,例如设计高质量的库例程来计算标准数学库函数(正弦、对数等)。

对于简单的代码,证明可能很简单。如果你知道答案应该是一个整数,并且你知道你没有做太多的浮点运算以致误差不可能像 0.5 一样大,那么产生正确答案的正确方法就是简单地添加 .5。 5 并截断。这并没有错,因为它可以证明是正确的。(实际上,这不仅仅是您执行的操作的数量,而是它们的性质。减去具有相似幅度的值会产生相对误差很大的误差。将这样的结果乘以很大的幅度可能会产生很大的绝对值错误。)

如果您不知道数学上正确的答案恰好是一个整数,那么截断是错误的。如果您不知道计算误差的界限,那么在截断之前添加任何更正都是错误的。这个问题没有一般的答案;你必须了解你的计算

于 2012-06-28T13:13:11.303 回答
0

您必须计算出您期望的错误,然后为您的截断添加它是安全的。比如你说1.8应该映射到1,那么1.9呢?1.99呢?如果您知道在您的问题域中不能得到大于 1.8 的任何值,那么添加 0.001 以使截断起作用是安全的。

于 2012-06-28T12:37:26.937 回答