How can I open this VBA workbook faster? - vba

I am currently trying to make a macro that will go to a directory, open a workbook (there are 38 currently with an eventual total of 52), filter two columns, get the total (repeat this 4 times), and the close the workbook. Currently it takes my application about 7 minutes just to process the current 38 workbooks.
How can I speed this up? I have already disables screen updating, events, and I changed the calculation methods to xlCalculationManual. I don't know if it common practice but I have seen people asking about a way to access a workbook without it being open but the suggestion to turn off screen updating is always made, which I have done.
When I run it in debug mode the Workbooks.Open() can take up to 10 seconds. The file directory is actually on a company network but accessing the file normally barely takes any time, under 5 seconds.
The data in the workbooks can contain the same points but at a different status. I do not think combining all of the data into one workbook would be possible.
I am going to experiment with direct cell references. Once I have some results I will update my post.
Private UNAME As String
Sub FileOpenTest()
Call UserName
Dim folderPath As String
Dim filename As String
Dim tempFile As String
Dim wb As Workbook
Dim num As Integer
Dim values(207) As Variant
Dim arryindex
Dim numStr As String
Dim v As Variant
Dim init As Integer
init = 0
num = 1
arryindex = 0
numStr = "0" & CStr(num)
'Initialize values(x) to -1
For Each v In values
values(init) = -1
init = init + 1
Next
With Excel.Application
.ScreenUpdating = False
.Calculation = Excel.xlCalculationManual
.EnableEvents = False
.DisplayAlerts = False
End With
'File path to save temp file
tempFile = "C:\Users\" & UNAME & "\Documents\TEMP.xlsm"
'Directory of weekly reports
folderPath = "path here"
'First file to open
filename = Dir(folderPath & "file here" & numStr & ".xlsm")
Do While filename <> ""
Set wb = Workbooks.Open(folderPath & filename)
'Overwrite previous "TEMP.xlsm" workbook without alert
Application.DisplayAlerts = False
'Save a temporary file with unshared attribute
wb.SaveAs filename:=tempFile, AccessMode:=xlExclusive
'operate on file
Filters values, arryindex
wb.Close False
'Reset file name
filename = Dir
'I use this loop to add the appropriate number to the end ie 01, 02, 03 etc
If num >= 9 Then
num = num + 1
If num = 33 Then
num = num + 1
End If
numStr = CStr(num)
ElseIf num < 9 Then
num = num + 1
numStr = "0" & CStr(num)
End If
filename = Dir(folderPath & "filename here" & numStr & ".xlsm")
Loop
output values
'Delete "TEMP.xlsm" file
On Error Resume Next
Kill tempFile
On Error GoTo 0
End Sub
Function Filters(ByRef values() As Variant, ByRef arryindex)
On Error Resume Next
ActiveSheet.ShowAllData
On Error GoTo 0
'filter column1
ActiveSheet.Range("B6").End(xlDown).AutoFilter Field:=2, Criteria1:=Array( _
"p1", "p2", "p3", "p4", "p5"), Operator:=xlFilterValues
'filter column2
ActiveSheet.Range("J6").End(xlDown).AutoFilter Field:=10, Criteria1:=Array( _
"s1", "d2", "s3"), Operator:=xlFilterValues
'get the total of points
values(arryindex) = TotalCount
arryindex = arryindex + 1
'filter column2 for different criteria
ActiveSheet.Range("J6").End(xlDown).AutoFilter Field:=10, Criteria1:="s"
'filter colum3 for associated form
ActiveSheet.Range("AZ6").End(xlDown).AutoFilter Field:=52, Criteria1:="<>"
'get the total of points
values(arryindex) = TotalCount
arryindex = arryindex + 1
'filter coum 3 for blank forms
ActiveSheet.Range("AZ6").End(xlDown).AutoFilter Field:=52, Criteria1:="="
'get the total of points
values(arryindex) = TotalCount
arryindex = arryindex + 1
'filter for column4 if deadline was made
ActiveSheet.Range("J6").End(xlDown).AutoFilter Field:=52
ActiveSheet.Range("J6").End(xlDown).AutoFilter Field:=10, Criteria1:=Array( _
"s1", "s2", "s3", "s4", "s5", "s6"), Operator:=xlFilterValues
ActiveSheet.Range("BC6").End(xlDown).AutoFilter Field:=55, Criteria1:=RGB(146 _
, 208, 80), Operator:=xlFilterCellColor
'get total of points
values(arryindex) = TotalCount
arryindex = arryindex + 1
End Function
Public Function TotalCount() As Integer
Dim rTable As Range, r As Range, Kount As Long
Set rTable = ActiveSheet.AutoFilter.Range
TotalCount = -1
For Each r In Intersect(Range("A:A"), rTable)
If r.EntireRow.Hidden = False Then
TotalCount = TotalCount + 1
End If
Next
End Function
Function UserName() As String
UNAME = Environ("USERNAME")
End Function
Function output(ByRef values() As Variant)
Dim index1 As Integer
Dim index2 As Integer
Dim t As Range
Dim cw As Integer
'Calendar week declariations
Dim cwstart As Integer
Dim cstart As Integer
Dim cstop As Integer
Dim data As Integer
data = 0
start = 0
cw = 37
cstart = 0
cstop = 3
ThisWorkbook.Sheets("Sheet1").Range("B6").Activate
For index1 = start To cw
For index2 = cstart To cstop
Set t = ActiveCell.Offset(rowOffset:=index1, columnOffset:=index2)
t.value = values(data)
data = data + 1
Next
Next
End Function

In general there are five rules to making Excel-VBA macros fast:
Don't use .Select methods,
Don't use Active* objects more than once,
Disable screen-updating and automatic calculations,
Don't use visual Excel methods (like Search, Autofilter, etc),
And most of all, always use range-array copying instead of browsing individual cells in a range.
Of these, you have only implemented #3. Additionally, you are exacerbating things by re-Saving your worksheets, just so that you can execute Visual modification methods (AutoFilter in your case). What you need to do to make it fast is to first implement the rest of these rules, and secondly, stop modifying your source worksheets so that you can open them read-only.
The core of what's causing your problems and forcing all of these other undesirable decisions is how you have implemented the Filters function. Instead of trying to do everything with the visual Excel functions, which are slow compared to (well-written) VBA (and that modify the worksheets, forcing your redundant Saves), just range-array copy all of the data you need from the sheet and use straight-forward VBA code to do your counting.
Here is an example of your Filters function that I converted to these principles:
Function Filters(ByRef values() As Variant, ByRef arryindex)
On Error GoTo 0
Dim ws As Worksheet
Set ws = ActiveSheet
'find the last cell that we might care about
Dim LastCell As Range
Set LastCell = ws.Range("B6:AZ6").End(xlDown)
'capture all of the data at once with a range-array copy
Dim data() As Variant, colors() As Variant
data = ws.Range("A6", LastCell).Value
colors = ws.Range("BC6", "BC" & LastCell.Row).Interior.Color
' now scan through every row, skipping those that do not
'match the filter criteria
Dim r As Long, c As Long, v As Variant
Dim TotCnt1 As Long, TotCnt2 As Long, TotCnt3 As Long, TotCnt4 As Long
TotCnt1 = -1: TotCnt2 = -1: TotCnt3 = -1: TotCnt4 = -1
For r = 1 To UBound(data, 1)
'filter column1 (B6[2])
v = data(r, 2)
If v = "p1" Or v = "p2" Or v = "p3" Or v = "p4" Or v = "p5" Then
'filter column2 (J6[10])
v = data(r, 10)
If v = "s1" Or v = "d2" Or d = "s3" Then
'get the total of points
TotCnt1 = TotCnt1 + 1
End If
'filter column2 for different criteria
If data(r, 10) = "s" Then
'filter colum3 for associated form
If CStr(data(r, 52)) <> "" Then
'get the total of points
TotCnt2 = TotCnt2 + 1
Else
' filter coum 3 for blank forms
'get the total of points
TotCnt3 = TotCnt3 + 1
End If
End If
'filter for column4 if deadline was made
v = data(r, 10)
If v = "s1" Or v = "s2" Or v = "s3" Or v = "s4" Or v = "s5" Then
If colors(r, 1) = RGB(146, 208, 80) Then
TotCnt4 = TotCnt4 + 1
End If
End If
End If
Next r
values(arryindex) = TotCnt1
values(arryindex + 1) = TotCnt2
values(arryindex + 2) = TotCnt3
values(arryindex + 3) = TotCnt4
arryindex = arryindex + 4
End Function
Please note that because I cannot test this for you and also because there is a lot of implicitness to the Autofilter/Range effects in the original code, I cannot tell if it is correct. You will have to do that.
Note: If you do decided to implement this, please let us know what impact it had, if any. (I try to keep track of what works and how much)

Related

Efficiency problems moving data from CSV -> excel cells

I started VBA - programming 2 days ago and I'm having efficiency problems given my limited knowledge of VBA and EXCEL.
I'm moving data from .CSV to .xlsm files. The .CSV file i receive is structured as
SHEET;COL;ROW;VALUE.
This .CSV is then read to a multidimension array and populates an excel file withwb.Worksheets(ARRAY(i, SHEET)).Cells(R,C) = ARRAY(i, VALUE)
To my understanding, applying the array to a range of cells does not work, as there is no continuous surface to which i'm sending each individual .csv row.
What i've tried most of it can be seen below. I believe a big issue i'm having is the pass-through between VBA -> EXCEL for each .CSV row. Is there any way this can be done by bulk instead?
All types of comments about efficiency and general how-to in VBA is greatly appreciated!
Option Explicit
Private Sub imp_Data()
'----------------------------File Dialog for data input-----------------
Dim Valarr As Variant
Dim fullpath As String
Dim CSVSHEET As Integer, CSVCOL As Integer, CSVROW As Integer, CSVVALUE As Integer
fullpath = [YOUR TEST FILE.CSV]
'----------------------------Read rawdata----------------------------
Dim RawData As String
Open fullpath For Binary As #1
RawData = Space$(LOF(1))
Get #1, , RawData
Close #1
'----------------------------Split rawdata into array-------------------
Dim r As Long, Nrow As Long, Ncol As Long
Dim c As Integer
Dim lineArr As Variant, cellArr As Variant
If Len(RawData) > 0 Then
'Split each row in CSV to str array
lineArr = Split(Trim$(RawData), vbCrLf)
'Dim final array
Nrow = UBound(lineArr) + 1
Ncol = UBound(Split(lineArr(0), ";")) + 1
ReDim Valarr(1 To Nrow, 1 To Ncol)
'Split each col on delimiter ";"
For r = 1 To Nrow
If Len(lineArr(r - 1)) > 0 Then
cellArr = Split(lineArr(r - 1), ";")
For c = 1 To Ncol
Valarr(r, c) = cellArr(c - 1)
Next c
End If
Next r
Else
Debug.Print "No data read"
' do more stuff
End If
'----------------------------Read Table positions-----------------------
Dim i As Integer
For i = 1 To Ncol
If UCase(Valarr(1, i)) = "SHEET" Then
CSVSHEET = i
ElseIf UCase(Valarr(1, i)) = "COL" Then
CSVCOL = i
ElseIf UCase(Valarr(1, i)) = "ROW" Then
CSVROW = i
ElseIf UCase(Valarr(1, i)) = "VALUE" Then
CSVVALUE = i
End If
Next i
'Turn off calculation and screen update for efficiency
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
'--------------------------------------------Send data to Cells----------
Dim L As Long
Dim wb As Workbook
Set wb = ThisWorkbook
L = UBound(Valarr, 1) - LBound(Valarr, 1) + 1
For i = 2 To L
If IsEmpty(Valarr(i, 1)) = 0 Then
wb.Worksheets(Valarr(i, CSVSHEET)).Cells(Valarr(i, CSVROW), Valarr(i, CSVCOL)) = Valarr(i, CSVVALUE)
End If
Next i
'Release ValArr memory
ReDim Valarr(0)
Erase Valarr
'Reapply calculation/screen update
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
End Sub
From tests with 1 million rows it takes 14 seconds to read data to array and 5+ minutes to move the data to their designated cell. So the issue in the code below is (what i believe)
For i = 2 To L
If IsEmpty(Valarr(i, 1)) = 0 Then
wb.Worksheets(Valarr(i, CSVSHEET)).Cells(Valarr(i, CSVROW), Valarr(i, CSVCOL)) = Valarr(i, CSVVALUE)
End If
Next i

Counting Contiguous Sets of Data provided no other sets occur within 500 rows

I want to write some VBA code that will count how many sets of "contiguous rows of Ts" there are in a single column in a worksheet. However I want such data sets to only be counted if there are more than 500 rows after the final T in a set that contain F values. For example, if T values are found at rows 500-510, then rows 511- 1010 would have to contain F values for one to be added to the count. If another T is encountered before reaching 1010, then the code would "reset" the 500 row counter and begin again.
row 1 - 1000 = F
row 1001 - 1011 = T
row 1012 - 1600 = F
row 1601 - 1611 = T
row 1612 - 3000 = F
In this case the counter would display 2
Conversely:
row 1 - 1000 = F
row 1001 - 1011 = T
row 1012 - 1400 = F
row 1401 - 1411 = T
row 1412 - 3000 = F
The counter would only display 1 as the Ts in cluster 1001-1011 are <500 rows within cluster 1401-1411.
I am also aware that in some scenarios there may be a set of Ts that are within 500 rows of the end of overall data. These would also need to be ignored from the count (I.e. using the example above, if Ts occurred a 2,700 - 2710, in a set of data with 3,000 rows, these would need to be ignored from the count). Similarly I would need to exclude rows 1-500 from the count also.
I don't know if this would be possible or even how to begin writing the code for this, so any assistance will be greatly appreciated. Excerpt of data:
F
F
F
F
F
F
F
F
F
T
T
T
T
T
F
F
F
F
F
F
F
F
This is going to be added to a much larger macro which then goes to filter out all rows containing Ts and deleting them. However I want to perform the count of contiguous Ts first before taking this step.
Code for rest of macro (This code is called by another macro which takes the values generated and pastes them into a master file):
Sub RollMap_Ensocoat(Wb As Workbook)
Dim ws As Worksheet
Dim Rng As Range, Cell As Range
Dim finalRow As Long
'Set name of first sheet in spreadsheet to "1"
With Wb.Sheets(1)
.Name = "1"
End With
'Code to delete all rows that contain a "T" in column G" (Indicating a tab was fired and thus is waste)
With Sheets("1")
finalRow = .Range("G" & Rows.Count).End(xlUp).Row
.AutoFilterMode = False
With .Range("G4:G" & finalRow)
.AutoFilter Field:=1, Criteria1:="T"
.Offset(1, 0).SpecialCells(xlCellTypeVisible).EntireRow.Delete
End With
.AutoFilterMode = False
'Code to calculate all the important values of each reel that will be pasted into the master report.
End With
Set ws = Wb.Sheets.Add(After:=Sheets(Wb.Sheets.Count))
With ws
.Range("A3").FormulaR1C1 = "=MAX('1'!C)"
.Range("B3").Formula = "=A3*I3"
.Range("C3").Formula = "=SUBTOTAL(109,'1'!B4:B10000)"
.Range("D3").Formula = "=SUBTOTAL(109,'1'!C4:C10000)"
.Range("E3").Formula = "=SUBTOTAL(109,'1'!D4:D10000)"
.Range("F3").Formula = "=SUBTOTAL(109,'1'!E4:E10000)"
.Range("G3").Formula = "=SUBTOTAL(109,'1'!F4:F10000)"
.Range("H3").Formula = "=SUM(C3:G3)"
.Range("I3").Formula = "='1'!A1"
.Range("J3").Formula = "=H3/(A3*I3)"
.Range("K3").Value = "0.21"
.Range("L3").Value = Wb.Name
.Range("M3").Formula = "=Left(L3, Len(L3) - 4)"
.Range("M3").Copy
.Range("M3").PasteSpecial xlPasteValues
.Range("N3").Formula = "=RIGHT(M3, 11)"
.Range("O3").Formula = "=LEFT(N3,2) & ""/"" & MID(N3,3,2) & ""/20"" & MID(N3,5,2)"
.Range("P3").Formula = "=MID(N3,8,2)& "":"" & MID(N3,10,2)"
.Range("Q3").Formula = "=Left(L3, Len(L3) - 16)"
.Range("A3:Q3").Copy
.Range("A3:Q3").PasteSpecial xlPasteValues
Application.CutCopyMode = False
.Range("A3:Q3").Copy
End With
End Sub
Code with Tim's suggested additions:
Sub Populate_Ensocoat()
On Error GoTo eh
Dim MyBook As String
Dim Wb As Workbook
Dim strFolder As String
Dim strFil As String
Dim StartTime As Double
Dim SecondsElapsed As Double
Dim xCount As Long
Dim SourceRang1 As Range
Dim FillRange1 As Range
'Code to improve performance
Application.ScreenUpdating = False
Application.EnableEvents = False
'Code to Prompt user to select file location
With Application.FileDialog(msoFileDialogFolderPicker)
.AllowMultiSelect = False
.Show
strFolder = .SelectedItems(1)
Err.Clear
End With
'Code to count how many files are in folder and ask user if they wish to continue based on value counted
strFil = Dir(strFolder & "\*.csv*")
Do While strFil <> ""
xCount = xCount + 1
strFil = Dir()
Loop
If MsgBox("You have selected " & xCount & " files. Are you sure you wish to continue?", vbYesNo) = vbNo Then GoTo eh
'Code to Start timer
StartTime = Timer
'Code to make final report sheet visible and launch sheet hidden
Sheet1.Visible = True
Sheet1.Activate
Sheets("Sheet3").Visible = False
'declaring existing open workbook's name
MyBook = ActiveWorkbook.Name
'Code to cycle through all files in folder and paste values into master report
strFil = Dir(strFolder & "\*.csv*")
Do While strFil <> vbNullString
Set Wb = Workbooks.Open(strFolder & "\" & strFil)
Call RollMap_Ensocoat(Wb)
Workbooks(MyBook).Activate
ActiveSheet.Paste
Selection.HorizontalAlignment = xlCenter
ActiveCell.Offset(1).Select
Wb.Close SaveChanges:=False
strFil = Dir
Loop
'Formatting of values in final report
Range("B:I").NumberFormat = "#,##0"
Range("J:K").NumberFormat = "0.000"
Range("L:L").NumberFormat = "0.00"
Range("P:P").NumberFormat = "dd/MM/yyyy"
Range("Q:Q").NumberFormat = "hh:mm"
'Code to add header data to report (i.e. total files, name of person who created report, date and time report was created)
Range("Y2").Value = Now
Range("H2").Value = "# of Files Reported on: " & xCount
Range("P2").Value = Application.UserName
'Re-enabling features disabled for improved macro performance that are now needed to display finished report
Application.EnableEvents = True
Application.ScreenUpdating = True
'Code to refresh sheet so that graphs display properly
ThisWorkbook.RefreshAll
'Code to automatically save report in folder where files are located. Overrides warning prompting user that file is being saved in Non-macro enabled workbook.
Application.DisplayAlerts = False
ActiveWorkbook.SaveAs Filename:=strFolder & "\" & "Summary Report", FileFormat:=xlOpenXMLWorkbook
Application.DisplayAlerts = True
'Code to display message box letting user know the number of files reported on and the time taken.
SecondsElapsed = Round(Timer - StartTime, 2)
MsgBox "Operation successfully performed on " & xCount & " files in " & SecondsElapsed & " seconds." & vbNewLine & vbNewLine & "Report created at location: " & Application.ActiveWorkbook.FullName, vbInformation
Done:
Exit Sub
eh:
MsgBox "No Folder Selected. Please select re-select a board grade"
End Sub
Sub RollMap_Ensocoat(Wb As Workbook)
Dim ws As Worksheet
Dim finalRow As Long
'Set name of first sheet in spreadsheet to "1"
With Wb.Sheets(1)
.Name = "1"
.Range("H1").Formula = "=TCount(G3:G10000)"
End With
'Code to delete all rows that contain a "T" in column G" (Indicating a tab was fired and thus is waste)
With Sheets("1")
finalRow = .Range("G" & Rows.Count).End(xlUp).Row
.AutoFilterMode = False
With .Range("G4:G" & finalRow)
.AutoFilter Field:=1, Criteria1:="T"
.Offset(1, 0).SpecialCells(xlCellTypeVisible).EntireRow.Delete
End With
.AutoFilterMode = False
'Code to calculate all the important values of each reel that will be pasted into the master report.
End With
Set ws = Wb.Sheets.Add(After:=Sheets(Wb.Sheets.Count))
With ws
.Range("A3").FormulaR1C1 = "=MAX('1'!C)"
.Range("B3").Formula = "=A3*I3"
.Range("C3").Formula = "=SUBTOTAL(109,'1'!B4:B10000)"
.Range("D3").Formula = "=SUBTOTAL(109,'1'!C4:C10000)"
.Range("E3").Formula = "=SUBTOTAL(109,'1'!D4:D10000)"
.Range("F3").Formula = "=SUBTOTAL(109,'1'!E4:E10000)"
.Range("G3").Formula = "=SUBTOTAL(109,'1'!F4:F10000)"
.Range("H3").Formula = "=SUM(C3:G3)"
.Range("I3").Formula = "='1'!A1"
.Range("J3").Formula = "=H3/(A3*I3)"
.Range("K3").Value = "0.21"
.Range("L3").Value = Wb.Name
.Range("M3").Formula = "=Left(L3, Len(L3) - 4)"
.Range("M3").Copy
.Range("M3").PasteSpecial xlPasteValues
.Range("N3").Formula = "=RIGHT(M3, 11)"
.Range("O3").Formula = "=LEFT(N3,2) & ""/"" & MID(N3,3,2) & ""/20"" & MID(N3,5,2)"
.Range("P3").Formula = "=MID(N3,8,2)& "":"" & MID(N3,10,2)"
.Range("Q3").Formula = "=Left(L3, Len(L3) - 16)"
.Range("R3").Formula = "='1'!H1"
.Range("A3:R3").Copy
.Range("A3:R3").PasteSpecial xlPasteValues
Application.CutCopyMode = False
.Range("A3:R3").Copy
End With
End Sub
Function TCount(rng As Range)
Const GAP_SIZE As Long = 5 '<< low number for testing...
Dim rv As Long, i As Long, fCount As Long, n As Long, d
Dim haveT As Boolean
rv = 0
d = rng.Value
n = UBound(d, 1)
fCount = 0
If n > GAP_SIZE Then
For i = 1 To n
If d(i, 1) = "T" Then
fCount = 0
haveT = True
Else
fCount = fCount + 1
If fCount = GAP_SIZE And haveT Then
rv = rv + 1
haveT = False
End If
End If
Next i
End If
TCount = rv
End Function
Something like this.
You may need to adjust if I made wrong assumptions about your rules.
Function TCount(rng As Range)
Const GAP_SIZE As Long = 5 '<< low number for testing...
Dim rv As Long, i As Long, fCount As Long, n As Long, d
Dim haveT As Boolean, earlyT as Boolean
rv = 0
d = rng.Value
n = UBound(d, 1)
fCount = 0
If n > GAP_SIZE Then
For i = 1 To n
If d(i, 1) = "T" Then
fCount = 0
If i <= GAP_SIZE Then earlyT = True '<<EDIT
haveT = True
Else
fCount = fCount + 1
If fCount = GAP_SIZE And haveT Then
rv = rv + 1
haveT = False
End If
End If
Next i
End If
TCount = rv - IIf(earlyT, 1, 0) '<< EDIT
End Function

VBA - Split string into individual cells

I have a string compressed into one cell. I need to separate each part of the string into their own cell, while copying the data from the same row.
Here is my example data:
A | B
Row1 ABC ABD ABE ABF | CODE1
Row2 BCA DBA EBA FBA | CODE2
Row3 TEA BEF | CODE3
The result would be:
A B
ABC CODE1
ABD CODE1
ABE CODE1
ABF CODE1
BCA CODE2
DBA CODE2
EBA CODE2
FBA CODE2
TEA CODE3
BEF CODE3
I have about 2000 rows and would literally take 30 years to use the text to column function for this. So I am trying to write a vba macro. I think I am making this harder than it needs to be. Any thoughts or pushes in the right direction would be appreciated. Thanks in advance for any help.
This will work, (but it's mighty inefficient unless you do it in an array... nevertheless for only 2000 rows, you won't even notice the lag)
Function SplitThis(Str as String, Delimiter as String, SerialNumber as Long) As String
SplitThis = Split(Str, Delimiter)(SerialNumber - 1)
End Function
Use it as
= SPLITTHIS("ABC EFG HIJ", " ", 2)
' The result will be ...
"EFG"
You will still need to put in a whole lot of extra error checking, etc. if you need to use it for a distributed application, as the users might put in values greater than the number of 'split elements' or get delimiters wrong, etc.
I like iterating over cells for problems like this post.
' code resides on input sheet
Sub ParseData()
Dim wksOut As Worksheet
Dim iRowOut As Integer
Dim iRow As Integer
Dim asData() As String
Dim i As Integer
Dim s As String
Set wksOut = Worksheets("Sheet2")
iRowOut = 1
For iRow = 1 To UsedRange.Rows.Count
asData = Split(Trim(Cells(iRow, 1)), " ")
For i = 0 To UBound(asData)
s = Trim(asData(i))
If Len(s) > 0 Then
wksOut.Cells(iRowOut, 1) = Cells(iRow, 2)
wksOut.Cells(iRowOut, 2) = s
iRowOut = iRowOut + 1
End If
Next i
Next iRow
MsgBox "done"
End Sub
Assuming your data is on the first sheet, this populates the second sheet with the formatted data. I also assume that the data is uniform, meaning there is the same type of data on every row until the data ends. I did not attempt the header line.
Public Sub FixIt()
Dim fromSheet, toSheet As Excel.Worksheet
Dim fromRow, toRow, k As Integer
Dim code As String
Set fromSheet = Me.Worksheets(1)
Set toSheet = Me.Worksheets(2)
' Ignore first row
fromRow = 2
toRow = 1
Dim outsideArr() As String
Dim insideArr() As String
Do While Trim(fromSheet.Cells(fromRow, 1)) <> ""
' Split on the pipe
outsideArr = Split(fromSheet.Cells(fromRow, 1), "|")
' Split left of pipe, trimmed, on space
insideArr = Split(Trim(outsideArr(0)), " ")
' Save the code
code = Trim(outsideArr(UBound(outsideArr)))
' Skip first element of inside array
For k = 1 To UBound(insideArr)
toSheet.Cells(toRow, 1).Value = insideArr(k)
toSheet.Cells(toRow, 2).Value = code
toRow = toRow + 1
Next k
fromRow = fromRow + 1
Loop
End Sub
Let me try as well using Dictionary :)
Sub Test()
Dim r As Range, c As Range
Dim ws As Worksheet
Dim k, lrow As Long, i As Long
Set ws = Sheet1 '~~> change to suit, everything else as is
Set r = ws.Range("B1", ws.Range("B" & ws.Rows.Count).End(xlUp))
With CreateObject("Scripting.Dictionary")
For Each c In r
If Not .Exists(c.Value) Then
.Add c.Value, Split(Trim(c.Offset(0, -1).Value))
End If
Next
ws.Range("A:B").ClearContents
For Each k In .Keys
lrow = ws.Range("A" & ws.Rows.Count).End(xlUp).Row
If lrow = 1 Then i = 0 Else i = 1
ws.Range("A" & lrow).Offset(i, 0) _
.Resize(UBound(.Item(k)) + 1).Value = Application.Transpose(.Item(k))
ws.Range("A" & lrow).Offset(i, 1).Resize(UBound(.Item(k)) + 1).Value = k
Next
End With
End Sub
Above code loads all items in Dictionary and then return it in the same Range. HTH.
Here is an approach using a User Defined Type, Collection and arrays. I've been using this lately and thought it might apply. It does make writing the code easier, once you get used to it.
The user defined type is set in a class module. I called the type "CodeData" and gave it two properties -- Code and Data
I assumed your data was in columns A & B starting with row 1; and I put the results on the same worksheet but in columns D & E. This can be easily changed, and put on a different worksheet if that's preferable.
First, enter the following code into a Class Module which you have renamed "CodeData"
Option Explicit
Private pData As String
Private pCode As String
Property Get Data() As String
Data = pData
End Property
Property Let Data(Value As String)
pData = Value
End Property
Property Get Code() As String
Code = pCode
End Property
Property Let Code(Value As String)
pCode = Value
End Property
Then put the following code into a Regular module:
Option Explicit
Sub ParseCodesAndData()
Dim cCodeData As CodeData
Dim colCodeData As Collection
Dim vSrc As Variant, vRes() As Variant
Dim V As Variant
Dim rRes As Range
Dim I As Long, J As Long
'Results start here. But could be on another sheet
Set rRes = Range("D1:E1")
'Get Source Data
vSrc = Range("A1", Cells(Rows.Count, "B").End(xlUp))
'Collect the data
Set colCodeData = New Collection
For I = 1 To UBound(vSrc, 1)
V = Split(vSrc(I, 1), " ")
For J = 0 To UBound(V)
Set cCodeData = New CodeData
cCodeData.Code = Trim(vSrc(I, 2))
cCodeData.Data = Trim(V(J))
colCodeData.Add cCodeData
Next J
Next I
'Write results to array
ReDim vRes(1 To colCodeData.Count, 1 To 2)
For I = 1 To UBound(vRes)
Set cCodeData = colCodeData(I)
vRes(I, 1) = cCodeData.Data
vRes(I, 2) = cCodeData.Code
Next I
'Write array to worksheet
Application.ScreenUpdating = False
rRes.EntireColumn.Clear
rRes.Resize(rowsize:=UBound(vRes, 1)) = vRes
Application.ScreenUpdating = True
End Sub
Here is the solution I devised with help from above. Thanks for the responses!
Sub Splt()
Dim LR As Long, i As Long
Dim X As Variant
Application.ScreenUpdating = False
LR = Range("A" & Rows.Count).End(xlUp).Row
Columns("A").Insert
For i = LR To 1 Step -1
With Range("B" & i)
If InStr(.Value, " ") = 0 Then
.Offset(, -1).Value = .Value
Else
X = Split(.Value, " ")
.Offset(1).Resize(UBound(X)).EntireRow.Insert
.Offset(, -1).Resize(UBound(X) - LBound(X) + 1).Value = Application.Transpose(X)
End If
End With
Next i
Columns("B").Delete
LR = Range("A" & Rows.Count).End(xlUp).Row
With Range("B1:C" & LR)
On Error Resume Next
.SpecialCells(xlCellTypeBlanks).FormulaR1C1 = "=R[-1]C"
On Error GoTo 0
.Value = .Value
End With
Application.ScreenUpdating = True
End Sub

How to Import CSV to Excel with VBA

I Have a Red Lion Data station that records temperatures from about 25 ovens and the data is stored on a network in CSV files. I would like to import this data into an Excel file once every two minutes, but only import the changes after the initial import. After the import, the VBA must release the CSV file for the 2 minutes so that it can be updated by the data station. I have searched the web, this site and the closest thing I have found is the following code. This code looks for changes but it does not import the file. any help would be appreciated.
Dim NextTime As Date
Function LastModTime(FileSpec As String) As Date
'Returns the date-time the file specified by FileSpec (path string) was last modified
Dim fs, f, s
Set fs = CreateObject("Scripting.FileSystemObject")
Set f = fs.GetFile(FileSpec)
LastModTime = f.DateLastModified
End Function
Sub Check4Changes()
'Checks the file FilePath for changes every 60 seconds
'If file has changed, pops up a message box. Stores the
'last modified time in cell M1 of Sheet1
ChDir "Q:\Manufacturing\Equipment\DispatchLogs\logs\7-DES"
Const FilePath As String = "Q:\Manufacturing\Equipment\DispatchLogs\logs\7-DES\*.csv"
Dim LastMod As Date
On Error GoTo ReSchedule
LastMod = LastModTime(FilePath)
With Worksheets("Sheet1").Range("C1")
If IsEmpty(.Value) Then
.Value = LastMod
GoTo ReSchedule
ElseIf .Value < LastMod Then
.Value = LastMod
MsgBox FilePath & " updated.", vbInformation, "Check4Changes"
End If
End With
ReSchedule:
'Reschedule this same routine to run in one minute.
NextTime = Now + 2 / 1440
Application.StatusBar = "Next check at " & NextTime
Application.OnTime NextTime, "Check4Changes"
End Sub
Sub CancelChecking()
Application.OnTime NextTime, "Check4Changes", Schedule:=False
Application.StatusBar = False
End Sub
I have written some code which will give you the basic idea
Sub Main()
Dim Wbk_CSV As Excel.Workbook
Dim Excel_Wbk As Excel.Workbook
Dim Var_WholeCSVData As Variant
Dim Var_ExcelData As Variant
Dim Var_ToUpdate As Variant
Dim NumOfRows As Long
Dim Last_Row As Long
Set Wbk_CSV = Workbooks.Open("PathWithFileName")
Wbk_CSV.Sheets(1).Activate
Last_Row = ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row
'Taking whole data in a variant
'Change the range as per the data in csv file
Var_WholeCSVData = Wbk_CSV.Sheets(1).Range("A2:D" & Last_Row).Value 'Considering first row as header row and there are 4 columns
'After taking whole data in varinat close csv file without saving
Wbk_CSV.Close savechanges:=False
Set Wbk_CSV = Nothing
'Now open excel file in which data will be updated
Set Excel_Wbk = Workbooks.Open("PathWithFileName")
Excel_Wbk.Sheets(1).Activate
Last_Row = ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row
Var_ExcelData = Excel_Wbk.Sheets(1).Range("A2:D" & Last_Row).Value 'Considering first row as header row
NumOfRows = 0
'This function will return count of updated rows and data to update
Var_ToUpdate = Delete_Duplicates(Var_WholeCSVData, Var_ExcelData, NumOfRows)
Excel_Wbk.Sheets(1).Activate
'paste data
If NumOfRows > 0 Then
ActiveSheet.Range("A" & Last_Row + 1 & ":D" & Last_Row + NumOfRows).Value = Var_ToUpdate
End If
Excel_Wbk.Close savechanges:=True
Set Excel_Wbk = Nothing
'result
MsgBox ("Number of rows imported: " & NumOfRows)
End Sub
Function Delete_Duplicates(Var_FromCSV As Variant, Var_FromExcel As Variant, ByRef NumberOfRowToupdate As Long) As Variant
'using dictinpary objects
Dim dict_Duplicates As Object
Dim i_AddToDict, i, j As Long
Dim Str_value As String
Dim Var_Temp As Variant
Dim lng_temp As Long
Set dict_Duplicates = CreateObject("Scripting.Dictionary")
ReDim Var_Temp(1 To UBound(Var_FromCSV, 1), 1 To UBound(Var_FromCSV, 2))
'Add excel data to dict. by concatenating
'All unique values will be added
For i_AddToDict = 1 To UBound(Var_FromCSV)
Str_value = CStr(Var_FromExcel(i_AddToDict, 1) & Var_FromExcel(i_AddToDict, 2) & Var_FromExcel(i_AddToDict, 3) & Var_FromExcel(i_AddToDict, 4))
If dict_Duplicates.exists(Str_value) Then
'do nothing
Else
dict_Duplicates.Add Str_value, 1
End If
Next i_AddToDict
'looking for values which are not available in excel file
For i = 1 To UBound(Var_FromCSV)
Str_value = CStr(Var_FromCSV(i_AddToDict, 1) & Var_FromCSV(i_AddToDict, 2) & Var_FromCSV(i_AddToDict, 3) & Var_FromCSV(i_AddToDict, 4))
If dict_Duplicates.exists(Str_value) Then
'do nothing
Else
'storing values in a variant
For j = 1 To 4
Var_Temp(lng_temp, j) = Var_FromCSV(i, j)
Next j
lng_temp = lng_temp + 1
dict_Duplicates.Add Str_value, 1
End If
Next i
NumberOfRowToupdate = lng_temp - 1
Delete_Duplicates = Var_Temp
End Function

Excel UDF is returning #value! when changing worksheets and copying and pasting

I have a sheet that has a table on it, the following function will be used to search another sheet and return the number of times the agents name shows up;
The problem I'm having is that if I try to copy and paste the value it will change from a number to '#Value!'. Also, when switching worksheets and then switching back to the worksheet that has the UDF being called it changes all values to '#Value!'
Here is the function and how it is called. Any help would be greatly appreciated.
Public Function GetMatrixCount(AgentName As String) As Integer
Dim matrixSheet As Worksheet, mContainer() As String, c As Integer, m As Integer, y As Integer
Dim fullRange As Range, l As Range, lastRow As Integer
Dim firstThree As String, curAgent As String
'toDo
'return zero if the matrix updates worksheet doesn't exist or the input string is empty
On Error Resume Next
Set matrixSheet = Sheets("Matrix Updates")
On Error GoTo 0
If matrixSheet Is Nothing Or Not Trim(AgentName) <> "" Then
GetMatrixCount = 0
Exit Function
End If
'get month number user wants to input from the title at the top of the page - used to do value check on matrix updates data
mContainer() = Split(Range("B1").Value, " ")
m = month(DateValue(mContainer(UBound(mContainer) - 1) & " 1"))
y = mContainer(UBound(mContainer))
firstThree = Left(AgentName, 3)
lastRow = matrixSheet.Cells(Rows.Count, 1).End(xlUp).Row
c = 0
Set fullRange = matrixSheet.Range("B2:B" & lastRow)
For Each l In fullRange.Cells
curAgent = l.Offset(0, 1).Value
If month(l.Value) = m And year(l.Value) = y And Left(curAgent, 3) = firstThree And Mid(curAgent, InStrRev(curAgent, " ") + 1) = Mid(AgentName, InStrRev(AgentName, " ") + 1) Then
c = c + 1
End If
If l.Value = "" Then
Exit For
End If
Next
GetMatrixCount = c
End Function
Usage:
=GetMatrixCount(B4)
B4: John Doe
UPD:
Try to write following:
Set wb = ThisWorkbook
Set matrixSheet = wb.Sheets("Matrix Updates")
It should fix the problem when you switch workbooks.