How Excel VBA rounds Doubles to Integers? - vba

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.

Related

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

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

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

How to Truncate a Double in VBA

I have a variable in VBA that I need to truncate to 4 significant figures. I can't seem to find anything that won't round the number up or down. But I just want to remove the numbers after the 4th significant figure. I've tried,
compressibility = round(compress, -3 - (Int(Log(Abs(compress)))))
It removes the numbers after the 4th digit but it still rounds the number up.
Compress is a number around 0.000245848385 as an example, and I need the compressibility number to be 0.0002458.
Any suggestions would be great! Thanks.
Try this function:
Function RoundSignificant(ByVal dValue As Double, iFigures As Integer)
Dim dSig As Double
dSig = Abs(dValue)
dSig = Application.Log10(dSig)
dSig = 1 + Int(dSig)
dSig = iFigures - dSig
RoundSignificant = Round(dValue, dSig)
End Function
Sub test()
Debug.Print RoundSignificant(0.000245848385, 4)
End Sub
Using worksheet functions:
=VALUE(TEXT(compress,"0.000E+00"))
For VBA
CDbl(Format(compress,"0.000E+00"))
Hope that helps.
It seems to me that you want to avoid rounding UP, but not rounding down, since rounding down should produce the exact result you want. So, instead of using VBA Round function, you could use Excel WorksheetFunction.RoundDown method to achieve the result you need.
ROUNDDOWN(0.00024586548385;7)=0.000245800000
ROUNDDOWN(0.00024583548385;7)=0.000245800000
Sub test()
Dim compress As Double
compress = 0.000245858
Dim compressibility As Double
compressibility = Int(compress * 10 ^ -(Int(Log(Abs(compress))) - 3)) / 10 ^ -(Int(Log(Abs(compress))) - 3)
Debug.Print compressibility
End Sub

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