I wanted to implement Dijkstra's Algorithm in an Excel VBA Add-In and built it to be used as follows:
- Define a list of paths with distances between points. This list needs to contain 3 headings that are used as flags to pick up where the list is. The 3 headings are
!dijk:dat:from,!dijk:dat:toand!dijk:dat:dist - Specify from which point to which point you want to go. This is indicated with flags to the left of the cell. The flags are
!dijk:get:fromand!dijk:get:to - If the list of paths is on a different sheet, specify which sheet it is on by putting the name of the sheet in a cell next to a cell with the text
!dijk:dat - Specify where the output should go. This is defined with a flag at the top left of where it should go. The flag is
!dijk:steps - Push a button in the Ribbon that triggers
Sub sCalcDijkstra()in a module in my Add-In
An example of a dummy sheet I used for testing:
This is the procedure that does all the work:
Sub sCalcDijkstra()
'Calculate the shortest path between 2 points
Dim vError As String
Dim vRange As Range
Dim vRangeDat As Range
Dim vRow As Long
Dim vRowDatHead As Long
Dim vRowSteps As Long
Dim vRowFirst As Long
Dim vRowCount As Long
Dim vRowCountDat As Long
Dim vCol As Long
Dim vColDatFrom As Long
Dim vColDatTo As Long
Dim vColDatDist As Long
Dim vColSteps As Long
Dim vColFirst As Long
Dim vColCount As Long
Dim vColCountDat As Long
Dim vCell As String
Dim vCellFrom As String
Dim vCellTo As String
Dim vValDist As Double
Dim vParFrom As String
Dim vParTo As String
Dim vParDat As String
Dim vDist As Scripting.Dictionary
Dim vKey As Variant
Dim vCurNode As String
Dim vCurDist As Double
Dim vCurDistTo As Double
Dim vSteps() As String
On Error GoTo 0
vError = ""
'Check that there is a workbook open
If ActiveSheet Is Nothing Then vError = "You need to open a workbook in order to do this"
If vError <> "" Then GoTo ErrorHandler
'Get the settings from the current sheet
Set vRange = ActiveSheet.UsedRange
vRowCount = vRange.Rows.Count
vColCount = vRange.Columns.Count
vRowFirst = vRange.Row
vColFirst = vRange.Column
vRowSteps = 0
vColSteps = 0
vParFrom = ""
vParTo = ""
vParDat = ""
For vRow = 1 To vRowCount
For vCol = 1 To vColCount
vCell = ""
On Error Resume Next
vCell = Trim(UCase(vRange.Cells(vRow, vCol).Value))
On Error GoTo 0
If vCell = "!DIJK:GET:FROM" Then
vParFrom = Trim(UCase(vRange.Cells(vRow, vCol + 1).Value))
ElseIf vCell = "!DIJK:GET:TO" Then
vParTo = Trim(UCase(vRange.Cells(vRow, vCol + 1).Value))
ElseIf vCell = "!DIJK:DAT" Then
vParDat = Trim(UCase(vRange.Cells(vRow, vCol + 1).Value))
ElseIf vCell = "!DIJK:STEPS" Then
vRowSteps = vRow
vColSteps = vCol
End If
Next
Next
If vParFrom = "" Then vError = vError & "Need to specify a Source with the parameter !dijk:get:from" & vbCrLf & vbCrLf
If vParTo = "" Then vError = vError & "Need to specify a Destination with the parameter !dijk:get:to" & vbCrLf & vbCrLf
If vRowSteps = 0 Then vError = vError & "Need to designate an area to print the results with the parameter !dijk:steps" & vbCrLf & vbCrLf
If vError <> "" Then GoTo ErrorHandler
'Clean up the output area
vRange.Range(vRange.Cells(vRowSteps + 2 - vRowFirst, vColSteps + 1 - vColFirst).Address, vRange.Cells(vRowCount + vRowFirst - 1, vColSteps + 3 - vColFirst).Address).ClearContents
'Get the paths from the data sheet
If vParDat = "" Then
Set vRangeDat = vRange
Else
Set vRangeDat = ActiveWorkbook.Worksheets(vParDat).UsedRange
End If
vRowCountDat = vRangeDat.Rows.Count
vColCountDat = vRangeDat.Columns.Count
vRowDatHead = 0
vColDatFrom = 0
vColDatTo = 0
vColDatDist = 0
For vRow = 1 To vRowCountDat
For vCol = 1 To vColCountDat
vCell = ""
On Error Resume Next
vCell = Trim(UCase(vRangeDat.Cells(vRow, vCol).Value))
On Error GoTo 0
If vCell = "!DIJK:DAT:FROM" Then
vRowDatHead = vRow
vColDatFrom = vCol
ElseIf vCell = "!DIJK:DAT:TO" Then
vRowDatHead = vRow
vColDatTo = vCol
ElseIf vCell = "!DIJK:DAT:DIST" Then
vRowDatHead = vRow
vColDatDist = vCol
End If
Next
If vRowDatHead > 0 Then Exit For
Next
If vColDatFrom = 0 Then vError = vError & "Data sheet is missing !dijk:dat:from column" & vbCrLf & vbCrLf
If vColDatTo = 0 Then vError = vError & "Data sheet is missing !dijk:dat:to column" & vbCrLf & vbCrLf
If vColDatDist = 0 Then vError = vError & "Data sheet is missing !dijk:dat:dist column" & vbCrLf & vbCrLf
If vError <> "" Then GoTo ErrorHandler
Set vDist = New Scripting.Dictionary
For vRow = vRowDatHead + 1 To vRowCountDat
vCellFrom = ""
vCellTo = ""
vValDist = -1
On Error Resume Next
vCellFrom = Trim(UCase(vRangeDat.Cells(vRow, vColDatFrom).Value))
vCellTo = Trim(UCase(vRangeDat.Cells(vRow, vColDatTo).Value))
vValDist = Val(Trim(UCase(vRangeDat.Cells(vRow, vColDatDist).Value)))
On Error GoTo 0
If vCellFrom <> "" And vCellTo <> "" And vValDist >= 0 Then
If Not vDist.Exists(vCellFrom) Then Set vDist.Item(vCellFrom) = New Scripting.Dictionary
If Not vDist.Exists(vCellTo) Then Set vDist.Item(vCellTo) = New Scripting.Dictionary
vDist(vCellFrom).Item(vCellTo) = vValDist
If Not vDist(vCellTo).Exists(vCellFrom) Then vDist(vCellTo).Item(vCellFrom) = vValDist
End If
Next
If Not vDist.Exists(vParFrom) Then vError = vError & "Source " & vParFrom & " not listed in data" & vbCrLf & vbCrLf
If Not vDist.Exists(vParTo) Then vError = vError & "Destination " & vParTo & " not listed in data" & vbCrLf & vbCrLf
If vError <> "" Then GoTo ErrorHandler
'Calculate the shortest path
For Each vKey In vDist.Keys()
vDist(vKey).Item("!dist") = -1
vDist(vKey).Item("!scan") = False
vDist(vKey).Item("!steps") = ""
Next
vDist(vParFrom).Item("!dist") = 0
vDist(vParFrom).Item("!steps") = vParFrom
Do While True
vCurNode = ""
vCurDist = 0
For Each vKey In vDist.Keys()
If vDist(vKey)("!scan") = False Then
If vDist(vKey)("!dist") >= 0 Then
If vCurNode = "" Or vCurDist > vDist(vKey)("!dist") Then
vCurNode = vKey
vCurDist = vDist(vKey)("!dist")
End If
End If
End If
Next
If vCurNode = "" Then Exit Do
If vCurNode = vParTo Then Exit Do
vDist(vCurNode).Item("!scan") = True
For Each vKey In vDist(vCurNode).Keys()
If Left(vKey, 1) <> "!" And vKey <> vCurNode Then
vCurDistTo = vCurDist + vDist(vCurNode)(vKey)
If vDist(vKey)("!dist") < 0 Or vCurDistTo < vDist(vKey)("!dist") Then
vDist(vKey).Item("!dist") = vCurDistTo
vDist(vKey).Item("!steps") = vDist(vCurNode)("!steps") & "!" & vKey
End If
End If
Next
Loop
'Print the result
If vDist(vParTo)("!dist") < 0 Then
vRange.Cells(vRowSteps + 1, vColSteps).Value = "No path found from source to destination"
Else
vSteps = Split(vDist(vParTo)("!steps"), "!")
For vRow = 1 To UBound(vSteps)
vRange.Cells(vRowSteps + vRow, vColSteps).Value = vSteps(vRow - 1)
vRange.Cells(vRowSteps + vRow, vColSteps + 1).Value = vSteps(vRow)
vRange.Cells(vRowSteps + vRow, vColSteps + 2).Value = vDist(vSteps(vRow - 1))(vSteps(vRow))
Next
vRange.Cells(vRowSteps + vRow, vColSteps).Value = "Total:"
vRange.Cells(vRowSteps + vRow, vColSteps + 2).Value = vDist(vParTo)("!dist")
End If
'Done
MsgBox "Done", vbOKOnly + vbInformation, "Path and Distance"
GoTo Finalize
ErrorHandler:
Err.Clear
MsgBox vError, vbOKOnly + vbCritical, "Error"
Finalize:
Set vDist = Nothing
End Sub
The code works, but I would like some feedback on the following aspects:
- How can I make this easier and more intuitive for a user? I know I can use named ranges instead of flags, but I would prefer to keep it more visibly obvious what the code is using
- How can I apply the DRY principle more here. I'm repeating the same patterns all the time, but it seems like the details vary too much for me to just stick something like the nested for loops into a function
- I use
Scripting.Dictionaryfor almost everything due to it's flexibility and simply due to the fact that I am comfortable with how it works, but I suspect there may be better data structures that I can use which work better for this use case - At the heart of this is the
Do While Trueloop which is probably a horribly inefficient way to implement Dijkstra's Algorithm. How can I make it more efficient? - Any help/critique is greatly appreciated. I taught myself VBA with the help of Google and I may be doing some bad things I never even realised




