5
\$\begingroup\$

I have a procedure for looping through all files in folders and subfolders starting at a folder which the user can select. The user can select both the source and target folder. I'm using Excel VBA for this. The worksheets contain all the files names or part of it, to search for.

It works like this. I have function GetFiles which returns a string (including the path) separated by a pipeline (|). Then I loop through all the cells in column A which contains the filename (or part of it) to search for. The GetFiles loops through all the folders and subfolders from the selected Source path. This takes longer if a High level of the source folder is selected.

The main function looks like this:

Sub MoveFilesToFolder()
 Dim filePath As String: filePath = ""
 Dim moveToPath As String: moveToPath = ""
 Dim filename As String
 Dim fileNameFront As String
 Dim fileNameRear As String
 Dim currentFileName As String
 Dim cell As Range
 Dim fileCopied As Boolean: fileCopied = False
 Dim i As Integer
 Dim J As Long
 Dim StartTime As Double
 Dim SecondsElapsed As Double
 Dim result As String
 Dim ws As Worksheet
 Dim frm As ufImageSearcher
 ExactMatch = True
 OverwriteExistingFile = False
 Application.DisplayAlerts = False
 Application.ScreenUpdating = False
 On Error GoTo ErrorHandling
 If (wsExists("Images")) Then
 fileNameString = ""
 'filePath = InputBox("Path to the files, close with backslash (\)", "Source folder", ActiveWorkbook.Path)
 'moveToPath = InputBox("Path to copy files to! Close with backslash (\)", "Target folder", ActiveWorkbook.Path & "\copy\")
 filePath = GetFolderPath("Bron directory")
 If (IsStringEmpty(filePath)) Then
 Exit Sub
 End If
 moveToPath = GetFolderPath("Doel directory")
 If (IsStringEmpty(moveToPath)) Then
 Exit Sub
 End If
 If Not (IsStringEmpty(filePath) Or IsStringEmpty(moveToPath)) Then
 If ((FolderExists(filePath)) And _
 (FolderExists(moveToPath))) And (filePath <> moveToPath) Then
 If Right(moveToPath, 1) <> "\" Then
 moveToPath = moveToPath & "\"
 End If
 If (Dir(moveToPath & "*.*") <> "") Then
 result = MsgBox(moveToPath & " contains files! Choose an empty folder!" & _
 vbCrLf & vbCrLf & "Go to folder: " & moveToPath & "?", vbYesNo + vbQuestion, "Result!")
 If (result = vbYes) Then
 OpenFolderInExplorer (moveToPath)
 End If
 Exit Sub
 End If
 wsActivate ("Images")
 Set frm = New ufImageSearcher
 With frm
 .lblSource.Caption = filePath
 .lblTarget.Caption = moveToPath
 .Show
 If .Tag <> "Canceled" Then
 ExactMatch = .cbxExactMatch.Value
 OverwriteExistingFile = .cbxOverwrite.Value
 Else
 Exit Sub
 End If
 End With
 StartTime = Timer
 'Get all files, including the path, seperated with a pipeline.
 GetFiles (filePath)
 If Not (IsStringEmpty(fileNameString)) Then
 Dim imgArray As Variant: imgArray = Split(fileNameString, "|")
 'Column A contains all strings which are used to compare to the found files from the GetFiles-function 
 For Each cell In ActiveSheet.Range("A1:A" & Range("A1").End(xlDown).row)
 DoEvents
 fileCopied = False
 filename = Mid(cell.Value, lastpositionOfChar(cell.Value, "/") + 1, Len(cell.Value))
 Application.StatusBar = "(Nr. of files:" & CStr(UBound(imgArray)) & ")"
 If Not (IsStringEmpty(filename)) Then
 For i = LBound(imgArray) To UBound(imgArray)
 DoEvents
 If Not (IsStringEmpty(CStr(imgArray(i)))) Then
 If ExactMatch Then
 If (GetFileName(imgArray(i)) = filename) Then
 If DoesFileExist(moveToPath & GetFileName(imgArray(i))) And Not OverwriteExistingFile Then
 FileCopy imgArray(i), moveToPath & GetFileName(imgArray(i)) & "-" & Format(Now, "yyyymmddhhmmss")
 Else
 FileCopy imgArray(i), moveToPath & GetFileName(imgArray(i))
 End If
 fileCopied = True
 If fileCopied Then
 ActiveSheet.Range("B" & cell.row).Value = imgArray(i)
 For J = 2 To 15
 Dim newFileName As String
 newFileName = CreateFileName(CStr(imgArray(i)), LeadingZeroString(J))
 If Not (IsStringEmpty(newFileName)) Then
 If (DoesFileExist(newFileName)) Then
 If Not (IsFileOpen(newFileName)) Then
 FileCopy newFileName, moveToPath & Right(newFileName, Len(newFileName) - lastpositionOfChar(newFileName, "\") + 1)
 ActiveSheet.Range(GetColLetter(J + 1) & cell.row).Value = newFileName
 ActiveSheet.Range(GetColLetter(J + 1) & cell.row).Font.Color = RGB(0, 102, 0)
 End If
 Else
 ActiveSheet.Range(GetColLetter(J + 1) & cell.row).Value = "(Niet aanwezig) " & Right(newFileName, Len(newFileName) - lastpositionOfChar(newFileName, "\") + 1)
 ActiveSheet.Range(GetColLetter(J + 1) & cell.row).Font.Color = RGB(255, 153, 51)
 End If
 End If
 Next J
 End If
 End If
 Else
 If (InStr(1, GetFileName(imgArray(i)), filename, vbTextCompare) > 0) Then
 If Not (IsFileOpen(CStr(imgArray(i)))) Then
 If DoesFileExist(moveToPath & GetFileName(imgArray(i))) And Not OverwriteExistingFile Then
 FileCopy imgArray(i), moveToPath & GetFileName(imgArray(i)) & "-" & Format(Now, "yyyymmddhhmmss")
 Else
 FileCopy imgArray(i), moveToPath & GetFileName(imgArray(i))
 End If
 fileCopied = True
 'Find first empty columnid.
 lCol = Cells(cell.row, Columns.Count).End(xlToLeft).Column
 ActiveSheet.Cells(cell.row, lCol + 1).Value = imgArray(i)
 End If
 End If
 End If
 End If
 Next i
 If Not fileCopied Then
 ActiveSheet.Range("B" & cell.row).Value = "** NOT FOUND **"
 ActiveSheet.Range("B" & cell.row).Font.Color = RGB(250, 0, 0)
 End If
 End If
 Next
 End If
 Worksheets("Images").Columns("B:Z").AutoFit
 SecondsElapsed = Timer - StartTime
 Application.DisplayAlerts = True
 Application.ScreenUpdating = True
 result = MsgBox("Date Exported in: " & moveToPath & vbCrLf & "This was done in: " & Format(SecondsElapsed / 86400, "hh:mm:ss") & " seconds." & _
 vbCrLf & vbCrLf & "Go to folder: " & moveToPath & "?", vbYesNo + vbQuestion, "Resultaat!")
 If (result = vbYes) Then
 OpenFolderInExplorer (moveToPath)
 End If
 Else
 If Not (FolderExists(filePath)) Then
 MsgBox (filePath & ": Path is niet gevonden!")
 End If
 If Not (FolderExists(moveToPath)) Then
 MsgBox (moveToPath & ": Path is niet gevonden!")
 End If
 End If
 Else
 MsgBox ("No Source and/or Target selected" & vbCrLf & _
 "Source: " & filePath & vbCrLf & _
 "Target: " & moveToPath)
 End If
Else
 MsgBox ("This procedure expect a worksheet 'Images' " & vbCrLf & _
 "and the name or part of the name of the image to find in column A")
End If
Done:
 If (IsObject(ws)) Then
 Set ws = Nothing
 End If
 Application.DisplayAlerts = True
 Application.ScreenUpdating = True
 Exit Sub
ErrorHandling:
 MsgBox ("Something went wrong!(" & err.Description & ")")
End Sub

The GetFiles function looks like:

Sub GetFiles(ByVal path As String)
 On Error GoTo ErrorHandling
 Dim fso As Object: Set fso = CreateObject("Scripting.FileSystemObject")
 Dim folder As Object: Set folder = fso.GetFolder(path)
 Dim subfolder As Object
 Dim file As Object
 For Each subfolder In folder.SubFolders
 DoEvents
 GetFiles (subfolder.path)
 Next subfolder
 For Each file In folder.Files
 fileNameString = fileNameString & file.path & "|"
 Next file
Done:
 Set fso = Nothing
 Set folder = Nothing
 Set subfolder = Nothing
 Set file = Nothing
 Exit Sub
ErrorHandling:
 MsgBox ("Something went wrong!(" & err.Description & ")")
End Sub

It all works, but it takes a long time to run, especially when there are a lot of folders and subfolders under the selected source folder.

To give you an idea, the procedure takes 13 minutes to compare 100 rows in column A against 10.000 files found. The means it loops 100 x 10.000 = 1milion times.

I have two questions:

  1. Is there a more efficient way of doing this using Excel VBA?
  2. Is the DoEvents function used in the correct way?
Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Jun 12, 2019 at 5:42
\$\endgroup\$
3
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers . \$\endgroup\$ Commented Mar 23, 2020 at 15:19
  • 1
    \$\begingroup\$ I have rolled back Rev 7 → 4. Please see What to do when someone answers. Protip: you might be able to gain more reputation score if you opt for option 1 or 2 \$\endgroup\$ Commented Mar 23, 2020 at 15:24
  • \$\begingroup\$ This was not an update off code, but adding a piece of code which was missing in the total answer. It is more complete now. It does not influence the answer. \$\endgroup\$ Commented Mar 24, 2020 at 16:37

1 Answer 1

5
\$\begingroup\$

MoveFilesToFolder()

MoveFilesToFolder() is doing too much.

Testing filePath and moveToPath in a separate sub would greatly reduce MoveFilesToFolder() size making it easier to read, test and modify.

Private Const DirctoryBron As String = "Bron directory"
Private Const DirctoryDoel As String = "Doel directory"
Private Const WorksheetImages As String = "Images"
Sub Main()
 Dim filePath As String, moveToPath As String
 
 If Not (wsExists(WorksheetImages)) Then
 MsgBox WorksheetImages & " worksheet not found"
 Else
 filePath = GetFolderPath(DirctoryBron)
 If Len(filePath) > 0 And Not IsStringEmpty(filePath) Then
 moveToPath = GetFolderPath(DirctoryDoel)
 If Len(moveToPath) > 0 Then
 MoveFilesToFolder filePath, moveToPath
 End If
 End If
 End If
 
End Sub
Function GetFolderPath(ByVal SubFolderName As String)
 Dim filePath As String
 '..... Some Code...
 
 If Len(Dir(filePath, vbDirectory)) = 0 Then
 MsgBox (filePath & ": Path is niet gevonden!")
 Else
 GetFolderPath = filePath
 End If
End Function
Sub MoveFilesToFolder(filePath As String, moveToPath As String)
 '..... Some Code...
End Sub
Function IsStringEmpty(filePath As String) As Boolean
 If Len(Dir(filePath)) = 0 Then
 MsgBox filePath & " has no files"
 IsStringEmpty = True
 End If
End Function

GetFiles()

fileNameString should not be a global variable. It is a best practice to avoid global variables whenever possible. The name GetFiles() implies that it is a function ans it should be a function.
A single FileSystemObject is being created every time GetFiles() is getting called. It is better to create a single instance of the FileSystemObject and pass it as a parameter.

Function GetFiles(ByVal path As String, Optional fso As Object) As String
 If fso Is Nothing Then Set fso = CreateObject("Scripting.FileSystemObject")

The main reason that GetFiles() is so slow is string concatenation is inefficient. Everytime a string is concatenated a new string variable is created. Let's say that the average file path is 50 bytes long. After 2K files, fileNameString would be 100K bytes and by the time we reach the 10K fileNameString would be 500k bytes. Creatin an array of filename and using Join() to concatenate the array would be much faster.

An easier solution is to use WScript.Shell to return filenames:

Function GetFiles(ByVal rootPath As String) As Variant
 Dim result As String
 result = CreateObject("WScript.Shell").exec("cmd /c dir """ & rootPath & """ /a:d-h-s /b /s").StdOut.ReadAll
 result = Left(result, Len(result) - 2)
 result = Replace(result, vbNewLine, "|")
 GetFiles = result
End Function

For faster lookups I would add the file paths to a dictionary.

Function GetFileMap(ByVal rootPath As String) As Scripting.Dictionary
 Dim map As New Scripting.Dictionary
 Dim key
 
 Dim result As String
 result = CreateObject("WScript.Shell").exec("cmd /c dir """ & rootPath & """ /a:d-h-s /b /s").StdOut.ReadAll
 
 For Each key In Split(result, vbNewLine)
 If Len(key) > 0 Then
 map.Add key, vbNullString
 End If
 Next
 
 Set GetFileMap = map
End Function

Addendum

I didn't elaborate much on using a dictionary but it is much faster than looping over all the cells for each item in the file array. It looks like you would need to have the file name for the dictionary keys and the file paths for the the dictionary values.

I personally don't like GetColLetter(). I can see where it my be handy for creating cell formulas but there is always another way when working with ranges.

I'm not a fan of creating functions to that basically rename built-in functions. In this project lastpositionOfChar() was used instead Instr(). 2 years from now you might forget lastpositionOfChar() and write lastCharPosition(). It also makes code reuse more difficult because you created a dependency on another function.

 filename = Mid(cell.Value, lastpositionOfChar(cell.Value, "/") + 1, Len(cell.Value))

Len(cell.Value) is not needed. I prefer filename = Mid(cell.Value, InStrRev(cell.Value, "/")).

LeadingZeroString() I would use a public Const to store the number format.

Public Const LeadingZero As String = "000"

Although you have done an outstanding job of naming your custom functions I would still use the built-in ones.

Here is a small sample of how I would refactor the code:

Before

If fileCopied Then
 ActiveSheet.Range("B" & cell.Row).Value = imgArray(i)
 For J = 2 To 15
 Dim newFileName As String
 newFileName = CreateFileName(CStr(imgArray(i)), LeadingZeroString(J))
 If Not (IsStringEmpty(newFileName)) Then
 If (DoesFileExist(newFileName)) Then
 If Not (IsFileOpen(newFileName)) Then
 FileCopy newFileName, moveToPath & Right(newFileName, Len(newFileName) - lastpositionOfChar(newFileName, "\") + 1)
 ActiveSheet.Range(GetColLetter(J + 1) & cell.Row).Value = newFileName
 ActiveSheet.Range(GetColLetter(J + 1) & cell.Row).Font.Color = RGB(0, 102, 0)
 End If
 Else
 ActiveSheet.Range(GetColLetter(J + 1) & cell.Row).Value = "(Niet aanwezig) " & Right(newFileName, Len(newFileName) - lastpositionOfChar(newFileName, "\") + 1)
 ActiveSheet.Range(GetColLetter(J + 1) & cell.Row).Font.Color = RGB(255, 153, 51)
 End If
 End If
 Next J
End If

After

If fileCopied Then
 cell.EntireColumn.Columns("B").Value = imgArray(i)
 For J = 2 To 15
 Dim newFileName As String
 newFileName = CreateFileName(CStr(imgArray(i)), Format(J, LeadingZero))
 If Len(newFileName) > 0 Then
 If Len(Dir(newFileName)) > 0 Then
 If Not (IsFileOpen(newFileName)) Then
 FileCopy newFileName, moveToPath & Right(newFileName, Len(newFileName) - InStrRev(newFileName, "\") + 1)
 cell.Offset(0, J).Value = newFileName
 cell.Offset(0, J).Font.Color = RGB(0, 102, 0)
 End If
 Else
 cell.Offset(0, J).Value = "(Niet aanwezig) " & Right(newFileName, Len(newFileName) - InStrRev(newFileName, "\") + 1)
 cell.Offset(0, J).Font.Color = RGB(255, 153, 51)
 End If
 End If
 Next J
End If
answered Jun 12, 2019 at 20:37
\$\endgroup\$
5
  • \$\begingroup\$ Totally agree on making it better readable. I like your suggestions and improvement. Thank you for that. Now that you mentioned the concatenation thing, I do remember reading about it. I will adjust the code. Thanks again. \$\endgroup\$ Commented Jun 13, 2019 at 7:42
  • \$\begingroup\$ I see your point. And I must admit, I often forget about functions I wrote before, doing the whole thing again. Totally against the D.N.R.Y.. Selecting the file infor using the CMD command is superfast indeed. \$\endgroup\$ Commented Jun 13, 2019 at 12:34
  • 1
    \$\begingroup\$ D.N.R.Y? Dictionary? You have a nice coding style, It is really easy to follow. Keep in mind that my review is just my opinion. I am by no means a guru. \$\endgroup\$ Commented Jun 13, 2019 at 13:26
  • 1
    \$\begingroup\$ But is it good thinking. I like it. I'm no Guru either. And this is my first code review. Learning every day. ;-) \$\endgroup\$ Commented Jun 13, 2019 at 14:14
  • 1
    \$\begingroup\$ DNRY is more commonly known as DRY: Do Not Repeat Yourself or Don't Repeat Yourself. It's anti-WET: Write Everything Twice. \$\endgroup\$ Commented Mar 5, 2020 at 13:55

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.