Why does re-ordering the operands in VBA avoid "Run-time Error 6: Overflow"? - vba

I just want to know the reason for the run-time error: Overflow when running these VBA codes
Function target_time(Time) As Double
target_time = 1200 * 60 * Time
End Function
Sub Test()
x = target_time(24)
MsgBox x
End Sub
I already addressed this issue by rearranging the function formula into:
target_time = Time*1200 * 60
I just want to know why is repositioning the variable causes the runtime error overflow

Here's what VBA does:
1200 and 60 <-- Those are small enough to be a (16-bit) Integer, so I'll assume they are 16-bit Integer constants.
1200 * 60 <-- A multiplication of two 16-bit Integers yields an Integer - there is no automatic promotion to a 32-bit Long. Since 1200 * 60 = 72000 exceeds the maximum value of a 16-bit integer, the expression overflows.
On the other hand, Time * 1200 is not a multiplication of two 16-bit Integers, it's a multiplication of a "Variant containing an Integer" with an Integer. In this case, the result is automatically promoted to a "Variant containing a Long":
Dim v As Variant
v = 1200
Debug.Print TypeName(v) ' prints Integer
Debug.Print TypeName(v * 60) ' prints Long
Debug.Print TypeName(v * 6000000) ' prints Double(!)
The reason for this can be found in section 5.6.9.3 of the VBA spec:
If one or both operands have a declared type of Variant:
If the operator’s effective value type is Integer, Long, Single or Double, the operator’s
value type is the narrowest type of either Integer, Long or Double such that the
operator value is within the valid range of the type. If the result does not fit within
Double, runtime error 6 (Overflow) is raised.

As an additon to Heinzi's answer which addresses the OP's question I would like to add that I would add some of the following operators: CLng, CDbl to make sure that the result will be correct (or better closer to correct :-) )
Instead of target_time = 1200 * 60 * Time use target_time = CDbl(1200) * CDbl(60) * CDbl(Time).

Related

Variant and if statement - VBA [duplicate]

I have trouble comparing 2 double in Excel VBA
suppose that I have the following code
Dim a as double
Dim b as double
a = 0.15
b = 0.01
After a few manipulations on b, b is now equal to 0.6
however the imprecision related to the double data type gives me headache because
if a = b then
//this will never trigger
end if
Do you know how I can remove the trailing imprecision on the double type?
You can't compare floating point values for equality. See this article on "Comparing floating point numbers" for a discussion of how to handle the intrinsic error.
It isn't as simple as comparing to a constant error margin unless you know for sure what the absolute range of the floats is beforehand.
if you are going to do this....
Dim a as double
Dim b as double
a = 0.15
b = 0.01
you need to add the round function in your IF statement like this...
If Round(a,2) = Round(b,2) Then
//code inside block will now trigger.
End If
See also here for additional Microsoft reference.
It is never wise to compare doubles on equality.
Some decimal values map to several floating point representations. So one 0.6 is not always equal to the other 0.6.
If we subtract one from the other, we probably get something like 0.00000000051.
We can now define equality as having a difference smaller that a certain error margin.
Here is a simple function I wrote:
Function dblCheckTheSame(number1 As Double, number2 As Double, Optional Digits As Integer = 12) As Boolean
If (number1 - number2) ^ 2 < (10 ^ -Digits) ^ 2 Then
dblCheckTheSame = True
Else
dblCheckTheSame = False
End If
End Function
Call it with:
MsgBox dblCheckTheSame(1.2345, 1.23456789)
MsgBox dblCheckTheSame(1.2345, 1.23456789, 4)
MsgBox dblCheckTheSame(1.2345678900001, 1.2345678900002)
MsgBox dblCheckTheSame(1.2345678900001, 1.2345678900002, 14)
As has been pointed out, many decimal numbers cannot be represented precisely as traditional floating-point types. Depending on the nature of your problem space, you may be better off using the Decimal VBA type which can represent decimal numbers (base 10) with perfect precision up to a certain decimal point. This is often done for representing money for example where 2-digit decimal precision is often desired.
Dim a as Decimal
Dim b as Decimal
a = 0.15
b = 0.01
Late answer but I'm surprised a solution hasn't been posted that addresses the concerns outlined in the article linked in the (currently) accepted answer, namely that:
Rounding checks equality with absolute tolerance (e.g. 0.0001 units if rounded to 4d.p.) which is rubbish when comparing different values on multiple orders of magnitude (so not just comparing to 0)
Relative tolerance that scales with one of the numbers being compared meanwhile is not mentioned in the current answers, but performs well on non-zero comparisons (however will be bad at comparing to zero as the scaling blows up around then).
To solve this, I've taken inspiration from Python: PEP 485 -- A Function for testing approximate equality to implement the following (in a standard module):
Code
'#NoIndent: Don't want to lose our description annotations
'#Folder("Tests.Utils")
Option Explicit
Option Private Module
'Based on Python's math.isclose https://github.com/python/cpython/blob/17f94e28882e1e2b331ace93f42e8615383dee59/Modules/mathmodule.c#L2962-L3003
'math.isclose -> boolean
' a: double
' b: double
' relTol: double = 1e-09
' maximum difference for being considered "close", relative to the
' magnitude of the input values
' absTol: double = 0.0
' maximum difference for being considered "close", regardless of the
' magnitude of the input values
'Determine whether two floating point numbers are close in value.
'Return True if a is close in value to b, and False otherwise.
'For the values to be considered close, the difference between them
'must be smaller than at least one of the tolerances.
'-inf, inf and NaN behave similarly to the IEEE 754 Standard. That
'is, NaN is not close to anything, even itself. inf and -inf are
'only close to themselves.
'#Description("Determine whether two floating point numbers are close in value, accounting for special values in IEEE 754")
Public Function IsClose(ByVal a As Double, ByVal b As Double, _
Optional ByVal relTol As Double = 0.000000001, _
Optional ByVal absTol As Double = 0 _
) As Boolean
If relTol < 0# Or absTol < 0# Then
Err.Raise 5, Description:="tolerances must be non-negative"
ElseIf a = b Then
'Short circuit exact equality -- needed to catch two infinities of
' the same sign. And perhaps speeds things up a bit sometimes.
IsClose = True
ElseIf IsInfinity(a) Or IsInfinity(b) Then
'This catches the case of two infinities of opposite sign, or
' one infinity and one finite number. Two infinities of opposite
' sign would otherwise have an infinite relative tolerance.
'Two infinities of the same sign are caught by the equality check
' above.
IsClose = False
Else
'Now do the regular computation on finite arguments. Here an
' infinite tolerance will always result in the function returning True,
' since an infinite difference will be <= to the infinite tolerance.
'This is to supress overflow errors as we deal with infinity.
'NaN has already been filtered out in the equality checks earlier.
On Error Resume Next
Dim diff As Double: diff = Abs(b - a)
If diff <= absTol Then
IsClose = True
ElseIf diff <= CDbl(Abs(relTol * b)) Then
IsClose = True
ElseIf diff <= CDbl(Abs(relTol * a)) Then
IsClose = True
End If
On Error GoTo 0
End If
End Function
'#Description "Checks if Number is IEEE754 +/- inf, won't raise an error"
Public IsInfinity(ByVal Number As Double) As Boolean
On Error Resume Next 'in case of NaN
IsInfinity = Abs(Number) = PosInf
On Error GoTo 0
End Function
'#Description "IEEE754 -inf"
Public Property Get NegInf() As Double
On Error Resume Next
NegInf = -1 / 0
On Error GoTo 0
End Property
'#Description "IEEE754 +inf"
Public Property Get PosInf() As Double
On Error Resume Next
PosInf = 1 / 0
On Error GoTo 0
End Property
'#Description "IEEE754 signaling NaN (sNaN)"
Public Property Get NaN() As Double
On Error Resume Next
NaN = 0 / 0
On Error GoTo 0
End Property
'#Description "IEEE754 quiet NaN (qNaN)"
Public Property Get QNaN() As Double
QNaN = -NaN
End Property
Updated to incorporate great feedback from Cristian Buse
Examples
The IsClose function can be used to check for absolute difference:
assert(IsClose(0, 0.0001233, absTol:= 0.001)) 'same to 3 d.p.?
... or relative difference:
assert(IsClose(1234.5, 1234.6, relTol:= 0.0001)) '0.01% relative difference?
... but generally you specify both and if either tolerance is met then the numbers are considered close. It has special handling of +-infinity which are only close to themselves, and NaN which is close to nothing (see the PEP for full justification, or my Code Review post where I'd love feedback on this code :)
The Currency data type may be a good alternative. It handles relatively large numbers with fixed four digit precision.
Work-a-round??
Not sure if this will answer all scenarios, but I ran into a problem comparing rounded double values in VBA. When I compared to numbers that appeared to be identical after rounding, VBA would trigger false in an if-then compare statement.
My fix was to run two conversions, first double to string, then string to double, and then do the compare.
Simulated Example
I did not record the exact numbers that caused the error mentioned in this post, and the amounts in my example do not trigger the problem currently and are intended to represent the type of issue.
Sub Test_Rounded_Numbers()
Dim Num1 As Double
Dim Num2 As Double
Let Num1 = 123.123456789
Let Num2 = 123.123467891
Let Num1 = Round(Num1, 4) '123.1235
Let Num2 = Round(Num2, 4) '123.1235
If Num1 = Num2 Then
MsgBox "Correct Match, " & Num1 & " does equal " & Num2
Else
MsgBox "Inccorrect Match, " & Num1 & " does not equal " & Num2
End If
'Here it would say that "Inccorrect Match, 123.1235 does not equal 123.1235."
End Sub
Sub Fixed_Double_Value_Type_Compare_Issue()
Dim Num1 As Double
Dim Num2 As Double
Let Num1 = 123.123456789
Let Num2 = 123.123467891
Let Num1 = Round(Num1, 4) '123.1235
Let Num2 = Round(Num2, 4) '123.1235
'Add CDbl(CStr(Double_Value))
'By doing this step the numbers
'would trigger if they matched
'100% of the time
If CDbl(CStr(Num1)) = CDbl(CStr(Num2)) Then
MsgBox "Correct Match"
Else
MsgBox "Inccorrect Match"
End If
'Now it says Here it would say that "Correct Match, 123.1235 does equal 123.1235."
End Sub
Depending on your situation and your data, and if you're happy with the level of precision shown by default, you can try comparing the string conversions of the numbers as a very simple coding solution:
if cstr(a) = cstr(b)
This will include as much precision as would be displayed by default, which is generally sufficient to consider the numbers equal.
This would be inefficient for very large data sets, but for me was useful when reconciling imported data which was identical but was not matching after storing the data in VBA Arrays.
Try to use Single values if possible.
Conversion to Double values generates random errors.
Public Sub Test()
Dim D01 As Double
Dim D02 As Double
Dim S01 As Single
Dim S02 As Single
S01 = 45.678 / 12
S02 = 45.678
D01 = S01
D02 = S02
Debug.Print S01 * 12
Debug.Print S02
Debug.Print D01 * 12
Debug.Print D02
End Sub
45,678
45,678
45,67799949646
45,6780014038086

Randomize seems to miss many possible seeds

In trying to solve this question, I wrote the following in an attempt to implement the Box-Muller transform to generate random normal variables in pure VBA:
Function RandNorm(Optional mean As Double = 0, Optional sd As Double = 1) As Double
Dim s As Double
s = Sqr(-2 * Log(Rnd())) * Cos(6.283185307 * Rnd()) '6.28 etc. is 2*pi
RandNorm = mean + sd * s
End Function
The following somewhat weak test always works, returning a number close to 0:
Sub test1()
Randomize
Dim s As Double
Dim i As Long
For i = 1 To 17000000
s = s + RandNorm()
Next i
Debug.Print s / 17000000
End Sub
On the other hand, the following test never works (because it tries to take the log of 0, which is undefined):
Sub test2()
Randomize
Dim s As Double
Dim i As Long
Debug.Print Rnd() 'just to clock it
For i = 1 To 17000000
s = s + RandNorm()
Next i
Debug.Print s / 17000000
End Sub
The problem is that rnd() returns 0 on average once out of every 2^24 (a bit less than 17,000,000) calls. It is of course easy enough to tweak the definition of RandNorm to avoid the zero (see the linked-to question), but I am still puzzled by the above code. It would make perfect sense to me if each test failed half the time (when the zero is fed into Log()) and worked half the time (when the zero is fed into Cos()). It seems that Randomize avoids at least half of the possible seeds.
Why does Randomize behave this way? Is there a way to seed the random number generator so that all possible states of the random number generator can occur?
On Edit
If I define the following sub:
Sub ReRandomize()
Dim r As Double
Randomize
If Rnd() > 0.5 Then r = Rnd()
End Sub
And modify test1 and test2 above to use ReRandomize instead of Randomize, both of the test subs will fail 50% of the time, so that might answer the part of the question about if there is "a way to seed the random number generator so that all possible states of the random number generator can occur?" It is still mysterious as to why Randomize behaves the way that it does. This is the second time that an Excel VBA question made me realize that Randomize is a weird sub. None of this matters very much for typical use of rnd(), but it does underscore that it is a somewhat low quality random number generator which shouldn't be used for serious statistical work.
I simply modified the Rnd calc to not include 0 or 1. You have to remember that the Rnd Function can produce a number (of type double) in the range of 0 or 1. Therefore, it's chances of having a duplicate number are pretty low.
dbl1stRnd = Rnd()
dblRnd = (0.9999 - 0.0001) * dbl1stRnd + 0.0001
s = Sqr(-2 * Log(dblRnd)) * Cos(6.283185307 * dblRnd) '6.28 etc. is 2*pi
Some example outputs of the regular Rnd() function with Randomize:
3.633606E-02
0.2324036
0.3460443
0.5870923
5.553758E-02
0.2629338
0.2400494
0.1982901
0.5923058
0.7915452
0.4874671
0.2062811
0.5676001
0.1178594
1.932621E-03
0.4326598
0.8291379
I hope this explains some and is what you are looking for.

Overflow Error evaluating arithmetic expression with integers in VBA

The following evaluates to 32760, an integer:
Debug.Print (20 * 1638 - 1)
But this raises an overflow error:
Dim t as Integer
t = 1639
Debug.Print (20 * t - 1)
It seems like this is implicitly expecting an Integer return value, because if I do either of the following, the error is avoided.
Dim t as Long OR Debug.print (20 * CLng(t) - 1) OR Debug.Print (20# * t - 1)
Is this behavior documented?
Is my assumption accurate? Namely, that arithmetic of integers implies an integer return value, and that simply introducing one Long or Double value in to the equation will avoid the error?
If my logic is correct, Dim or 'Dimension' is a way of telling the application that you expect use a variable of a certain type, and that type pertains to a certain amount of 'bits' (of memory).
This reserves a section of the system's memory, which has been allocated a certain amount of bits dependant on the variable type that you have instructed in your code. These bits then define how many (If you're familiar with C++ or similar then you will probably already know all this...)
An Integer is 16 bits in VBA and is a signed integer which means we can store negative values too, so the limit is 32,767 because this is the biggest number we can achieve with 16 bits:
(generally a variable can hold 2^n where n = number of bits)
unsigned 16 bits = 0 - 65,536 (2^16)
signed 16 bits = -32,768 - 32,767
32,767 = 111111111111111 (Binary)
32,768 = 1000000000000000 <--- note the extra bit
This extra bit is what causes the "overflow" error - because the amount of bits required to produce the number overflows the amount of bits that the memory has to store the number safely.
I don't think the method of the calculation is documented to this extent, however your code snippet:
Dim t as Integer
t = 1639
Debug.Print (20 * t - 1)
would require t to be first be multiplied by 20, resulting in a figure of 32,780:
20 * t = 20 * 1639 = 32,780
32,780 = 1000000000001100 (Binary)
which overflows the bit limit for the Integer data type. At this point the system throws an error before it has the chance to proceed with the rest of the calculation because it tries to multiply t whilst still in it's allocated memory address, for which only 16 bits of memory have been reserved.
Also, not declaring t as a type will force VBA to default to type Variant which will assess that t needs to have more memory allocated when the calculation runs and push it into the Long boundary automatically.
Update: It would appear that VBA will only permit the highest amount of bits held by a variable within the equation for the return value, as can be seen in this example:
Sub SO()
Dim t As Integer, c As Long
t = 1639
c = 20
Debug.Print (20 * (t - 1)) '// No Error
Debug.Print (c * (t - 1)) '// No Error
Debug.Print ((c * t) - 1) '// No Error
c = (20 * t - 1) '// Error
Debug.Print (20 * t - 1) '// Error
End Sub
Although I don't believe this is documented anywhere, it would lead one to believe that VBA limits memory usage to the highest amount of bits being used by a variable at any one time.

Overflow when multiplying Integers and assigning to Long

If I type the following into the immediate window I get Runtime error '6': Overflow.
MsgBox 24 * 60 * 60
Why is this?
This also fails:
Dim giveTime As Long
giveTime = 24 * 60 * 60
Why is this? giveTime is declared as a Long type, so 24 × 60 × 60 = 86400 should comfortably fit.
This is a really odd VBA quirk. I'm amazed I've never bumped into this.
Dim x As Long
x = 24 * 60 * 60 ' Overflow
x = 32767 + 1 ' Overflow.
x = 32768 + 1 ' Works fine!
So it looks like the * and + operators return an Integer in the first two examples. Sure enough, in the help file for the *operator (similar for the + operator):
result = number1 * number2
[...]
The data type of result is usually the same as that of the most precise expression.
Your literals 24, 60, and 60 are all of type Integer by default, so your * (or +) operator returns an Integer, which overflows because the result is greater than 32,767.
However, the literal 32,768 in the third example above defaults to Long type (since it is too large to be an Integer) and so the + returns a Long; no overflow.
The help file also says this:
If [...] the data type of result is an Integer variant that overflows its legal range [...] Then result is [...] converted to a Long variant.
Emphasis mine. Now this little rule sounds like common sense, and anyone would reasonably assume that it applies in your case. But your numbers are of type Integer, not Variant/Integer, so VBA doesn't apply this rule! Makes absolutely no sense to me, but that's how it is, and that's what the documentation says.
Solution: make one of the arguments of your * operator be of a type more precise than Integer (e.g. Long) and the problem will go away.
x = CLng(24) * 60 * 60 ' Result is Long, works fine.
In fact, and this is probably why I've never bumped into this quirk, I make a habit of declaring all of my Integer variables as Long instead, unless there is a specific concern that having Longs instead of Integers will cause problems with memory use or execution time (which is almost never the case). Granted, this won't help in cases when you operate on literals smaller than 32,768, because they default to Integer type.
You ask in a comment what a Variant/Integer is. Variant is basically a container type for any other data type. In the particular case where you make it contain an Integer:
Dim a As Variant ' a is now Empty
a = CInt(32767) ' a is now Variant/Integer
x = a + 1 ' works fine
But as noted above, a plain old Integer triggers the overflow error:
Dim b As Integer
b = 32767
x = b + 1 ' overflow
After every number, place #. It defines each number as a double. Think of it as, each number is placed in to memory for the calculation as sort of a temp variable.
If you define each number, it will allow enough space for the calculations.
eg:
Dim x As Long
x = 24# * 60# * 60#
or 24& 'indicates long

Use of symbol # (hash) in VBA Macro

What is the meaning of the use of the # symbol in Excel VBA?
It is used like this:
a = b /100#
I don't understand the significance of # after the 100?
The type-declaration character for Double is the number sign (#). Also called HASH
Other type declaration characters are:
Integer %
Long &
Currency #
Single !
Double #
String $
Don't understand the significance of # here.
It implies that when the expression is evaluated, the number in front of the
type declaration character is treated as a specific data type instead of as
a Variant.
See this example, which are basically the same.
Sub Sample1()
Dim a#
a = 1.2
Debug.Print a
End Sub
Sub Sample2()
Dim a As Double
a = 1.2
Debug.Print a
End Sub
EDIT
Let me explain it a little more in detail.
Consider this two procedures
Sub Sample1()
Dim a As Double, b As Integer
b = 32767
a = b * 100
Debug.Print a
End Sub
Sub Sample2()
Dim a As Double, b As Integer
b = 32767
a = b * 100#
Debug.Print a
End Sub
Question: One of them will fail. Can you guess which one?
Ans: The 1st procedure Sub Sample1() will fail.
Reason:
In Sample2, when you do b * 100# the result of calculation will be of type Double. Since it is within the limits of Double, so the calculation succeeds and the result is assigned to variable a.
Now in Sample1, when you do b * 100 the result of calculation will be of type Integer, since both the operands are of type integer. But the result of calculation exceeds the limits of Integer storage. As a result it will error out.
Hope it helps :)