ReDim Preserve raise error Subscript out of range - arrays

Getting a
Run-time : Error No. 9
Subscript out of range
at the line:
ReDim Preserve aryFileNames(UBound(aryFileNames) - 1)
In the code below which is meant to convert text files to Excel files in a particular folder.
Sub ConvertTextFiles()
Dim fso As Object '<---FileSystemObject
Dim fol As Object '<---Folder
Dim fil As Object '<---File
Dim strPath As String
Dim aryFileNames As Variant
Dim i As Long
Dim wbText As Workbook
Application.ScreenUpdating = False
'// I am assuming the textfiles are in the same folder as the workbook with //
'// the code are. //
strPath = ThisWorkbook.Path & Application.PathSeparator
'// Set a reference to the folder using FSO, so we can use the Files collection.//
Set fso = CreateObject("Scripting.FileSystemObject")
Set fol = fso.GetFolder(strPath)
'// Using FSO's Files collection, we'll run through and build an array of //
'// textfile names that exist in the folder. //
ReDim aryFileNames(0)
For Each fil In fol.Files
If fil.Type = "Text Document" Then
'// If correct Type (a text file), we'll assign the name of the found //
'// textfile to the last element in the array - then add an empty //
'// element to the array for next loop around... //
aryFileNames(UBound(aryFileNames)) = fil.Name
ReDim Preserve aryFileNames(UBound(aryFileNames) + 1)
End If
Next
'// ... now since we were adding an empty element to the array, that means we'll//
'// have an emmpty ending element after the above loop - get rid of it here. //
ReDim Preserve aryFileNames(UBound(aryFileNames) - 1)
'// Basically, For Each element in the array... //
For i = LBound(aryFileNames) To UBound(aryFileNames)
'// ...open the textfile, set a reference to it, SaveAs and Close. //
Workbooks.OpenText Filename:=strPath & aryFileNames(i), _
Origin:=xlWindows, _
StartRow:=1, _
DataType:=xlFixedWidth, _
FieldInfo:=Array(Array(0, 1), _
Array(7, 1), _
Array(55, 1), _
Array(68, 1))
Set wbText = ActiveWorkbook
wbText.Worksheets(1).Columns("A:D").EntireColumn.AutoFit
wbText.SaveAs Filename:=strPath & Left(aryFileNames(i), Len(aryFileNames(i)) - 4), _
FileFormat:=xlWorkbookNormal
wbText.Close
Next
Application.ScreenUpdating = True
End Sub

You'll get a subscript out of range any time your For Each loop doesn't execute or you don't find any text documents. The starting bound of the array is 0 and in that case it never gets incremented, so this line of code...
ReDim Preserve aryFileNames(UBound(aryFileNames) - 1)
...is trying to size the array to a bound of -1. Since you're working with strings, you can take advantage of a quirk in the Split function to simplify your array sizing. If you Split a vbNullString, VBA will return a String array with a UBound of -1. Instead of initializing it with ...
ReDim aryFileNames(0)
... and then trimming it afterward, you can just do this:
aryFileNames = Split(vbNullString)
'UBound of the array is now -1.
For Each fil In fol.Files
If fil.Type = "Text Document" Then
ReDim Preserve aryFileNames(UBound(aryFileNames) + 1)
aryFileNames(UBound(aryFileNames)) = fil.Name
End If
Next
'Array is correct size - you can skip "trimming" it.
'ReDim Preserve aryFileNames(UBound(aryFileNames) - 1)

Related

The code below works in Windows but not in Mac. Error in Redim Preserve

The code below works in excel for windows, but not in excel for mac.
It gives error 9 subscript out of range.
When I run the code in my Windows machine, everything is fine.
When I try to run it in a Mac with excel for Mac then I get the error message in the procedure below and the line added at the end of this post.
Sub GetUniqueID(aFirstArray() As Variant, DataUniqueID As Variant)
Dim aUniqueArray() As Variant
Dim lngCountFirst As Long
Dim lngCountUnique As Long
Dim bolFoundIt As Boolean
Dim strOne As String
Dim strTwo As String
Dim i As Integer
Dim j As Integer
Dim ColIndex As Integer
ColIndex = 1
'// Redim with one element, empty at this point.//
ReDim aUniqueArray(0)
'// loop thru ea element in our first array. (This is our outer loop)//
For lngCountFirst = LBound(aFirstArray()) To UBound(aFirstArray())
'// ensure that we flag as False at the start of ea loop//
bolFoundIt = False
'// In a secondary, inner loop, we can build the unique array, only //
'// adding items that have not already been added. //
For lngCountUnique = LBound(aUniqueArray()) To UBound(aUniqueArray())
'// For ea element in our unique array, see if it matches the //
'// current element being looked at in our frist array. If we //
'// find a match, mark our flag/boolean and exit the inner loop.//
'// On the other hand, if no match is found after every element //
'// in our unique array is looked at, then bolFoundIt will still//
'// be False. //
If aUniqueArray(lngCountUnique) = aFirstArray(lngCountFirst, ColIndex) Then
bolFoundIt = True
Exit For
End If
Next lngCountUnique
'// Now if bolFound is still False, then we didn't find a match, so //
'// we'll add it to the last available element in our unique array //
'// and add another empty element to the unique array for the next //
'// round... Note the use of Redim Preserve, so that we don't //
'// lose the values already added. //
If Not bolFoundIt Then
aUniqueArray(UBound(aUniqueArray())) = aFirstArray(lngCountFirst, ColIndex)
ReDim Preserve aUniqueArray(UBound(aUniqueArray()) + 1)
End If
Next lngCountFirst
'// Now after we're all done, we left our unique array with one //
'// extra/unused element. We'll drop/kill the extra element here. //
ReDim Preserve aUniqueArray(UBound(aUniqueArray()) - 1)
ReDim DataUniqueID(1 To UBound(aUniqueArray()) + 1, 1 To UBound(aFirstArray, 2))
For i = 1 To UBound(DataUniqueID)
DataUniqueID(i, 1) = aUniqueArray(i - 1)
Next i
For i = 1 To UBound(DataUniqueID)
For j = 2 To UBound(aFirstArray)
If DataUniqueID(i, 1) = aFirstArray(j, 1) Then
DataUniqueID(i, 5) = DataUniqueID(i, 5) & "," & aFirstArray(j, 5)
DataUniqueID(i, 7) = DataUniqueID(i, 7) & "," & aFirstArray(j, 7)
DataUniqueID(i, 12) = DataUniqueID(i, 12) & "," & aFirstArray(j, 12)
DataUniqueID(i, 14) = DataUniqueID(i, 14) & "," & aFirstArray(j, 14)
End If
Next j
Next i
End Sub
The line with error is this one:
ReDim Preserve aUniqueArray(UBound(aUniqueArray()) - 1)
Any ideas?
--Edit2---
this sub is called from another sub:
Dim Data() As Variant
'more code between
'but nothing that changes the dimensions of Data. Just assigning values loops etc
'***** Create an array with the Data for faster operation
Data = ThisWorkbook.Worksheets("CDR").Range("A1:V" & LastRow).Value
'more code here
'Test the conditional compiler constant #Mac
#If Mac Then
'I am a Mac
'Define Folder Path
'Function to create folder if it not exists in the Microsoft Office Folder
'Ron de Bruin : 13-July-2020
Dim OfficeFolder As String
Dim TestStr As String
OfficeFolder = MacScript("return POSIX path of (path to desktop folder) as string")
OfficeFolder = Replace(OfficeFolder, "/Desktop", "") & "Library/Group Containers/UBF8T346G9.Office/Script #1 output - " & FilenameDateStamp
On Error Resume Next
TestStr = Dir(OfficeFolder & "*", vbDirectory)
On Error GoTo 0
If TestStr = vbNullString Then
MkDir OfficeFolder
'You can use this msgbox line for testing if you want
'MsgBox "You find the new folder in this location :" & PathToFolder
End If
'14. SAVE OUTPUT FILES part 2 - save first set of output files
' File #1 & 2 --Save the current spreadsheet in it's entirety, as a new Excel and CSV version named dynamically as
'"For import - Mojo CallDetailParsed_yyyy-mm-dd_hh-mm.xlsx"
'(and a copy in .csv format)
' copies a worksheet into a new file and saves it onto desktop
Dim newWB As Workbook
ThisWorkbook.Sheets("CDR").Select
Set newWB = Workbooks.Add
ThisWorkbook.Sheets("CDR").Copy Before:=newWB.Sheets(1)
'this saves the new workbook
With newWB
'.SaveAs FileName:="[FONT=arial][COLOR=black]Macintosh HD:Users:robertandres[/COLOR][/FONT]:Desktop:TEST.xlsx", FileFormat:=xlOpenXMLWorkbook, CreateBackup:=False
.SaveAs FileName:=OfficeFolder & "/For import - Mojo CallDetailParsed_" & FilenameDateStamp, FileFormat:=51
.Saved = True
.SaveAs FileName:=OfficeFolder & "/For import - Mojo CallDetailParsed_" & FilenameDateStamp, FileFormat:=22
.Close
End With
'15. SAVE OUTPUT FILES part 2 - save second set of output files
'File #2 & 3 which is the same as the above but contains only columns A, F, L, V
'"For import - Mojo phone tags from calldetail Parsed_yyyy-mm-dd_hh-mm.xslx"
'(and a copy in .csv format)
' copies a worksheet into a new file and saves it onto desktop
Dim newWB2 As Workbook
ThisWorkbook.Sheets("CDR").Select
Set newWB2 = Workbooks.Add
ThisWorkbook.Sheets("CDR").Copy Before:=newWB2.Sheets(1)
newWB2.Sheets("CDR").Columns("M:U").Delete
newWB2.Sheets("CDR").Columns("G:K").Delete
newWB2.Sheets("CDR").Columns("B:E").Delete
'this saves the new workbook
With newWB2
'.SaveAs FileName:="[FONT=arial][COLOR=black]Macintosh HD:Users:robertandres[/COLOR][/FONT]:Desktop:TEST.xlsx", FileFormat:=xlOpenXMLWorkbook, CreateBackup:=False
.SaveAs FileName:=OfficeFolder & "/For import - Mojo phone tags from calldetail Parsed" & FilenameDateStamp, FileFormat:=51
.Saved = True
.SaveAs FileName:=OfficeFolder & "/For import - Mojo phone tags from calldetail Parsed" & FilenameDateStamp, FileFormat:=22
.Close
End With
#Else
'I am Windows
'code for windows that works ok
#End If
'prevent flickering and make faster
Application.ScreenUpdating = True
Dim DataUniqueID() As Variant
Call GetUniqueID(Data, DataUniqueID)
I created the macro for windows, and then it had to work also in Mac.
I did not have access to mac so in mind everything was tested but only in windows, not in mac.
The problem was that above the code in the question, I was importing data. The code to import data was not working for mac. When I fixed that code, everything worked fine.
Assumptions and poor testing was the problem. I solved this when I got full access to a mac.
Thanks #faneduru

Cannot able to store value in a array - VBA

I am trying to store a filename in a array but i am getting Type mismatch error. I have changed the data type but it didn't work. Kindly help.
The code block that is throwing error,
Sub Example2()
Dim objFile,objFile1,objFolder,objFolder1 As Object
Dim splitting, counter, filename, filename1, splitting1, counter1,As Variant
Dim myarray() As Variant
For Each objFile In objFolder.Files
splitting = Split(objFile.Name, "\", 9)
counter = UBound(splitting)
filename = splitting(counter)
For Each objFile1 In objFolder1.Files
splitting1 = Split(objFile1.Name, "\", 9)
counter1 = UBound(splitting1)
filename1 = splitting1(counter1)
If srch1 = srch2 Then
ReDim Preserve myarray(UBound(myarray) + 1)
myarray() = filename1
End If
Next
Next
Get File Paths (to Array) Function
Links
Objects
FileSystemObject Object
GetFolder Method
File Object
The Code
Option Explicit
Function getFilePaths(ByVal FolderPath As String, _
Optional ByVal FirstIndex As Long = 1) _
As Variant
Dim fso As Object
Dim fsoFldr As Object
Dim fsoFile As Object
Set fso = CreateObject("Scripting.FileSystemObject")
Set fsoFldr = fso.GetFolder(FolderPath)
Dim LastIndex As Long
LastIndex = FirstIndex - 1
Dim Data() As Variant
For Each fsoFile In fsoFldr.Files
LastIndex = LastIndex + 1
ReDim Preserve Data(FirstIndex To LastIndex)
Data(LastIndex) = fsoFile.Path ' or .Name, .ParentFolder ...
Next fsoFile
getFilePaths = Data
End Function
Sub TESTgetFilePath()
' Define Folder Path ('fPath').
Const fPath As String = "F:\Test\2020"
' Populate File Paths Array ('Data').
Dim Data As Variant
Data = getFilePaths(fPath)
' Validate File Paths Array.
If IsEmpty(Data) Then
MsgBox "No files found.", vbCritical, "Fail"
Exit Sub
End If
' Write title to the Immediate window (CTRL+G).
Debug.Print "The List"
' Write values from File Paths Array to a String ('Result').
Dim Result As String
Result = Join(Data, vbLf)
' Write file paths to the Immediate window (CTRL+G).
Debug.Print Result
End Sub
EDIT 1:
Sub Example2()
Const FolderPath As String = "C:\Test"
Dim fso As Object
Dim objFolder As Object
Dim objFile As Object
Set fso = CreateObject("Scripting.FileSystemObject")
Set objFolder = fso.GetFolder(FolderPath)
Dim LastIndex As Long
LastIndex = -1
Dim MyArray() As Variant
For Each objFile In objFolder.Files
LastIndex = LastIndex + 1
ReDim Preserve MyArray(LastIndex)
MyArray(LastIndex) = objFile.Name
Next objFile
Dim n As Long
For n = LBound(MyArray) To UBound(MyArray)
Debug.Print n, MyArray(n)
Next n
End Sub
EDIT 2:
Sub Example3()
' For a fileformat aaa-bbb-rev*.*, where 'rev' is to be tested if greater.
' Two hyphens only.
Const FolderPath As String = "F:\Test\2020\64568450"
Const fSep As String = "-"
Dim pSep As String
pSep = Application.PathSeparator
Dim fso As Object
Dim objFolder As Object
Dim objFile As Object
Set fso = CreateObject("Scripting.FileSystemObject")
Set objFolder = fso.GetFolder(FolderPath)
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Dim FileParts As Variant ' An array containing the split file name.
Dim fName As String ' File part before the 2nd hyphen (minus) '-'
Dim fRevision As String ' File part after the 2nd hyphen (minus) '-'
Dim LastIndex As Long
LastIndex = -1
Dim MyArray() As Variant
' Write file paths to array.
For Each objFile In objFolder.Files
FileParts = Split(objFile.Name, fSep)
fName = FileParts(0) & fSep & FileParts(1)
fRevision = FileParts(2)
If Not dict.Exists(fName) Then
dict(fName) = fRevision
Else
LastIndex = LastIndex + 1
ReDim Preserve MyArray(LastIndex)
If dict(fName) < fRevision Then
MyArray(LastIndex) = FolderPath & pSep & fName _
& fSep & fRevision
dict(fName) = fRevision
Else
MyArray(LastIndex) = objFile.Path
End If
End If
Next objFile
' Now 'MyArray' contains the list of file paths of the files to be moved.
Dim n As Long
For n = LBound(MyArray) To UBound(MyArray)
Debug.Print n, MyArray(n)
Next n
End Sub
Arrays in VBA can be either static or dynamic.
A static array is declared with a fixed size:
Dim myStaticArr(10) As String
declares an array with a fixed number of members (usually 11 as the lower index starts at 0, but you can overwrite this).
If you want to be sure about the lower index, you can specify
Dim myStaticArr(1 to 10) As String
Now you have 10 elements (from 1 to 10).
Similar, a multidimensional array can be defined
Dim myStaticArr3D(1 to 10, 1 to 5, 1 to 8) As String
Now you have an array with 10 * 5 * 8 members.
All of these arrays have in common that you need to declare at compile time the size of the array. The VBA compiler will reserve the necessary amount of memory and you cannot resize it.
If you don't know at compile time how large your array will be, you can declare it as dynamic array (as you do)
Dim myDynamicArr() as String
This reserves no memory at all. Before you can write something into the array, you need to tell VBA how big the array will be. This is done using the Redim statement. Easiest form:
Redim myDynamicArr(1 to 10) as String
Usually, this is done after calculating the size needed, so you will usually find the Redim having a variable that was used to calculate the needed size:
Redim myDynamicArr(1 to sizeNeeded) as String
Now there are cases where you find at runtime that the needed size is too small. You can issue another Redim to increase the size - but as you want to keep the content of the array, you specify the option Preserve:
Redim Preserve myDynamicArr(1 to 2*sizeNeeded) as String
This will double the size and keep the content of the first members (omitting the Preserve option will double the size but the content of the existing members will get lost).
To get the current size of an array, you can use the functions LBound and UBound. This can be used on static and dynamic arrays:
Dim myStaticArr(5 to 99) As String
Debug.Print LBound(myStaticArr), UBound(myStaticArr)
>> 5 99
Dim myDynamicArr() As String
ReDim myDynamicArr(1 to 20)
Debug.Print LBound(myDynamicArr), UBound(myDynamicArr)
>> 1 20
However, if you have a dynamic array and you never assigned memory to it, the functions LBound and UBound will throw a runtime error 9 "Subscript out of range"
Now what you want to do is to increase the size of the array by 1 every time you find a new value. You achieve this with
ReDim Preserve myarray(UBound(myarray) + 1)
which will look to the current size of the array using the UBound-function and resize it by 1, preserving its contents. That's fine, except for the fact that the very first time this statement is hit, the size of the array is undefined.
The easiest way to handle this is to use a variable that keeps track of your array size:
Dim myArray() as String, myArraySize as Long
(...)
myArraySize = myArraySize + 1
ReDim Preserve myArray(1 to myArraySize)
myarray(myArraySize) = filename1
One remark: ReDim Preserve is a rather expensive command. If you are dealing with a few entries, this doesn't matter, but if you are dealing with 100s or 1000s of elements, you should consider to use a Collection.
You should index to the array to set the value after Redim:
ReDim Preserve myarray(UBound(myarray) + 1)
myarray(ubound(myarray)) = filename1
VBA arrays are so finicky and frustrating. I would add items to a string and after then split it to an array:
Dim strArray As String
strArray = ""
REM .....
strArray = strArray + filename1 + ","
REM .....
myarray = Split(strArray,",")

VBA function calls VBA sub to print array to worksheet

Currently I have a user-defined function which runs several routines involving the generation of matrices. To check these matrices have been produced correctly I want to put them on a worksheet. The user defined function works fine and I have added the below into it at the place where I want to find out what is in the array:
Call CheckArray(TestArray)
Where 'TestArray' in the above varies depending on the array I want to look at.
The 'CheckArray' subroutine is as follows:
Sub CheckArray(MyArray As Variant)
MatrixRows = UBound(MyArray, 1)
MatrixCols = UBound(MyArray, 2)
MsgBox MyArray(11, 2)
MsgBox "Matrix size = " & MatrixRows & " rows x " & MatrixCols & " columns"
ActiveWorkbook.Worksheets("Check array").[A1].Resize(MatrixRows, MatrixCols) = MyArray
End Sub
Note that I placed the two MsgBox commands in there to check the sub was called correctly and that it was working, which it is. Moreover, it returned the value in the specific location I requested and it also stated the size of the particular matrix I was looking at, so the data does seem to be getting read correctly - the issue is with subsequently writing that from the new sub.
When I include that final line it does not print my array to the worksheet and it also stops the user-defined function from working correctly. Does anyone know why this isn't working?
Can user-defined functions call subs which print to a worksheet or not? Is there a way to fix this?
To summarise:
As I am using a user-defined function which is entered in a cell in a worksheet I cannot export data to anywhere but that cell. However, I can view the arrays in the Immediate Window.
In MS Excel 2010 the Immediate Window can be found in the VBA editor (Alt+F11) and then click on View -> Immediate Window (Ctrl+G).
To export my array to the Immediate Window I should enter this into my code after the array I want to view:
Call WriteArrayToImmediateWindow(MyArray)
Where 'MyArray' is the name of my array, whatever that is.
This will then call the 'WriteArrayToImmediateWindow' sub, which is:
Sub WriteArrayToImmediateWindow(arrSubA As Variant)
Dim rowString As String
Dim iSubA As Long
Dim jSubA As Long
rowString = ""
Debug.Print
Debug.Print "The array is: "
For iSubA = 1 To UBound(arrSubA, 1)
rowString = arrSubA(iSubA, 1)
For jSubA = 2 To UBound(arrSubA, 2)
rowString = rowString & "," & arrSubA(iSubA, jSubA)
Next jSubA
Debug.Print rowString
Next iSubA
End Sub
Credit for the above goes to User3706920: How to print two dimensional array in Immediate window in VBA?
EDIT:
I decided that viewing the arrays in the Immediate Window wasn't always useful and I needed to view the data comma-separated in Excel. If you want to do the same follow the below instructions:
To export the array to a text file you should enter this into your code after the array you want to view:
Call WriteToFile(MyArray)
Where 'MyArray' is again the name of the array you want to review. The 'WriteToFile macro is as follows:
Sub WriteToFile(arrSubA As Variant)
'To export array to a text file
'Setup
Dim FSO As Object
Dim ofs As Object
Dim Output As Variant
Dim rowString As String
Dim iSubA As Long
Dim jSubA As Long
rowString = ""
'Create file
Set FSO = CreateObject("Scripting.FileSystemObject")
Dim oFile As Object
Set oFile = FSO.CreateTextFile("C:\Users\" & Environ$("username") & "\Desktop\Array.txt")
oFile.Close
For iSubA = 1 To UBound(arrSubA, 1)
rowString = arrSubA(iSubA, 1)
For jSubA = 2 To UBound(arrSubA, 2)
rowString = rowString & "," & arrSubA(iSubA, jSubA)
Next jSubA
If Len(Dir("C:\Users\" & Environ$("username") & "\Desktop\Array.txt")) > 0 Then
Set ofs = FSO.OpenTextFile("C:\Users\" & Environ$("username") & "\Desktop\Array.txt", 8, True)
End If
ofs.WriteLine rowString
ofs.Close
Next iSubA
End Sub
To quickly view the output array in Excel without any commas I would recommend assigning the below macro to a button:
Sub OpenArrayFile()
'To open array text file in Excel
Workbooks.OpenText Filename:="C:\Users\" & Environ$("username") & "\Desktop\Array.txt", Origin:= _
xlMSDOS, StartRow:=1, DataType:=xlDelimited, TextQualifier:=xlDoubleQuote _
, ConsecutiveDelimiter:=False, Tab:=True, Semicolon:=False, Comma:=True _
, Space:=False, Other:=False, FieldInfo:=Array(1, 1), _
TrailingMinusNumbers:=True
End Sub
Hopefully this is useful to others too!

How do I create and pass string array to a sub in Excel VBA?

VBA arrays are new to me and it seems like there are multiple ways to create string arrays.
I believe I need to create a dynamic array
But I can't find any examples how to pass dynamic arrays to a subroutine
I know how many items there need to be in the array by the count of the User range (so maybe I don't need a dynamic array??). I'm having trouble passing the array through to another Subroutine.
The thought process is as follows:
Iterate through a list of user names
Create a sheet for each
Save each user name in an array as I iterate through
In another Subroutine, select all the sheets I created and save as a PDF
Below is my code. I'm getting Run-time error 9 - Subscript out of range (Referring to the array object)
I appreciate any help! Thank you!
Sub CreateAllDashboards(StartDate As Date, EndDate As Date)
'Used to iterate through the list of users and call the Sub to create Dashboards
Dim UserNameRangeStart As Range
Set UserNameRangeStart = Range("UserName")
Dim SheetNames() As String
'Cyle through users
For i = 1 To GetUserNameRange().CounT
'Some code
ReDim Preserve SheetNames(i)
SheetNames(i) = UserNameRangeStart.Offset(i, 0).Value
Next i
Call CreatePDF(EndDate, SheetNames) 'Also tried SheetNames()
End Sub
Sub CreatePDF(FileDate As Date, ByRef SheetNames As Variant)
Dim FilePath As String, FileName As String
FilePath = Application.ActiveWorkbook.Path
FileName = "Production Dashboards - " & Format(FileDate, "mmddyy") & ".pdf"
ThisWorkbook.Sheets(SheetNames).Select
ActiveSheet.ExportAsFixedFormat Type:=xlTypePDF, FileName:= _
FileName, Quality:=xlQualityStandard, IncludeDocProperties:=True, _
IgnorePrintAreas:=False, OpenAfterPublish:=True
End Sub
The array parameter is not a problem it is passed correctly to method CreatePDF(...). The parameter type can be changed to SheetNames() As String but SheetNames As Variant works as well.
Then the Run-time error 9 - Subscript out of range is raised here ThisWorkbook.Sheets(SheetNames).Select because the array SheetNames contains invalid sheet name, which is the very first item. This item is an empty string and empty string is not valid as a sheet name.
In the For Next loop index starts with value 1 but the array starts with 0. So the very first item of the array SheetNames remains untouched and is finally an empty string. To solve it set the lower bound in ReDim explicitly to 1. HTH
(Note: if you omit lower bound then Option Base is used and if no Option Base is specified then 0 is used.)
'Cyle through users
For i = 1 To GetUserNameRange().Count
'Some code
ReDim Preserve SheetNames(1 To i)
SheetNames(i) = UserNameRangeStart.Offset(i, 0).value
Next i
I would change this:
Sub CreatePDF(FileDate As Date, ByRef SheetNames As Variant)
To this:
Sub CreatePDF(FileDate As Date, SheetNames() As String)
But your problem is at this line:
ThisWorkbook.Sheets(SheetNames).Select
Edited from dee's comment:
You can put an array of sheet names in .Sheets() but without empty rows. So in your sub "CreateAllDashboards" do this:
ReDim Preserve SheetNames(i - 1)
SheetNames(i - 1) = UserNameRangeStart.Offset(i, 0).Value
and you could read that about arrays in VBA.
I've tested the following using a single sheet workbook with a range named Users and another named FileDate. It does what you asked.
The reason for the Run-time error 9 - Subscript out of range error is that you have to reference the array element. ThisWorkbook.Sheets(SheetNames).Select will throw an error but ThisWorkbook.Sheets(SheetNames(x)).Select won't (as long as x is initialised and within the bounds of the array)
Sub PopulateArray()
Dim user As Range
Dim SheetNames As Variant
ReDim SheetNames(1 To 1) 'Initialise the array
For Each user In [Users]
ThisWorkbook.Sheets.Add After:=Worksheets(Worksheets.Count)
With Worksheets(Worksheets.Count)
.Name = user.Value2
.[A1] = user.Value2 'you can't print a blank sheet!
End With
SheetNames(UBound(SheetNames)) = user.Value2
ReDim Preserve SheetNames(1 To UBound(SheetNames) + 1)
Next user
ReDim Preserve SheetNames(1 To UBound(SheetNames) - 1) 'Delete the last element
Call CreatePDF([FileDate], SheetNames)
End Sub
Sub CreatePDF(FileDate As Date, ByRef SheetNames As Variant)
Dim FilePath As String, FileName As String
Dim x As Long
FilePath = Application.ActiveWorkbook.Path & "\" 'Note backslash added to path.
FileName = "Amtec Production Dashboards - " & Format(FileDate, "mmddyy")
For x = 1 To UBound(SheetNames)
ThisWorkbook.Sheets(SheetNames(x)).ExportAsFixedFormat Type:=xlTypePDF, FileName:= _
FileName & SheetNames(x) & ".pdf", Quality:=xlQualityStandard, IncludeDocProperties:=True, _
IgnorePrintAreas:=False, OpenAfterPublish:=True
Next x
End Sub
The above demonstrates how to parse an array to another sub as requested but you could integrate the CreatePDF code into the calling sub fairly easily too.

Efficient way to create an inverted index in VBA

I am creating an inverted index to get a dictionary of words with an associated list of the line numbers that the word appears on (starting the line numbers and a list of words that appear in a given cell within that line).
I have managed to get some code working for this, but I found dealing with adding to the arrays (the values in the dictionary) to be a little cumbersome and I wonder is there is a more efficient or more elegant way to handle this.
I am open to using arrays, collections or any other data type that can be easily searched to store the list of line numbers in the values of the dictionary. I have pasted a cut down version of my code to demonstrate the core problem below, the question is really just about the BuildInvertedIndex procedure, but the rest is included to try to make it easier to recreate the scenario:
Sub Test()
' minimum included here to demonstrate use of buildInvertedIndex procedure
Dim vRange As Range
Dim vDict As Dictionary
Set vRange = ActiveSheet.Range("F2:F20585")
Set vDict = New Dictionary
BuildInvertedIndex vDict, vRange
' test values returned in dictionary (word: [line 1, ..., line n])
Dim k As Variant, vCounter As Long
vCounter = 0
For Each k In vDict.Keys
Debug.Print k & ": " & ArrayToString(vDict.Item(k))
vCounter = vCounter + 1
If vCounter >= 10 Then
Exit For
End If
Next
End Sub
Sub BuildInvertedIndex(pDict As Dictionary, pRange As Range)
Dim cell As Range
Dim words As Variant, word As Variant, val As Variant
Dim tmpArr() As Long
Dim newLen As Long, i As Long
' loop through cells (one col wide so same as looping through lines)
For Each cell In pRange.Cells
' loop through words in line
words = Split(cell.Value)
For Each word In words
If Not pDict.exists(word) Then
' start line array with first row number
pDict.Add word, Array(cell.Row())
Else
i = 0
If Not InArray(cell.Row(), pDict.Item(word)) Then
newLen = UBound(pDict.Item(word)) + 1
ReDim tmpArr(newLen)
For Each val In tmpArr
If i < newLen Then
tmpArr(i) = pDict.Item(word)(i)
Else
tmpArr(i) = cell.Row()
End If
i = i + 1
Next val
pDict.Item(word) = tmpArr
End If
End If
Next word
Next cell
End Sub
Function ArrayToString(vArray As Variant, _
Optional vDelim As String = ",") As String
' only included to support test (be able to see what is in the arrays)
Dim vDelimString As String
Dim i As Long
For i = LBound(vArray) To UBound(vArray)
vDelimString = vDelimString & CStr(vArray(i)) & _
IIf(vCounter < UBound(vArray), vDelim, "")
Next
ArrayToString = vDelimString
End Function
To run this you will need values in column F of the active sheet (sentences), if you do not already have it you will also need to add a reference to the Microsoft Scripting Runtime in your VBA environment for the dictionary data type to be available (tools -> references -> Microsoft Scripting Runtime).
As you will see from the code this gets a bit messy where I have to insert a new line number into an existing array (that is stored as a value within the dictionary). As I do not know of a way to just extend this array (without clearing the existing values), I have used the variable tmpArr to create an array of the appropriate size and then copy the values one by one from the existing array in the dictionary and then add the current row number to the end. The temporary array is then used to replace the existing value for that key (the current word).
Any advice on this would be greatly appreciated.
I am open to using arrays, collections or any other data type
As I see, using collection instead array would be much simplier:
Sub BuildInvertedIndex(pDict As Dictionary, pRange As Range)
Dim cell As Range
Dim words, word
Dim i As Long
' loop through cells (one col wide so same as looping through lines)
For Each cell In pRange.Cells
' loop through words in line
words = Split(cell.Value)
For Each word In words
If Not pDict.Exists(word) Then
' initialize collection
pDict.Add word, New Collection
End If
'try to add to collection. If row is already in collecton, nothing happend. Storing key makes you sure there're only unique rows
On Error Resume Next
pDict.Item(word).Add Item:=cell.Row, Key:=CStr(cell.Row)
On Error GoTo 0
Next word
Next cell
End Sub
Next step, is to slightly modify ArrayToString to ColToString:
Function ColToString(vCol As Collection, _
Optional vDelim As String = ",") As String
' only included to support test (be able to see what is in the arrays)
Dim vDelimString As String
Dim i As Long
For i = 1 To vCol.Count
vDelimString = vDelimString & CStr(vCol.Item(i)) & _
IIf(i < vCol.Count, vDelim, "")
Next
ColToString = vDelimString
End Function
and the test subroutine (changed only one row - Debug.Print k & ": " & ColToString(vDict.Item(k)) and target range to "F2:F5"):
Sub Test()
' minimum included here to demonstrate use of buildInvertedIndex procedure
Dim vRange As Range
Dim vDict As Dictionary
Set vRange = ActiveSheet.Range("F2:F5")
Set vDict = New Dictionary
BuildInvertedIndex vDict, vRange
' test values returned in dictionary (word: [line 1, ..., line n])
Dim k As Variant, vCounter As Long
vCounter = 0
For Each k In vDict.Keys
Debug.Print k & ": " & ColToString(vDict.Item(k))
vCounter = vCounter + 1
If vCounter >= 10 Then
Exit For
End If
Next
'clean up memory
Set vDict = Nothing
End Sub
RESULT:
UPDATE:
to improve speed of your code you could store range in array (next approach work only with single-column range, but you could easily modify it):
Test sub:
Sub TestWirhArray()
' minimum included here to demonstrate use of buildInvertedIndex procedure
Dim vRange As Range
Dim vDict As Dictionary
Dim myArr As Variant
Set vDict = New Dictionary
Set vRange = ActiveSheet.Range("F2:F20585")
myArr = vRange.Value
BuildInvertedIndexWithArr vDict, myArr, vRange.Row
' test values returned in dictionary (word: [line 1, ..., line n])
Dim k As Variant, vCounter As Long
vCounter = 0
For Each k In vDict.Keys
Debug.Print k & ": " & ColToString(vDict.Item(k))
vCounter = vCounter + 1
If vCounter >= 10 Then
Exit For
End If
Next
'clean up memory
Set vDict = Nothing
End Sub
new version of BuildInvertedIndexWithArr:
Sub BuildInvertedIndexWithArr(pDict As Dictionary, pArr, firstRow As Long)
Dim cell, words, word
Dim i As Long, j As Long
j = firstRow
' loop through cells (one col wide so same as looping through lines)
For Each cell In pArr
' loop through words in line
words = Split(cell)
For Each word In words
If Not pDict.exists(word) Then
' initialize collection
pDict.Add word, New Collection
End If
On Error Resume Next
pDict.Item(word).Add Item:=j, Key:=CStr(j)
On Error GoTo 0
Next word
j = j + 1
Next cell
End Sub

Resources