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?
\$\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\$
    – Mast
    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\$
    – Stephan
    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
\$\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\$
    – Stephan
    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\$
    – Stephan
    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\$
    – TinMan
    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\$
    – Stephan
    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\$
    – Mast
    Commented Mar 5, 2020 at 13:55

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.