Имитировать арифметику VBA в .NET.

Отказ от ответственности: я знаю, что 0.025 не может быть точно представлен в переменных IEEE с плавающей запятой, и, таким образом, округление может не вернуть то, что можно было бы ожидать. Это не мой вопрос!


Можно ли моделировать поведение арифметических операторов VBA в .NET?

Например, в 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 выполняет Let-coercion to Long, а Let-coercion между числовыми типами использует округление Banker. То же самое верно и для VB.NET CInt.
  • 0.025 - это константа с плавающей точкой IEEE с двойной точностью в обоих случаях.

Таким образом, некоторые детали реализации операции умножения с плавающей запятой или оператора целочисленного преобразования изменились. Однако, по соображениям совместимости с устаревшей системой VBA, мне нужно будет реплицировать математическое поведение VBA (как бы оно ни было неправильно) в приложении .NET.

Есть ли способ сделать это? Кто-нибудь написал библиотеку Microsoft.VBA.Math? Или точный алгоритм VBA документирован где-то, чтобы я мог сделать это сам?

Ответ 1

VBA и VB.NET ведут себя по-другому, потому что VBA использует 80-разрядную "расширенную" точность для промежуточных вычислений с плавающей запятой (хотя Double - это 64-разрядный тип), тогда как VB.NET всегда использует 64-битную точность. При использовании 80-битной точности значение 0.025 * 100 немного больше 2,5, поэтому CLng(0.025 * 100) округляется до 3.

К сожалению, VB.NET не предлагает 80-битную арифметику точности. В качестве обходного пути вы можете создать родную библиотеку Win32 с использованием Visual С++ и вызвать ее через P/Invoke. Например:

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

#pragma comment(linker, "/EXPORT:[email protected]")

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

Ответ 2

Учитывая, что VBA должен использовать округление Banker, мне кажется на первый взгляд ясным, что ошибка на самом деле связана с VBA. Банкиры округляют раунды в средней точке (.5), так что цифра результата четна. Таким образом, чтобы сделать правильное округление Banker, 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. Поскольку мы больше не находимся в средней точке, правило Banker не применяется, и мы округлим до 3.

Поэтому, чтобы исправить эту проблему, вам нужно выяснить, что этот код VBA действительно делает с этой инструкцией по умножению. Независимо от того, что документировано, наблюдения показывают, что VBA обрабатывает эту часть иначе, чем .Net. Как только вы выясните, что происходит, с удачей вы сможете имитировать это поведение.

Один из возможных вариантов - вернуться в старый режим ожидания для чисел с плавающей запятой: проверьте, находитесь ли вы в какой-то небольшой дельте средней точки, и если да, просто используйте среднюю точку. Вот несколько (untested) наивный код для этого:

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 эпсилона для своей дельты. Кроме того, лучшее, на что можно надеяться, из этого кода, это то, что он может заставить VBA соответствовать .Net, когда то, что вы действительно на самом деле, - это наоборот.