Comparing doubles returns false - vba

I have three numbers from my database and want to compare them in an if statement.
I have a simple conevert function that returns only doubles.
Public Function RetDbl(ByVal obj As Variant) As Double
On Error Resume Next
RetDbl = val(Replace(Nz(obj, 0), ",", "."))
End Function
The statement is
If RetDbl(rs.value("NumA")) + RetDbl(rs.value("NumB")) <> (RetDbl(rs.value("NumC")) * 1000) Then
'[... do some code ...]
End If
With RetDbl(rs.value("NumA")) = 0.33, RetDbl(rs.value("NumB") = 0.5 and RetDbl(rs.value("NumC")) = 0.00083
This always returns false
I also tried:
In the direct field (STRG + G): ?cdbl(0.33) + cdbl(0.50) = cdbl(0.83) returns false. When i leave out the last part it returns 0.83
How can i compare these numbers?

Comparing floating numbers is hard. Only yesterday, I've posted this question
My solution:
Public Function DblSafeCompare(ByVal Value1 As Variant, ByVal Value2 As Variant) As Boolean
'Compares two variants, dates and floats are compared at high accuracy
Const AccuracyLevel As Double = 0.00000001
'We accept an error of 0.000001% of the value
Const AccuracyLevelSingle As Single = 0.0001
'We accept an error of 0.0001 on singles
If VarType(Value1) <> VarType(Value2) Then Exit Function
Select Case VarType(Value1)
Case vbSingle
DblSafeCompare = Abs(Value1 - Value2) <= (AccuracyLevelSingle * Abs(Value1))
Case vbDouble
DblSafeCompare = Abs(Value1 - Value2) <= (AccuracyLevel * Abs(Value1))
Case vbDate
DblSafeCompare = Abs(CDbl(Value1) - CDbl(Value2)) <= (AccuracyLevel * Abs(CDbl(Value1)))
Case vbNull
DblSafeCompare = True
Case Else
DblSafeCompare = Value1 = Value2
End Select
End Function
Note that the AccuracyLevel (epsilon) could be set to a smaller value, and I'm using the same value for singles and doubles, but it did well for my purposes.
I'm using a relative epsilon, but multiplying it with the first, and not the largest value, since if there's a significant difference the comparison will fail anyway.
Note that I'm using <= and not < since else DblSafeCompare(cdbl(0) ,cdbl(0)) would fail
Note that this function checks for type equality, so comparing integers to longs, doubles to singles, etc. all fails. Comparing Null to Null passes, however.
Implement it:
?DblSafeCompare(cdbl(0.33) + cdbl(0.50) ,cdbl(0.83))
?DblSafeCompare(cdbl(0.331) + cdbl(0.50) ,cdbl(0.83))

Comparing floating point numbers is really an issue, if you try to do it without understanding of the nature of the floating numbers.
Here is a nice article about it - https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html and How dangerous is it to compare floating point values?
In general, this problem is so big, that some languages like C# have developed a specific class called Decimal which makes comparing run as it would be expected by a non-programmer. Decimal info. In VBA, a similar class is Currency. Thus
CCur(0.33) + CCur(0.50) = CCur(0.83)
Returns True. VBA supports the function CDec, which converts a double to a decimal number, but it does not support the class Decimal. Thus:
CDec(0.33) + CDec(0.50) = CDec(0.83)
would also return True. And is with some better accuracy than Currency. CDec documentation.

Related

How do I produce a number with absolute precision of some power of 10?

Vb.net has a decimal data type.
Unlike normal double or floating points, decimal data type can store values like 0.1
Now say I have a variable like precision.
Say precision is 8
So basically I want to do
Protected Overridable Sub setPairsPricesStep2(decimalPrecission As Long, Optional base As String = "", Optional quote As String = "")
If decimalPrecission = 8 Then
Return
End If
Dim price = 10D ^ (-decimalPrecission)
setPairsPriceStep1(price, base, quote)
End Sub
There is a problem there
the result of Dim price = 10D ^ (-decimalPrecission) is double, not decimal. I can convert it to decimal but then I will lost the precission.
So what is the right way to do it? Should I just use for next but that's hardly elegant.
It's simple
I want a function that given precisions give decimal value.
For example, if precision is 1 I got 0.1. If precision is 5, I got 0.00001
I ended up doing this
For i = 1 To decimalPrecission
price *= 0.1D
Next
But surely there is a better way
Update:
Per comment, I tried
Dim e = 10D ^ -5
Dim e1 = 10D ^ -5L
The type of e and e1 are both double.
I suppose I can do Cdec(e). But then it means I have lost accuracy because normal double cannot store .1 correctly.
I want a function that given precisions give decimal value.
For example, if precision is 1 I got 0.1. If precision is 5, I got 0.00001
Since you are working with the Decimal type, the simplest way to get this result is to use the Decimal constructor that allows you to specify the scale factor.
Public Sub New (lo As Integer, mid As Integer, hi As Integer, isNegative As Boolean, scale As Byte)
From the Remarks section of the above referenced documentation,
The binary representation of a Decimal number consists of a 1-bit sign, a 96-bit integer number, and a scaling factor used to divide the integer number and specify what portion of it is a decimal fraction. The scaling factor is implicitly the number 10 raised to an exponent ranging from 0 to 28.
So you can see that if take the value of one divided by 10 to the first power, the result is 0.1. Likewise, one divided by 10 to the fifth power, the result is 0.00001.
The lo, mid, and hi arguments in the constructor could be obtained by uisng the [Decimal.GetBits Method](Decimal.GetBits Method), but for this simple case, I chose to hard code the values for the value of one stored as a decimal.
To obtain a value of 0.1D:
New Decimal(1, 0, 0, False, 1)
To obtain a value of 0.00001D:
New Decimal(1, 0, 0, False, 5)
Dim stringrepresentation = "1E-" + decimalPrecission.ToString
Dim price = Decimal.Parse(stringrepresentation, System.Globalization.NumberStyles.AllowExponent)
This is what I basically did. Basically I created a string 1E-5, for example, and use decimal.parse to get the decimal
I wonder if there is a better way but I have no idea.
Actually Jimy ways may work too but rounded to a number

Strange result of floating-point operation

Problems like this drive me crazy. Here's the relevant piece of code:
Dim RES As New Size(Math.Floor(Math.Round(mPageSize.Width - mMargins.Left - mMargins.Right - mLabelSize.Width, 4) / (mLabelSize.Width + mSpacing.Width) + 1),
Math.Floor((mPageSize.Height - mMargins.Top - mMargins.Bottom - mLabelSize.Height) / (mLabelSize.Height + mSpacing.Height)) + 1)
Values of the variables (all are of Single type):
mPageSize.Width = 8.5
mMargins.Left = 0.18
mMargins.Right = 0.18
mLabelSize.Width = 4.0
mSpacing.Width = 0.14
For God-knows-what reason, RES evaluates to {Width=1,Height=5} instead of {Width=2,Height=5}. I have evaluated the expressions on the right-side individually and as a whole and they correctly evaluate to {2,5}, but RES would never get correct value. Wonder what am I missing here.
EDIT
I have simplified the problem further. The following code will produce 2.0 if you QuickWatch the RHS, but the variable on the LHS will get 1.0 after you execute this line:
Dim X = Math.Floor(Math.Round(mPageSize.Width - mMargins.Left - mMargins.Right - mLabelSize.Width, 4) / (mLabelSize.Width + mSpacing.Width) + 1)
Time for MS to check it out?
EDIT 2
More info. The following gives correct results:
Dim Temp = mPageSize.Width - mMargins.Left - mMargins.Right - mLabelSize.Width
Dim X = Math.Floor(Temp / CDec(mLabelSize.Width + mSpacing.Width)) + 1
The problem is that the following expression evaluates to a value just below 1:
Math.Round(mPageSize.Width - mMargins.Left - mMargins.Right - mLabelSize.Width, 4) / (mLabelSize.Width + mSpacing.Width)
= 0.99999999985602739 (Double)
But what's the reason for that? The truth is that I don't know exactly. The MSDN does not offer enough information about the implementation of / but here's my guess:
Math.Round returns a Double with value 4.14. The right-hand side of the division is a Single. So you're dividing a Double by a Single. This results in a Double (see MSDN). So far, so good. The MSDN states that all integral data types are widened to Double before the division. Although Single is not an integral data type, this is probably what happens. And here is the problem. The widening does not seem to be performed on the result of the addition, but on its operands.
If you write
Dim sum = (mLabelSize.Width + mSpacing.Width) 'will be 4.14 Single
Math.Round(mPageSize.Width - mMargins.Left - mMargins.Right - mLabelSize.Width, 4) / sum
= 1 (Double)
Here sum is converted to double (resulting in 4.14) and everything is fine. But, if we convert both operands to double, then the conversion of 0.14 introduces some floating point error:
Dim dblLabelSizeWidth As Double = mLabelSize.Width ' will be 4.0
Dim dblSpacing As Double = mSpacing.Width ' will be 0.14000000059604645
The sum is slightly bigger than 4.14, resulting in a quotient slightly smaller than 1.
So the reason is that the conversion to double is not performed on the division's operand, but on the operand's operands, which introduces floating point errors.
You could overcome this problem by adding a small epsilon to the quotient before rounding off. Alternatively you might consider using a more precise data type such as Decimal. But at some point, there will also be floating-point errors with Decimal.
This is due to rounding error: you're taking the floor of a value that is very close to 2, but is less than 2 (while the mathematical value is 2). You should do all your computations with integers, or take rounding errors into account before using operations like floor (not always possible if you want the true value).
EDIT: Since vb.net has a Decimal datatype, you can also use it instead of integers. It may help in some cases like here: the base conversions for 0.18 and 0.14 (not representable exactly in binary) are avoided and the additions and subtractions will be performed exactly here, so that the operands of the division will be computed exactly. Thus, if the result of the division is an integer, you'll get it exactly (instead of possibly a value just below, like what you got with binary). But make sure that your inputs are already in decimal.

get decimal part of number in MS Access

How do you access the decimal part of a number in MS Access? More specifically I want only the component after the decimal point, but not including the decimal point. This must also work for all whole numbers. I've seen this answered for other SQL engines, but they don't work in Access. I can't be much more specific than this because of the sensitive nature of what I'm actually working on.
For example given the following numbers the input is on the left and the output is on the right. Output can be either text or a number.
Source Correct Incorrect1 Incorrect2
10.0 0 0.0 .0
3.14159 14159 0.14159 .14159
45.65 65 0.65 .65
173.0 0 0.0 .0
143.15 15 0.15 .15
If I was using C# the following code would give me what I want:
private string getDecimalComponent(double input)
{
String strInput = input.ToString();
if (strInput.Contains('.'))
{
return strInput.Split('.')[1];
}
else
{
return "0";
}
}
Subtract the integer portion of the value.
Example:
4.25 - Int(4.25) = 0.25
Or, as a sample SQL expression:
SELECT
[myDecimalNumber],
[myDecimalNumber] - Int([myDecimalNumber]) as [rightOfDecimal]
FROM tableA
Something like:
SELECT 3.14%1 AS mycolumn from mytable
Depends on the circumstance. If this is, for instance, in a textbox, you can use InStr to find the decimal, and then use the Mid() function to get the number after it. If it's part of an arithmetic equation, then I would use the Int() function and subtract one number from the other to get the difference.
If you can elaborate on how it's being used, and in what context, I can edit my answer to give you more specifics.
EDIT: After more info came to light, try this:
Public Function GetParts(Temp1 as Double)
Temp2 = Int(Temp1)
Temp3 = Mid(Temp1, InStr(Temp1, ".") + 1)
MsgBox Temp2
MsgBox Temp3
End Function
A stable solution producing String containing decimal places from Double.
SELECT DecimalPlaces(MyColumn) FROM MyTable
where the above user-defined function contains the following code:
Function DecimalPlaces(ByVal value As Double) As String
Dim intPartLen As String
intPartLen = Len(CStr(CInt(Abs(value))))
If Len(CStr(Abs(value))) > intPartLen Then
DecimalPlaces = Mid(CStr(Abs(value)), intPartLen + 2)
Else
DecimalPlaces = "0"
End If
End Function
It avoids common mistakes, so it is
locale-independent - works with any decimal separator (it only assumes it is a single character)
preserves precision - avoids subtraction, takes the result only from string representation
Note: those Abs() calls are really required (hint: -0.1)

vb.net mantissa and exponent calculation from double

Can anyone offer any advice on how to get the mantissa and exponent from a double in VB.net? I know I can do a string parse and some conversion to ints but I wondered if anyone had a mathematical equivalent formula that would allow me to do this?
Many thanks
Do you want to get the "native" mantissa and exponent within the IEEE-754 value? That's actually fairly easy: use BitConverter.DoubleToInt64Bits to get the value as an integer (which is easier to perform bit operations on) and then see my article on .NET binary floating point types for which bits are where.
I have some C# code which extracts the various parts in order to convert it to a precise decimal representation - you could convert the relevant bits of that into VB reasonably easily.
You should try this:
Public Function DeclString(ByVal dDegrees As Double) As String
Dim Flag As String
Dim ddecimal As Double
Dim iDegrees As Integer
If dDegrees < 0 Then Flag = "S" Else Flag = "N"
iDegrees = Int(Abs(dDegrees))
ddecimal = (Abs(dDegrees) - iDegrees) * 60 ' + 0.5
If ddecimal > 59.5 Then iDegrees = iDegrees + 1: ddecimal = 0
DeclString = Format$(iDegrees, "00") + Flag + Format$(ddecimal, "00")
End Function

Rounding a number to the nearest 5 or 10 or X

Given numbers like 499, 73433, 2348 what VBA can I use to round to the nearest 5 or 10? or an arbitrary number?
By 5:
499 -> 500
2348 -> 2350
7343 -> 7345
By 10:
499 -> 500
2348 -> 2350
7343 -> 7340
etc.
It's simple math. Given a number X and a rounding factor N, the formula would be:
round(X / N)*N
Integrated Answer
X = 1234 'number to round
N = 5 'rounding factor
round(X/N)*N 'result is 1235
For floating point to integer, 1234.564 to 1235, (this is VB specific, most other languages simply truncate) do:
int(1234.564) 'result is 1235
Beware: VB uses Bankers Rounding, to the nearest even number, which can be surprising if you're not aware of it:
msgbox round(1.5) 'result to 2
msgbox round(2.5) 'yes, result to 2 too
Thank you everyone.
To round to the nearest X (without being VBA specific)
N = X * int(N / X + 0.5)
Where int(...) returns the next lowest whole number.
If your available rounding function already rounds to the nearest whole number then omit the addition of 0.5
In VB, math.round has additional arguments to specify number of decimal places and rounding method. Math.Round(10.665, 2, MidpointRounding.AwayFromZero) will return 10.67 . If the number is a decimal or single data type, math.round returns a decimal data type. If it is double, it returns double data type. That might be important if option strict is on.
The result of (10.665).ToString("n2") rounds away from zero to give "10.67". without additional arguments math.round returns 10.66, which could lead to unwanted discrepancies.
'Example: Round 499 to nearest 5. You would use the ROUND() FUNCTION.
a = inputbox("number to be rounded")
b = inputbox("Round to nearest _______ ")
strc = Round(A/B)
strd = strc*B
msgbox( a & ", Rounded to the nearest " & b & ", is" & vbnewline & strd)
For a strict Visual Basic approach, you can convert the floating-point value to an integer to round to said integer. VB is one of the rare languages that rounds on type conversion (most others simply truncate.)
Multiples of 5 or x can be done simply by dividing before and multiplying after the round.
If you want to round and keep decimal places, Math.round(n, d) would work.
Here is our solution:
Public Enum RoundingDirection
Nearest
Up
Down
End Enum
Public Shared Function GetRoundedNumber(ByVal number As Decimal, ByVal multiplier As Decimal, ByVal direction As RoundingDirection) As Decimal
Dim nearestValue As Decimal = (CInt(number / multiplier) * multiplier)
Select Case direction
Case RoundingDirection.Nearest
Return nearestValue
Case RoundingDirection.Up
If nearestValue >= number Then
Return nearestValue
Else
Return nearestValue + multiplier
End If
Case RoundingDirection.Down
If nearestValue <= number Then
Return nearestValue
Else
Return nearestValue - multiplier
End If
End Select
End Function
Usage:
dim decTotal as Decimal = GetRoundedNumber(CDec(499), CDec(0.05), RoundingDirection.Up)
Simply ROUND(x/5)*5 should do the job.
I cannot add comment so I will use this
in a vbs run that and have fun figuring out why the 2 give a result of 2
you can't trust round
msgbox round(1.5) 'result to 2
msgbox round(2.5) 'yes, result to 2 too
something like that?
'nearest
n = 5
'n = 10
'value
v = 496
'v = 499
'v = 2348
'v = 7343
'mod
m = (v \ n) * n
'diff between mod and the val
i = v-m
if i >= (n/2) then
msgbox m+n
else
msgbox m
end if
Try this function
--------------start----------------
Function Round_Up(ByVal d As Double) As Integer
Dim result As Integer
result = Math.Round(d)
If result >= d Then
Round_Up = result
Else
Round_Up = result + 1
End If
End Function
-------------end ------------
I slightly updated the function provided by the "community wiki" (the best answer), just to round to the nearest 5 (or anything you like), with this exception : the rounded number will NEVER be superior to the original number.
This is useful in cases when it is needed to say that "a company is alive for 47 years" : I want the web page to display "is alive for more than 45 years", while avoiding lying in stating "is alive for more than 50 years".
So when you feed this function with 47, it will not return 50, but will return 45 instead.
'Rounds a number to the nearest unit, never exceeding the actual value
function RoundToNearestOrBelow(num, r)
'#param num Long/Integer/Double The number to be rounded
'#param r Long The rounding value
'#return OUT Long The rounded value
'Example usage :
' Round 47 to the nearest 5 : it will return 45
' Response.Write RoundToNearestBelow(47, 5)
Dim OUT : OUT = num
Dim rounded : rounded = Round((((num)) / r), 0) * r
if (rounded =< num) then
OUT = rounded
else
OUT = rounded - r
end if
'Return
RoundToNearestOrBelow = OUT
end function 'RoundToNearestOrBelow
To mimic in Visual Basic the way the round function works in Excel, you just have to use:
WorksheetFunction.Round(number, decimals)
This way the banking or accounting rounding don't do the rounding.