How to speed up a data file import in Excel VBA - excel

Update as of June 11, 2019: I still haven’t figured out why practically all of my delay happens in those two lines, but current status is that I put up with the delay. So far, I have about 6000 rows of data in the master document, and an import process takes about 20 seconds regardless of how many rows I import.
—
I have a "master document" and I import data from lots and lots of little documents all day long. I admit I'm not a super-genius here, and a lot of my coding habits come from doing it "old school" so there may be "Excel ways" that I don't know (but want to learn!).
The issue I'm seeing is how much time a data file import can take.
When I started the tool out, data imports took only a few seconds.
Now that I have about 3500 rows of data, data imports take about 15-20 seconds. It doesn't matter if I am importing one row or a hundred rows. I expect this to keep going up. By the time I get to 7000 rows or 10,000 rows, I expect it to become unbearable.
By using message boxes (remember: "old school"), I've been able to narrow the speed bottleneck down to two lines of code. Between "Step 1" and "Step 2" is about 30% of my delay, and between "Step 2" and "Step 3" is about 70% of my delay.
I've included the whole sub below to make sure I'm not missing something obvious, but I made sure to UNINDENT my message boxes so you can go r-i-g-h-t to the code I suspect. Also, I included the entire sub because usually one of the first responses is “can you show the whole sub so I have better context?”
Thank you kindly for any thoughts or suggestions you might have. :)
Private Sub Btn_ImportDataFiles_Click()
' Search the current worksheet and assign the next TransactionID
Dim TransactionCounter As Integer
Dim TransactionID As Long ' This is the next available Transaction ID
TransactionID = Application.WorksheetFunction.Max(Range("a:a")) + 1
' open the file and import the data
Dim customerBook As Workbook
Dim filter As String
Dim caption As String
Dim customerFilename As String
Dim customerWorkbook As Workbook
Dim targetWorkbook As Workbook
' make weak assumption that active workbook is the target
Set targetWorkbook = Application.ActiveWorkbook
' get the customer workbook
filter = "Text files (*.xlsx),*.xlsx"
caption = "Please Select an input file "
customerFilename = Application.GetOpenFilename(filter, , caption)
If customerFilename <> "False" Then
' If they have uploaded the file before, let them know.
' If they want to keep uploading it, no harm done,
' but no need to stupidly add data that is already present.
' Select the archive sheet
Sheets("Upload_Archive").Select
Dim FileNameHunt As String
Dim cell As Range
Dim ContinueUpload As Boolean
ContinueUpload = True
FileNameHunt = Mid(customerFilename, InStrRev(customerFilename, "\") + 1)
Columns("A:A").Select
Set cell = Selection.Find(what:=FileNameHunt, after:=ActiveCell, LookIn:=xlFormulas, lookat:=xlWhole, searchorder:=xlByRows, searchdirection:=xlNext, MatchCase:=False, searchformat:=False)
If cell Is Nothing Then ' Add the new filename to the archive
Sheets("Upload_Archive").Select
Rows(1).Insert shift:=xlDown
Range("a1:a1").Value = FileNameHunt
Sheets("MasterSheet").Select
Application.Cells.Font.Name = "Calibri Light"
Application.Cells.Font.Size = "8"
Application.Cells.Font.Bold = False
Else
response = MsgBox("This data file has previously been uploaded. " & vbCrLf & "Do you want to cancel this upload?" & vbCrLf & vbCrLf & "Pressing [yes] will cancel the process." & vbCrLf & "Pressing [no] will continue with the file upload" & vbCrLf & "and add the data to the tracking sheet.", vbYesNo)
If response = vbYes Then
ContinueUpload = False
Sheets("MasterSheet").Select
Exit Sub
End If
End If ' If cell Is Nothing Then...
If ContinueUpload = True Then
' Continue with data upload procedure
Sheets("MasterSheet").Select
Set customerWorkbook = Application.Workbooks.Open(customerFilename)
' Copy data from customer to target workbook
Dim targetSheet As Worksheet
Set targetSheet = targetWorkbook.Worksheets(1)
Dim sourceSheet As Worksheet
Set sourceSheet = customerWorkbook.Worksheets(1)
Dim ImportRecordCount As Integer
ImportRecordCount = sourceSheet.Range("B1")
Dim ReconciliationID As String
ReconciliationID = ""
If sourceSheet.Range("E3") = "Removed from Depot" Then ReconciliationID = "1"
MsgBox ("Step 1")
targetSheet.Range("A1").EntireRow.Offset(1).Resize(ImportRecordCount).Insert shift:=xlDown ' Add the blank rows
MsgBox ("Step 2")
targetSheet.Range("B2:AB" & ImportRecordCount + 1).Value = sourceSheet.Range("A3:AA" & ImportRecordCount + 2).Value ' Bring in the big pile of data
MsgBox ("Step 3")
targetSheet.Range("AJ2:AJ" & ImportRecordCount + 1).Value = ReconciliationID ' To help with reconciling shipments
targetSheet.Range("AK2:AK" & ImportRecordCount + 1).Value = ReconciliationID ' To help with deployment timing
'targetSheet.Range("AI2:AI" & ImportRecordCount + 1).Value = "=COUNTIFS($D:$D, D2, $F:$F, F2)" ' This is the helper formula for identifying duplicates (deprecated, but I'm saving the code)
For TransactionCounter = 2 To ImportRecordCount + 1 ' Create and add the new Transaction ID values
targetSheet.Range("a" & TransactionCounter) = TransactionID + ImportRecordCount - TransactionCounter + 1
Next
' Close customer workbook
customerWorkbook.Close
' Format the sheet properly
Application.Cells.Font.Name = "Calibri Light"
Application.Cells.Font.Size = "8"
Application.Cells.Font.Bold = False
Application.Range("1:1").Font.Size = "10"
Application.Range("1:1").Font.Bold = True
' Query the User -- delete the file?
If MsgBox("Delete the local client-generated data file?" & vbCrLf & vbCrLf & "(this will NOT affect your email)", vbYesNo, "Confirm") = vbYes Then
Kill customerFilename
' MsgBox ("File: " & vbCrLf & customerFilename & vbCrLf & "has been deleted.")
End If
End If ' If ContinueUpload = True Then
End If ' If customerFilename <> "False" Then
End Sub
edit
I edited your original question to highlight things I found as suspect. These are things I felt are worth pointing out to you. I shaved everything else out as to focus on these particular issue. Review them and do soem research to see if you can find yourself in a better situation.
MsgBox ("Step 2")
'Ive never moved large amounts of data using this method. Ive always just used arrays. I have moved smaller bits of data though.
' I suspect that this might take a moment if the data set is large. Again use arrays to grab the data and move it.
' Edward says “This step takes about 70% of my delay — even if bringing in only a single line of data.”
targetSheet.Range("B2:AB" & ImportRecordCount + 1).Value = sourceSheet.Range("A3:AA" & ImportRecordCount + 2).Value ' Bring in the big pile of data
MsgBox ("Step 3")
' this loop is probably your main culprit of your performance issue.
' Edward says “Nope, this flies by. It is not the issue at all. I have verified this already.”
' Learn how to construct an array of data on the fly and then learn how to dump the entire array to
' sheet using a simple method.
For TransactionCounter = 2 To ImportRecordCount + 1 ' Create and add the new Transaction ID values
targetSheet.Range("a" & TransactionCounter) = TransactionID + ImportRecordCount - TransactionCounter + 1
Next

It looks like you have a lot of good things going here. A few things that I saw that could potentially be changed to improve your performance.
First, between "Step 1" and "Step 2": In my experience, adding rows takes longer than using rows that already exist. It looks like you are basically "pushing" everything down to make room for the new data, such that the newly entered data is at the top and the oldest data is at the bottom. (Correct me if I am wrong on any of this.) If you were to simply add the data to the end of the sheet, you would probably see some performance improvements, although I don't know how big of an improvement it would be.
Second, between "Step 2" and "Step 3": I have found that using .Value2 as opposed to .Value can give you some performance improvements, and the larger the data the bigger the improvement. This has a down side - Value2 does not retain any of the formatting that might be present, meaning that the number type (date, accounting, etc) does not pull over correctly. If this is something that you do not need, then you can use Value2.
Finally, other methods: When I run extensive macros, I always try to do everything I can to get a performance boost. You can get slight boosts across the board by using tricks like turning off screen updating (Application.ScreenUpdating = False), just be sure to turn it back on at the end of the macro.
I hope that this helps you figure it out! If all else fails, you can do it once or twice by hand to remember how much faster it is using the macro! Haha. Good Luck!

Have you tried using .value2? In some scenarios it might bring you better performance. Check some performance comparatives here: https://fastexcel.wordpress.com/2011/11/30/text-vs-value-vs-value2-slow-text-and-how-to-avoid-it/
It's difficult seeing where's the issue without having access to the original sheets. Maybe the issue is with the data itself instead of your VBA code and sometimes you might need to clean your source data of the heavy stuff and then add it again if needed.
You could also look into doing some parts with Python but I guess that's out of the question if you don't want to add additional software layers to your solution.

Try adding this at the beginning and end of your script. Just be sure to set everything back to TRUE!!
Application.ScreenUpdating = False
Application.DisplayAlerts = False
...CODE HERE...
Application.ScreenUpdating = True
Application.DisplayAlerts = True

Related

Inserting shapes gets progressively slower

I make entomological specimen labels that come with an embedded QR code. Museum curators can scan the QR codes of a series of specimens in the same group and manipulate data.
The QR code images are inserted as "shapes" (I believe--they respond to shape commands in the macro), generated via VBA code by Jiri Gabriel, with editing by Jonas Heidelberg (https://github.com/JonasHeidelberg/barcode-vba-macro-only).
The macro takes data, populates cells with strings and values (i.e., what gets printed on the human-readable part of the individual labels). When all of the printed text is inserted, the macro iteratively generates one QR code image at a time and places each generated image next to the corresponding human-readable label.
The macro is quick to generate and insert the first few QR code images then gets progressively slower. I presume because Excel is not built to handle a large number of high-resolution images on the same spreadsheet. My sheet design accommodates 220 individual QR code images, but it takes nearly 10 minutes to populate the spreadsheet with 50 QR code images (it takes less than 30 seconds to populate 10 QR code images, so the slowdown is appreciable).
I have tried:
Disable screen updating - does not seem to improve the processing speed
Set calculation to manual - does not seem to improve the processing speed
After generating each QR code image, hide the image by using the following code, and then at the very end, turn all the images visible - seems to help a little bit but not nearly sufficient to make the macro usable at scale.
ActiveSheet.Shapes.SelectAll
Selection.ShapeRange.Visible = False
I looked for solutions to see if the QR code image shapes can be merged into one shape, because after all, wouldn't it be easier to manage a single shape than 200+ individual small shapes? There seems to be no functionality to combine all of the shapes into a single shape.
Another solution I thought about is simultaneously generating all of the QR codes, instead of iteratively, then perhaps it won't have the issue of the later-coming shapes being slow to render due to having to hold all of the previously rendered codes in its memory. I haven't found a way to write the code such that all QR code image shapes are generated in parallel, rather than in sequence.
Another solution I toyed with is to paste the shapes as PNG or some other image that could potentially be easier to deal with, but I get a lot of loss of quality, which seems strange because the QR code should be just a matrix of black and white cells, right? Why do they lose so much quality?
I would suggest an approach based on built in MS Word 2013+ feature (https://support.microsoft.com/en-us/office/field-codes-displaybarcode-6d81eade-762d-4b44-ae81-f9d3d9e07be3). Below is an example of generating 200 QR codes in 10.6 seconds:
Option Explicit
Sub MakeQRcodes()
Const QR_COUNT = 200
Dim fld As Field, tbl As Table, rng As Range
Dim Code As String, i As Integer, t As Single
t = Timer
ThisDocument.Range.Delete
Set tbl = ThisDocument.Tables.Add(Range:=Selection.Range, NumRows:=QR_COUNT, NumColumns:= _
2, DefaultTableBehavior:=wdWord9TableBehavior, AutoFitBehavior:= _
wdAutoFitFixed)
For i = 1 To QR_COUNT
Code = "Insect #" & i ' data can be obtained from Excel spreadsheet
tbl.Cell(i, 1).Range.Text = "QR code for [" & Code & "]:"
Set rng = tbl.Cell(i, 2).Range
rng.Collapse Direction:=wdCollapseStart
Set fld = ThisDocument.Fields.Add(Range:=rng, Type:=wdFieldEmpty, _
Text:="DisplayBarcode """ & Code & """ QR \q 3")
Next i
ThisDocument.ActiveWindow.View.ShowFieldCodes = False
With tbl.Range
.ParagraphFormat.Alignment = wdAlignParagraphCenter
.Cells.VerticalAlignment = wdCellAlignVerticalCenter
End With
Debug.Print "Done " & QR_COUNT & " items in " & Timer - t & " seconds"
End Sub
' Done 200 items in 10,62109 seconds
Result:
Edit2 (VBA Excel code)
Please note that in my experience the DisplayBarcode field works well only with the Latin alphabet. If you have other symbols, check the code on real lines.
Option Explicit
Sub makeQRs()
Dim arr
arr = ThisWorkbook.Sheets("Sheet1").ListObjects("Table1").DataBodyRange.Columns(3)
Call MakeQRcodes(arr)
End Sub
Sub MakeQRcodes(arr) ' arr(n,1)
'you need to add a reference to the "Microsoft Word Object Library" in the Tools-References VBE menu
Dim wd As New Word.Application, doc As Word.Document, fld As Word.Field, tbl As Word.Table, rng As Word.Range
Dim Code As String, i As Integer, QR_count As Integer, t As Single
QR_count = UBound(arr, 1)
t = Timer
wd.Visible = False ' hide the Word app
Set doc = wd.Documents.Add ' create a new Word document
Set tbl = doc.Tables.Add(Range:=doc.Range, NumRows:=QR_count, _
NumColumns:=2, DefaultTableBehavior:=1) 'wdWord9TableBehavior = 1
For i = 1 To QR_count
Code = arr(i, 1)
tbl.Cell(i, 1).Range.Text = "QR code for [" & Code & "]:"
Set rng = tbl.Cell(i, 2).Range
rng.Collapse Direction:=wdCollapseStart
Set fld = doc.Fields.Add(Range:=rng, Type:=-1, _
Text:="DisplayBarcode """ & Code & """ QR \q 3")
Next i
doc.ActiveWindow.View.ShowFieldCodes = False
With tbl.Range ' center text and QR-code in the table cells
.ParagraphFormat.Alignment = 1 'wdAlignParagraphCenter
.Cells.VerticalAlignment = 1 'wdCellAlignVerticalCenter
End With
Application.DisplayAlerts = 0 'wdAlertsNone
With doc
' save the Word doc as .pdf in the same folder as this Excel workbook
.SaveAs2 ThisWorkbook.Path & "\QR.pdf", 17 'wdFormatPDF
.Close False ' close Word document without saving
wd.Quit ' close Word app
End With
Application.DisplayAlerts = -1 'wdAlertsAll
MsgBox "Done " & QR_count & " QR-codes in " & Round(Timer - t, 1) & " seconds," & vbLf _
& "saved in " & ThisWorkbook.Path & "\QR.pdf"
End Sub
Data & result MsgBox
QR.pdf

VBA Find and Replace within a For loop not replacing as expected

I'm taking Excel worksheets and putting them into XML format, to then import to SQL. I have a worksheet of a list of file links, called "Files List". The code opens up the necessary sheet within the file links and puts the data into a sheet called "XML format", which is suitable to then import to SQL. The issue is that the Find and Replace within the loop of transferring the data does run, but not seem to get passed the first "Files List" reference.
Sub LinkFile()
Application.ScreenUpdating = False
ThisWorkbook.Sheets("Files List").Activate
oldfile = Sheets("Files List").Range("oldFile")
Debug.Print oldfile
strfnd = oldfile
Set db1 = connect_sql_server()
strfnd = Format_link(oldfile)
Debug.Print strfnd
For i = 3 To 50
Sheets("Files List").Range("FIleIndex").value = i
filelink = Sheets("Files List").Range("fileindex2")
curfile = ThisWorkbook.Sheets("Files List").Range("curfile")
Set wkbkNew = Workbooks.Open(filelink)
ThisWorkbook.Sheets("XML Format").Range("B3") = ActiveWorkbook.Name
Debug.Print (ThisWorkbook.Sheets("XML Format").Range("B3"))
Set wbsheet = wkbkNew.Sheets("Calculations")
filelink = ThisWorkbook.Sheets("Files List").Range("E3")
strRplc = filelink
Debug.Print (strRplc)
Set sht = ThisWorkbook.Sheets("XML Format")
Application.Volatile
ActiveWorkbook.Close SaveChanges:=False
Application.Volatile
sht.Cells.Replace What:=strfnd, Replacement:=strRplc, _
LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:=False, _
SearchFormat:=False, ReplaceFormat:=False
strfnd = strRplc
Debug.Print (strfnd)
ThisWorkbook.Sheets("XML Format").Activate
strxml = concatenate_xml()
Debug.Print (strxml)
'this imports data into sql server
WriteDB strxml, db1
strfnd = strRplc
continue:
Next i
db1.Close
End Sub
Say the loop is set as i=2 to 2. The expected result is that in the "XML Format" sheet, the data should be from the second files link in "Files List". The actual output is that the data is always from the first files link, the original "oldfile". The only thing that works as expected is the file name, "ActiveWorkbook.Name".
sorry if I cannot spend a lot of time answering your problem in detail. Trying to work though loads of cells in Excel in various workbooks generally gave me a lot of issues. Especially if you have macros running on worksheet events. The best advice I can give is that you get the full range from the old worksheet, process it with VBA, and then put it back into the new worksheet. It will also speed up your macro by orders of magnitude.
the process will then look a bit like this:
XML_table = thisWorkbook.range()
for i = 1 to nr_rows
for j = 1 to nr_columns
XML_table(i,j) = replace_str(XML_table(i,j))
next j
next i
thatWorkbook.range() = XML_table
good luck, hope the quick advice helped a bit
If I interpret this correctly (which is kind of tough since you use a lot of named ranges which I have no way of knowing where they link to) you do not account for iterations of your loop. You write away the iteration number on tab "Files list" and that's it. This means since you use absolute references for everything, it will repeat all steps exactly as you wrote them for the first iteration, never progressing. You need to feed i back into your loop as a row number to advance to the second row like so:
filelink = ThisWorkbook.Sheets("Files List").Range("E" & i)
Which from i = 3 to i = 50 actually loops through rows 3 to 50 in your "Files list" sheet, rather than taking "E3" every time.

Excel Crashes, Repairs and Restarts itself when I delete worksheets programmatically

I have an issue with my excel. To begin with, I create worksheets dynamically based on some parameters and then I export the worksheets as PDF. So far everything is fine. Now, when I am done with worksheets and I don't need them anymore I want to delete them. When I run the code excel crashes, repairs and then restarts itself. I am wondering if anyone has any idea why this keeps happening. I also delete named ranges for each sheet just in case.
Btw, if I delete them manually everything is fine.
Here's the code
Application.DisplayAlerts = False
Dim theName As Name
Dim newSheet As Worksheet
For i = 1 To unitsQty
Set newSheet = ThisWorkbook.Worksheets("Project Info " & i)
For Each theName In Names
If (TypeOf theName.Parent Is Worksheet) And (newSheet.Name = theName.Parent.Name) Then
theName.Delete
End If
Next
Set newSheet = ThisWorkbook.Worksheets("System Spec " & i)
For Each theName In Names
If (TypeOf theName.Parent Is Worksheet) And (newSheet.Name = theName.Parent.Name) Then
theName.Delete
End If
Next
Next i
Dim myArray() As Variant
ReDim myArray(unitsQty * 2)
With Sheets("Tables")
For i = 1 To unitsQty
myArray(i - 1) = "Project Info " & i
Debug.Print myArray(i)
Next i
For i = 1 To unitsQty
myArray(i - 1 + unitsQty) = "System Spec " & i
Debug.Print myArray(i)
Next i
End With
ThisWorkbook.Sheets(myArray(0)).Select
ThisWorkbook.Sheets(myArray).Select
ThisWorkbook.Sheets(myArray((unitsQty * 2 - 1))).Activate
ActiveWindow.SelectedSheets.Delete
Application.DisplayAlerts = True
thanks for your help. So basically in the worksheets I had OLE Object and therefore every time I was trying to delete a worksheets I had "Can't enter break mode at this time" and when I clicked continue but excel was crashing.
Even when I tried to delete all objects from the worksheet before i deleted it, excel still crashing.
I couldn't find any solution in the web, but I found a workaround that it is working fine and gives the desired results. The workaround is to copy the worksheets to a temp workbook and do all the work there and when I am done, i am just closing the workbook without saving it of course. That keeps my original workbook tidied up.

code does only run correctly in break mode

After hours and days of searching the web in order to find a hint how to fix a bug in my code, I am completely clueless about what might be going on, and am hoping to get some advise from this community.
The code is somewhat complicated, therefore I will not add any snippets, but rather try to explain as simple as I can get it.
I have created a tool (excel macro) that does a lot of analyses on certain data, collected at customer sites using our software (mostly multiple users)
This tool runs well since years, including e.g. filtering to only take into account users that match certain criteria
I want to expand that tool in a way so that it automatically runs multiple times - once for each user.
The way how this works is:
The tool processes data from the first user, and saves the result as a new excel spreadsheet (in which the code continues to run).
the tool processes data from the next user, and again saves the result as a new spreadsheet, and so on.
In the second run the weird behaviour happens: If run in regular mode, the code breaks due to an error; if the code is interrupted by a 'stop' right before the line producing the error and code completion is continued, everything works perfectly fine.
The problem occurs at assignment of a table as range:
Dim rr As Range
Set rr = Workbooks(actWBK).Worksheets(shtName).Range(tableName & "[[#All],[" & header & "]]")
From the second run onwards, the line starting with Set... produces an error (application-defined or object-defined error).
The fact that this assignment works perfectly in the first run made me believe that there is some kind of unspecified assignment to a workbook or the like, but I tested all options and could rule that out;
The really staggering thing is that, as mentioned, when I add a "stop" before, the code works perfectly fine.
I am really out of any ideas, so every answer is more than welcome!
Thank you in advance,
Alexander
I will try to add some code.
The problem occurs in the module sortTable (relevant part after "code here", I always use this kind of "template" to set some standard things):
Sub sortTable(sheetName As String, tableName As String, header As String, dir As XlSortOrder)
' here only logging and error handling settings
'---------------------------------------------------------------------------------------
' code here
'---------------------------------------------------------------------------------------
' deal with #-sign in header
Dim headerParts() As String
headerParts = Split(header, "#")
Dim cleanHeader As String
If UBound(headerParts) = -1 Then
successcode = 2
GoTo errorHandler
ElseIf UBound(headerParts) = 0 Then
cleanHeader = header
Else
cleanHeader = headerParts(0)
Dim i As Integer
For i = 1 To UBound(headerParts)
cleanHeader = cleanHeader & "'#" & headerParts(i)
Next i
End If
' sorting
Dim actWBK As String
actWBK = ActiveWorkbook.name
Dim rr As Range
Set rr = Workbooks(actWBK).Worksheets(sheetName).Range(tableName & "[[#All],[" & cleanHeader & "]]")
ActiveWorkbook.Worksheets(sheetName).ListObjects(tableName).Sort.SortFields _
.Clear
ActiveWorkbook.Worksheets(sheetName).ListObjects(tableName).Sort.SortFields _
.Add key:=Range(tableName & "[[#All],[" & cleanHeader & "]]"), SortOn:=xlSortOnValues, Order _
:=dir, DataOption:=xlSortNormal
With ActiveWorkbook.Worksheets(sheetName).ListObjects(tableName).Sort
.header = xlYes
.MatchCase = True
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
'---------------------------------------------------------------------------------------
' sub cleanup on exit; don't make changes below this line
'---------------------------------------------------------------------------------------
' here only logging and error handling
End Sub
The procedure is called from a different module called QuickSort which takes as argument an array:
Public Sub QuickSort(vArray As Variant)
' here only logging and error handling
Dim wsName As String
wsName = "tempSort"
Application.DisplayAlerts = False
On Error Resume Next
Sheets(wsName).Delete
On Error GoTo errorHandler
Application.DisplayAlerts = True
Worksheets.Add After:=Sheets(Sheets.count)
ActiveSheet.name = wsName
Cells(1, 1) = "Header"
Dim rr As Range
Set rr = Range(Cells(2, 1), Cells(UBound(vArray) + 2 - LBound(vArray), 1))
Set rr = rr.Resize(UBound(vArray) + 1 - LBound(vArray), 1)
rr.value = myTransposeArray(vArray)
Set rr = Nothing
ActiveSheet.ListObjects.Add( _
xlSrcRange, _
Range(Cells(1, 1), Cells(UBound(vArray) + 2 - LBound(vArray), 1)), _
, xlYes).name = "tempSortTable"
sortTable sheetName:=wsName, tableName:="tempSortTable", header:="Header", dir:=xlAscending
' more code hereafter
Try changing this
Dim actWBK As String
actWBK = ActiveWorkbook.name
Dim rr As Range
Set rr = Workbooks(actWBK).Worksheets(sheetName).Range(tableName & "[[#All],[" & cleanHeader & "]]")
To
Dim actWbk as workbook
Set actwbk = activeworkbook
dim ws as worksheet
set ws = actwbk.worksheets(sheetname)
dim s as string
s = tableName & "[[#All],[" & cleanHeader & "]]"
Dim rr as range
Set rr = ws.range(s)
Then when it breaks you can inspect each in turn to see if they point to what you think they should
A collegue of mine found a workaround to overcome the problem (all credits onto you, Thomas!):
instead of referencing the range as
Set rr = Workbooks(actWBK).Worksheets(sheetName).Range(tableName & "[[#All],[" & cleanHeader & "]]")
I changed the code to
Set rr = Workbooks(actWBK).Worksheets(sheetName).ListObjects(tableName).ListColumns(cleanHeader).Range
By that, everything works perfectly fine, no matter if in the first run or in subsequent runs.
There is also a suspicion what might cause the problem (which clearly is not resolved itself, this solution is just a workaround!), and it has to do with something going on during saving the workbook using SaveAs.
I am not clear at all what could be this reason, but for those who face a similar problem, I'd like to explain what I did in my code:
By opening a file containing the tool
AnalysisTool.xlsm
the macro starts running. In order to get data, the code opens an xml file as excel table; this table temporarily is called something like
Book1.xlsx
The code copies the data from Book1 to AnalysisTool; in order to remain the tool unchanged, the file is saved as something like
AnalysisResult_20180222_01.xlsm <- this is the file in which code is executed!
Book1 is closed without saving.
When the analysis is finished, the workbook is saved without close.
Upon re-run,
All result tabs in AnalysisResult_20180222_01.xlsm are deleted, a new xml data file is opened, data is copied, and the code-bearing file is saved as
AnalysisResult_20180222_02.xlsm <- this is the file in which code now is executed!
etc.
As I said, I am not sure what goes wrong, but by changing this one line as described makes everything work perfectly fine.
Hope this might be helpful to anybody!

Create a VBA version of dictionaries with 2 values per key

I am trying to make my excel macro dynamic. The excel macro essentially looks at only 2 columns, one which contains the name and the other contains the numeric part. I have my macro working perfectly, the only problem is that it is hard coded when I created the program. In my code, I hard coded the name in column 2 and the numeric part in column 3. However, that is not the case in real life. The name and numeric data could appear in column 1 and 5, for example. I've been manually rearranging the data in the columns so that it fits into what hard coded. However, I want to make this process dynamic and less manual work for the user.
There are 5 different versions of spreadsheets this macro will be used on and in each spreadsheet, the name and number columns are different. I am looking to make a user form box of some sort, where the user selects "Vendor XYZ" and since Vendor XYZ always sends their data sheets the same way I know that Vendor XYZ's name column is 2 and number is 4. So I was thinking that the dictionary would be something in the form of {Vendor XYZ: 2,4} (where the first number is the name column and the second number is the numeric columnnumber...I know the syntax is wrong)
I think my work around this would be to hard code the different vendors and then use if statements ( I haven't tried it yet)
I will have a user input/dropdown box of 5 different vendors. Then something like
If userinput="A"
then namecol=2 and numcol=1
If userinput="B"
then namecol="3" and numcol="4"
I don't know if that would even work. The problem with that is that the number of vendors is small now, but will be scaling up and I can't do that if we have 100 or 1000 vendors.
Any ideas?
Depending on how your initial dataset is retrieved, you can use something like this:
Public Function GetHeaderIndices(ByVal InputData As Variant) As Scripting.Dictionary
If IsEmpty(InputData) Then Exit Function
Dim HeaderIndices As Scripting.Dictionary
Set HeaderIndices = New Scripting.Dictionary
HeaderIndices.CompareMode = TextCompare
Dim i As Long
For i = LBound(InputData, 2) To UBound(InputData, 2)
If Not HeaderIndices.Exists(Trim(InputData(LBound(InputData, 1), i))) Then _
HeaderIndices.Add Trim(InputData(LBound(InputData, 1), i)), i
Next
Set GetHeaderIndices = HeaderIndices
End Function
This Function takes an array as an input and gives the user a dictionary with the indices of the headers from the input.
If you are smart (and I say this because too many users just don't use tables) you will have your data in a table, and you will have named that table. If you did, you could do something like this:
Sub DoSomething()
Dim MyData as Variant
MyData = ThisWorkbook.Worksheets("MyDataSheet").ListObjects("MyTableName").Range.Value
End Sub
So, if you data looked like this:
Foo Baz Bar
1 Car Apple
3 Van Orange
2 Truck Banana
The function would give you a dictionary like:
Keys Items
Foo 1
Baz 2
Bar 3
Then your subroutines could do something like this:
Sub DoEverything()
Dim MyData as Variant
MyData = ThisWorkbook.Worksheets("MyDataSheet").ListObjects("MyTableName").Range.Value
DoSomething(MyData)
End Sub
Sub DoSomething(ByRef MyData as Variant)
Dim HeaderIndices as Scripting.Dictionary
Set HeaderIndices = GetHeaderIndices(MyData)
Dim i as Long
' Loop through all the rows after the header row.
For i = LBound(MyData, 1) + 1 to Ubound(MyData, 1)
If MyData(i, HeaderIndices("Baz")) = "Truck" Then
?MyData(i, HeaderIndices("Foo"))
?MyData(i, HeaderIndices("Baz"))
?MyData(i, HeaderIndices("Bar"))
End If
Next
End Sub
This does require a reference to Scripting.Runtime so if you don't want to add a reference you will need to change any reference to As Scripting.Dictionary to As Object and any New Scripting.Dictionary to CreateObject("Scripting.Dictionary").
Alternatively, I use the following code module to take care of adding references programmatically for all my users:
Public Sub PrepareReferences()
If CheckForAccess Then
RemoveBrokenReferences
AddReferencebyGUID "{420B2830-E718-11CF-893D-00A0C9054228}"
End If
End Sub
Public Sub AddReferencebyGUID(ByVal ReferenceGUID As String)
Dim Reference As Variant
Dim i As Long
' Set to continue in case of error
On Error Resume Next
' Add the reference
ThisWorkbook.VBProject.References.AddFromGuid _
GUID:=ReferenceGUID, Major:=1, Minor:=0
' If an error was encountered, inform the user
Select Case Err.Number
Case 32813
' Reference already in use. No action necessary
Case vbNullString
' Reference added without issue
Case Else
' An unknown error was encountered, so alert the user
MsgBox "A problem was encountered trying to" & vbNewLine _
& "add or remove a reference in this file" & vbNewLine & "Please check the " _
& "references in your VBA project!", vbCritical + vbOKOnly, "Error!"
End Select
On Error GoTo 0
End Sub
Private Sub RemoveBrokenReferences()
' Reference is a Variant here since it requires an external reference.
' It isnt possible to ensure that the external reference is checked when this process runs.
Dim Reference As Variant
Dim i As Long
For i = ThisWorkbook.VBProject.References.Count To 1 Step -1
Set Reference = ThisWorkbook.VBProject.References.Item(i)
If Reference.IsBroken Then
ThisWorkbook.VBProject.References.Remove Reference
End If
Next i
End Sub
Public Function CheckForAccess() As Boolean
' Checks to ensure access to the Object Model is set
Dim VBP As Variant
If Val(Application.Version) >= 10 Then
On Error Resume Next
Set VBP = ThisWorkbook.VBProject
If Err.Number <> 0 Then
MsgBox "Please pay attention to this message." _
& vbCrLf & vbCrLf & "Your security settings do not allow this procedure to run." _
& vbCrLf & vbCrLf & "To change your security setting:" _
& vbCrLf & vbCrLf & " 1. Select File - Options - Trust Center - Trust Center Settings - Macro Settings." & vbCrLf _
& " 2. Place a checkmark next to 'Trust access to the VBA project object model.'" _
& vbCrLf & "Once you have completed this process, please save and reopen the workbook." _
& vbCrLf & "Please reach out for assistance with this process.", _
vbCritical
CheckForAccess = False
Err.Clear
Exit Function
End If
End If
CheckForAccess = True
End Function
And I have the following command in each Workbook_Open event (less than ideal, but only good solution I have so far)
Private Sub Workbook_Open()
PrepareReferences
End Sub

Resources