What VBA variable type to use when reading values from a cell in Excel? - vba

According to this answer one should always use Variant when assigning values in a cell to a variable in the code. Is this correct? I seem to recall reading elsewhere that using Variant indiscriminately is not a good practice.

You can read a cell value into any type you want, VBA will (try to) implicitly convert it to that type for you.
There are dozens of questions on this site involving run-time errors raised from reading cell values into a specific data type - perhaps you've seen this error message before?
Type mismatch
That's the error you get when you try to read a cell containing an error value (e.g. #REF!) into anything other than a Variant.
So if you read a cell value into, say, a Double, everything will work fine as long as you're reading something that VBA can coerce into that data type. The problem is that, well, data is never 100% clean, worksheets do break down, users delete columns and break formulas, lookups fail and the person that wrote the formula didn't bother wrapping it with IFERROR, etc.
That's why you read cell values into a Variant.
That doesn't mean you work with a Variant.
Dim cellValue As Variant
cellValue = someRange.Value
If IsError(cellValue) Then Exit Sub 'bail out before we blow up
Dim workingValue As String
workingValue = CStr(cellValue)
By assigning to another data type, you effectively cast the Variant to that more specific type - here a String. And because you like explicit type conversions, you use VBA's conversion functions to make the conversion explicit - here CStr.
Now, in real code, you probably wouldn't even bother reading it into a Variant - you can use IsError to test the cell value:
If IsError(someRange.Value) Then Exit Sub 'bail out before we blow up
Dim cellValue As String
cellValue = someRange.Value ' or cellValue = CStr(someRange.Value)
The flipside here is that you're accessing the cell twice. Whether or not that's better that reading it into a Variant is for you to decide; performance-wise, it's usually best to avoid accessing ranges as much as possible though.

The value you get from a cell (which is a Range) is a Variant according to the documentation:
Range.Value Property (Excel)
Returns or sets a Variant value that represents the value of the specified range.
Since a Variant can represent different data types, you could loose information if you would assign a cell's value to -- for instance -- a variable of type String.
The mere fact that there is data type information in a Variant already means you lose that type of information. If for instance the original type was numeric and you store it in a String variable, there is no way to know from that string value what the original data type was. You could also lose precision (on Date milliseconds for instance).
Furthermore, a Variant type value cannot always be cast to the data type of your variable, and so you could get a Type mismatch error. In practice this often happens with the Error sub data type.
Only when you know beforehand what the data type is of a certain cell's value, it would be good to define your receiving variable in that data type.

Not strictly answering your question, but thought I'd add this for reference anyway.
With native Excel functions you can usually provide either a range object or a value directly to a function. For example, you can either write =AVERAGE(A1,A2,A3) or =AVERAGE(10,20,30). If you want to do something similar for any user defined functions, you will need to check the type of object passed to your function:
Function test(input As Variant)
Dim var As Variant
If TypeName(input) = "Range" Then
var = input.Value
Else
var = input
End If
You may also want to check for other objects if your function can accept them, but doing this will make your functions behave more like users expect them to.

Related

VB.Net - Can you access the expected data type within a function?

I was wondering if there is any way to access the expected data type within a function similar to an event arg. I am doubtful that this is possible, though it would be an excellent feature.
I frequently work with (old and disorganized)Mysql databases creating interfaces through VB.Net. Often I will have an optional field which contains a NULL value in the database. I am frequently dealing with errors due to NULL and dbnull values in passing data to and from the database.
To complicate things, I often am dealing with unexpected datatypes. I might have an integer zero, a double zero, an empty string, or a string zero.
So I spend a fair amount of code checking that each entry is of the expected type and or converting NULLs to zeros or empty strings depending on the case. I have written a function ncc(null catch convert) to speed up this process.
Public Function ncc(obj As Object, tp As Type) As Object 'Null Catch Convert Function...
My function works great, but I have to manually set the type every time I call the function. It would be so much easier if it were possible to access the expected type of the expression. Here is an example of what I mean.
Dim table as datatable
adapter.fill(table)
dim strinfo as string
dim intinfo as long
strinfo = ncc(table.Rows(0).Item(0),gettype(String)) 'here a string is expected
intinfo = ncc(table.Rows(0).Item(0),gettype(Long)) 'here a long is expected
It would be so much more efficient if it were possible to access the expected type directly from the function.
Something like this would be great:
Public Function ncc(obj As Object, optional tp As Type = nothing) As Object
If tp Is Nothing Then tp = gettype(ncc.expectedtype)
That way I do not have to hard code the type on each line.
strinfo = ncc(table.Rows(0).Item(0))
You can make the ncc function generic to simplify calling it:
Public Function ncc(Of T)(obj As T) As T
If DbNull.Value.Equals(obj) Then Return Nothing
Return Obj
End Function
This kind of function will be able to in some cases infer the type, but if there's any possibility of null you'll still want to include a type name (because DBNull will be the inferred type for those values). The advantage is not needing to call gettype() and so gaining a small degree of type safety:
strinfo = ncc(Of String)(table.Rows(0).Item(0))
But I think this has a small chance to blow up at run time if your argument is not implicitly convertible to the desired type. What you should be doing is adding functions to accept a full row and return a composed type. These functions can exist as static/shared members of the target type:
Shared Function FromDataRow(IDataRow row) As MyObject
And you call it for each row like this:
Dim record As MyObject = MyObject.FromDataRow(table.Rows(i))
But, you problem still exists.
What happens if the column in the database row is null?
then you DO NOT get a data type!
Worse yet? Assume the data column is null, do you want to return null into that variable anyway?
Why not specify a value FOR WHEN its null.
You can use "gettype" on the passed value, but if the data base column is null, then you can't determine the type, and you right back to having to type out the type you want as the 2nd parameter.
You could however, adopt a nz() function (like in VBA/Access).
So, this might be better:
Public Function ncc(obj As Object, Optional nullv As Object = Nothing) As Object
If obj Is Nothing OrElse IsDBNull(obj) Then
Return nullv
End If
Return obj
End Function
So, I don't care if the database column is null, or a number, for such numbers, I want 0.
So
dim MyInt as integer
Dim MyDouble As Double
MyInt = ncc(rstData.Rows(0).Item("ContactID"), 0)
MyDouble = ncc(rstData.Rows(0).Item("ContactID"), 0)
dim strAddress as string = ""
strAddress = ncc(rstData.Rows(0).Item("Address"), "")
Since in NEAR ALL cases, you need to deal with the null from the DB, then above not only works for all data types, but also gets you on the fly conversion.
I mean, you CAN declare variables such as integer to allow null values.
eg:
dim myIntValue as integer?
But, I not sure above would create more problems than it solves.
So,
You can't get exactly what you want, because a function never has knowledge of how it's going to be used. It's not guaranteed that it will be on the right-hand side of an assignment statement.
If you want to have knowledge of both sides, you either need to be assigning to a custom type (so that you can overload the assignment operator) or you need to use a Sub instead of an assignment.
You could do something like this (untested):
Public Sub Assign(Of T)(ByVal field As Object, ByRef destination As T,
Optional ByVal nullDefault As T = Nothing)
If TypeOf field Is DBNull Then
destination = nullDefault
Else
destination = CType(field, T)
End If
End Sub
I haven't tested this, so I'm not completely certain that the compiler would allow the conversion, but I think it would because field is type Object. Note that this would yield a runtime error if field is not convertible to T.
You could even consider putting on a constraint requiring T to be a value type, though I don't think that would be likely to work because you probably need to handling String which is a reference type (even though it basically acts like a value type).
Because the destination is an argument, you wouldn't ever need to specify the generic type argument, it would be inferred.

Returning the Default Property

I fill A1 and A2 as follows:
I then run:
Sub WhatIsGoingOn()
Dim r As Range, sh As Worksheet
Set r = Range(Cells(1, 1))
Set sh = Sheets(Cells(2, 1))
End Sub
I expected that in both cases, VBA would use the default property of Cells (the Value) property to Set each variable. However I get a runtime error 13 on the last line of code!
In order to avoid errors, I must use:
Sub WhatIsGoingOn2()
Dim r As Range, sh As Worksheet
Set r = Range(Cells(1, 1))
Set sh = Sheets(Cells(2, 1).Value)
End Sub
What is going on here ??
The difference is in how the input to their default properties is handled by the implementation of the Range and Sheets objects.
The default property of both the Range and the Sheets object takes a parameter of type Variant. You can pass anything to it, so no type coercion will be necessary. In your first example you pass a Range object to both.
How the default properties handle the input is up to themselves. Apparently the property of the Range tries to retrieve the default value of the passed parameter, in your example an address as String. The Sheets object doesn't seem to be so forgiving and raises an error because you neither passed a number nor a String.
Inconsistency is one of the strengths of VBA...
Btw., passing CStr(Cells(2, 1)) would also work, because you explicitly cast to String before passing as a parameter.
Perhaps Leviathan's comment that "Inconsistency is one of the strengths of VBA..." may ring true, but there are some contextual details that his answer neglects (and is technically incorrect on some subtle points). He is correct that for the given code that all of the parameters are variants, but the statement that "no type coercion will be necessary" can be misleading and perhaps just wrong in many cases. Even if many objects and methods are programmed to handle multiple types and default values, this very question reveals that it is a mistake to avoid purposely ensuring (or coercing) the correct data type. Avoiding default properties altogether (by always typing out the full reference) can avoid many headaches.
A significant difference between the lines of code for this particular question is this: Range is a property that takes parameters, while Sheets is also a property but has no parameters. Range and Sheets are NOT objects in this context, even though they are properties which do return Range and Sheets objects, respectively. They are properties of an (automatic global) object defined for the particular module or Excel workbook instance. This detail is not trivial for understanding what the code is actually doing.
The Obect Browser in the VBA window reveals the following metadata for the two properties:
Property Range(Cell1, [Cell2]) As Range
Property Sheets As Sheets
For Range(Cells(1, 1)), the argument Cells(1,1) is passed to the parameter Cell1. Cells itself is a property of the Excel.Global hidden object and it returns a Range object, so that Cells(rowindex, colindex) is calling a hidden default property of the Range class equivalent to Cells._Default(rowindex, colindex). The return type of the property _Default() is not declared, so technically it could return any type in a variant, but inspection shows that it returns a Range object. Apparently passing a Range object to its default property will attempt to take the default value and if it is a valid range value, like a string with a range expression, then it will execute without error.
The default property for the Sheets class is the parameterized hidden _Default(Index) method. Thus, Sheets(Cells(2, 1)) is equivalent to Sheets._Default(Cells(2, 1)). More importantly, it means that Sheets._Default(Cells(2, 1)) is passing a Range object as an index value, but documentation says that it expects an integer or string value. We already mentioned that the index parameter is variant... and when passing an object to a variant, it always passes the actual object and never its default property. So we know that Sheets.Item obtains a Range object in that call. Here is were Levithan was correct in that Sheets.Item can decide what to do with it. It is likely that it could have been smart enough to get the single string value and continue without error. Other collection objects (with a default Item(index) property) in MS Office objects do not seem to exhibit this same "pickiness", so it appears that Sheets._Default() (and perhaps Sheets.Item()) is being rather strict on validating its arguments. But this is just a particular design issue with this method only... not necessarily an overall issue with VBA.
What can be difficult is determining exactly what the source objects of the properties are. Inside the ThisWorkbook module, Me.Sheets reveals that Sheets is a property of the particular Workbook for the module. But Me.Range is not valid in the Workbook module, but right-clicking on the Range property (without the Me qualifier) and choosing "Definition" results in the message "Cannot jump to Range because it is hidden". However, once in the Object Browser, righ-clicking within the browser one can choose "Show Hidden Members" which will then allow navigating to the hidden Global objects and other hidden members.
Why the inconsistency and the hidden properties? In an attempt to make the current instance of Excel and all of its components accessible in a "natural" way, Excel (and all Office applications) implement these various hidden properties to avoid the "complexity" of having to repeatedly discover and type out full references. For instance, the automatic global Application object also has both Range and Sheets properties. Actual documentation for Application.Sheets, for example, says "Using this property without an object qualifier is equivalent to using ActiveWorkbook.Sheets". Even that documentation fails to say is that ActiveWorkbook is in turn a property of the global Excel Application object'.

Cell ColumnType is NULL using Smartsheet API

I'm trying to update my SmartSheet API from v1 to v2 and am having some difficulty on the code below.
The code returns rows for the selected sheet, however the "ColumnType" property of all the Cell's within the rows are NULL.
I know to return this you have to specify it as an inclusion - which I believe I have.
Dim sheet As Sheet = smartsheet.SheetResources.GetSheet(curSheet.Id, New RowInclusion() {RowInclusion.COLUMN_TYPE, RowInclusion.COLUMNS}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
For Each Row As Row In sheet.Rows
If Row.ParentRowNumber Is Nothing Then
Dim i As Integer = 0
Dim colType As ColumnType
If Not Row.Cells(i).ColumnType = ColumnType.TEXT_NUMBER Then
'Do some stuff here...
End if
Next
Any help would be great.
Thanks,
Steve
The short answer is just get the latest SDK from https://github.com/smartsheet-platform/smartsheet-csharp-sdk/pull/60 and update your GetSheet to the following:
Dim sheet As Sheet = client.SheetResources.GetSheet(SHEETID, New SheetLevelInclusion() {SheetLevelInclusion.COLUMN_TYPE}, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
Notice the use of SheetLevelInclusion rather than RowInclusion. You should be all set.
If you care about the details, the longer answer is... The GetSheet method doesn't accept an array/IEnumerable of RowInclusion as the second argument. It expects an array/IEnumerable of SheetLevelExclusion. In C#, the same invocation call would fail as C# imposes stricter type checking on the generic type parameter of IEnumerable. However, due to Visual Basic's leniency around implicit conversions between Enum types and its lenient conversions for arrays (and similar types like IEnumerable) it is possible to invoke a function with the "wrong" type of argument when the argument is an array/IEnumerable and the elements are Enums. In this case, Visual Basic is actually converting the RowInclusion values to their underlying numeric value (Enum is always implicitly or explicitly backed by an underlying numeric type) and converting those values to the SheetLevelExclusion value corresponding to the same underlying numeric value so that it can invoke the GetSheet method.
The other complication here is that the SDK didn't have COLUMN_TYPE as an available SheetLevelExclusion value. So the pull request/branch I linked to above adds that. In my simple test here that made it work.

How do I assign an Excel VBA variable to a Defined Name

How do I return a value from a VBA Function variable to a Defined Name in Excel 2010?
In the example below, I want i to be returned to Excel as a Defined Name Value_Count that can be reused in other formulas, such that if MsgBox shows 7, then typing =Value_Count in a cell would also return 7.
Everything else below is about what I've tried, and why I'd like to do it. If it's inadvisable, I'd be happy to know why, and if there's a better method.
Function process_control_F(raw_data As Variant)
Dim i As Integer
i = 0
For Each cell In raw_data
i = i + 1
Next cell
MsgBox i
End Function
My goal is to have the value returned by the MsgBox be returned instead to a Defined Name that can be reused in other forumulas. However, I cannot get the value to show. I have tried a variety of forms (too numerous to recall, let alone type here) similar to
Names.Add Name:="Value_Count", RefersTo:=i
I am trying to accomplish this without returning a ton of extra info to cells, just to recall it, hence the desire to return straight to a Defined Name.
I'm using a Function rather than Sub to streamline my use, but if that's the problem, I can definitely change types.
I am creating a version of a Statistical Control Chart. My desired end result is to capture a data range (generally about 336 values) and apply a series of control rules to them (via VBA), then return any values that fall outside of the control parameters to Defined Names that can then be charted or otherwise manipulated.
I've seen (and have versions of) spreadsheets that accomplish this with digital acres of helper columns leading to a chart and summary statistics. I'm trying to accomplish it mostly in the background of VBA, to be called via Defined Names to Charts — I just can't get the values from VBA to the Charts.
The interest in using a Function rather than a Sub was to streamline access to it. I'd rather not design a user interface (or use one), if I can just keystroke the function into a cell and access the results directly. However, as pointed out by Jean-François Corbett, this is quickly turning into a circuitous route to my goal. However, I still think it is worthwhile, because in the long-term I have a lot of iterations of this analysis to perform, so some setup time is worth it for future time savings.
With minor changes to your function, you can use its return value to accomplish what you want:
Function process_control_F(raw_data As Variant) As Integer ' <~~ explicit return type
Dim i As Integer
Dim cell As Variant ' <~~~~ declare variable "cell"
i = 0
For Each cell In raw_data
i = i + 1
Next cell
process_control_F = i ' <~~~~ returns the value i
End Function
You can then use that function in formulas. For example:

Difference between Long and Object data type in VBA

In VBA, the Long and Object data type are both 4-bytes, which is the size of a memory address. Does this mean that, technically, the Object data type doesn't do anything that a Long couldn't do? If yes, then is it safe to say that the Object data type exists simply to make it easier for the programmer to distinguish between the purpose of the variable?
This question came up as I was considering Win32 API function declarations. They are often times declared as Long, and, unless I am mistaken, their return value is simply a memory address. Seems like defining these functions as Object would have been more appropriate, then.
Am I totally off? Thanks in advance.
Based on VBA/MSDN help:
Long (long integer) variables are stored as signed 32-bit (4-byte)
numbers ranging in value from -2,147,483,648 to 2,147,483,647.
and the other definition:
Object variables are stored as 32-bit (4-byte) addresses that refer to
objects. Using the Set statement, a variable declared as an Object can
have any object reference assigned to it.
From practical point of view they are both different and used in different situation. Which are essential: Long >> refers to numbers and Object >> refers to object.
Look into the following VBA code (for Excel) where I added comments which is allowed and which is not:
Sub test_variables()
Dim A As Object
Dim B As Long
'both below are not allowed, throwing exceptions
'A = 1000
'Set B = ActiveSheet
'both are appropriate
Set A = ActiveSheet
B = 1000
End Sub
Finally, in terms of API it's better to stay with original declaration and not manipulate with that to avoid any risk on unexpected behaviour of API functions.