As mentioned at the end of Part 2, after the creation date, the rest of the string is called the Description. It can contain projects that start with a plus sign(+) or contexts that start with an at symbol(@) or key/value pairs with a colon(:). We’ll test the projects piece now.

Sub TEST_Projects()

    Dim clsTodo As CTodo
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom+Dad due:2016-05-30"
    Debug.Assert clsTodo.Projects.Count = 0
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-05-20 2016-04-30 Call Mom @Phone +Family due:2016-05-30"
    Debug.Assert clsTodo.Projects.Count = 1
    Debug.Assert clsTodo.Projects(1).Tag = "Family"
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom +Phone +Family due:2016-05-30"
    Debug.Assert clsTodo.Projects.Count = 2
    
    Debug.Print "TEST_Projects"

End Sub

I’m testing zero, one, and two projects. Now let’s update Raw to make this pass

Public Property Let Raw(ByVal sRaw As String)
    
    Dim vaSplit As Variant
    Dim lNext As Long
    Dim i As Long
    Dim clsProject As CProject
    
    vaSplit = Split(sRaw, Space(1))
        
    Me.Complete = vaSplit(0) = "x"
            
    If vaSplit(0) = "x" Then
        lNext = lNext + 1
    End If

    If vaSplit(lNext) Like "([A-Z])" Then
        Me.Priority = Mid$(vaSplit(lNext), 2, 1)
        lNext = lNext + 1
    End If

    If IsDate(vaSplit(lNext)) Then
        If IsDate(vaSplit(lNext + 1)) Then
            Me.CompleteDate = DateValue(vaSplit(lNext))
            Me.CreationDate = DateValue(vaSplit(lNext + 1))
            lNext = lNext + 2
        Else
            Me.CreationDate = DateValue(vaSplit(lNext))
            lNext = lNext + 1
        End If
    End If
    
    For i = lNext To UBound(vaSplit)
        If Left$(vaSplit(i), 1) = "+" Then
            Set clsProject = New CProject
            clsProject.Tag = Mid$(vaSplit(i), 2, Len(vaSplit(i)))
            Me.Projects.Add clsProject
        End If
    Next i
    
End Property

This loops through the rest of the elements of the split array and looks for a plus sign at the start. If it finds one, it creates a Project instance and adds it to the Projects collection class. The contexts will be handled similarly.

Sub TEST_Contexts()

    Dim clsTodo As CTodo
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom@home due:2016-05-30"
    Debug.Assert clsTodo.Contexts.Count = 0
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-05-20 2016-04-30 Call Mom @Phone +Family due:2016-05-30"
    Debug.Assert clsTodo.Contexts.Count = 1
    Debug.Assert clsTodo.Contexts(1).Tag = "Phone"
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom @Phone @Family due:2016-05-30"
    Debug.Assert clsTodo.Contexts.Count = 2
    
    Debug.Print "TEST_Contexts"

End Sub

Public Property Let Raw(ByVal sRaw As String)
    
    Dim vaSplit As Variant
    Dim lNext As Long
    Dim i As Long
    Dim clsProject As CProject
    Dim clsContext As CContext
    
    vaSplit = Split(sRaw, Space(1))
        
    Me.Complete = vaSplit(0) = "x"
            
    If vaSplit(0) = "x" Then
        lNext = lNext + 1
    End If

    If vaSplit(lNext) Like "([A-Z])" Then
        Me.Priority = Mid$(vaSplit(lNext), 2, 1)
        lNext = lNext + 1
    End If

    If IsDate(vaSplit(lNext)) Then
        If IsDate(vaSplit(lNext + 1)) Then
            Me.CompleteDate = DateValue(vaSplit(lNext))
            Me.CreationDate = DateValue(vaSplit(lNext + 1))
            lNext = lNext + 2
        Else
            Me.CreationDate = DateValue(vaSplit(lNext))
            lNext = lNext + 1
        End If
    End If
    
    For i = lNext To UBound(vaSplit)
        If Left$(vaSplit(i), 1) = "+" Then
            Set clsProject = New CProject
            clsProject.Tag = Mid$(vaSplit(i), 2, Len(vaSplit(i)))
            Me.Projects.Add clsProject
        ElseIf Left$(vaSplit(i), 1) = "@" Then
            Set clsContext = New CContext
            clsContext.Tag = Mid$(vaSplit(i), 2, Len(vaSplit(i)))
            Me.Contexts.Add clsContext
        End If
    Next i
    
End Property

The final special case inside the description is key/value pairs.

Sub TEST_KeyValue()

    Dim clsTodo As CTodo
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom@home"
    Debug.Assert clsTodo.KeyValues.Count = 0
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-05-20 2016-04-30 Call Mom @Phone +Family due:2016-05-30"
    Debug.Assert clsTodo.KeyValues.Count = 1
    Debug.Assert clsTodo.KeyValues(1).Key = "due"
    Debug.Assert clsTodo.KeyValues(1).Value = "2016-05-30"
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom @Phone @Family due:2016-05-30 key:val:ue"
    Debug.Assert clsTodo.KeyValues.Count = 2
    
    Debug.Print "TEST_KeyValue"

End Sub

Again I’m testing zero, one, and two instances.

Public Property Let Raw(ByVal sRaw As String)
    
    Dim vaSplit As Variant
    Dim lNext As Long
    Dim i As Long
    Dim clsProject As CProject
    Dim clsContext As CContext
    Dim clsKeyValue As CKeyValue
    Dim vaKeys As Variant
    
    vaSplit = Split(sRaw, Space(1))
        
    Me.Complete = vaSplit(0) = "x"
            
    If vaSplit(0) = "x" Then
        lNext = lNext + 1
    End If

    If vaSplit(lNext) Like "([A-Z])" Then
        Me.Priority = Mid$(vaSplit(lNext), 2, 1)
        lNext = lNext + 1
    End If

    If IsDate(vaSplit(lNext)) Then
        If IsDate(vaSplit(lNext + 1)) Then
            Me.CompleteDate = DateValue(vaSplit(lNext))
            Me.CreationDate = DateValue(vaSplit(lNext + 1))
            lNext = lNext + 2
        Else
            Me.CreationDate = DateValue(vaSplit(lNext))
            lNext = lNext + 1
        End If
    End If
    
    For i = lNext To UBound(vaSplit)
        If Left$(vaSplit(i), 1) = "+" Then
            Set clsProject = New CProject
            clsProject.Tag = Mid$(vaSplit(i), 2, Len(vaSplit(i)))
            Me.Projects.Add clsProject
        ElseIf Left$(vaSplit(i), 1) = "@" Then
            Set clsContext = New CContext
            clsContext.Tag = Mid$(vaSplit(i), 2, Len(vaSplit(i)))
            Me.Contexts.Add clsContext
        ElseIf InStr(1, vaSplit(i), ":") > 1 Then
            vaKeys = Split(vaSplit(i), ":", 2)
            Set clsKeyValue = New CKeyValue
            clsKeyValue.Key = vaKeys(0)
            clsKeyValue.Value = vaKeys(1)
            Me.KeyValues.Add clsKeyValue
        End If
    Next i
    
End Property

Everything else is the description

Sub TEST_Description()

    Dim clsTodo As CTodo
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom@home"
    Debug.Assert clsTodo.Desc = "Call Mom@home"
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-05-20 2016-04-30 Call Mom @Phone +Family due:2016-05-30"
    Debug.Assert clsTodo.Desc = "Call Mom"
    
    Set clsTodo = New CTodo
    clsTodo.Raw = "(A) 2016-04-30 Call Mom @Phone and Dad @Family due:2016-05-30 key:val:ue"
    Debug.Assert clsTodo.Desc = "Call Mom and Dad"
    
    Debug.Print "TEST_Description"

End Sub

Here are the changes to the bottom of Raw

For i = lNext To UBound(vaSplit)
        If Left$(vaSplit(i), 1) = "+" Then
            Set clsProject = New CProject
            clsProject.Tag = Mid$(vaSplit(i), 2, Len(vaSplit(i)))
            Me.Projects.Add clsProject
        ElseIf Left$(vaSplit(i), 1) = "@" Then
            Set clsContext = New CContext
            clsContext.Tag = Mid$(vaSplit(i), 2, Len(vaSplit(i)))
            Me.Contexts.Add clsContext
        ElseIf InStr(1, vaSplit(i), ":") > 1 Then
            vaKeys = Split(vaSplit(i), ":", 2)
            Set clsKeyValue = New CKeyValue
            clsKeyValue.Key = vaKeys(0)
            clsKeyValue.Value = vaKeys(1)
            Me.KeyValues.Add clsKeyValue
        Else
            Me.Desc = Me.Desc & vaSplit(i) & Space(1)
        End If
    Next i
    
    'remove the trailing space
    If Len(Me.Desc) > 1 Then
        Me.Desc = Left$(Me.Desc, Len(Me.Desc) - 1)
    End If

And that’s it. A properly parsed Todo.txt string ready to be used in your application. And if I make an changes to my app, I can run these tests to make sure I didn’t break anything.

You can download TodoTxt.zip

Series:

  1. todo-txt-tdd-part-1/
  2. todo-txt-tdd-part-2/
  3. todo-txt-tdd-part-3/