VBA - optimizing code in functions for faster runtime - vba

The code works very well but before I added sections (13) and (14), it ran in 6 minutes and now runs in 16 minutes. If there is a way to streamline this to cut down the runtime, that would be extraordinary.
Main part of code grabs values from under the header 'CUTTING TOOL' in various opening files in a designated folder. They are then printed to the workbook with code where all the information is printed to, StartSht, and the function alters the output information so that TL- has exactly 6 numbers following it and CT- has 4, plus an extra 2 if there is a "-" after the four numbers (ie CT-0081-01). If less than the specified length, 0s are added immediately after the "-". If greater than the specific length, 0s are deleted immediately after the "-".
Any suggestions on how to potentially streamline this code or general tips would be great. I have tried implementing the tips at this website but not much changed.
Main Code:
With WB
For Each ws In .Worksheets
'(3)
'find CUTTING TOOL on the source sheet
If Not ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues) Is Nothing Then
Set hc = ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues)
Set dict = GetValues(hc.Offset(1, 0), "SplitMe")
If dict.count > 0 Then
'add the values to the master list, column 3
Set d = StartSht.Cells(Rows.count, hc2.Column).End(xlUp).Offset(1, 0)
d.Resize(dict.count, 1).Value = Application.Transpose(dict.items)
Else
'if no items are under the CUTTING TOOL header
StartSht.Cells(Rows.count, hc2.Column).End(xlUp).Offset(1, 0) = " "
End If
For k = 2 To StartSht.Range("C2").End(xlDown).Row
ret = ""
str = StartSht.Range("C" & k).Value
ret = ExtractNumberWithLeadingZeroes(str, "TL", 6)
If ret <> "" Then
StartSht.Range("C" & k).Value = "TL-" & ret
Else
'for CT numbers
ret = ExtractNumberWithLeadingZeroes(str, "CT", 4)
If ret <> "" Then
StartSht.Range("C" & k).Value = "CT-" & ret
End If
End If
Next k
...
...
...
Functions:
'(8)
'Get the Values from columns with specified headers
Function GetValues(ch As Range, Optional vSplit As Variant) As Scripting.Dictionary
Dim dict As Scripting.Dictionary
Dim dataRange As Range
Dim cell As Range
Dim theValue As String
Dim splitValues As Variant
Dim counter As Long
Set dict = New Scripting.Dictionary
Set dataRange = ch.Parent.Range(ch, ch.Parent.Cells(Rows.count, ch.Column).End(xlUp)).Cells
' If there are no values in this column then return an empty dictionary
' If there are no values in this column, the dataRange will start at the row
' *above* ch and end at ch
If (dataRange.Row = (ch.Row - 1)) And (dataRange.Rows.count = 2) And (Trim(ch.Value) = "") Then
GoTo Exit_Function
End If
For Each cell In dataRange.Cells
counter = counter + 1
theValue = Trim(cell.Value)
If Len(theValue) = 0 Then
theValue = " "
End If
'exclude any info after ";"
If Not IsMissing(vSplit) Then
splitValues = Split(theValue, ";")
theValue = splitValues(0)
End If
'exclude any info after ","
If Not IsMissing(vSplit) Then
splitValues = Split(theValue, ",")
theValue = splitValues(0)
End If
If Not dict.exists(theValue) Then
dict.Add counter, theValue
End If
Next cell
Exit_Function:
Set GetValues = dict
End Function
'(9)
'find a header on a row: returns Nothing if not found
Function HeaderCell(rng As Range, sHeader As String) As Range
Dim rv As Range, c As Range
For Each c In rng.Parent.Range(rng, rng.Parent.Cells(rng.Row, Columns.count).End(xlToLeft)).Cells
'copy cell value if it contains some string "holder" or "cutting tool"
If Trim(c.Value) = sHeader Then
'If InStr(c.Value, sHeader) <> 0 Then
Set rv = c
Exit For
End If
Next c
Set HeaderCell = rv
End Function
'(10)
'gets the last row in designated column
Function GetLastRowInColumn(theWorksheet As Worksheet, col As String)
With theWorksheet
GetLastRowInColumn = .Range(col & .Rows.count).End(xlUp).Row
End With
End Function
'(11)
'gets the last row in designated sheet
Function GetLastRowInSheet(theWorksheet As Worksheet)
Dim ret
With theWorksheet
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
ret = .Cells.Find(What:="*", _
After:=.Range("A1"), _
LookAt:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
Else
ret = 1
End If
End With
GetLastRowInSheet = ret
End Function
'(12)
'get the file name without the extension
Function GetFilenameWithoutExtension(ByVal FileName)
Dim Result, i
Result = FileName
i = InStrRev(FileName, ".")
If (i > 0) Then
Result = Mid(FileName, 1, i - 1)
End If
GetFilenameWithoutExtension = Result
End Function
'(13)
Public Function ExtractNumberWithLeadingZeroes(ByRef theWholeText As String, ByRef idText As String, ByRef numCharsRequired As Integer) As String
' Finds the first entry of idText, TL/CT, in theWholeText
' Returns the first number found after idText formatted with leading zeroes
Dim returnValue As String
Dim extraValue As String
Dim tmpText As String
Dim firstPosn As Integer
Dim secondPosn As Integer
Dim ctNumberPosn As Integer
returnValue = ""
firstPosn = InStr(1, theWholeText, idText)
If firstPosn > 0 Then
' remove any text before first idText, also remove the first idText
tmpText = Mid(theWholeText, firstPosn + Len(idText))
'if more than one idText value, delete everything after (and including) the second idText
secondPosn = InStr(1, tmpText, idText)
If secondPosn > 0 Then
tmpText = Mid(tmpText, 1, secondPosn)
End If
returnValue = ExtractTheFirstNumericValues(tmpText, 1)
If idText = "CT" Then
ctNumberPosn = InStr(1, tmpText, returnValue)
' Is the next char a dash? If so, must include more numbers
If Mid(tmpText, ctNumberPosn + Len(returnValue), 1) = "-" Then
' There are some more numbers, after the dash, to extract
extraValue = ExtractTheFirstNumericValues(tmpText, ctNumberPosn + Len(returnValue))
End If
End If
'force to numCharsRequired numbers if too short; add 0s immediately after idText
'force to numCharsRequired numbers if too long; eliminate 0s immediately after idText
' The CLng gets rid of leading zeroes and the Format$ adds any required up to numCharsRequired chars
If returnValue <> "" Then
returnValue = Format$(CLng(returnValue), String(numCharsRequired, "0"))
If extraValue <> "" Then
returnValue = returnValue & "-" & extraValue
End If
End If
End If
ExtractNumberWithLeadingZeroes = returnValue
End Function
'(14)
Private Function ExtractTheFirstNumericValues(ByRef theText As String, ByRef theStartingPosition As Integer) As String
Dim i As Integer
Dim j As Integer
Dim tmpText As String
Dim thisChar As String
' Find first number
For i = theStartingPosition To Len(theText)
If IsNumeric(Mid(theText, i, 1)) Then
tmpText = Mid(theText, i)
Exit For
End If
Next i
' Find where the numbers end
For j = 1 To Len(tmpText)
thisChar = Mid(tmpText, j, 1)
If Not IsNumeric(thisChar) Then
tmpText = Mid(tmpText, 1, j - 1)
Exit For
End If
Next j
ExtractTheFirstNumericValues = tmpText
End Function

have you put in break point to see which parts are taking the time ? For example is the For loop in the first part taking very much time ? The easiest way i can see you could speed things up is any time you do a Loop, For Each Cell instead set a variable equal to that range and loop through the variable. This can insanely increase speed especially if your touch a lot of cells. In my experience basically anything to do with cells is the slowest thing in excel. I often convert everything to variables, do all my work, then drop it back down when i am done. I have cut things for 2 hours to 2 minutes doing this.
Make it faster?

A large time saver was moving the section of code that the two functions are called from outside of the looping through files. That way, it would not stop after every file to fix it but rather fix all of the final output at the end. Cut the runtime in half!

Related

Error when trying to cycle through Autofiltered Cells using vba

I'm using the below code to remove invalid instances of text, in this case statements starting with colons. I know all of the steps I need to take, but I'm having issues after Autofitering. I've tried iterating through the visible cells using
for x=1 to currentFilter.rows.count
and
for each x in currentFilter.rows
But regardless of how I've tried I keep receiving some sort of error when trying to get rid of the the first character (the colon) by using (basic gist):
Cell Value = Right(Cell Value, Len(Cell Value) - InStr(Cell Value, ",", vbTextCompare))
My full code is as follows:
Sub PRTCheck()
'Column AN is Production Time & Column AP is Rush Time
Dim endRange As Integer, ShipandRush As Range, CommaColons As Collection, cell, i
endRange = ActiveSheet.Cells(Rows.count, "AN").End(xlUp).Row
Set ShipandRush = Union(ActiveSheet.Range("AN2:AN" & endRange), ActiveSheet.Range("AP2:AP" & endRange))
ShipandRush.NumberFormat = "#"
Set CommaColons = FindAllMatches(ShipandRush, ",:")
If Not CommaColons Is Nothing Then
Dim times() As String
For Each cell In CommaColons
times = Split(cell.Value, ",")
For i = LBound(times) To UBound(times)
If InStr(times(i), ":") = 1 Then times(i) = ""
Next
cell.Value = Join(times, ",")
Do While InStr(cell.Value, ",,") <> 0
cell.Value = Replace(cell.Value, ",,", ",", vbTextCompare)
Loop
If InStr(cell.Value, ",") = 1 Then
cell.Value = Right(cell.Value, Len(cell.Value) - 1)
End If
If InStr(Len(cell.Value), cell.Value, ",") = Len(cell.Value) Then
cell.Value = Left(cell.Value, Len(cell.Value) - 1)
End If
Next cell
End If
Set ShipandRush = ActiveSheet.Range("AN1:AN" & endRange)
Dim currentFilter As Range, r
ShipandRush.AutoFilter Field:=1, Criteria1:=":*" 'Starts with colon
Set currentFilter = ShipandRush.Offset(1).SpecialCells(xlCellTypeVisible)
If currentFilter.Rows.count > 0 Then
For r = 1 To currentFilter.Rows.count
'-------Error occurs on the next line-------
currentFilter.Cells(r).Value = Right(currentFilter.Cells(r).Value, Len(currentFilter.Cells(r).Value) - InStr(currentFilter.Cells(r).Value, ",", vbTextCompare))
Next
End If
ActiveSheet.AutoFilterMode = False
End Sub
'Custom find and replace that circumvents 255 character find limitation
Function FindAllMatches(rng As Range, txt As String) As Collection
Dim rv As New Collection, f As Range, addr As String, txtSrch As String
Dim IsLong As Boolean
IsLong = Len(txt) > 250
txtSrch = IIf(IsLong, Left(txt, 250), txt)
Set f = rng.Find(what:=txtSrch, lookat:=xlPart, LookIn:=xlValues, MatchCase:=False)
Do While Not f Is Nothing
If f.Address(False, False) = addr Then Exit Do
If Len(addr) = 0 Then addr = f.Address(False, False)
'check for the *full* value (case-insensitive)
If InStr(1, f.Value, txt, vbTextCompare) > 0 Then rv.Add f
Set f = rng.FindNext(After:=f)
Loop
Set FindAllMatches = rv
End Function
My Question:
What am I doing wrong? How can I iterate through each value in the visible cells and perform the formula I noted above successfully?
You are really only dealing with a single column but I will try to stick with your method of looping through the rows instead of the cells which in this instance are essentially the same thing (although Range.Rows is not the same thing as Range.Cells).
Discontiguous ranges need to be cycled through by their Range.Areas property first and then the Range.Rows property within each area.
dim a as long, r as long
with currentFilter
If .Rows.count > 0 Then
for a = 1 to .Areas.count
For r = 1 To .Areas(a).Rows.count
.Areas(a).Rows(r).Cells(1).Value = _
Right(.Areas(a).Rows(r).Cells(1).Value, _
Len(.Areas(a).Rows(r).Cells(1).Value) - _
InStr(1, .Areas(a).Rows(r).Cells(1).Value, ","))
Next r
Next a
End If
end with
It may be simpler to just use a For Each ... Next.
dim cfr as range
with currentFilter
for each cfr in .Cells
cfr = Right(cfr.Value, Len(cfr.Value) - InStr(1, cfr.Value, ","))
Next cfr
end with

always print out 6 digits following specific string

I have many strings with the name "TL-" followed by 6 digits (ie TL-000456, TL-000598). Sometimes it will print out having fewer than 6 digits (ie TL-09872, TL-345, TL-02).
I want my code to add a zero after the "TL-" until it contains 6 digits.
Start: Output:
TL-000456 -> TL-000456
TL-000598 -> TL-000598
TL-09872 -> TL-009872
TL-345 -> TL-000345
TL-02 -> TL-000002
If possible, I would like it to do this so that even if a space is included in the string (ie "TL - ", "TL -"), 6 digits would always be grabbed.
TL - 987 -> TL-000987
TL- 839 -> TL-000839
I have a function in my code which trims the "TL" values to get everything before a semicolon or comma so ideally the code would go in there. Thoughts?
CURRENT ATTEMPTS GIVEN COMMENTS:
Code gets values from under the header "CUTTING TOOL" in the ws (worksheet) and prints it to the StartSht (workbook with code)
(1) Returns error on Trim line saying in valid procedure or argument
With WB
For Each ws In .Worksheets
Dim sIn, sOut As String
'find CUTTING TOOL on the source sheet
If Not ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues) Is Nothing Then
Set hc = ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues)
Set dict = GetValues(hc.Offset(1, 0), "SplitMe")
If dict.count > 0 Then
'add the values to the workbook, column 3
Set d = StartSht.Cells(Rows.count, hc2.Column).End(xlUp).Offset(1, 0)
d.Resize(dict.count, 1).Value = Application.Transpose(dict.items)
'trim values **implement new code here**
With StartSht
Trim (Left(sIn, InStr(1, sIn, "-", vbTextCompare) - 1)) & "-" & Right("000000" & Trim(Right(sIn, Len(sIn) - InStr(1, sIn, "-", vbTextCompare))), 6)
End With
(2) Runs fully but does not change the values
With WB
For Each ws In .Worksheets
'find CUTTING TOOL on the source sheet
If Not ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues) Is Nothing Then
Set hc = ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues)
Set dict = GetValues(hc.Offset(1, 0), "SplitMe")
If dict.count > 0 Then
'add the values to the master list, column 3
Set d = StartSht.Cells(Rows.count, hc2.Column).End(xlUp).Offset(1, 0)
d.Resize(dict.count, 1).Value = Application.Transpose(dict.items)
Dim str As String, ret As String, tmp As String, j As Integer
With StartSht
For j = 1 To Len(str)
tmp = Mid(str, j, 1)
If IsNumeric(tmp) Then ret = ret + tmp
Next j
For j = Len(ret) + 1 To 6
ret = "0" & ret
Next
Debug.Print ret
End With
StartSht Excel document looks like this
A B C D
1 TDS HOLDER CUTTING TOOL File Name
2 TDS-1 H1 TL-000289 TDS-1.xlsx
3 TDS-2 H2 TL-000274 TDS-2.xlsx
4 TDS-3 H3 TL-0002 TDS-3.xlsx
5 TDS-4 H4 TL-0343 TDS-4.xlsx
after the "CUTTING TOOL" code I have below, it just looks like the output below the code because that is the first section I grab information for
CODE:
With WB
For Each ws In .Worksheets
'find CUTTING TOOL on the source sheet
If Not ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues) Is Nothing Then
Set hc = ws.Range("A1:M15").Find(What:="CUTTING TOOL", LookAt:=xlWhole, LookIn:=xlValues)
Set dict = GetValues(hc.Offset(1, 0), "SplitMe")
If dict.count > 0 Then
'add the values to the master list, column 3
Set d = StartSht.Cells(Rows.count, hc2.Column).End(xlUp).Offset(1, 0)
d.Resize(dict.count, 1).Value = Application.Transpose(dict.items)
output of StartSht:
A B C D
1 TDS HOLDER CUTTING TOOL File Name
2 TL-000289
3 TL-000274
4 TL-0002
5 TL-0343
I want to add a line str = StartSht.Range(''set correct range here'') and then code to make the StartSht look like this
A B C D
1 TDS HOLDER CUTTING TOOL File Name
2 TL-000289
3 TL-000274
4 TL-000002
5 TL-000343
There is a way using an excel formula:
="TL-" & TEXT(TRIM(RIGHT(A1,LEN(A1)-FIND("-",A1,1))),"000000")
Expanding on Orphid's anwswer to include the 6 digits:
Sub Test()
Dim str as string, ret as string, tmp as string, i as integer, j as integer
for j = 2 to StartSht.Range("C2").End(xlDown).Row
ret = ""
str = StartSht.Range("C" & j).Value
for i = 1 to len(str)
tmp = mid(str, i, 1)
if IsNumeric(tmp) then ret = ret + tmp
next i
For i = Len(ret) + 1 To 6
ret = "0" & ret
Next
ret = "TL-" & ret
StartSht.Range("C" & j).Value = ret
next j
End Sub
This is going to write 'ret' in column B beside the original. The sheet you are working on needs to be active when this runs because as you can see I didn't specify which Sheet was to be used. You can do that yourself if it's necessary. I assumed it only needed to be done on 1 worksheet of 1 workbook for this. Let me know if i was wrong.
What have you tried so far? Do you have any code to show us?
This should be a starting point, you'll need to strip out spaces and loop through the whole file of course.
Public Sub PaddingTest()
Dim PaddingArray() As String
Dim PaddingVar As String
PaddingArray() = Split(Range("A1").Value, "-", 2, vbTextCompare)
PaddingVar = PaddingArray(1)
While Len(PaddingVar) < 6
PaddingVar = "0" & PaddingVar
Wend
Range("A2").Value = PaddingArray(0) & "-" & PaddingVar
End Sub
msdn.microsoft.com for usage of Split command
For extracting the number, it sounds like what you want is a regular expression similar to \d{1,6}. However, I've never really enjoyed working regex in VBA, so another way of extracting the number is:
Sub Test()
Dim str as string, ret as string, tmp as string, i as integer
str = "T- 087652"
for i = 1 to len(str) 'vba strings are 1-indexed
tmp = mid(str, i, 1) 'get the character at position i
if IsNumeric(tmp) then ret = ret + temp 'if numeric, add to the return value
next i
debug.print ret 'print the resulting number to the console. To convert to a number, simply assign to a variable typed as "long"
End Sub
What this does is a simple forward loop through the string, extracting every character which IsNumeric. It should ignore whitespace wherever it occurs in the string, but they shouldn't be more than one whole number per string.
For formatting the number, you probably just want to pad the string.
Here is a one liner. I am grabbing the data before and after the hypen, trimming them to remove spaces, and adding the hyphen and extra 0's.
Sub splitAddZeros()
Dim sIn, sOut As String
sIn = "TL - 987"
out = Trim(Left(sIn, InStr(1, sIn, "-", vbTextCompare) - 1)) & "-" & Right("000000" & Trim(Right(sIn, Len(sIn) - InStr(1, sIn, "-", vbTextCompare))), 6)
Debug.Print out
End Sub
Put this in a new module:
Option Explicit
Public Function getDigits(strInput As String) As String
Dim strOutput As String
Dim strCharacter As String
Dim i As Integer
strOutput = ""
For i = 1 To Len(strInput)
strCharacter = Mid(strInput, i, 1)
If strCharacter >= "0" And strCharacter <= "9" Then
strOutput = strOutput & strCharacter
End If
Next
getDigits = strOutput
End Function
Public Function addZeros(strInput As String) As String
Dim intCurrentLength As Integer
Dim strNumber As String
Dim i As Integer
strNumber = getDigits(strInput)
intCurrentLength = Len(strNumber)
If intCurrentLength < 6 Then
For i = 1 To 6 - intCurrentLength
strNumber = "0" & strNumber
Next i
End If
addZeros = "TL-" & strNumber
End Function
Then just run addZeros([your string here]) to convert to the required format.
(for user4888 in the comments of this question; an example of how to check whether 'TL' is in a string. This checks cells A1 to A10, and populates a 1 or a 0 in the corresponding cell in column B depending on whether there is a 'TL' in the cell)
Private Sub TLcheck()
Set wb = ThisWorkbook
Set ws = wb.ActiveSheet
For i = 1 To 10
ws.Cells(i, 2) = InStr(1, ws.Cells(i, 1), "TL")
Next i
End Sub

Excel VBA Macro to get the substring of text before semicolon

I have working code here.
In section (3) it grabs values from a cell under a specific header and prints them to a masterfile. These values typically look like
TL-18273982; 10MM
TL-288762; 76DK
CT-576
N/A
I would like to grab just the information that is before the first semicolon. Not all the cells have a semi colon in them so it would probably need an if statement along the lines of if ; then print everything in front of it.
I have been trying to utilize a split function to do this but I am not very experienced with VBA so I am having some trouble. Any suggestions?
Option Explicit
Sub LoopThroughDirectory()
Const ROW_HEADER As Long = 10
Dim objFSO As Object
Dim objFolder As Object
Dim objFile As Object
Dim MyFolder As String
Dim StartSht As Worksheet, ws As Worksheet
Dim WB As Workbook
Dim i As Integer
Dim LastRow As Integer, erow As Integer
Dim Height As Integer
Dim RowLast As Long
Dim f As String
Dim dict As Object
Dim hc As Range, hc1 As Range, hc2 As Range, hc3 As Range, d As Range
Set StartSht = Workbooks("masterfile.xlsm").Sheets("Sheet1")
'turn screen updating off - makes program faster
Application.ScreenUpdating = False
'location of the folder in which the desired TDS files are
MyFolder = "C:\Users\trembos\Documents\TDS\progress\"
'find the headers on the sheet
Set hc1 = HeaderCell(StartSht.Range("B1"), "HOLDER")
Set hc2 = HeaderCell(StartSht.Range("C1"), "CUTTING TOOL")
'create an instance of the FileSystemObject
Set objFSO = CreateObject("Scripting.FileSystemObject")
'get the folder object
Set objFolder = objFSO.GetFolder(MyFolder)
i = 2
'loop through directory file and print names
'(1)
For Each objFile In objFolder.Files
If LCase(Right(objFile.Name, 3)) = "xls" Or LCase(Left(Right(objFile.Name, 4), 3)) = "xls" Then
'(2)
'Open folder and file name, do not update links
Set WB = Workbooks.Open(fileName:=MyFolder & objFile.Name, UpdateLinks:=0)
Set ws = WB.ActiveSheet
'(3)
'find CUTTING TOOL on the source sheet
Set hc = HeaderCell(ws.Cells(ROW_HEADER, 1), "CUTTING TOOL")
If Not hc Is Nothing Then
Set dict = GetValues(hc.Offset(1, 0))
If dict.count > 0 Then
Set d = StartSht.Cells(Rows.count, hc2.Column).End(xlUp).Offset(1, 0)
'add the values to the masterfile, column 3
d.Resize(dict.count, 1).Value = Application.Transpose(dict.items)
End If
Else
'header not found on source worksheet
End If
'(4)
'find HOLDER on the source sheet
Set hc3 = HeaderCell(ws.Cells(ROW_HEADER, 1), "HOLDER")
If Not hc3 Is Nothing Then
Set dict = GetValues(hc3.Offset(1, 0))
If dict.count > 0 Then
Set d = StartSht.Cells(Rows.count, hc1.Column).End(xlUp).Offset(1, 0)
'add the values to the master list, column 2
d.Resize(dict.count, 1).Value = Application.Transpose(dict.items)
End If
Else
'header not found on source worksheet
End If
'(5)
With WB
'print TDS information
For Each ws In .Worksheets
'print the file name to Column 1
StartSht.Cells(i, 1) = objFile.Name
'print TDS name from J1 cell to Column 4
With ws
.Range("J1").Copy StartSht.Cells(i, 4)
End With
i = GetLastRowInSheet(StartSht) + 1
'move to next file
Next ws
'(6)
'close, do not save any changes to the opened files
.Close SaveChanges:=False
End With
End If
'move to next file
Next objFile
'turn screen updating back on
Application.ScreenUpdating = True
ActiveWindow.ScrollRow = 1
'(7)
End Sub
'(8)
'get all unique column values starting at cell c
Function GetValues(ch As Range) As Object
Dim dict As Object, rng As Range, c As Range, v
Set dict = CreateObject("scripting.dictionary")
For Each c In ch.Parent.Range(ch, ch.Parent.Cells(Rows.count, ch.Column).End(xlUp)).Cells
v = Trim(c.Value)
If Len(v) > 0 And Not dict.exists(v) Then
dict.Add c.Address, v
End If
Next c
Set GetValues = dict
End Function
'(9)
'find a header on a row: returns Nothing if not found
Function HeaderCell(rng As Range, sHeader As String) As Range
Dim rv As Range, c As Range
For Each c In rng.Parent.Range(rng, rng.Parent.Cells(rng.Row, Columns.count).End(xlToLeft)).Cells
If Trim(c.Value) = sHeader Then
Set rv = c
Exit For
End If
Next c
Set HeaderCell = rv
End Function
'(10)
Function GetLastRowInColumn(theWorksheet As Worksheet, col As String)
With theWorksheet
GetLastRowInColumn = .Range(col & .Rows.count).End(xlUp).Row
End With
End Function
'(11)
Function GetLastRowInSheet(theWorksheet As Worksheet)
Dim ret
With theWorksheet
If Application.WorksheetFunction.CountA(.Cells) <> 0 Then
ret = .Cells.Find(What:="*", _
After:=.Range("A1"), _
Lookat:=xlPart, _
LookIn:=xlFormulas, _
SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, _
MatchCase:=False).Row
Else
ret = 1
End If
End With
GetLastRowInSheet = ret
End Function
If you one use Split function, you will love it -> https://msdn.microsoft.com/en-us/library/6x627e5f%28v=vs.90%29.aspx
Look for this example:
Sub TestSplit()
Dim String1 As String
Dim Arr1 As Variant
String1 = "TL-18273982; 10MM"
Arr1 = Split(String1, ";")
Debug.Print "TEST1: String1=" & String1
Debug.Print "TEST1: Arr1(0)=" & Arr1(0)
Debug.Print "TEST1: Arr1(1)=" & Arr1(1)
String1 = "CT-576"
Arr1 = Split(String1, ";")
Debug.Print "TEST2: String1=" & String1
Debug.Print "TEST2: Arr1(0)=" & Arr1(0)
String1 = "N/A"
Arr1 = Split(String1, ";")
Debug.Print "TEST3: String1=" & String1
Debug.Print "TEST3: Arr1(0)=" & Arr1(0)
End Sub
Results:
TEST1: String1=TL-18273982; 10MM
TEST1: Arr1(0)=TL-18273982
TEST1: Arr1(1)= 10MM
TEST2: String1=CT-576
TEST2: Arr1(0)=CT-576
TEST3: String1=N/A
TEST3: Arr1(0)=N/A
Edit:
Maybe simple modification GetValues will resolve problem?
Change function call to:
Set dict = GetValues(hc.Offset(1, 0), "SplitMe")
And change function like this:
'(8)
'get all unique column values starting at cell c
Function GetValues(ch As Range, Optional vSplit As Variant) As Object
Dim dict As Object, rng As Range, c As Range, v
Dim spl As Variant
Set dict = CreateObject("scripting.dictionary")
For Each c In ch.Parent.Range(ch, ch.Parent.Cells(Rows.Count, ch.Column).End(xlUp)).Cells
v = Trim(c.Value)
If Len(v) > 0 And Not dict.exists(v) Then
If Not IsMissing(vSplit) Then
spl = Split(v, ";")
v = spl(0)
End If
dict.Add c.Address, v
End If
Next c
Set GetValues = dict
End Function
Consider:
Public Function PreSemicolon(sIN As String) As String
If InStr(sIN, ";") = 0 Then
PreSemicolon = ""
Exit Function
Else
PreSemicolon = Split(sIN, ";")(0)
End If
End Function
The following VBA code snippet demonstrates the possible solution assuming the text is entered in "A1" cell (Note: it does not need Split() Function):
Sub GetSubstringDemo()
Dim position As Integer
Dim substring As String
position = InStr(Cells(1, 1), ";")
If (position > 0) Then
substring = Left(Cells(1, 1), position - 1)
'or use the following one to exclude "["
'substring = Replace(Left(Cells(1, 1), position - 1), "[", "")
Debug.Print substring
End If
End Sub
The same Sub can be extended to loop through the range of cells (e.g. A1 to A10):
Sub GetSubstringDemo()
Dim position As Integer
Dim substring As String
For i = 1 To 10
position = InStr(Cells(i, 1), ";")
If (position > 0) Then
substring = Replace(Left(Cells(i, 1), position - 1), "[", "")
Debug.Print substring
End If
Next i
End Sub
Hope this may help.
PS. Pertinent to you additional question in comments: business logic is a bit unclear, but following that sample code, it could be modified as :
Set dict = GetValues(hc.Offset(1, 0))
If dict.count > 0 Then
Set d = StartSht.Cells(Rows.count, hc2.Column).End(xlUp).Offset(1, 0)
position = InStr(d.Value, ";")
substring = Replace(Left(d.Value, position - 1), ";", "")
d.Resize(dict.count, 1).Value = Application.Transpose(dict.items)
End If
Best Regards,
Try something like that.I'm not sure about your variables, you might have to adapt them.
You can use instr to locate a character within the string (returning the position of '[' for example). You can then use mid to extract a substing, using the positions of ']' and '['.
openPos = instr (hc , "[") closePos = instr (hc , ";")
if closePos = 0 then
closePos = instr (hc , "]")
end if
dict = mid (hc , openPos+1, closePos - openPos - 1)

Fairly involved debugging of a VBA searching function in Excel, not sure where the issue is

I've been working on a searching function for Excel documents using a VBA macro. I had it working just before I made some changes that added the ability to identify cells already queried so I can include an "Others" column in my results for cells that didn't match any of the search terms. I need this column to display the actual cell value, but all other results columns can just show cell addresses of cells that contain the search term.
Currently, my program opens a text file of the user's choosing that contains just search terms, line by line. The program reads the file correctly, as the results worksheet populates with the search terms correctly, but the results don't populate with any actual results. My code for all relevant parts of the program is below:
Option Explicit
Option Compare Text
Private inputString As String, strPath As String, strLine As String
Private resultsCol As Integer, numberofOccurrences As Integer, searchTerms As Integer, intResult As Integer
Private wsTest As Worksheet
Private dataChart As Chart
Sub List_based_word_query()
Dim targetRng As Range
resultsCol = 2
searchTerms = 0
'Create new worksheet for List Results
Set wsTest = Nothing
On Error Resume Next
Set wsTest = ActiveWorkbook.Sheets("List Results")
On Error GoTo 0
If wsTest Is Nothing Then
Worksheets.Add.Name = "List Results"
End If
'Clear the List Results Sheet
Worksheets("List Results").Cells.Clear
On Error Resume Next
Worksheets("List Results").ChartObjects.Delete
On Error GoTo 0
Application.FileDialog(msoFileDialogOpen).AllowMultiSelect = False
intResult = Application.FileDialog(msoFileDialogOpen).Show
If intResult <> 0 Then
strPath = Application.FileDialog(msoFileDialogOpen).SelectedItems(1)
On Error Resume Next
Open strPath For Input As #1
Line Input #1, strLine 'first line
Do
inputString = strLine 'inputString = first line
Line Input #1, strLine 'second line
numberofOccurrences = Single_word_occurrences(inputString, ColumnLetter(resultsCol))
Sheets("List Results").Cells(2, ColumnLetter(resultsCol)).Value = numberofOccurrences
resultsCol = resultsCol + 1
searchTerms = searchTerms + 1
Loop While EOF(1) = False
inputString = strLine 'the loop exits when at the last word, so we have to run one more time
Line Input #1, strLine
numberofOccurrences = Single_word_occurrences(inputString, ColumnLetter(resultsCol))
Sheets("List Results").Cells(2, ColumnLetter(resultsCol)).Value = numberofOccurrences
resultsCol = resultsCol + 1
searchTerms = searchTerms + 1
End If
Close #1
Exit Sub
End Sub
Function ColumnLetter(ColumnNumber As Integer) As String 'This function takes the column number and converts it to a letter
Dim n As Integer
Dim c As Byte
Dim s As String
n = ColumnNumber
Do
c = ((n - 1) Mod 26)
s = Chr(c + 65) & s
n = (n - c) \ 26
Loop While n > 0
ColumnLetter = s
End Function
Function Others(startCell As Range, endCell As Range) As Integer
Dim i As Long
For i = startCell.Row To endCell.Row Step 1
If startCell.Offset(0, i).Address = endCell.Address Then
Others = i
Exit Function
End If
Sheets("List Results").Cells(2 + i, ColumnLetter(1)).Value = endCell.Value
Next
End Function
Function Single_word_occurrences(datatoFind As String, resultsCol As String) As Integer
'Initializations
Dim strFirstAddress As String
Dim foundRange As Range, LastAddress As Range
Dim currentSheet As Integer, sheetCount As Integer, LastRow As Integer, loopedOnce As Integer, FoundCount As Integer, numberofOthers As Integer
loopedOnce = 0
FoundCount = 0
currentSheet = ActiveSheet.Index
sheetCount = ActiveWorkbook.Sheets.Count
Sheets("Sheet1").Activate
Set foundRange = Range("F2:F30000").Find(What:=datatoFind, After:=Cells(2, 6), LookIn:=xlFormulas, LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False)
Msgbox(foundRange)
Sheets("List Results").Cells(1, resultsCol).Value = datatoFind
If Not foundRange Is Nothing Then 'if datatoFind is found in search range
strFirstAddress = foundRange.Address 'strFirstAddress = address of first occurrence of datatoFind
Do 'Find next occurrence of datatoFind
Set foundRange = Range("F2:F30000").Find(What:=datatoFind, After:=foundRange.Cells, LookIn:=xlFormulas, LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:=False)
'Place the value of this occurrence in the next cell down in the column that holds found values (resultsCol column of List Results worksheet)
LastRow = Sheets("List Results").Range(resultsCol & Rows.Count).End(xlUp).Row + 1
Sheets("List Results").Range(resultsCol & LastRow).Value = foundRange.Address
numberofOthers = Others(LastAddress, foundRange)
Sheets("List Results").Cells(2, ColumnLetter(1)).Value = numberofOthers
If loopedOnce = 1 Then
FoundCount = FoundCount + 1
End If
If loopedOnce = 0 Then
loopedOnce = 1
End If
LastAddress = foundRange.Address
'The Loop ends on reaching the first occurrence of datatoFind
Loop While foundRange.Address <> strFirstAddress And Not foundRange Is Nothing
End If
Single_word_occurrences = FoundCount
Application.ScreenUpdating = True
Sheets(currentSheet).Activate
End Function
The program runs without any errors. I'm assuming there is a logical error in the Single_word_occurrences function, but I can't for the life of me find it. Any ideas?
I think that this
Sheets("List Results").Cells(2, ColumnLetter(1)).Value = numberofOthers
in Single_word_occurrences, should be
Sheets("List Results").Cells(1, 2).Value = numberofOthers
if search terms are i.e in column A and numberofothers in ColumnB. If they are in same column, data will be overwritten.

Trying to extract data from curly braces but not working

I need to sync up the values in the curly braces {} found in column C and put them against the user id in column F as seen below.
E.g. on the Emails sheet
becomes this on a new sheet
Sub CopyConditional()
Dim wshS As Worksheet
Dim WhichName As String
Set wshS = ActiveWorkbook.Sheets("Emails")
WhichName = "NewSheet"
Const NameCol = "C"
Const FirstRow = 1
Dim LastRow As Long
Dim SrcRow As Long
Dim TrgRow As Long
Dim wshT As Worksheet
Dim cpt As String
Dim user As String
Dim computers() As String
Dim computer As String
On Error Resume Next
Set wshT = Worksheets(WhichName)
If wshT Is Nothing Then
Set wshT = Worksheets.Add(After:=wshS)
wshT.Name = WhichName
End If
On Error GoTo 0
If wshT.Cells(1, NameCol).value = "" Then
TrgRow = 1
Else
TrgRow = wshT.Cells(wshT.Rows.Count, NameCol).End(xlUp).Row + 1
End If
LastRow = wshS.Cells(wshS.Rows.Count, NameCol).End(xlUp).Row
For SrcRow = FirstRow To LastRow
cpt = wshS.Range("C" & SrcRow).value
user = wshS.Range("F" & SrcRow).value
If InStr(cpt, ":") Then
cpt = Mid(cpt, InStr(1, cpt, ":") + 1, Len(cpt))
End If
If InStr(cpt, ";") Then
computers = Split(cpt, ";")
For i = 0 To UBound(computers)
If computers(i) <> "" Then
wshT.Range("A" & TrgRow).value = user
wshT.Range("B" & TrgRow).value = Mid(Left(computers(i), Len(computers(i)) - 1), 2)
TrgRow = TrgRow + 1
End If
Next
Else
computer = cpt
If computer <> "" Then
wshT.Range("A" & TrgRow).value = user
wshT.Range("B" & TrgRow).value = Mid(Left(computer, Len(computer) - 1), 2)
TrgRow = TrgRow + 1
End If
End If
Next SrcRow
End Sub
I managed to resolve it with the above code but there are 3 niggling issues:
1) The first curly brace is always copied, how do I omit this so something like {Computer1 looks like Computer 1
2) Where there are two computers in a row, then the output looks something like this:
when it should really be split into two different rows i.e.
User 1 | Computer 1
User 1 | Computer 2
3) If there is text after the last curly brace with text in it e.g. {Computer1};{Computer2};Request submitted then that text is added as a new row, I don't want this, I want it to be omitted e.g.
should just be:
User 1 | Computer 1
User 1 | Computer 2
How do I go about rectifying these issues?
Try this:
Sub Collapse()
Dim uRng As Range, cel As Range
Dim comps As Variant, comp As Variant, r As Variant, v As Variant
Dim d As Dictionary '~~> Early bind, for Late bind use commented line
'Dim d As Object
Dim a As String
With Sheet1 '~~> Sheet that contains your data
Set uRng = .Range("F1", .Range("F" & .Rows.Count).End(xlUp))
End With
Set d = CreateObject("Scripting.Dictionary")
With d
For Each cel In uRng
a = Replace(cel.Offset(0, -3), "{", "}")
comps = Split(a, "}")
Debug.Print UBound(comps)
For Each comp In comps
If InStr(comp, "Computer") <> 0 _
And Len(Trim(comp)) <= 10 Then '~~> I assumed max Comp# is 99
If Not .Exists(cel) Then
.Add cel, comp
Else
If IsArray(.Item(cel)) Then
r = .Item(cel)
ReDim Preserve r(UBound(r) + 1)
r(UBound(r)) = comp
.Item(cel) = r
Else
r = Array(.Item(cel), comp)
.Item(cel) = r
End If
End If
End If
Next
Next
End With
For Each v In d.Keys
With Sheet2 '~~> sheet you want to write your data to
If IsArray(d.Item(v)) Then
.Range("A" & .Rows.Count).End(xlUp).Offset(1, 0) _
.Resize(UBound(d.Item(v)) + 1) = v
.Range("B" & .Rows.Count).End(xlUp).Offset(1, 0) _
.Resize(UBound(d.Item(v)) + 1) = Application.Transpose(d.Item(v))
Else
.Range("A" & .Rows.Count).End(xlUp).Offset(1, 0) = v
.Range("B" & .Rows.Count).End(xlUp).Offset(1, 0) = d.Item(v)
End If
End With
Next
Set d = Nothing
End Sub
Above code uses Replace and Split Function to pass your string to array.
a = Replace(cel.Offset(0, -3), "{", "}") '~~> standardize delimiter
comps = Split(a, "}") '~~> split using standard delimiter
Then information are passed to dictionary object using User as key and computers as items.
We filter the items passed to dictionary using Instr and Len Function
If InStr(comp, "Computer") <> 0 _
And Len(Trim(comp)) <= 10 Then
As I've commented, I assumed your max computer number is 99.
Else change 10 to whatever length you need to check.
Finally we return the dictionary information to the target worksheet.
Note: You need to add reference to Microsoft Scripting Runtime if you prefer early bind
Result: I tried it on a small sample data patterned on how I see it in you SS.
So assuming you have this data in Sheet1:
Will output data in Sheet2 like this:
I use a custom parse function for this type of operation:
Sub CopyConditional()
' some detail left out
Dim iRow&, Usern$, Computer$, Computers$
For iRow = ' firstrow To lastrow
Usern = Sheets("Emails").Cells(iRow, "F")
Computers = Sheets("Emails").Cells(iRow, "C")
Do
Computer = zParse(Computers) ' gets one computer
If Computer = "" Then Exit Do
' Store Computer and Usern
Loop
Next iRow
End Sub
Function zParse$(Haystack$) ' find all {..}
Static iPosL& '
Dim iPosR&
If iPosL = 0 Then iPosL = 1
iPosL = InStr(iPosL, Haystack, "{") ' Left
If iPosL = 0 Then Exit Function ' no more
iPosR = InStr(iPosL, Haystack, "}") ' Right
If iPosR = 0 Then MsgBox "No matching }": Stop
zParse = Mid$(Haystack, iPosL + 1, iPosR - iPosL - 1)
iPosL = iPosR
End Function
1) Use the Mid function to drop the first character:
str = "{Computer1"
str = Mid(str,2)
now str = "Computer1"
2) You can use the Split function to separate these out and combine with the Mid function above
str = "{Computer1}{Computer2}"
splt = Split(str,"}")
for a = 0 to Ubound(splt)
result = Mid(splt(a),2)
next a
3) Add a conditional statement to the above loop
str = "{Computer1}{Computer2}"
splt = Split(str,"}")
for a = 0 to Ubound(splt)
if Left(splt(a),1) = "{" then result = Mid(splt(a),2)
next a
Use this loop and send each result to the desired cell (in the for-next loop) and you should be good to go.