在 .NET 中模拟 VBA 算法
Posted
技术标签:
【中文标题】在 .NET 中模拟 VBA 算法【英文标题】:Simulate VBA arithmetic in .NET 【发布时间】:2013-10-21 15:15:32 【问题描述】:免责声明:我知道 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 位整数类型。 根据the VBA specification,CLng 对 Long 执行 Let-coercion,数字类型之间的 Let-coercion 使用 Banker 的舍入。 The same is true 用于 VB.NET 的 CInt。0.025
在这两种情况下都是双精度 IEEE 浮点常量。
因此,浮点乘法运算符或整数转换运算符的一些实现细节发生了变化。但是,出于与旧版 VBA 系统兼容的原因,我需要在 .NET 应用程序中复制 VBA 的数学行为(尽管它可能是错误的)。
有没有办法做到这一点?有人写了Microsoft.VBA.Math
库吗?还是 precise VBA 算法记录在某处,以便我自己做?
【问题讨论】:
VBA/VBScript/VB6 确实按照您在学校的教学方式进行了四舍五入,0.5 一直在上升。在 DotNet 中,他们转向了真正的数学四舍五入,一次向上一次向下,在计算平均值时为您提供更准确的数字。如果你想像 VBA 那样四舍五入,你需要考虑使用 FLOOR 或 CEILING。 @Steve:不。 VBA 规范明确提到 VBA 使用 Banker 的舍入,这与 .NET 使用的完全相同。在 VBA 中尝试CLng(0.035 * 100)
和 CLng(0.045 * 100)
进行比较。
看来*
在 VBA 与 .NET 中返回不同的结果:2.5 - 0.025*100 = -1.38777878078145E-16 在 VBA,0 在 .NET。
@MichaelLiu:是的,但这可能是 VBA 编译器优化的结果:请注意,2.5 - CDbl(0.025*100)
在 VBA 中产生 0
。 (对于双打,CDbl 应该是 NO-OP。)
@varocarbas - 否决投票是匿名的,您无法确定谁 否决了您。显然有两个人(与 OP 或任何回答者无关)出于某种原因不喜欢您的回答。无论如何,您的回答确实没有解决 Heinzi 的主要问题。
【参考方案1】:
VBA 和 VB.NET 的行为不同,因为 VBA 使用 80-bit "extended" precision 进行中间浮点计算(即使 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
【讨论】:
你可能想尝试改变编译器处理浮点的方式:见msdn.microsoft.com/en-us/library/e7s85ffb.aspx【参考方案2】:鉴于 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 匹配,而您真正追求的是相反的结果。
【讨论】:
以上是关于在 .NET 中模拟 VBA 算法的主要内容,如果未能解决你的问题,请参考以下文章