There are a few cases where API calls return 8 byte integers. FileSize and FileTime are a couple of them. These are never easily handled with VB6. Essentially, there are a couple of ways it's typically done: 1) Create a UDT with two Longs, 2) Use Currency.
Both of these have the downside of not being able to treat the results as a true 8 byte integer for arithmetic or just printing/viewing.
Also, there may be certain times when a Long integer just doesn't have the range to do something you'd like to do, and Currency is always a pain because you have to deal with the implicit-four-decimal-places.
VB6 actually has a "type" on which you can do 8 byte integer math. In fact, you can do 12 byte integer math with it. It's the Decimal data type.
Sadly, Decimal is not one of the native types. However, it is one of the data types that a Variant will handle. And VB6 will do arithmetic with it when it's in a Variant.
In fact, it's the only data type I'm aware of that uses all 16 bytes of a Variant (with a Variant always having 16 bytes). The Decimal type uses 14 bytes, and 2 bytes are reserved by the Variant to denote the data type within the variant. When a Decimal is contained with a Variant, it has the following structure:
The following is also in the comments of the code, but let me spell them out here as well.
Let's examine the above Decimal structure a bit. It's a bit strange that the bytes of mantissa have a mixed-endianness about them. But we really don't need to worry about that. VB6 deals with all of that.
To some degree, we can think of these Decimals as an unsigned 12 byte integer. As such, they range from 0 to 79228162514264337593543950335, where that big number is just 2^96-1. The 96 is the 96 bits of 12 bytes (12*8).
In these Decimals, the sign bit is kept completely separate. It's in that Sign byte of the UDT structure. Only the high bit (&h80) of this byte is used. When it's on, the Decimal is interpreted as negative.
Now, the Base10NegExp is also somewhat strange. This is not a base 2 exponent like what is used in Single and Double. This is a base 10 exponent that moves the decimal the way we were taught in 5th grade. However, it's also a negative exponent, moving the decimal point to the left as the Base10NegExp byte gets larger. In other words, in 5th grade scientific notation, it's Mantissa * 10^-Base10NegExp.
Furthermore, this Base10NegExp is limited to the range of 0 to 28. Why limited and why to this range? If we count the digits in 79228162514264337593543950335, we find that there are 28. Therefore, using this Base10NegExp, we can change this number from 79228162514264337593543950335.0000 (where Base10NegExp=0) to 00007.9228162514264337593543950335 (where Base10NegExp=28). In other words, we can place the decimal point anywhere within the number (including at the end, as an integer), but we can't force zeroes on either end. Another way to say this is that "perfect precision, with no rounding, is preserved at all times with Decimals". This is particularly true with the +, -, and * operators. However, it will be forced to round to those 28 significant digits with division.
Now, here's the code related to the Decimal type that I keep in my library:
Let me go through each procedure.
IDK, maybe this all isn't the standard fare for the CodeBank. But I had it all put together for my own edification, and I thought I'd share.
Enjoy,
Elroy
p.s. Any and all comments are more than welcome.
Both of these have the downside of not being able to treat the results as a true 8 byte integer for arithmetic or just printing/viewing.
Also, there may be certain times when a Long integer just doesn't have the range to do something you'd like to do, and Currency is always a pain because you have to deal with the implicit-four-decimal-places.
VB6 actually has a "type" on which you can do 8 byte integer math. In fact, you can do 12 byte integer math with it. It's the Decimal data type.
Sadly, Decimal is not one of the native types. However, it is one of the data types that a Variant will handle. And VB6 will do arithmetic with it when it's in a Variant.
In fact, it's the only data type I'm aware of that uses all 16 bytes of a Variant (with a Variant always having 16 bytes). The Decimal type uses 14 bytes, and 2 bytes are reserved by the Variant to denote the data type within the variant. When a Decimal is contained with a Variant, it has the following structure:
Code:
Private Type DecimalStructure ' (when sitting in a Variant)
VariantType As Integer ' Reserved, to act as the Variant Type when sitting in a 16-Byte-Variant. Equals vbDecimal(14) when it's a Decimal type.
Base10NegExp As Byte ' Base 10 exponent (0 to 28), moving decimal to right (smaller numbers) as this value goes higher. Top three bits are never used.
Sign As Byte ' Sign bit only. Other bits aren't used.
Hi32 As Long ' Mantissa.
Lo32 As Long ' Mantissa.
Mid32 As Long ' Mantissa.
End Type
- Decimal is at the top of the math-implicit-conversion hierarchy. Therefore, in most cases, you don't have to worry about the results being rounded when you do Decimal + Integer, or Decimal * Long, etc. In other words, with the +, -, *, and / operators, if a Decimal is on either side, a Decimal will be the result.
- The ^ operator will result in a Double (not a Decimal).
- The MOD and \ operators will result in a Long (not a Decimal).
- Int(), Fix(), and Abs() work just fine on Decimals, returning a Decimal type.
- Sgn() works on Decimals, although return is an Integer but this shouldn't matter.
- Sqr() and the trig functions will work, although they will return Double (not Decimal).
Let's examine the above Decimal structure a bit. It's a bit strange that the bytes of mantissa have a mixed-endianness about them. But we really don't need to worry about that. VB6 deals with all of that.
To some degree, we can think of these Decimals as an unsigned 12 byte integer. As such, they range from 0 to 79228162514264337593543950335, where that big number is just 2^96-1. The 96 is the 96 bits of 12 bytes (12*8).
In these Decimals, the sign bit is kept completely separate. It's in that Sign byte of the UDT structure. Only the high bit (&h80) of this byte is used. When it's on, the Decimal is interpreted as negative.
Now, the Base10NegExp is also somewhat strange. This is not a base 2 exponent like what is used in Single and Double. This is a base 10 exponent that moves the decimal the way we were taught in 5th grade. However, it's also a negative exponent, moving the decimal point to the left as the Base10NegExp byte gets larger. In other words, in 5th grade scientific notation, it's Mantissa * 10^-Base10NegExp.
Furthermore, this Base10NegExp is limited to the range of 0 to 28. Why limited and why to this range? If we count the digits in 79228162514264337593543950335, we find that there are 28. Therefore, using this Base10NegExp, we can change this number from 79228162514264337593543950335.0000 (where Base10NegExp=0) to 00007.9228162514264337593543950335 (where Base10NegExp=28). In other words, we can place the decimal point anywhere within the number (including at the end, as an integer), but we can't force zeroes on either end. Another way to say this is that "perfect precision, with no rounding, is preserved at all times with Decimals". This is particularly true with the +, -, and * operators. However, it will be forced to round to those 28 significant digits with division.
Now, here's the code related to the Decimal type that I keep in my library:
Code:
Option Explicit
'
' +, -, *, / Decimal is at the top of the hierarchy.
' The hierarchy is: Byte, Integer, Long, Single, Double, Currency, Decimal.
' \, Mod Results is Long, not Decimal.
' ^ Results is Double, not Decimal.
' Int() Works on Decimals.
' Fix() Works on Decimals.
' Abs() Works on Decimals.
' Sgn() Works on Decimals, although result is an Integer.
' Sqr(), etc. Most of these functions (including trig) return Double, not Decimal.
'
' Largest Decimal: +/- 79228162514264337593543950335. 2^96-1 (sign bit handled separately)
' Smallest Decimal: +/- 0.0000000000000000000000000001 Notice that both largest and smallest are same width.
'
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Dst As Any, Src As Any, ByVal numbytes As Long)
Private Declare Function VariantChangeTypeEx Lib "oleaut32" (ByRef pvargDest As Variant, ByRef pvarSrc As Variant, ByVal lcid As Long, ByVal wFlags As Integer, ByVal vt As Integer) As Long
'
Private Type DecimalStructure ' (when sitting in a Variant)
VariantType As Integer ' Reserved, to act as the Variant Type when sitting in a 16-Byte-Variant. Equals vbDecimal(14) when it's a Decimal type.
Base10NegExp As Byte ' Base 10 exponent (0 to 28), moving decimal to right (smaller numbers) as this value goes higher. Top three bits are never used.
Sign As Byte ' Sign bit only. Other bits aren't used.
Hi32 As Long ' Mantissa.
Lo32 As Long ' Mantissa.
Mid32 As Long ' Mantissa.
End Type
'
Public Function DecFromCurrAsInt(ByVal c As Currency, Optional Signed As Boolean = False) As Variant
' Moves Currency type used as an 8 byte integer into a Decimal.
' Assumes 4 decimal points of Currency are undesired (and ignored).
' Useful for API calls returning 8 byte integer, provides a way to report the actual integer.
'
Const DECIMAL_NEG As Byte = 128 ' The high-bit of a byte. Sign bit for Decimal.
Dim u As DecimalStructure
'
If Signed Then
DecFromCurrAsInt = CDec(c) * 10000 ' Note that signed Currency can be moved to Decimal relatively easily.
Else
CopyMemory ByVal VarPtr(u) + 8, c, 8
u.VariantType = vbDecimal ' Set up for moving to Variant type variable.
CopyMemory DecFromCurrAsInt, u, 16 ' Copy the 16 Bytes from u over into the Variant V.
End If
End Function
Public Function CurrAsIntFromDec(ByVal v As Variant, Optional Signed As Boolean = False) As Currency
' This moves a Decimal into a Currency, assuming that the Currency 4 decimal points are being ignored.]
' It's the reverse of DecFromCurrAsInt.
' Note that any precision seen in the Decimal beyond an 8 byte integer will throw an overflow error.
'
Const DECIMAL_NEG As Byte = 128 ' The high-bit of a byte. Sign bit for Decimal.
Dim u As DecimalStructure
'
' Let's make sure our decimal is an integer.
If Sgn(v) = -1 Then v = Fix(v - 0.5) Else v = Int(v + 0.5)
'
CopyMemory u, v, 16
If u.Hi32 <> 0 Then Err.Raise 6: Exit Function ' Overflow.
'
If Signed Then
If (u.Mid32 And &H80000000) = &H80000000 Then Err.Raise 6: Exit Function ' Overflow.
u.Base10NegExp = 4 ' Safest way to prevent any division rounding.
CopyMemory v, u, 16
CurrAsIntFromDec = CCur(v)
Else
CopyMemory CurrAsIntFromDec, ByVal VarPtr(u) + 8, 8
End If
End Function
Public Function CDecEx(StringLiteral As String) As Variant
' The downside of CDec() is that it's locale-aware with strings.
' So to do this correctly, we must use a call to VariantChangeTypeEx passing LOCALE_INVARIANT to it.
Dim ret As Long
Const LOCALE_INVARIANT As Long = &H7F&
' This will auto-typecast StringLiteral to a variant before making the call.
ret = VariantChangeTypeEx(CDecEx, StringLiteral, LOCALE_INVARIANT, 0, vbDecimal)
If ret Then Err.Raise ret, "CDecEx()"
End Function
Public Function DecRoundToInt(vDec As Variant) As Variant
' Round a Decimal to its integer, keeping Decimal type.
' Only really needed for division in the case where we want to force integers.
If Sgn(vDec) = -1 Then
DecRoundToInt = Fix(vDec - 0.5) ' This is the only way to insure that ???.5 gets rounded higher (in absolute value terms).
Else
DecRoundToInt = Int(vDec + 0.5)
End If
End Function
- DecFromCurrAsInt: This moves a Currency type into a Decimal (Variant) type, and treats the Currency as an 8 byte integer. In other words, it ignores the implicit four-decimal-points. This is particularly useful for "seeing" and possibly manipulating these 8 byte integers, and actually treating them as integers.
- CurrAsIntFromDec: This is the inverse of DecFromCurrAsInt. It puts a Decimal type (or, the integer portion) back into a Currency type. However, in this case, the Decimal carries 96 bits of mantissa precision, rather than only 64 (or 63 if the Currency sign bit is important). Therefore, there may be overflow conditions. This function is useful for possibly getting an 8 byte integer from an API, manipulating it, and then passing it back in with another API call.
- CDecEx: It's not trivially easy to get a large integer value into a Decimal type (with full precision) within VB6 code. About the only way to do it is to type out the number as a String. Possibly something like, "12341232342345.54235423". And then, we could possibly put this into a Decimal (Variant) using the CDec("12341232342345.54235423") function. However, anytime we have a String, there is the potential for Locale issues. Therefore, CDecEx puts a String into a Decimal (Variant) while circumventing the Locale issues.
- DecRoundToInt: This function is designed to expect a Decimal and it returns a Decimal. It's only applicable with division, as division is about the only way to start with integers and wind up with non-integers. We may desire to use strictly integers, with appropriate rounding. This DecRoundToInt provides this functionality.
IDK, maybe this all isn't the standard fare for the CodeBank. But I had it all put together for my own edification, and I thought I'd share.
Enjoy,
Elroy
p.s. Any and all comments are more than welcome.