15

免责声明:我知道 0.025 不能在 IEEE 浮点变量中精确表示,因此,舍入可能不会返回预期的结果。那不是我的问题!


是否可以在 .NET 中模拟 VBA 算术运算符的行为?

例如,在 VBA 中,以下表达式产生3

Dim myInt32 As Long
myInt32 = CLng(0.025 * 100)      ' yields 3

但是,在 VB.NET 中,以下表达式产生2

Dim myInt32 As Integer
myInt32 = CInt(0.025 * 100)      ' yields 2

根据规范,两者都应该返回相同的值:

  • Long (VBA) 和 Integer (VB.NET) 是 32 位整数类型。
  • 根据VBA 规范,CLng 对 Long 进行 Let-coercion,数字类型之间的 Let-coercion 使用 Banker 的舍入。VB.NET 的 CInt也是如此
  • 0.025在这两种情况下都是双精度 IEEE 浮点常数。

因此,浮点乘法运算符或整数转换运算符的某些实现细节发生了变化。但是,出于与旧版 VBA 系统兼容的原因,我需要在 .NET 应用程序中复制 VBA 的数学行为(无论它可能是错误的)。

有没有办法做到这一点?有人写了一个Microsoft.VBA.Math库吗?或者是否在某处记录了精确的VBA 算法,以便我自己做?

4

2 回答 2

15

VBA 和 VB.NET 的行为不同,因为 VBA 使用80 位“扩展”精度进行中间浮点计算(即使Double是 64 位类型),而 VB.NET 始终使用 64 位精度。使用 80 位精度时,0.025 * 100 的值略大于 2.5,因此CLng(0.025 * 100)向上取整为 3。

不幸的是,VB.NET 似乎不提供 80 位精度的算术。作为一种解决方法,您可以使用 Visual C++ 创建本机 Win32 DLL 并通过 P/Invoke 调用它。例如:

#include <cmath>
#include <float.h>

#pragma comment(linker, "/EXPORT:MultiplyAndRound=_MultiplyAndRound@16")

extern "C" __int64 __stdcall MultiplyAndRound(double x, double y)
{
    unsigned int cw = _controlfp(0, 0);
    _controlfp(_PC_64, _MCW_PC); // use 80-bit precision (64-bit significand)
    double result = floor(x * y + 0.5);
    if (result - (x * y + 0.5) == 0 && fmod(result, 2))
        result -= 1.0; // round down to even if halfway between even and odd
    _controlfp(cw, _MCW_PC); // restore original precision
    return (__int64)result;
}

在 VB.NET 中:

Declare Function MultiplyAndRound Lib "FPLib.dll" (ByVal x As Double, ByVal y As Double) As Long

Console.WriteLine(MultiplyAndRound(2.5, 1))       ' 2
Console.WriteLine(MultiplyAndRound(0.25, 10))     ' 2
Console.WriteLine(MultiplyAndRound(0.025, 100))   ' 3
Console.WriteLine(MultiplyAndRound(0.0025, 1000)) ' 3
于 2013-10-21T16:19:16.250 回答
5

鉴于 VBA 应该使用银行家的四舍五入,乍一看我似乎很清楚该错误实际上是在 VBA 方面。银行家在中点(0.5)四舍五入,所以结果数字是偶数。因此,要进行正确的银行家四舍五入,2.5应该四舍五入为 2,而不是3。这与 .Net 结果匹配,而不是 VBA 结果。

但是,根据从当前删除的答案中提取的信息,我们也可以在 VBA 中看到以下结果:

Dim myInt32 As Integer
myInt32 = CInt(2.5) ' 2
myInt32 = CInt(0.025 * 100) ' 3

这使得 VBA 中的舍入看起来是正确的,但乘法运算产生的结果不知何故大于 2.5。由于我们不再处于中间点,因此银行家规则不适用,我们四舍五入为 3。

因此,要解决此问题,您需要弄清楚 VBA 代码对该乘法指令的实际作用。不管记录了什么,观察结果证明 VBA 处理这部分的方式与 .Net 不同。一旦你弄清楚到底发生了什么,幸运的是你将能够模拟这种行为。

一种可能的选择是回到浮点数的旧备用:检查您是否在中点的某个小增量内,如果是,则使用中点。这是一些(未经测试的)天真的代码:

Dim result As Double = 0.025 * 100
Dim delta As Double = Double.Epsilon
Dim floor As Integer = Math.Floor(result)
If Math.Abs(result - (CDbl(floor) + 0.5)) <= delta Then
   result = floor + 0.5
End

我强调未经测试的,因为在这一点上,我们已经在处理来自小的计算机舍入误差的奇怪结果。在这种情况下,幼稚的实现不太可能足够好。至少,您可能希望为您的增量使用 3 或 4 epsilons 的因子。此外,您可以从这段代码中获得的最大希望是,它可以强制 VBA 与 .Net 匹配,而您真正追求的是相反的情况。

于 2013-10-21T16:31:38.447 回答