VBA - Allocating multidimensional array directly using brackets - vba

I am trying to find a practical way of directly allocating values to a multidimensional array in VBA without iterating. I Googled a solution which kind of works, but it fails when I try to use it in combination with variables..
This works:
Sub SomeSub()
Dim vArray As Variant
Dim iCounter As Integer
vArray = [{"Zip", "22150";"City", "Springfield"; "State", "VA"}]
For iCounter = LBound(vArray, 1) To UBound(vArray, 1)
Debug.Print vArray(iCounter, 1), vArray(iCounter, 2)
Next iCounter
End Sub
This, however, does not, and raises a "Type mismatch" error instead. The difference is that I try (and wish) to use variables instead of constant values:
Sub SomeSub()
Dim vArray As Variant
Dim iZip As Integer
Dim sCity As String
Dim sState As String
iZip = 22150
sCity = "Springfield"
sState = "VA"
Dim iCounter As Integer
vArray = [{"Zip", iZip;"City", sCity; "State", sState}]
For iCounter = LBound(vArray, 1) To UBound(vArray, 1)
Debug.Print vArray(iCounter, 1), vArray(iCounter, 2)
Next iCounter
End Sub
I find little or no information on this method for allocating arrays, so I'm hoping that someone has some insight to offer.
Thanks!

VBA also has a built-in function called Array. You can use Array within the Array function to make it "multidimensional."
Sub SomeSub()
Dim vArray As Variant
Dim iZip As Integer
Dim sCity As String
Dim sState As String
iZip = 22150
sCity = "Springfield"
sState = "VA"
vArray = Array(Array("Zip", iZip), Array("City", sCity), Array("State", sState))
Dim iCounter As Integer
For iCounter = LBound(vArray, 1) To UBound(vArray, 1)
Debug.Print vArray(iCounter, 1), vArray(iCounter, 2)
Next iCounter
End Sub
Though the above code does answer the OP, I am editing this answer with more information. Technically, this is not "multidimensional." Rather, it is "jagged." It's a Variant of a single-dimensional array of single-dimensional arrays. In VBA, when you define a truly multidimensional array, you would use this syntax:
Sub someSub()
Dim vArray(5, 5) As Long
vArray(0, 1) = 5
vArray(0, 2) = 3
vArray(1, 3) = 4
Debug.Print vArray(1, 3) 'Prints 4
End Sub
Another Stackflow user suggested this technical correction, and I wanted to incorporate that knowledge into the answer.

The [...] syntax is a shortcut for Application.Evaluate() and as you have discovered it only works for runtime constant expressions, you can use a real call to Application.Evaluate() passing a dynamc string, but (*imo) thats quite hacky.
Alternatives:
Create a factory function that accepts a paramarray() of arguments, step through it using a modulus counter to figure out what dimension to allocate to.
As the first dimension appears to be a fixed string use a User Defined Type to allow udt_var.City = "XXX" / udt_var.Zip = 12345 ...
Use a Collection (or Dictionary) which unlike the above is iterable (the latter by key).

Related

Converting VBA Collection to Array

I am trying to create an array with all the worksheet names in my workbook that have the word 'Template' in it. I thought the easiest way to do this would be to create a collection first and then convert it to an array but I am having trouble. Right now the error I am getting is on the
collectionToArray (col)
line. I am receiving an
Argument Not Optional error
Pretty stuck, any help is super appreciated. Thanks!!
Public col As New Collection
Public Sub Test()
For Each ws In ThisWorkbook.Worksheets
If InStr(ws.Name, "Template") <> 0 Then
col.Add ws.Name
End If
Next ws
collectionToArray (col)
End Sub
Function collectionToArray(c As Collection) As Variant()
Dim a() As Variant: ReDim a(0 To c.Count - 1)
Dim i As Integer
For i = 1 To c.Count
a(i - 1) = c.Item(i)
Next
collectionToArray = a
End Function
collectionToArray (col)
Notice that whitespace between the function's name and its argument list? That's the VBE telling you this:
I'll take that argument, evaluate it as a value, then pass it ByVal to that procedure you're calling, even if the signature for that procedure says ByRef, explicitly or not.
This "extraneous parentheses" habit is inevitably going to make you bump into weird "Object Required" runtime errors at one point or another: lose it.
The Function is overdoing it IMO: a Variant can perfectly well wrap an array, so I'd change its signature to return a Variant instead of a Variant().
Integer being a 16-bit signed integer type (i.e. short in some other languages), it's probably a better idea to use a Long instead (32-bit signed integer, i.e. int in some other languages) - that way you'll avoid running into "Overflow" issues when you need to deal with more than 32,767 values (especially common if a worksheet is involved).
Public col As New Collection
This makes col an auto-instantiated object variable, and it has potentially surprising side-effects. Consider this code:
Dim c As New Collection
c.Add 42
Set c = Nothing
c.Add 42
Debug.Print c.Count
What do you expect this code to do? If you thought "error 91, because the object reference is Nothing", you've been bitten by auto-instantiation. Best avoid it, and keep declaration and assignments as separate instructions.
Other than that, CLR's answer has your solution: a Function should return a value, that the calling code should consume.
result = MyFunction(args)
You'll notice the VBE clearing any whitespace you might be tempted to add between MyFunction and (args) here: that's the VBE telling you this:
I'll take that argument, pass it to MyFunction, and assign the function's return value to result.
It's all there, you're just not using the Function as a function. You need to store the result in something, like 'NewArray'..?
Public col As New Collection
Public Sub Test()
For Each ws In ThisWorkbook.Worksheets
If InStr(ws.Name, "Template") <> 0 Then
col.Add ws.Name
End If
Next ws
' Tweaked as per Vityata's comment
If col.Count > 0 Then
newarray = collectionToArray(col)
Else
' Do something else
End If
End Sub
Function collectionToArray(c As Collection) As Variant()
Dim a() As Variant: ReDim a(0 To c.Count - 1)
Dim i As Integer
For i = 1 To c.Count
a(i - 1) = c.Item(i)
Next
collectionToArray = a
End Function
This is my collectionToArray function:
Public Function CollectionToArray(myCol As Collection) As Variant
Dim result As Variant
Dim cnt As Long
If myCol.Count = 0 Then
CollectionToArray = Array()
Exit Function
End If
ReDim result(myCol.Count - 1)
For cnt = 0 To myCol.Count - 1
result(cnt) = myCol(cnt + 1)
Next cnt
CollectionToArray = result
End Function
It is better than the one you are using, because it will not give an error, if the collection is empty.
To avoid the error on an empty collection in your case, you may consider adding a check like this:
If col.Count > 0 Then k = CollectionToArray(col)
Instead of using a standard collection, you can use a dictionary because of the .keys method that can transfer all info to an array. Less coding, simple :)
Dim ws As Worksheet
Dim arr
Dim Dic As Scripting.Dictionary
Set Dic = New Dictionary
For Each ws In ActiveWorkbook.Worksheets
If ws.Name Like "Template" Then
Dic.Add ws.Name, "Awesome"
End If
Next ws
arr = Dic.Keys

function to use an array

I am trying to write a function that uses the upper and lower bound of an array and returns the number of elements. It is confusing because of the lack of experience I have with functions but nonetheless I have come up with this code below:
Function numelement(LBound(array1), UBound(array2)) as Integer
numelement = UBound(array1) - LBound(array2)
End Function
Is this code correct, I know its simple but thats what the purpose of this code is to be.
If you realy want to have a function (you can do it easily without it), then try the code below:
Function numelement(array1 As Variant) As Long
numelement = (UBound(array1) - LBound(array1)) + 1 '<-- since array iz zero based
End Function
Sub Code to test the Function:
Sub TestFunc()
Dim Arr As Variant
Arr = Array("Shai", "Rado", "Tel-Aviv")
MsgBox numelement(Arr)
End Sub
Note: instead of the function, you can use:
Dim array1 As Variant
array1 = Array("Shai", "Rado", "Tel-Aviv")
MsgBox "number of elements in array are " & UBound(array1 ) + 1
You don't need a function for this. Ubound +1 works nicely for the total number of elements, if you initalize array like this Array(). It is a built-it function in VBA, it works great:
Option Explicit
Sub TestMe()
Dim myArr As Variant
myArr = Array("A", "B", "C")
Debug.Print LBound(myArr)
Debug.Print UBound(myArr)
End Sub
Unless you decide to initialize an array like this:
Dim arr(3 To 4)
arr(3) = 1
arr(4) = 2
Then you should use the following:
debug.print UBound(myArr)-LBound(myArr)+1

Excel VBA: Consolidate the iterating group in "For Each" block

I found the concatenation/appending of two arrays to be a too cumbersome process for my example. But how to iterate through two arrays of sheets using For Each in a single block (how to shorten the following code)?
arr1 = Array("Sheet2", "Sheet3")
arr2 = Array("Sheet5", "Sheet6")
For Each sh In Sheets(arr1)
sh.Visible = True
Next sh
For Each sh In Sheets(arr2)
sh.Visible = True
Next sh
You can always combine your small arrays into a super array. For example:
Sub Klai()
arr1 = Array("Sheet2", "Sheet3")
arr2 = Array("Sheet5", "Sheet6")
arr3 = Array(arr1, arr2)
For Each a In arr3
For Each b In a
MsgBox Sheets(b).Name
Sheets(b).Visible = True
Next b
Next a
End Sub
Just to throw in yet another solution:
Option Explicit
Sub tmpSO()
Dim lngItem As Long
Dim strArray() As String
Dim strOneLongList As String
Dim arr1 As Variant, arr2 As Variant
'Your starting point
arr1 = Array("Sheet2", "Sheet3")
arr2 = Array("Sheet5", "Sheet6")
'Bring all of them together into one long string containing all sheets
strOneLongList = Join(arr1, "/") & "/" & Join(arr2, "/")
MsgBox "This is what strOneLongList currently looks like:" & Chr(10) & Chr(10) & strOneLongList
'Convert the list into a string array with four elements
strArray = Split(Join(arr1, "/") & "/" & Join(arr2, "/"), "/")
For lngItem = 0 To UBound(strArray)
ThisWorkbook.Worksheets(strArray(lngItem)).Visible = True
Next lngItem
End Sub
Explanations:
The Join functions brings all elements of an array together into one long string.
The Split function is very similar to the array function you originally used. Yet, array requires that you list the items in separate strings delimited by commas. Split expects one long string in which one character is chosen to separate all elements.
Caution: Choose the character wisely you are using in a split list to separate all elements in that list and make sure that this character can never be found in any element of the list. I chose the / character because it cannot be used in a name for a sheet. Alternative characters could be \ or * or something very exotic like ChrW(12484).
It is certainly not as simplistic (and thereby possibly "visually appealing") as the other solution provided by #Garys-Student. Yet, this solution avoids the Arrays function and thereby we can bypass the variables of type variant. The same applies to the For Each... loop. It requires also variables of type variant or object. So, I replaced this one too with a For ... Next loop.
I am not sure if I'd use this construction or the other solution. Maybe this answer is better in terms of speed and overhead. Yet, the other solution is certainly faster coded and easier to read. Is that really worth the potential benefit?
Update:
In short, the following sub is a re-write of your original post and does exactly the same. Yet, it does not make use of variant variables (which reduces overhead and is therefore favorable).
Dim lngItem As Long
Dim strArray() As String
strArray = Split("Sheet2/Sheet3/Sheet5/Sheet6", "/")
For lngItem = LBound(strArray) To UBound(strArray)
ThisWorkbook.Worksheets(strArray(lngItem)).Visible = True
Next lngItem

Count all Comma In Selection or selected text

I want to count all Commas "," that occur only in selected text after that I will use Count as Integer to run the loop
My question is how do i Count , as following Image shows:
I Don't know how to use split and ubound. what is wrong with following code?
Sub CountComma()
Dim x As String, count As Integer, myRange As Range
Set myRange = ActiveDocument.Range(Selection.Range.Start, Selection.Range.End)
x = Split(myRange, ",")
count = UBound(x)
Debug.Print count
End Sub
A simple split will work.
x = Split("XXX,XXX,XXX,XXX,XX,XX", ",")
Count = UBound(x)
Debug.Print Count
B/c the array starts at zero you can take to Ubound number as is.
EDIT:
To use a range .
x = Split(Range("A1").Value, ",")
To break down the code.
Split("A string value","Delimiter to split the string by")
And if you want a single line of code than,
x = UBound(Split(myRange, ","))
your code is wrong in the initial declaration statement of x variable as of string type , since in the subsequent statement
with x = Split(myRange, ",")
you'd want x hold the return value of Split() function which is an array (see here), thus of Variant type
so you have to use
Dim x As Variant
But you can simplify your code as follows
Option Explicit
Sub CountComma()
Dim count As Integer
count = UBound(Split(Selection, ","))
Debug.Print count
End Sub
since:
you don't need any Range type variable to store Selection object into, being Selection the selected range already (see here)
you don't need the x Variant variable neither, feeding UBound()function (which expects an array as its first argument) directly with the Split() function which, as we saw above, returns just an array!
Finally I'd give out an alternative method of counting commas in a range
Sub CountComma()
Dim countAs Integer
count = Len(Selection) - Len(Replace(Selection, ",", ""))
Debug.Print count
End Sub
Thanks to KyloRen and Cindy Meister, Now I can use split and Ubound for Counting , in selection.text.
Following is working Code:
Sub Count_Words()
Dim WrdArray() As String, myRange As String
myRange = ActiveDocument.Range(Selection.Range.Start, Selection.Range.End)
WrdArray() = Split(myRange, ", ")
MsgBox ("Total , in the string : " & UBound(WrdArray()))
End Sub

VBA Sub to Remove Blanks From Row Improvements

I wrote a sub to remove the blank entries in a row without shifting the cells around but it seems unnecessarily clunky and I'd like to get some advice on how to improve it.
Public Sub removeBlankEntriesFromRow(inputRow As Range, pasteLocation As String)
'Removes blank entries from inputRow and pastes the result into a row starting at cell pasteLocation
Dim oldArray, newArray, tempArray
Dim j As Integer
Dim i As Integer
'dump range into temp array
tempArray = inputRow.Value
'redim the 1d array
ReDim oldArray(1 To UBound(tempArray, 2))
'convert from 2d to 1d
For i = 1 To UBound(oldArray, 1)
oldArray(i) = tempArray(1, i)
Next
'redim the newArray
ReDim newArray(LBound(oldArray) To UBound(oldArray))
'for each not blank in oldarray, fill into newArray
For i = LBound(oldArray) To UBound(oldArray)
If oldArray(i) <> "" Then
j = j + 1
newArray(j) = oldArray(i)
End If
Next
'Catch Error
If j <> 0 Then
'redim the newarray to the correct size.
ReDim Preserve newArray(LBound(oldArray) To j)
'clear the old row
inputRow.ClearContents
'paste the array into a row starting at pasteLocation
Range(pasteLocation).Resize(1, j - LBound(newArray) + 1) = (newArray)
End If
End Sub
Here is my take on the task you describe:
Option Explicit
Option Base 0
Public Sub removeBlankEntriesFromRow(inputRow As Range, pasteLocation As String)
'Removes blank entries from inputRow and pastes the result into a row starting at cell pasteLocation
Dim c As Range
Dim i As Long
Dim new_array As String(inputRow.Cells.Count - WorksheetFunction.CountBlank(inputRow))
For Each c In inputRow
If c.Value <> vbNullString Then
inputRow(i) = c.Value
i = i + 1
End If
Next
Range(pasteLocation).Resize(1, i - 1) = (new_array)
End Sub
You'll notice that it is quite different, and while it may be slightly slower than your solution, because it is using a for each-loop instead of looping through an array, if my reading of this answer is correct, it shouldn't matter all that much unless the input-range is very large.
It is significantly shorter, as you see, and I find it easier to read - that may just be familiarity with this syntax as opposed to yours though. Unfortunately I'm not on my work-computer atm. to test it out, but I think it should do what you want.
If your main objective is to improve the performance of the code, I think that looking into what settings you may turn off while the code is running will have more effect than exactly what kind of loop and variable assignment you use. I have found this blog to be a good introduction to some concepts to bear in mind while coding in VBA.
I hope you have found my take on your problem an interesting comparison to your own solution, which as others have mentioned should work just fine!
If I am to understand you want to delete blanks and pull the data left on any given row?
I would do it by converting the array to a string joined with pipe |, clean any double pipes out (loop this until there are no doubles left) then push it back to an array across the row:
Here is my code:
Sub TestRemoveBlanks()
Call RemoveBlanks(Range("A1"))
End Sub
Sub RemoveBlanks(Target As Range)
Dim MyString As String
MyString = Join(WorksheetFunction.Transpose(WorksheetFunction.Transpose(Range(Target.Row & ":" & Target.Row))), "|")
Do Until Len(MyString) = Len(Clean(MyString))
MyString = Clean(MyString)
Loop
Rows(Target.Row).ClearContents
Target.Resize(1, Len(MyString) - Len(Replace(MyString, "|", ""))).Formula = Split(MyString, "|")
End Sub
Function Clean(MyStr As String)
Clean = Replace(MyStr, "||", "|")
End Function
I put a sub to test in there for you.
If you have pipes in your data, substitute it with something else in my code.