Dynamically Create Dynamic Arrays in VBA - vba

My objective is to use an array of names to create dynamic variables in VBA, heres the code:
Sub mymacro()
Dim names()
names = Array("cat_code()", "dog_code()", "eagle_code()")
For Each c In names
Dim c As Integer
Next
end sub
And of course my real name array has hundreds of animals so it would be rather boring doing Dim for each and every one of them. The error I'm getting is Compile Error: Duplicate declaration in current scope
What is the best feasible solution to my objective?

The compile error you are getting is caused by a duplicate declaration in the current scope.
In other words: this means you are declaring more than one variable with the same name.
Adding an Option Explicit statement on top of you modules requires you to declare each variable you use. It's very helpful when you receive this error because you can quickly scan your code for duplicate declaration of the highlighted line Dim <variable_name>
This is a sample demonstrating why you are getting the error:
Option Explicit
Sub Main()
Dim c As Worksheet
For Each c In Sheets
Dim c As Long ' you are going to get an error in here because
' a variable named: c, is already declared within the sub
' you can't have two variables named: c.
For c = 1 To ws.Range("A" & Rows.Count).End(xlUp).Row
' some code
Next c
Next
End Sub
There is no easy work around your problem. We would have been able to provide a better solution to your problem if you better explain what you are trying to achieve.
There is a workaround to achieve what you want but I wouldn't recommend doing it this way if you are unsure of you are actually doing ;). The below code will create a new module in your current VBA project. While iterating over the array with the animal names it will be writing new lines to Module2 so after the execution your module two will be
In order for this code to work you have to add references to Microsoft Visual Basic for Applications Extensibility 5.3". You can do that by selectingTools>>References` in the VBE window.
Also, this requires you to Trust Access to VBA Project Object Model. Go to Excel Settings >> Trust Centre >> Macros >> tick Trust Access To VBA Project Object Model.
Run the sample code.
Option Explicit
' this VBA project requires
' 1 - references to Microsoft Visual Basic For Applications Extensibility 5.3
' add it via Tools > References
'
' 2 - trust access to VBA project object model
' In spreadsheet view go to Excel(application options) >> Trust Centre >> Macro Settings
' tick the Trust Access to VBA project object model
Sub mymacro()
Dim names
names = Array("cat_code", "dog_code", "eagle_code")
Dim c As Variant
AddAModule
For Each c In names
' dynamically create arrays
WriteToModule CStr(c)
Next
CloseModule
End Sub
Private Sub AddAModule()
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.vbComponent
Dim CodeMod As VBIDE.CodeModule
Set VBProj = ThisWorkbook.VBProject
Set VBComp = VBProj.VBComponents.Add(vbext_ct_StdModule)
Set CodeMod = VBComp.CodeModule
With CodeMod
.DeleteLines 1, .CountOfLines
.InsertLines 1, "Public Sub DynamicallyCreatedArrays()"
.InsertLines 2, " ' code for the sub"
End With
End Sub
Private Sub WriteToModule(arrayName As String)
With ActiveWorkbook.VBProject.VBComponents("Module2").CodeModule
.InsertLines .CountOfLines + 2, " Dim " & arrayName & " as Variant"
End With
End Sub
Private Sub CloseModule()
With ActiveWorkbook.VBProject.VBComponents("Module2").CodeModule
.InsertLines .CountOfLines + 2, "End Sub"
End With
End Sub

VBA can't really do what you're trying to do without getting into a horrible world of complications.
How about using a VBA Collection object instead? You'll need to create a simple class to hold the number, because VBA collections work with references, not values.
So I created a Class and set its name to "AnimalCounter", with this content:
Public Counter As Integer
Then your macro becomes something like this:
Sub mymacro()
Dim coll As New Collection
Dim c As Variant
Dim ac As AnimalCounter
For Each c In Array("cat", "dog", "eagle")
Set ac = New AnimalCounter
coll.Add ac, c
Next
Debug.Print coll("cat").Counter ' what's in "cat"?
coll("dog").Counter = coll("dog").Counter + 1 ' update "dog" by one
Debug.Print coll("dog").Counter ' "dog" should now be one more
End Sub
If you wanted arrays, put an array in to the class. Or another Collection, maybe?

Mike Woodhouse has the right idea of using a Collection with the keys of the animals. I add two notes:
First, I would recommend using a Dictionary instead. It is faster than a Collection, and allows explicit access to the Keys and Items collections. With a Collection, there is actually no way to fetch the keys, since the basic purpose is an ordered list of items rather than a order-agnostic hash as with a Dictionary.
For early-bound use of the Dictionary type, add a reference to Microsoft Scripting Runtime.
Second, do not use an array for the individual animals!. The reason is because arrays in VBA use by-value semantics ( see Collections in VBA – Overview, Values and References in VBA, Array Assignment Rules for more information). In short, every time you fetch an instance of an array from the containing Collection or Dictionary, you will be getting a new copy of the entire array. Thus any changes you make to the content of that array will not affect the actual array in the Dictionary or Collection. To get around this, use a Collection instead. This will use by-reference semantics and makes it much easier to append new items.
So here's what you'd want to do:
Sub ReadCodes()
Dim ws As Worksheet
Dim strAnimalName As String
Dim dctAnimalCodes As New Dictionary
Dim colAnimalCodes As Collection
Dim lngAnimalCode As Long
Set ws = Worksheets("Animal Code Data")
For iRow = 1 To ws.UsedRange.Rows.Count
strAnimalName = ws.Cells(iRow, 1)
lngAnimalCode = ws.Cells(iRow, 2)
' Easy to check if key exists
If Not dctAnimalCodes.Exists(strAnimalName) Then
Set dctAnimalCodes(strAnimalName) = New Collection
End If
' Getting the collection for this animal
Set colAnimalCodes = dctAnimalCodes(strAnimalName)
' Easy appending of new code
colAnimalCodes.Add lngAnimalCode
Next
End Sub

Related

VBA - remove duplicates (Case NOT sensitive) separated by a comma inside cells in excel? [duplicate]

I have set Dictionary as an object an added several items to that dictionary, however it seems to be case-sensitive. Is there anyway I can set the dictionary to recognize different versions?
My Code:
Sub Test()
Dim sheet1 As String
Dim Dict As Object
Dim c As Range
Sheet1= "TEST"
Set Dict = CreateObject("Scripting.Dictionary")
Dict.Add "MIKE", 0
Dict.Add "PHIL", 0
Dict.Add "Joe", 0
For Each c In ActiveWorkbook.Worksheets(Sheet1).UsedRange
If Dict.Exists(ActiveWorkbook.Worksheets(Sheet1).Cells(c.Row, c.Column).Value) Then
Dict(ActiveWorkbook.Worksheets(Sheet1).Cells(c.Row, c.Column).Value) = Dict(ActiveWorkbook.Worksheets(Sheet1).Cells(c.Row, c.Column).Value) + 1
End If
Next
Sheet1.Cells(25, 3) = Dict("MIKE")
Sheet1.Cells(25, 3) = Dict("PHIL")
Sheet1.Cells(25, 3) = Dict("Joe")
Set Dict = Nothing
End Sub
So I want to recognize "mike" for MIKE and "Phil" for PHIL etc.
Thanks in advance!
Adding onto #Ralph
dict.CompareMode = TextCompare
is what I changed the file to.
Some clarifications regarding the comments:
TextCompare is only available with Early Binding, it is a member of Scripting.
vbTextCompare is always available in VBA.
Both are = 1.
? Scripting.CompareMethod.TextCompare
1
? VBA.VbCompareMethod.vbTextCompare
1
Note: you can only set dict.CompareMode if dict is empty, i.e. you haven't added any members yet. Otherwise you will get an "Illegal procedure call" error.
I always like to set things straight for all of my coding. So, all modules and code lying on my sheets or in forms start with the following three lines before writing any additional code.
Option Base 0
Option Explicit
Option Compare Text
If I want to have something handled differently in a particular Sub for some reason, then I do so in this particular sub only and do as proposed in the comment above (example):
dict.CompareMode = BinaryCompare 'if I need a case-sensitive compare in this sub
Since VBE knows that dict is a Dictionary it can provide propositions for auto-complete. This is only possible with early-binding. With late binding VBE will not provide any auto-complete propositions.

Can I create a Jump table in VBA for Excel?

I wrote a simple translator / parser to process an EDI (830) document using multiple Select Case statements to determine the code to be executed. I’m opening a file in binary mode and splitting the document into individual lines, then each line is split into the various elements where the first element of every line has a unique segment identifier.
My code works perfectly as written. However, Select Case requires checking every Case until a match is found or the Case Else is executed. I’ve sequenced the Case statements in such a manner that the segments that appear most frequently (as in the case of loops), are placed first to minimize the number of "checks before code is actually executed.
Rather than using multiple Select Cases, I would prefer to determine an index for the segment identifier and simply call the appropriate routine using that index. I’ve used jump tables in C and Assembler and anticipated similar functionality may be possible in VBA.
You can do jump tables in VBA by using the Application.Run method to call the appropriate routine by name. The following code demonstrates how it works:
Public Sub JumpTableDemo()
Dim avarIdentifiers() As Variant
avarIdentifiers = Array("Segment1", "Segment2")
Dim varIdentifier As Variant
For Each varIdentifier In avarIdentifiers
Run "Do_" & varIdentifier
Next varIdentifier
End Sub
Public Sub Do_Segment1()
Debug.Print "Segment1"
End Sub
Public Sub Do_Segment2()
Debug.Print "Segment2"
End Sub
You can do this in Excel VBA, following the example below:
The example assumes you have split your EDI document into two columns, one with the 'processing instruction' and one with the data that instruction will process.
The jump table is to the right i.e. a distinct list of the 'processing instructions' plus a name of a Sub-routine to run for each instruction.
The code is:
Option Explicit
Sub JumpTable()
Dim wsf As WorksheetFunction
Dim ws As Worksheet
Dim rngData As Range '<-- data from your file
Dim rngCell As Range '<-- current "instruction"
Dim rngJump As Range '<-- table of values and sub to run for value
Dim strJumpSub As String
Dim strJumpData As String
Set wsf = Application.WorksheetFunction '<-- just a coding shortcut
Set ws = ThisWorkbook.Worksheets("Sheet1") '<-- change to your worksheet
Set rngData = ws.Range("A2:A17") '<-- change to your range
Set rngJump = ws.Range("E2:F4") '<-- change to your circumstances
For Each rngCell In rngData
strJumpSub = wsf.VLookup(rngCell.Value, rngJump, 2, False) '<-- lookup the sub
strJumpData = rngCell.Offset(0, 1).Value '<-- get the data
Application.Run strJumpSub, strJumpData '<-- call the sub with the data
Next rngCell
End Sub
Sub do_foo(strData As String)
Debug.Print strData
End Sub
Sub do_bar(strData As String)
Debug.Print strData
End Sub
Sub do_baz(strData As String)
Debug.Print strData
End Sub
Make sure that you have written a Sub for each entry in the jump table.

Can I get the text of the comments in the VBA code

Lets say I have the following:
Public Sub Information()
'TEST
End Sub
Is there a way to get "TEST" as a result?
Somehow through VBA?
E.g. - In PHP there is a good way to take the comments. Any ideas here?
Edit:
There should be a way, because tools like MZ-Tools are able to provide the comments when they generate the documentation.
You need to parse the code yourself, using the VBA Extensibility library (aka "VBIDE API"). Add a reference to the Microsoft Visual Basic for Applications Extentibility 5.3 type library, and then you can access types such as CodePane and VBComponent:
Sub FindComments()
Dim component As VBComponent
For Each component In Application.VBE.ActiveVBProject.VBComponents
Dim contents As String
contents = component.CodeModule.Lines(1, component.CodeModule.CountOfLines)
'"contents" now contains a string with the entire module's code.
Debug.Print ParseComments(contents) 'todo
Next
End Sub
Once you have a module's contents, you need to implement logic to find comments... and that can be tricky - here's some sample code to play with:
Sub Test()
Dim foo 'this is comment 1
'this _
is _
comment 2
Debug.Print "This 'is not a comment'!"
'..and here's comment 3
REM oh and guess what, a REM instruction is also a comment!
Debug.Print foo : REM can show up at the end of a line, given an instruction separator
End Sub
So you need to iterate the lines, track whether the comment is continuing on the next line / continued from the previous line, skip string literals, etc.
Have fun!
After some tests, I got to this solution:
simply pass the name of the code-module to the function and it will print all comment lines. Inline comments won't work(you have to change the condition)
Function findComments(moduleName As String)
Dim varLines() As String
Dim tmp As Variant
With ThisWorkbook.VBProject.VBComponents(moduleName).CodeModule
'split the lines of code into string array
varLines = Split(.lines(1, .CountOfLines), vbCrLf)
End With
'loop through lines in code
For Each tmp In varLines
'if line starts with '
If Trim(tmp) Like "'*" Then
'print comment line
Debug.Print Trim(tmp)
End If
Next tmp
End Function
You can use Microsoft Visual Basic for Applications Extensibility to examine code at runtime:
'Requires reference to Microsoft Visual Basic for Applications Extensibility
'and trusted access to VBA project object model.
Public Sub Information()
'TEST
End Sub
Public Sub Example()
Dim module As CodeModule
Set module = Application.VBE.ActiveVBProject.VBComponents(Me.CodeName).CodeModule
Dim code As String
code = module.lines(module.ProcStartLine("Information", vbext_pk_Proc), _
module.ProcCountLines("Information", vbext_pk_Proc))
Dim lines() As String
lines = Split(code, vbCrLf)
Dim line As Variant
For Each line In lines
If Left$(Trim$(line), 1) = "'" Then
Debug.Print "Found comment: " & line
End If
Next
End Sub
Note that the above example assumes that it's running in a Worksheet or Workbook code module (hence Me when locating the CodeModule). The best method for locating the correct module will depend on where you want to locate the procedure.
You could try with reading line by line of code in your module. Here is just idea returning first comment for further improvements:
Sub callIt()
Debug.Print GetComment("Module1")
End Sub
Function GetComment(moduleName As String)
Dim i As Integer
With ThisWorkbook.VBProject.VBComponents(moduleName).CodeModule
For i = 1 To .CountOfLines
If Left(Trim(.Lines(i, 1)), 1) = "'" Then
'here we have comments
'return the first one
GetComment = .Lines(i, 1)
Exit Function
End If
Next i
End With
End Function
Important! in Reference window add one to 'Microsoft Visual Basic for Applications Extensibility'.

VBA Excel - ways to store lists in VBA?

I didn't know where else to turn, and I tried finding a question like mine but with no luck. I have a raw ranged table and I want to copy the info over into a new sheet, then convert that copied info into a ListObject table. I've worked out 99% of it, but then I wanted to change the raw headers of the copied table into my own headers (because most of the raw headers are very lengthy).
I built a loop to look at the [#Headers] cells, find values that matched a certain raw value, then replace it with my own value. E.g.
For Each cl In Range("Table1[#Headers]")
If cl.Value = "Employee" Then
cl.Value = "Name"
ElseIf cl = "Employer Name" Then
cl.Value = "Company"
'...
End If
Next cl
Having a block of code that does this for 30+ instances is cumbersome, and if the raw information I receive somehow changes it's header values, I then have to hunt for this bit of code again and make the changes. I'm hoping there's a way to store a 2-columned list of before-and-after header names that any Sub can just reference, like a global Array (except global arrays are impossible). I looked into classes but again there are issues I'm having with globalizing the info.
I'm thinking about making a hidden worksheet with a 2-columned list but I'm really hoping that's not necessary, don't want any more sheets than I have to have. Is there a way to store lists for global use in Excel VBA?
Example image
SOLUTION:
Using #Mat's Mug advice, I'll show how I figured out how I added my Dictionary.
I made a public variant called DHeader and created a Sub to Call from:
Public DHeader As Dictionary
Sub Load_Headers()
If Not DHeader Is Nothing Then Exit Sub
Set DHeader = New Dictionary
With DHeader
.add "Employee", "Name"
.add "Employer Name", "Company"
'...
End With
End Sub
Then within my action Sub I added this:
Call Load_Headers
For Each i_1 In Range("Table1[#Headers]")
If DHeader.Exists(CStr(i_1.Value)) = True Then
i_1.Value = DHeader.Item(CStr(i_1.Value))
End If
Next i_1
Now my values and actions are separated into different parts of my code. I think I have to add a way to clear the dictionary in my action sub still, but it works!
No matter what you do, you're going to need to have the mappping code somewhere.
If a huge If-Then-Else block isn't very appealing, you can consider using a Dictionary object, from the Scripting library - using the "before" column name as your dictionary key, and the "after" column name as your dictionary value, the mapping code could look something like this:
Dim ColumnMap As New Scripting.Dictionary
With ColumnMap
.Add "Employee", "Name"
.Add "Employer Name", "Company"
'...
End With
Then when you iterate the cells in the header row, you can verify that the name/key exists in your dictionary, and then proceed with the rename by fetching the mapped value. Just don't assume the column name exists in the dictionary, or you'll eventually run into "Key does not exist" runtime errors.
An alternative to dictionaries (although that might be be my preferred method, I would initialize them in a separate procedure) would be to split strings:
Sub DoStuff()
Const RawList As String = "Employee,Employer Name"
Const UpdateList as String = "Name,Employer"
Dim rawHeaders as Variant
Dim headers as Variant
rawHeaders = Split(RawList, ",")
headers = Split(UpdateList, ",")
For Each cl In Range("Table1[#Headers]")
If Not IsError(Application.Match(cl.Value, rawHeaders, False)) Then
cl.Value = headers(Application.Match(cl.Value, rawHeaders, False))
End If
Next
End Sub
You can scope the arrays at the module level instead so they will be available for other procedure calls, etc.
Why not use the simple VBA Collection? No extra reference needed, no late binding needed, it is build directly into VBA.
Note: if the item is not found in the map, then the original raw header value is not replaced but it is simply skipped.
Option Explicit
Public Sub Main()
Dim header As Range
Set header = Worksheets("RawData").ListObjects("Table1").HeaderRowRange
ReplaceInheaderRow headerRow:=header
' header contains transformed values now
End Sub
Private Function ReplaceInheaderRow(ByVal headerRow As Range) As Range
Dim map As Collection
Set map = New Collection
map.Add "Name", "Employee"
map.Add "Company", "Employer Name"
map.Add "ID", "ID Numbers"
map.Add "Income", "Wages"
map.Add "etc.", "Some next column name"
On Error Resume Next
Dim rowHeaderCell As Range
For Each rowHeaderCell In headerRow
rowHeaderCell.Value = map(rowHeaderCell.Value)
Next rowHeaderCell
On Error GoTo 0
End Function

Excel array and collection passing between modules messing up values

This is really stumping me. I put a question up yesterday regarding collections being passed between modules (see here), but it doesn't seem like I am getting anymore explanations on that one, so i am attempting to restate the problem more clearly in a generic way.
I have a module (module1) and a userform (userform1). I create a collection (or array) in userform1 and add worksheet objects to this array. I then pass control to module1, which calls a sub in userform1 called addNewFile, which is supposed to add the newly created workbook to the collection. However, each time module1 calls addNewFile i get one of two scenarios: 1) the collection has been erased and all worksheets that had been added are now gone (for a collection), 2) i get an error saying that i have a type mismatch (for an array). I don't know why this is happening, so here is the code below to illustrate better. Any help would be appreciated, even if it is just to tell me that it is not possible to store worksheet objects in arrays.
UserForm1
Dim workBooksCollection as New Collection 'can also define as an array
Private Sub CommandButton1_click()
Dim mainWorkBook as workbook
Set mainWorkBook = ActiveWorkbook
Dim testwb As Workbook
workBooksCollection.Add Item:=mainWorkBook, key:="main" 'Adds successfully
workBooksCollection.Add Item:=testwb, key:="test" 'Adds successfully
MsgBox "the size of the array is: " & usedWorkBooks.Count 'Prints off as size 2
Module1.initialize
'After running initialize, prints off as size 0, meaning collection has been erased
MsgBox "the size of the array is: " & usedWorkBooks.Count 'Prints off as size 0
End Sub
Public Sub addNewFile(filepath As String, sheetKey As String)
Dim newWorkBook As Workbook
Set newWorkBook = Workbooks.Open(filepath)
MsgBox "The name of the workbook is: " & newWorkBook.name 'Prints off name of workbook successfully
workBooksCollection.Add Item:=newWorkBook, key:=sheetKey
MsgBox "the size of the array is: " & workBooksCollection.Count 'Prints off as size 1
End Sub
Module1
Public Sub intialize()
Dim filepath as string
'The filepath is set to any path of a workbook
'This will print out that the array size is 1
UserForm1.addNewFile filePath, "secondBook"
End Sub
Sorry if i seem to be beating a dead horse here, but I really don't have any idea what is going on here. I am used to the idea of collections and lists being global and not changing when being referenced by another module. Any help on what is going on here would be great.
I would like to comment, but I can't since I had to redo my account because I got locked out of my original.
If my answer isn't helpful, I will delete in a bit, but what if you replace -
Dim workBooksCollection as collection 'can also define as an array
from the UserForm module, into Module 1 as:
Public workBooksCollection as collection 'can also define as an array
Does it help?