5

I am using Excel 2013. In the following code fragment, VBA calculates 40 for damage:

Dim attack As Variant, defense As Variant, damage As Long
attack = 152 * 0.784637
defense = 133 * 0.784637
damage = Int(0.5 * attack / defense * 70)

If the data types are changed to Double, VBA calculates 39 for damage:

Dim attack As Double, defense As Double, damage As Long
attack = 152 * 0.784637
defense = 133 * 0.784637
damage = Int(0.5 * attack / defense * 70)

In the debugger, the Variant/Double and Double values appear the same. However, the Variant/Double seems to have more precision.

Can anyone explain this behavior?

ltleelim
  • 51
  • 1
  • 3

2 Answers2

6

tldr; If you need more precision than a Double, don't use a Double.

The answer lies in the timing of when the result is coerced into a Double from a Variant. A Double is an IEEE 754 floating-point number, and per the IEEE specification reversibility is guaranteed to 15 significant digits. Your value flirts with that limit:

0.5 * (152 * .784637) / (133 * .784637) * 70 = 39.99999999999997 (16 sig. digits)

VBA will round anything beyond 15 significant digits when it is coerced into a double:

Debug.Print CDbl("39.99999999999997") '<--Prints 40

In fact, you can watch this behavior in the VBE. Type or copy the following code:

Dim x As Double
x = 39.99999999999997

The VBE "auto-corrects" the literal value by casting it to a Double, which gives you:

Dim x As Double
x = 40#

OK, so by now you're probably asking what that has to do with the difference between the 2 expressions. VBA evaluates mathematical expressions using the "highest order" variable type that it can.

In your second Sub where you have all of the variable declared as Double on the right hand side, the operation is evaluated with the high order of Double, then the result is implicitly cast to a Variant before being passed as the parameter for Int().

In your first Sub where you have Variant declarations, the implicit cast to Variant isn't performed before passing to Int - the highest order in the mathematical expression is Variant, so no implicit cast is performed before passing the result to Int() - the Variant still contains the raw IEEE 754 float.

Per the documentation of Int:

Both Int and Fix remove the fractional part of number and return the resulting integer value.

No rounding is performed. The top code calls Int(39.99999999999997). The bottom code calls Int(40). The "answer" depends on what level of floating point error you want to round at. If 15 works, then 40 is the "correct" answer. If you want to floor anything up to 16 or more significant digits, then 39 is the "correct" answer. The solution is to use Round and specify the level of precision you're looking for explicitly. For example, if you care about the full 15 digits:

Int(Round((0.5 * attack / defense * 70), 15))

Keep in mind that the highest precision you use anywhere in the inputs is 6 digits, so that would be a logical rounding cut-off:

Int(Round((0.5 * attack / defense * 70), 6))
Graham
  • 7,431
  • 18
  • 59
  • 84
Comintern
  • 21,855
  • 5
  • 33
  • 80
  • 1
    Nice. Worth noting that both versions return 40 when the type is `Currency` instead of `Double`. – Mathieu Guindon Jan 30 '17 at 04:55
  • 2
    @Mat'sMug - [Currency](http://stackoverflow.com/documentation/vba/3418/data-types-and-limits/11782/currency#t=201701300402070599064) scales everything by 10,000 and only cares about 4 digits to the right of the decimal. It rounds at digit 5, so that would be equivalent to `Int(Round(399999.99999, 4)) / 10000` – Comintern Jan 30 '17 at 04:59
  • 1
    Please note that you have it backwards. The top code must be calling `Int(40)` and the bottom code must be calling `Int(39.something)`. Your answer implies that `Variant` has more precision than a standard IEEE double, possibly being stored as a long double or some kind of raw x86 double. As far as I know, this behavior is undocumented. – ltleelim Jan 31 '17 at 15:04
0

If you get rid of the Int() function on both lines where damage is calculated both end up being the same. You shouldn't be using Int as this is producing the errant behavour, you should be using CLng as you are converting to a Long variable or if damage were an Int you should use CInt.

Int and CInt behave differently. Int always rounds down to the next lower whole number - whereas CInt will round up or down using Banker's Rounding. You'll typically see this behaviour for numbers that have a mantissa of 0.5.

As for the variant and double differences, if you do a TypeName to a MsgBox for the 1st code block you'll find that both attack and defense after having been assigned values have been converted to a double despite having been declared as variant.

Amorpheuses
  • 1,403
  • 1
  • 9
  • 13
  • 1
    ...You get both versions to return 40 if you leave the `Int` call and change the RHS expressions to constants in the assignments of `attack` and `defense` values: `attack = 119.264824` and `defense = 104.356721`. Not sure what you're getting at. If the function is *supposed* to return an `Integer` value, then `CInt` is used exactly how it should be used, to make the type conversion explicit. The OP's question is *why* the result is different when `attack` and `defense` are `Variant` vs. explicit `Double`, not *how* to "fix it". – Mathieu Guindon Jan 30 '17 at 04:25
  • But yeah, with `damage As Long` the conversion should be `CLng`, not `CInt`. – Mathieu Guindon Jan 30 '17 at 04:33
  • Sorry you're right, the update better answers what's going on here. – Amorpheuses Jan 30 '17 at 04:37
  • TBH that one is quite a headscratcher ;-) – Mathieu Guindon Jan 30 '17 at 04:38
  • You are making an incorrect assumption. I did not say I wanted the answer to be either 39 or 40. It is what it is. However, I do not want to use CLng because I need to truncate the floating point value. I was asking why the behavior is different when Variant should behave the same as a Double. – ltleelim Jan 31 '17 at 14:49
  • @ComIntern explained that excel is coercing the Variant into a Double and that conversion will cause excel to automatically round to it to 40 given how close it is to that value. That might have to happen to get it into the IEEE format - basically we've run out of digits. If it's 39.99...999 with only 14 digits will that happen? The coersion happens because you're doing a mathematical operation on it. It isn't clear to me that anything can be done other than subtracting a 'delta' off one of the operands so this doesn't happen. – Amorpheuses Jan 31 '17 at 16:09