VBA: Variable alias - vba

Re-framed the question: I used a range (which contains the headers) to define an array, which I initialized and extracted the data from a table. Things went well (with redundant and repeated code), but I could directly write the array to an Excel row.
But the code required validation and initialization, hence I migrated to a class which encapsulates both requirements. The dynamic implementation required re-writing the class with InsertLines.
The problem is, now I have the data in a class, but I'm at a loss how to write it to a single row.
Previous algorithm:
Dim MyRange as Range
Dim sTR() as string
'set myrange to a specific range of cells'
'Redim the sTR array to match the number of unique variables in the range
'Extract values from multiples rows in a table to the sTR array'
'Write the array to the Excel file with:'
MyRange.Offset(x, y).value = sTR()
Current way:
Dim MyRange as Range
'set myrange to a specific range of cells (Nine columns and variable rows)'
'InsertCode into a class module with all unique values in the range as public variables
Dim sTR as ClassX
Set sTR as New ClassX
'Extract values from multiples rows in a table to the sTR Class'
Now the problem is, I have no idea about the order of the variables in the class (this was not an issue with the previous method with array, but that way certainly involves too much code redundancy).
Help please! Wanted to know if a variable can be addressed by different names (alias), which would solve my problem. Or is there a way to address all public variables in a class (say with a loop)?

Related

Excel VBA - Storing table column into a range variable

I am currently experimenting with excel VBAs ListObjects which is the object type of an excel table. I would like to store a table column range into a variable.
Here is what I can do:
'store a group of cells into a range variable
dim rng as Range
set rng = activesheet.Range("A1:A10")
'select a table column
dim table as ListObject
set table = activesheet.listobjects("Table1")
table.ListColumns(1).Range.Select
While both of the above work, I don't understand why the following does't work: (EDIT: it works)
dim rng_column as Range
set rng_column = table.ListColumns(1).Range
I experimented with other variable types, such as variant or ListColumn, however, nothing stored the cells into the variable. In the example above, there is no error, but the variable rng_column remains <empty>
I know there are other workarounds, but I really want to understand what's the issue here.
UPDATE
After some helpful comments I could narrow the problem down. Everything worked normally. However, I made two mistakes.
First, I had a spelling error in the variable names. (I should have included option explicit to notice this earlier).
Second, I passed the range variable on to another range variable, where I forgot to include the set keyword when doing so..
Hopefully someone can learn from my mistakes..
you have a misnomer in the title. technically you cannot store any data into a range variable.
a range variable is a reference (a pointer) to a cell, or a group of cells. the data gets stored in the cells that the range variable points to.
if you need to store data from a range, then assign the range.value to an array
if you want to store data from a single cell then assign the value to a "regular" (non object) variable (string, integer, long ... )

In VBA the Rows property has a weird behavior

I am trying to figure out how to work on a specific row among a big range. However it appears that a range created with the rows property does not behave the same as a simple range. Check the following code, the first time the variable SpecificRow is defined, it is not possible to select a specific cell. However with a weird workaround that redefines the range, it works fine. Do you have an idea why and how could I define the range with a more elegant way?
'The following shows the weird behavior of Rows property
Dim SourceRng As Range
Dim SpecificRow As Range
Dim i As Long
i = 3
Set SourceRng = Range("A1:D20")
Set SpecificRow = SourceRng.Rows(i)
'This will show the address of the selected row ("A3:D3")
MsgBox SpecificRow.Address
'Unexplicable behavior of the range when trying to select a specific cell
'where it will instead consider the whole row (within the limits of SourceRng)
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
'This would send an error
'MsgBox SpecificRow(1, 1).Address
'Workaround
Set SpecificRow = Intersect(SpecificRow, SpecificRow)
'The following will select the same address than before
MsgBox SpecificRow.Address
'However, now it has a correct behavior when selecting a specific cell
MsgBox SpecificRow(1).Address
MsgBox SpecificRow(2).Address
You should expect weird behavior if you're passing indexed properties the incorrect parameters. As demonstrated by your code, the Range returned by SourceRng.Rows(i) is actually correct. It just isn't doing what you think it's doing. The Rows property of a Range just returns a pointer to the exact same Range object that it was called on. You can see that in its typelib definition:
HRESULT _stdcall Rows([out, retval] Range** RHS);
Note that it doesn't take any parameters. The returned Range object is what you're providing the indexing for, and you're indexing it based on it's default property of Item (technically it's _Default, but the 2 are interchangeable). The first parameter (which is the only one you're passing with Rows(i), is RowIndex. So Rows(i) is exactly the same thing as Rows.Item(RowIndex:=i). You can actually see this in the IntelliSense tooltip that pops up when you provide a Row index:
Excel handles the indexing differently on this call though, because providing any value parameter for the second parameter is a Run-time error '1004'. Note that a similar property call is going on when you call SpecificRow(1).Address. Again, the default property of Range is Range.Item(), so you're specifying a row again - not a column. SpecificRow(1).Address is exactly the same thing as SpecificRow.Item(RowIndex:=1).Address.
The oddity in Excel appears to be that the Range returned by Range.Rows "forgets" the fact that it was called within the context of a Rows call and doesn't suppress the column indexer anymore. Remember from the typelib definition above that the object returned is just a pointer back to the original Range object. That means SpecificRow(2) "leaks" out of the narrowed context.
All things considered, I'd say the Excel Rows implementation is somewhat of a hack. Application.Intersect(SpecificRow, SpecificRow) is apparently giving you back a new "hard" Range object, but the last 2 lines of code are not what you should consider "correct" behavior. Again, when you provide only the first parameter to Range.Items, it is declared as the RowIndex:
What appears to happen is that Excel determines that there is only one row in the Range at this point and just assumes that the single parameter passed is a ColumnIndex.
As pointed out by #CallumDA, you can avoid all of this squirrelly behavior by not relying on default properties at all and explicitly providing all of the indexes that you need, i.e.:
Debug.Print SpecificRow.Item(1, 1).Address
'...or...
Debug.Print SpecificRow.Cells(1, 1).Address
This is how I would work with rows and specific cells within those rows. The only real difference is the use of .Cells():
Sub WorkingWithRows()
Dim rng As Range, rngRow As Range
Set rng = Sheet1.Range("A1:C3")
For Each rngRow In rng.Rows
Debug.Print rngRow.Cells(1, 1).Address
Debug.Print rngRow.Cells(1, 2).Address
Debug.Print rngRow.Cells(1, 3).Address
Next rngRow
End Sub
which returns:
$A$1
$B$1
$C$1
$A$2
$B$2
$C$2
$A$3
$B$3
$C$3
As you would expect
I cannot find any proper documentation on this, but this observed behaviour actually appears to be very logical.
The Range class in Excel has two important properties:
A single instance of Range is enough to represent any possible range on a sheet
It is iterable (can be used in a For Each loop)
I believe that in order to achieve logically looking iterability and yet avoid creating unnecessary entities (i.e. separate classes like CellsCollection, RowsCollection and ColumnsCollection), the Excel developers came up with a design where each instance of Range holds a private property that tells it in which units it is going to count itself (so that one range could be "a collection of rows" and another range could be "a collection of cells").
This property is set to (say) "rows" when you create a range via the Rows property, to (say) "columns" when you create a range via the Columns property, and to (say) "cells" when you create a range in any other way.
This allows you to do this and not become unnecessarily surprised:
For Each r In SomeRange.Rows
' will iterate through rows
Next
For Each c In SomeRange.Columns
' will iterate through columns
Next
Both Rows and Columns here return the same type, Range, that refers to the exactly same sheet area, and yet the For Each loop iterates via rows in the first case and via columns in the second, as if Rows and Columns returned two different types (RowsCollection and ColumnsCollection).
It makes sense that it was designed this way, because the important property of a For Each loop is that it cannot provide multiple parameters to a Range object in order to fetch the next item (cell, row, or column). In fact, For Each cannot provide any parameters at all, it can only ask "Next one please."
To support that, the Range class had to be able to give the next "something" without parameters, even though a range is two-dimensional and needs two coordinates to fetch the "something." Which is why each instance of Range has to remember in what units it will be counting itself.
A side effect of that design is that it is perfectly fine to look up "somethings" in a Range providing only one coordinate. This is exactly what the For Each mechanism would do, we are just directly jumping to the ith item.
When iterating over (or indexing into) a range returned by Rows, we're going to get the ith row, from top to bottom; for a range returned by Columns we're getting the ith column, from left to right; and for a range returned by Cells or by any other method we're going to get the ith cell, counting from top left corner to the right and then to the bottom.
Another side effect of this design is that can "step out" of a range in a meaningful way. That is, if you have a range of three cells, and you ask for the 4th cell, you still get it, and it will be the cell dictated by the shape of the range and the units it's counting itself in:
Dim r As Range
Set r = Range("A1:C3") ' Contains 9 cells
Debug.Print r.Cells(12).Address ' $C$4 - goes outside of the range but maintains its shape
So your workaround of Set SpecificRow = Intersect(SpecificRow, SpecificRow) resets the internal counting mode of that specific Range instance from (say) "rows" to (say) "cells".
You could have achieved the same with
Set SpecificRow = SpecificRow.Cells
MsgBox SpecificRow(1).Address
But it's better to keep the Cells close to the point of usage rather than the point of range creation:
MsgBox SpecificRow.Cells(1).Address

How do I use RefersToRange?

Can anyone tell me how to use RefersToRange in vba? and what and when is the need of it.
Please provide with simple example first.
Thanks in advance.
In Excel, there is the concept of a named range, that is a range of cells which has a name attached to it. This is represented by the Name object.
The RefersToRange method:
Returns the Range object referred to by a Name object.
For example, let's say I want to read the values only in the print area of the current worksheet. I need the Name object in order to access the print area, but I can't do anything useful with it; if I want to do something useful I have to access the range of cells referred to by that name:
Dim n As Name
Set n = ActiveWorkbook.Names("Print_Area")
Dim rng As Range
Set rng = n.RefersToRange
Dim values() As Variant
values = rng 'I can't read values with a Name object, but I can with a Range object
One thing I have discovered is that sometimes referring to a named range in this fashion:
ThisWorkbook.Range("DownloadPictures").Value = "yes"
results in:
Run-time error '1004':
Method 'Range' of object '_Worksheet' failed
But referring to the named range in this fashion:
ThisWorkbook.Names("DownloadPictures").RefersToRange = "yes"
works fine. My particular circumstance is that the named range ("DownloadPictures") is scoped to the entire workbook, which has several worksheets. I am referring to the name in one of the worksheet's Worksheet_Calculate subroutines. I don't understand why the first method causes an error and the second does not but apparently this is one reason to use the RefersToRange property.
RefersToRange is, one of many ways, to 'point' a range. For example,
ThisWorkbook.Names("DownloadPictures").RefersToRange = "yes"
the above code, points to the range, which can be a cell, named "DownloadPictures" and sets it value to "yes". As a matter of fact, I'd rather use,
ThisWorkbook.range("DownloadPictures").Value = "yes"
The 'Name' in MS Excel is similar to the Variant variable type, but unlike the variables you 'Dim' in the VB code, the 'Name' only be used (or you can consider it is only be used) in the Workbook. The 'Name' and variables are not interfered with each other.
If you open the 'Name Manager', the concept becomes easier to understand. The Name can be created to refer to a cell, a range of cell (a range), some value (so called constant), or a formula.
Each Name object represents a defined name for a range of cells. Names can be either built-in names—such as Database, Print_Area, and Auto_Open—or custom names. ---MSDN
As a Name can refer to range, constant or a formula .RefersToRange specifically refer to a range. And return false if not so.

How to paste part of two dimensional array into worksheet range without using loop?

How to paste part of two dimensional array into worksheet range without using loop ?
Pasting whole array is straightforward :
Sub TestPasteArrayIntoRange()
Dim MyArray As Variant
MyArray = [{1,2;3,4;5,101}]
selection.Resize(UBound(MyArray, 1), UBound(MyArray, 2)) = MyArray
End Sub
But what about pasting only second row (first row we could past in by simple reducing selection size to one row) ? I've seen answer for case of one dimensional array which use Index Excel function, but it doesn't apply here.
My first idea was to copy array into range and then copy row from it, but it seems, that I cannot create range object which is not part of some worksheet object (expecially cannot create virtual range without virtual worksheet).
I think you will just have to create another function like e.g. GetRow() or GetColumn() to return a reduced vector (or array) with the select values you need. There are some functions here that you can use for your purpose: http://www.cpearson.com/excel/vbaarrays.htm

Pass a range into a custom function from within a cell

Hi I'm using VBA in Excel and need to pass in the values from two ranges into a custom function from within a cell's formula. The function looks like this:
Public Function multByElement(range1 As String, range2 As String) As Variant
Dim arr1() As Variant, arr2() As Variant
arr1 = Range(range1).value
arr2 = Range(range2).value
If UBound(arr1) = UBound(arr2) Then
Dim arrayA() As Variant
ReDim arrayA(LBound(arr1) To UBound(arr1))
For i = LBound(arr1) To UBound(arr1)
arrayA(i) = arr1(i) * arr2(i)
Next i
multByElement = arrayA
End If
End Function
As you can see, I'm trying to pass the string representation of the ranges. In the debugger I can see that they are properly passed in and the first visible problem occurs when it tries to read arr1(i) and shows as "subscript out of range". I have also tried passing in the range itself (ie range1 as Range...) but with no success.
My best suspicion was that it has to do with the Active Sheet since it was called from a different sheet from the one with the formula (the sheet name is part of the string) but that was dispelled since I tried it both from within the same sheet and by specifying the sheet in the code.
BTW, the formula in the cell looks like this:
=AVERAGE(multByElement("A1:A3","B1:B3"))
or
=AVERAGE(multByElement("My Sheet1!A1:A3","My Sheet1!B1:B3"))
for when I call it from a different sheet.
First, see the comment Remou left, since that's really what you should be doing here. You shouldn't need VBA at all to get an element-wise multiplication of two arrays.
Second, if you want to work with Ranges, you can do that by declaring your function arguments to be of type Range. So you could have
Public Function multByElement(range1 As Range, range2 As Range)
and not need to resolve strings to range references yourself. Using strings prevents Excel from updating references as things get moved around in your worksheet.
Finally, the reason why your function fails the way it does is because the array you get from taking the 'Value' of a multi-cell Range is two-dimensional, and you'd need to acces its elements with two indices. Since it looks like you're intending to (element-wise) multiply two vectors, you would do either
arrayA(i) = arr1(i,1) * arr2(i,1)
or
arrayA(i) = arr1(1,i) * arr2(1,i)
depending on what orientation you expected from your input. (Note that if you do this with VBA, orientation of what is conceptually a 1-D array matters, but if you follow Remou's advice above, Excel will do the right thing regardless of whether you pass in rows or columns, or range references or array literals.)
As an epilogue, it also looks like you're not using 'Option Explicit'. Google around for some rants on why you probably always want to do this.