Public/ConvertTo-TodoTxt.ps1

#Requires -Version 3
<#
.SYNOPSIS
    Splits a todo text string.
.DESCRIPTION
    Splits a todo text string into parts and return back an object.
 
    See the project documentation for the format of the object.
.NOTES
    Author : Paul Broadwith (paul@pauby.com)
    History : 1.0 - 20/06/16 - Initial version
                  1.1 - 31/08/16 - Changed to return an object; Changed name.
                  1.2 - 19/01/17 - Function renamed; code refactored and cut down substantially
    Notes : If the task description is not present then you will find that various components of the todo end up as it.
.LINK
    http://www.github.com/pauby/pstodotxt
.PARAMETER Todo
    This is the raw todo text - ie. 'take car to garage @car +car_maintenance'
.INPUTS
    Input type [String]
.OUTPUTS
    Output type [PSObject]
.EXAMPLE
    ConvertTo-TodoTxt -Todo 'take car to garage @car +car_maintenance'
 
    Splits the todo text into it's components and returns them in an object.
.EXAMPLE
    $todo = 'take car to garage @car +car_maintenance'
    $todo | ConvertTo-TodoTxt
 
    Splits the todo text into it's components and returns them in an object
#>


function ConvertTo-TodoTxt
{
    [CmdletBinding()]
    [OutputType([object[]])]
    Param (
        [Parameter(Mandatory=$false, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Todo
    )

    Begin {
        # create a hashtable of regular expressions to extract the parts from the Input
        # the format should be:
        # name - the object property name that the extracted part will be assigned to
        # regex - the regular expression to extract the part
        #
        # Note that as each part is extracted it is also removed from the input so this will affect which
        # anchors used in the expressions
        $regexList = @(
            @{ "name" = "DoneDate"; "regex" = "^x\ \d{4}-\d{2}-\d{2}\ " },  # the done date - eg. 'x 2017-08-01'
            @{ "name" = "Priority"; "regex" = "^\(([A-Za-z])\)\ " },    # priority - eg. '(B)'
            @{ "name" = "CreatedDate"; "regex" = "^\d{4}-\d{2}-\d{2}\ " },  # created date - eg. '2016-05-23'
            @{ "name" = "Context"; "regex" = "(?:^|\s)@[a-z\d-_]+" },                # context - eg. '@computer' - can only have ONE @ to be recognised as a context
            @{ "name" = "Project"; "regex" = "(?:^|\s)\+[a-z\d-_]+" },              # project - eg. '+rebuild' - can only have ONE + to be recognised as a project
            @{ "name" = "Addon"; "regex" = "(?:^|\s)(\S+)\:((?!//)\S+)" }           # addon - eg. 'due:2017-02-01'
        )
    }

    Process {
        $Todo | ForEach-Object {
            Write-Verbose "Processing line: $_"
            $output = New-Object -TypeName PSObject -Property @{ "CreatedDate" = (Get-TodoTxtTodaysDate) }
            $line = $_

            foreach ($item in $regexList) {
                if ($line -match $item.regex) {
                    $found = [regex]::matches($line, $item.regex)
                    $line = $line -replace $item.regex, ""

                    switch ($item.name) {
                        "DoneDate" {
                            # the format of the 'done' is 'x <DATE>' so we need to skip over the x and the space
                            $output | Add-Member -MemberType NoteProperty -Name $_ -Value (Get-Date -Date $found.value.SubString(2) -Format "yyyy-MM-dd")
                            Write-Verbose "Found '$_': $($output.$_)"
                            break
                        }

                        "CreatedDate" {
                            $output.CreatedDate = (Get-Date -Date $found.value -Format "yyyy-MM-dd")
                            Write-Verbose "Found '$_': $($output.$_)"
                            break
                        }

                        "Priority"  {
                            # priority is returned as '(<PRIORITY>)' and that will match the numbered capture (1) in the regex so we use that
                            $output | Add-Member -MemberType NoteProperty -Name $_ -Value ([string]$found.groups[1].value).ToUpper()
                            Write-Verbose "Found '$_': $($output.$_)"
                            break
                        }

                        { $_ -in "Context", "Project" } {
                            $output | Add-Member -MemberType NoteProperty -Name $_ -Value @(
                                $found | foreach-object { 
                                    # trim the whitespace and then skip over the
                                    # first characvter which will be @ or +
                                    [string]$_.value.Trim().Remove(0,1)
                                } 
                            )
                            Write-Verbose "Found '$_': $($output.$_)"
                            break
                        }

                        "Addon" {
                            $addons = @{}
                            foreach ($f in $found) {
                                $addons.Add($f.groups[1].value.Trim(), $f.groups[2].value.Trim())
                                Write-Verbose "Found Addon '$($f.groups[1].value)': $($f.groups[2].value)"
                        }
                            $output | Add-Member -MemberType NoteProperty -Name $_ -Value $addons
                            break
                        }
                    }
                }
            }

            # what is left here is the task itself but we need to tidy it up
            # as each part is extracted it's leaving behind double spaces etc.
            $line = ($line -replace "\ {2,}", " ").Trim()
            if ($line.length -lt 1) {
                throw "Task description cannot be empty."
            }
            $output | Add-Member -MemberType NoteProperty -Name 'Task' -Value $line
            Write-Verbose "Found 'Task': $($output.task)"

            Write-Output $output
        }
    }

    End {
    }
}