Multiplying singles and decimals - vba

If btnTotalCost.Text = "Calculate total cost" Then
btnTotalCost.Text = "Refresh"
Else
btnTotalCost.Text = "Calculate total cost"
End If
Dim dHamSandwich As Decimal = 1.35
Dim sHamQuantity As Single
txtHam.Text = sHamQuantity
dTotalSandwichCost = dHamSandwich * sHamQuantity
If btnTotalCost.Text = "Refresh" Then
MsgBox(dTotalSandwichCost)
End If
When I calculate the message box that appears shows the number 0. I want it to multiply the quantity and the cost to give the total cost of the sandwich. For example, if I input 2 into the quantity text box, it should multiply 1.35 by 2.

first of all you should specify EXACTLY what you use. As the comment of Ste Griffiths suggest, it seems to be VB.NET.
Secondly please start learning the usage of data types and variables.
You do
Dim dHamSandwich As Decimal = 1.35 ' <- this is fine
Dim sHamQuantity As Single '<- this is an uninitialised Single variable, in .net it will be set to 0
txtHam.Text = sHamQuantity '<- then you assign the unassigned Single variable to your Textbox's property Text(which requires a string)
'That means it will implicitly convert the Single to String and then "show" it in the textbox (which will be 0)
dTotalSandwichCost = dHamSandwich * sHamQuantity 'here you multiply 1.35 * 0. Sure it will be 0. Also you multiply a decimal with a single data type which will implicitly convert the latter (if I remember correctly)
The question for me here is. Why do you even use Single and Decimal? You can use either one. But the main problem is actually not even the data types, but the assignment.

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

Formatting Main Part of Number While Keeping the decimal part untouched in VBA

I need to format the main part (whole) of a number without touching or affecting the decimal part:
12345.123456 becomes 12,345.123456
123.123 becomes 123.123
12345678.123 becomes 12,345,678.123
123 becomes 123
The fractional part length is variable in length of decimal places and need to be kept untouched (as is).
The formatting applies only to the whole number. Formatting the whole number is simple, but how to not affect the decimal part.
The Format parameter should work with any length of decimal places.
I am using the following:
Format(123456789.12345,"#,#.#############################")
However, the only problem with this solution is:
There is always an assumption on the maximum possible number of decimal places by the number of # used.
If the number is without a fraction say "123.0" or "123", the output will be "123." always with a decimal separator (dot).
Thanks
Like #nicomp said you'll want to break this into two parts.
dim num as string 'or a double converted to a string
dim nums() as string 'array
num = 123456789.123456
nums = split(num, ".") 'break into array at decimal
nums(0) = format(nums(0), "###,###") 'format whole numbers
num = nums(0) & "." & nums(1) 'recombine
This should add a comma after every three whole numbers

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

MPG calculator not giving correct answer

I am trying to create a MPG calculator in Visual Basic, but it only calculates in whole numbers not decimals. When I input 10 into the gallons section and 375 into the miles section, the calculator only calculates 37 not 37.5.
Public Class Form1
Private Sub btnCalculateMpg_Click(sender As Object, e As EventArgs) Handles btnCalculateMpg.Click
'Declare variables for the calculation.
Dim intMpg As Integer
lblMpgCalculated.Text = String.Empty
Try
'Calculate and display Miles per Gallon.
intMpg = CInt(txtMiles.Text) \
CInt(txtGallons.Text)
lblMpgCalculated.Text = intMpg.ToString("N")
Catch
'Error Message.
MessageBox.Show("All input must be valid numeric values.")
End Try
End Sub
intMpg should not be an integer if you don't want to receive a result that's an integer. You may want to use a Double, in which case the line where you declare intMpg would be:
Dim intMpg As Double
You also should use the / operator for division, not the \ operator, since the latter performs integral division, as explained here.
So, the line where you perform the division should be:
intMpg = CInt(txtMiles.Text) / CInt(txtGallons.Text)
If you perform these changes, intMpg would be a misleading name (since it's not an integer, even though the name makes it sound like it is), so you should change it to something else like milesPerGallon.
Integers are only whole numbers. Storing the result of a division in an Integer will do an integer division, which means your result will also be an integer and the decimal will get truncated. To get decimals, you should declare your result variable as a Double.

Strange results when adding two variables in MS Access VBA that equal Zero

I have a routine in my VBA MSAccess platform that has me baffled.
All I am doing is summing up an amount field as I loop through a recordset.
I have declared a variable called TempAmount (which holds the totals as we loop through).
The field name in the table is called BilledAmount in a records set called rsBilled.
As the loop begins, TempAmount is at Zero. As the loop progresses, we add the BilledAmount to the TempAmount.
Here is a mockup of the code.
Dim TempAmount as Double
TempAmount = 0
While NOT rsBilled.EOF
TempAmount = TempAmount + rsBilled.Fields("BilledAmount")
rsBilled.MoveNext
Wend
(Then we go and write TempAmount to another table that houses Totals.)
Now THIS ALL WORKS FINE. EXCEPT when TempAmount and BilledAmount are in opposite amounts which produce a Zero.
In other words, if TempAmount is 15.25 and BilledAmount = -15.25, the result should be zero. But that doesnt happen. Instead, I get some wild expanded notation value like 2.123423523E-10.
Again, this ONLY happens when the result will equal a zero because of a postive value being added to the opposite negative value.
Any clues?
You have declared TempAmount as a double, so you're dealing with floating-point numbers, and not fixed-point numbers.
Declare TempAmount as a currency instead.
Dim TempAmount as Double
TempAmount = 0
While NOT rsBilled.EOF
TempAmount = Round(TempAmount,4) + Round(rsBilled.Fields("BilledAmount"),4)
rsBilled.MoveNext
Wend