Variant and if statement - VBA [duplicate] - vba

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

Related

How Excel VBA rounds Doubles to Integers?

I am trying to understand the type of errors that could happen when a wrong type of variable is declared in VBA.
This is the code I am using:
Sub testTypes()
Dim test1 As Integer
test1 = 0.5
Debug.Print test1
End Sub
I tried to use Double number types on purpose to see how VBA will round them (up or down) to make them an Integer, given that the number ends on .5
I got puzzling results:
5.567 --> 6
5.5 --> 6
4.5 --> 4
3.5 --> 4
2.5 --> 2
1.5 --> 2
0.5 --> 0
Could anyone explain how Excel determines whether it will round up or down?
In order to avoid so called banker's rounding (= midpoint value 5 always rounds to the nearest even number ) you can use
(1) WorkSheetFunction.Round
(2) a user defined function.
Banker's rounding is a standard form of rounding used in financial and statistical operations in order to minimize significant rounding errors over multiple rounding operations by consistently rounding midpoint values in a single direction.
(1) Example using the WorksheetFunction Round()
Sub RoundWithWorkSheetFunction()
' Purpose: avoid so called bankers' rounding in VBA (5 always rounds even)
With WorksheetFunction
Debug.Print "WorksheetFunction.Round(3.5, 0)=" & .Round(3.5, 0), ":| VBA rounds to " & Round(3.5, 0)
Debug.Print "WorksheetFunction.Round(4.5, 0)=" & .Round(4.5, 0), ":| VBA rounds to " & Round(4.5, 0)
End With
End Sub
(2) An alternative to the worksheet function (avoiding bankers' rounding):
Function roundIt(ByVal d As Double, ByVal nDigits As Integer) As Double
' Purpose: avoid so called bankers' rounding in VBA (5 always rounds even)
If nDigits > 0 Then
' if continental european colon instead of point separartor
' roundIt= val(Replace(Format(d, "0." & String(nDigits, "0")), ",", "."))
roundIt = Val(Format(d, "0." & String(nDigits, "0")))
Else
' if continental european colon instead of point separartor
' roundIt = val(Replace(Format(d / (10 ^ nDigits), "0."), ",", "."))
roundIt = Val(Format(d / (10 ^ nDigits), "0."))
End If
End Function
Sandra, it will round up or down depending if it is an even or odd number. If it is an even number, it will be round down. Otherwise, it will round up.

Excel & VBA: 'If value is less than 1' condition being triggered by a value that is exactly 1

I've encountered a baffling behaviour in Excel VBA that I'm trying to understand and wondered if anyone can point me in the direction of the explanation please?
Background - I've inherited a reporting tool that is used to calculate whether there is enough remaining allowance of holiday each day to allow an additional holiday to be taken. I found that it is behaving unexpectedly when the remaining allowance is exactly '1'.
Below was the VBA as it already existed (the values of the variables in the real file are set by other queries but I've set them manually here in order to replicate the issue). With this code, the message box is triggered even though the result of (Total * Allowance) - Taken is exactly 1 and the 'If' condition should only be met by values less than 1
Dim Total As Double
Dim Allowance As Double
Dim Taken As Double
Total = 20
Allowance = 0.15
Taken = 2
If (Total * Allowance) - Taken < 1 Then
MsgBox "Not Enough Allowance Remaining"
End If
I tried changing the code to the below and found that when 'remaining' is declared as 'double' datatype, the same issue occurs. However if I change the datatype of 'remaining' to 'single', the code behaves as expected and the message box is not displayed:
Dim Total As Double
Dim Allowance As Double
Dim Taken As Double
Dim Remaining As Double
Total = 20
Allowance = 0.15
Taken = 2
Remaining = (Total * Allowance) - Taken
If Remaining < 1 Then
MsgBox "Not Enough Allowance Remaining"
End If
I reasoned it must be something to do with the way Excel / VBA handles the value '1' in different data types and some searching turned up the articles below but I'm unsure if I'm on the right track or am missing a simpler answer - any ideas please?
Article 1
Article 2
Thanks
This is a simple rounding problem. This works:
Sub dural()
Dim Total As Double
Dim Allowance As Double
Dim Taken As Double
Total = 20
Allowance = 0.15
Taken = 2
If ((Total * Allowance) - Taken) < 0.999999 Then
MsgBox "Not Enough Allowance Remaining"
End If
End Sub
Because the floating point arithmetic does not yield exactly 1For example:
Sub dural()
Dim Total As Double
Dim Allowance As Double
Dim Taken As Double
Total = 20
Allowance = 0.15
Taken = 2
If (Total * Allowance) - Taken < 0.999999 Then
MsgBox "Not Enough Allowance Remaining"
End If
MsgBox 1 - ((Total * Allowance) - Taken)
End Sub
Produces:
This is a known and well documented "issue" with Excel. As summarized here: https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel
"As with other spreadsheets, Microsoft Excel works only to limited accuracy because it retains only a certain number of figures to describe numbers (it has limited precision). [...] Although Excel can display 30 decimal places, its precision for a specified number is confined to 15 significant figures, and calculations may have an accuracy that is even less due to three issues: round off, truncation, and binary storage."
So, if you change your second code snippet to the following:
Dim Total As Double
Dim Allowance As Double
Dim Taken As Double
Dim Remaining As Double
Total = 20
Allowance = 0.15
Taken = 2
Remaining = (Total * Allowance) - Taken
If Remaining < 1 Then
MsgBox 1 - Remaining
End If
You will come to realize that the inaccuracy starts (exactly as described above) at the 15th decimal place. For more samples and a more elaborate analysis you might also want to have a look at the following post: http://www.cpearson.com/excel/rounding.htm
You are absolutely correct that the data types are the issue.
The problem is (basically) that Double can't represent certain numbers accurately (though they can do it very precisely), because of the peculiarities of how floating point types encode the numbers they represent (basically: as a number raised to a power).
If you're dealing with decimal data, or currency data, you're probably best off using a fixed precision type, like Currency, rather than Double, which is going to provide a guaranteed level of precision (currency is both precise and accurate to 4 decimal places).
If you need greater precision than 4 decimal places, using a Variant is probably your best bet, and coercing it to a decimal:
Dim Total As Variant
Dim Allowance As Variant
Dim Taken As Variant
Total = CDec(20)
Allowance = CDec(0.15)
Taken = CDec(2)
If (Total * Allowance) - Taken < 1 Then
MsgBox "Not Enough Allowance Remaining"
End If
(The coercion will probably happen implicitly, but if you're paranoid, CDec() will force it.)

How to make For loop work with non integers

The following code is easy and outputs as expected
CODE:
Option Explicit
Sub Test_loop2()
Dim i As Long
For i = -3 To 3 Step 1
Debug.Print i
Next i
End Sub
OUTPUT:
The following code is exiting early due to rounding
Option Explicit
Sub Test_loop2()
Dim i As Double
For i = -0.3 To 0.3 Step 0.1
Debug.Print i
Next i
End Sub
OUTPUT:
What is the most reliable method I can use whilst retaining a For Loop to ensure the last value is run in the loop for non integers?
Eg For i = X to Y step Z - Y must always be reached if it's multiple of Z
For i = 0 to 0.3 step 0.1 then 0.3 will be in loop
For i = 0 to 0.3 step 0.2 then 0.3 will NOT be in the loop
Floating point arithmetic will eventually screw you if you use a Double (or Single) as counter.
For counters, stick to whole numbers. Then derive your floating point value from that counter. Example:
Dim i As Long
Dim d As Double
For i = 0 To 6
d = (i - 3) * 0.1 ' or whatever formula needed
Debug.Print d
Next i
Another option for Double as loop limits and counter
Sub dTest()
Dim i As Double
For i = -0.31 To 0.31 Step 0.1
Debug.Print Round(i, 1)
Next
End Sub
Result:
-0.3
-0.2
-0.1
0
0.1
0.2
0.3
Looks like using decimal instead of double works here
Sub Test_loop2()
Dim i As Variant
Dim ffrom As Variant
Dim fto As Variant
Dim fInc As Variant
ffrom = CDec(-0.3)
fto = CDec(0.3)
fInc = CDec(0.1)
For i = ffrom To fto Step fInc
Debug.Print i
Next i
End Sub
GD,
The problem with using Double (or Float) values in a loop is that both of them are an approximation of a value.
To have a loop end on encountering an absolute, such as:
For i=1 to 5
Would require i to be exactly 5 in order for the loop to work properly, yet the double datatype could be anything from 4.999999999 to 5.000000001 (as example) which would subsequently not satisfy the =5 requirement of the For To loop. You could potentially opt for choosing a
Do
'do some code here
Loop Until i>5
where the threshold becomes a bit more vague, which is ideal for float or double datatypes.
Normally it's best practice as per #Jean-François Corbett 's answer to use only 'Integer' or 'Long' datatypes in the counter and step values and use a formula to adjust whatever counting/calculation method you require for your code variable.
I simply added a constant of 0.00001 to the upper limit to avoid the issue of floating point inaccuracy mentioned above, e.g.:
For s = 12 To 19.5 + 0.00001 Step 0.1
The code shows sample values that originally highlighted the problem to me, but apart from the constant 0.00001 these could be variables. If your increment could be less than 0.00001 you might need to fine tune this.
Probably the quickest solution I found was to use the Round operand.
Sub Test_loop2()
Dim i As Double
For i = -0.3 To 0.3 Step 0.1
Debug.Print Round(i, 1)
Next i
End Sub

How can I test if a user input is a decimal with no more than 2 digits after the decimal place?

I'm supposed to make a banking software form that allows a user to create accounts and make deposits and withdrawals for any account. But I cannot figure out how to make it accept an integer, a decimal with only one digit after the dot, or a decimal with the first digit after the dot being a 0.
Dim x As Decimal = Decimal.Parse(txtAmount.Text)
If (txtAmount.Text.IndexOf(".") <> -1 And txtAmount.Text.Substring(txtAmount.Text.IndexOf("." + 1)).Length > 2) Then
MessageBox.Show("No fractions of a penny")
Exit Sub
End If
Dim a As CAccount = lbxCustomers.SelectedItem
a.deposit(x)
Anyone know what I'm doing wrong?
Using the tryparse as suggested is most definitely a good starting point. One simple way to disregard extra digits is with the Truncate method:
Dim x As Decimal
If Decimal.TryParse(txtAmount.Text, x) Then
x = Decimal.Round(x * 100) / 100
Else
MessageBox.Show("Only valid numbers please")
End If
This will leave x with only a maximum of 2 decimal places. This also rounds the resulting value according to the value in the extra digits. This is useful for the Decimal type since it is prone to rounding errors and this will even them out.
You can compare the current value with the rounded value. If the two are equal, you know there isn't more digits. This won't work if you want to also limit zeros (2.0000 will pass).
Dim val As Decimal
val = 2.2345
If val * 100 = Math.Floor(val * 100) Then
' Has 2 or less digit
Else
' Has more than 2 digit
End If

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 :)