PsTodoTxt.psm1

function Test-ObjectProperty {
    <#
    .SYNOPSIS
        Tests an oobject for a property.
    .DESCRIPTION
        Tests an object for the existence of a property.
    .NOTES
    Author: Paul Broadwith (https://github.com/pauby)
    .LINK
        https://www.github.com/pauby/pstodotxt
    .INPUTS
        [System.Object]
    .OUTPUTS
        [System.Boolean]
    .EXAMPLE
        (New-Object -TypeName PSObject -Property @{ test = "testing"} | Test-ObjectProperty -PropertyName "test"

        Tests the object passed on the pipeline for a property named 'test'. In this example the result is $true.
    #>


    [CmdletBinding()]
    [OutputType([System.Boolean])]
    Param
    (
        # The object to be tested
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [ValidateScript( { [bool]($_.GetType().Name -eq "PSCustomObject") } )]
        [ValidateNotNull()]
        [PSObject]$InputObject,

        # The property name of the object to be tested
        [Parameter(Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$PropertyName
    )

    Begin {
        @('InputObject', 'PropertyName') | ForEach-Object {
            if (-not($PSBoundParameters.ContainsKey($_))) {
                throw [ArgumentException]"Mandatory parameter '$_' is missing."
            }
        }
    }

    Process {
        # check if the object has any properties to check NOTE: not entirely
        # sure we should be using [string]::IsNullOrEmpty() here but it works
        # whereas -eq $null doesn't
        if ([string]::IsNullOrEmpty($InputObject.PSObject.Properties)) {
            $false
        }
        else {
            [bool]($InputObject.PSobject.Properties.name -match $PropertyName)
        }

        # we should never get here
    }

    End    {
    }
}
function ConvertFrom-TodoTxt {
<#
.SYNOPSIS
    Converts a todo object to a todotxt string.
.DESCRIPTION
    Converts a todo object to a todotxt string.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)
.LINK
    http://www.github.com/pauby/pstodotxt
.INPUTS
    Input type [System.Object]
.OUTPUTS
    Output type [System.String]
.EXAMPLE
    $todoObject | ConvertFrom-TodoTxt

    Converts $todoObject into a todotxt string.
#>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("CommunityAnalyzerRules\Measure-Backtick", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseConsistentWhitespace", "", Justification = "Causes issue with multi-line if statement")]
    [CmdletBinding()]
    [OutputType([System.String])]
    Param (
        # This is the todotxt object (as output from ConvertTo-TodoTxt for example).
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [PSObject]$Todo
    )

    Begin {
        $objProps = @('DoneDate', 'Priority', 'CreatedDate', 'Task', 'Context', 'Project', 'Addon')
    }

    Process {
        $joined = ""    # just maiing it clear this is what we are using to hold joined text
        ForEach ($todoObj in $Todo) {

            # valid the todotxt object
            if (-not ($todoObj | Test-TodoTxt)) {
                throw 'Invalid TodoTxt object - invalid or missing properties'
            }

            # TODO: We should tidy this up to stop using backticks
            Foreach ($prop in $objProps) {
                if ( (Test-ObjectProperty -InputObject $todoObj -PropertyName $prop) `
                    -and ($null -ne $todoObj.$prop) `
                    -and (-not [string]::IsNullOrEmpty($todoObj.$prop)) ) {

                    switch ($prop) {
                        "DoneDate" {
                            $joined += "x $($todoObj.DoneDate) "
                            break
                        }

                        "Priority" {
                            $joined += "($($todoObj.Priority.ToUpper())) "
                            break;
                        }

                        "CreatedDate" {
                            $joined += "$($todoObj.CreatedDate) "
                            break
                        }

                        "Task" {
                            $joined += "$($todoObj.Task) "
                            break
                        }

                        "Context" {
                            $joined += "@$($todoObj.Context -join ' @') "
                            break
                        }

                        "Project" {
                            $joined += "+$($todoObj.Project -join ' +') "
                            break
                        }

                        "Addon" {
                            Foreach ($key in $todoObj.Addon.Keys) {
                                $joined += "$($key):$($todoObj.Addon.$key) "
                            }
                        }
                    } #end switch
                } #end if
            } #end foreach

            Write-Output $joined.Trim()

        } #end foreach
    }

    End {
    }
}
function ConvertTo-TodoTxt {
    <#
    .SYNOPSIS
        Converts a todo text string to a TodoTxt object.
    .DESCRIPTION
        Converts a todo text string to a TodoTxt object.

        If the task description is not present then you will find that various components of the todo end up as it.

        See the project documentation for the format of the object.
    .NOTES
        Author: Paul Broadwith (https://github.com/pauby)
    .LINK
        http://www.github.com/pauby/pstodotxt
    .INPUTS
        Input type [System.String]
    .OUTPUTS
        Output type [System.Object]
    .EXAMPLE
        ConvertTo-TodoTxt -Todo 'take car to garage @car +car_maintenance'

        Converts 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

        Converts the todo text into it's components and returns them in an object
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("CommunityAnalyzerRules\Measure-Backtick", "", Justification = "Warning as this is a small function with comment based help")]
    [OutputType([System.Object])]
    Param (
        # This is the raw todo text - ie. 'take car to garage @car +car_maintenance'
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Todo,

        # Requests that the todo not be validated and only parsed.
        [switch]
        $ParseOnly
    )

    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 = @(
            # the done date - eg. 'x 2017-08-01'
            @{ "name" = "DoneDate"; "regex" = "^x\ \d{4}-\d{2}-\d{2}\ " },
            # priority - eg. '(B)'
            @{ "name" = "Priority"; "regex" = "^\(([A-Za-z])\)\ " },
            # created date - eg. '2016-05-23'
            @{ "name" = "CreatedDate"; "regex" = "^\d{4}-\d{2}-\d{2}\ " },
            # context - eg. '@computer' - can only have ONE @ to be recognised as a context
            @{ "name" = "Context"; "regex" = "(?:^|\s)@[a-z\d-_]+" },
            # project - eg. '+rebuild' - can only have ONE + to be recognised as a project
            @{ "name" = "Project"; "regex" = "(?:^|\s)\+[a-z\d-_]+" },
            # addon - eg. 'due:2017-02-01'
            @{ "name" = "Addon"; "regex" = "(?:^|\s)(\S+)\:((?!//|\\)\S+)" }
        )
    }

    Process {
        $Todo | ForEach-Object {
            Write-Verbose "Processing line: $_"
            $output = New-Object -TypeName PSObject
            if (-not $ParseOnly.IsPresent) {
                $output | Add-Member -MemberType NoteProperty -Name 'CreatedDate' -Value (Get-TodoTxtTodaysDate)
            }
            $output.PSObject.TypeNames.Insert(0, 'TodoTxt')
            $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" {
                            try {
                                # 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
                            }
                            catch {
                                if (-not $ParseOnly.IsPresent) {
                                    throw $_
                                }
                            }
                        }

                        "CreatedDate" {
                            try {
                                $output.CreatedDate = (Get-Date -Date $found.value -Format "yyyy-MM-dd")
                                Write-Verbose "Found '$_': $($output.$_)"
                                break
                            }
                            catch {
                                if (-not $ParseOnly.IsPresent) {
                                    throw $_
                                }
                            }
                        }

                        "Priority" {
                            try {
                                # 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
                            }
                            catch {
                                if (-not $ParseOnly.IsPresent) {
                                    throw $_
                                }
                            }
                        }

                        { $_ -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
                        }
                    } # end switch
                } # end if
            } # end foreach

            # 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 -and (-not $ParseOnly.IsPresent)) {
                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 foreach
    } # end Process

# End {
# }
}
<#
.SYNOPSIS
Converts a date into the TodoTxt date format.

.DESCRIPTION
Converts a date into the TodoTxt date format.

If you do not pass a date then todays date is assumed.

.EXAMPLE
ConvertTo-TodoTxtDate -Date (Get-Date).AddDays(-20)

Converts the date, 20 days ago, into a TodoTxt date format.

.OUTPUTS
    [System.String]

.LINK
    https://github.com/pauby/PsTodoTxt/tree/master/docs/en-US/ConvertTo-TodoTxtDate.md

.NOTES
    Author: Paul Broadwith (https://github.com/pauby)
#>


function ConvertTo-TodoTxtDate {
    [CmdletBinding()]
    [OutputType([System.String])]

    Param (
        [ValidateNotNullOrEmpty()]
        [DateTime]
        $Date = (Get-Date)
    )

    Get-Date -Date $Date -Format 'yyyy-MM-dd'
}
function Export-TodoTxt {
<#
.SYNOPSIS
    Exports todotxt objects.
.DESCRIPTION
    Exports todotxt, previously created with ConvertTo-TodoTxt,
    to a text file. Before exporting the todotxt objects are converted
    back to todotxt strings by calling the cmdlet
    ConvertFrom-TodoTxt.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)
.LINK
    https://www.github.com/pauby/pstodotxt
.EXAMPLE
    $todo = Import-TodoTxt -Path c:\input.txt
    Export-TodoTxt -Todo $todo -Path c:\output.txt

    Converts the todotxt objects in $todo to todotxt strings and writes
    them to the file c:\output.txt.
.EXAMPLE
    Import-TodoTxt -Path c:\input.txt | Export-TodoTxt -Path c:\output.txt -Append

    Imports todotxt strings from c:\input.txt and then exports the file c:\output.txt
    by appending them to the end of the file.
#>


    [CmdletBinding()]
    Param(
        # Object(s) to export
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [PSObject]$Todo,

        # Path to the todo file. The file will be created if it does not exist
        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        # Append to todo file
        [switch]$Append
    )

    Begin {
        if (!$Append.IsPresent -and (Test-Path -Path $Path)) {
            Remove-Item -Path $Path -Force
        }
    }

    Process {
        Write-Verbose "We have $(@($Todo).count) objects in the pipeline to write to $Path."
        if ($VerbosePreference -ne "SilentlyContinue") {
            $Todo | ForEach-Object { Write-Verbose "Object: $_" }
        }

        $Todo | ConvertFrom-TodoTxt | Add-Content -Path $Path -Encoding UTF8
    }
    End {
    }
}
function Get-TodoTxtTodaysDate {
<#
.SYNOPSIS
    Gets todays date in todo.txt format.
.DESCRIPTION
    gets todays date in the correct todo.txt format.
.NOTES
    Author : Paul Broadwith (paul@pauby.com)
    History : 1.0 - 29/09/15 - Initial version
.LINK
    https://www.github.com/pauby/
.OUTPUTS
    Todays date. Output type is [datetime]
.EXAMPLE
    Get-TodoTodaysDate

    Outputs todays date.
#>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("CommunityAnalyzerRules\Measure-OverComment", "", Justification = "Warning as this is a small function with comment based help")]
    [CmdletBinding()]
    Param ()
    Get-Date -Format "yyyy-MM-dd"
}
function Import-TodoTxt {
<#
.SYNOPSIS
    Imports todotxt strings and converts them to objects.
.DESCRIPTION
    Imports todotxt strings from the source file and converts them to objects.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)
.LINK
    https://www.github.com/pauby/pstodotxt
.OUTPUTS
    System.Object
.EXAMPLE
    Import-Todo -Path c:\todo.txt

    Reads the todotxt strings from the file c:\todo.txt and converts them to objects.
#>


    [CmdletBinding()]
    [OutputType([System.Object])]
    Param (
        # Path to the todo file. The file must exist. Throws an exception if the
        # file does not exist. Nothing is returned if file is empty.
        [Parameter(Mandatory = $true)]
        [ValidateScript( { Test-Path $_ } )]
        [string]$Path
    )

    Write-Verbose "Reading todo file ($Path) contents."
    # skip any blank lines
    $todos = Get-Content -Path $Path -Encoding UTF8 | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    if ($null -eq $todos) {
        Write-Verbose "File $Path is empty."
    }
    else {
        Write-Verbose "Read $(@($todos).count) todos."
        $todos | Where-Object { -not [string]::ISNullOrEmpty($_) } | ConvertTo-TodoTxt
    }
}
function Set-TodoTxt {
<#
.SYNOPSIS
    Sets a todo's properties.
.DESCRIPTION
    Validates and sets a todo's properties.

    The function itself does not validate the Todotxt input object. It does validate the parameters.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)
.LINK
    https://www.github.com/pauby
.INPUTS
    [System.Object]
.OUTPUTS
    [System.Object]
.EXAMPLE
    $todoObj = $todoObj | Set-TodoTxt -Priority "B"

    Sets the priority of the $todoObj to "B" and outputs the modified todo.
#>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
    [OutputType([System.Object])]
    Param(
        # The todo object to set the properties of.
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [Object]$Todo,

        # The done date to set. This is only validated as a date in the correct
        # format and can be any date past, future or present. To remove this
        # property value from the object pass $null or an empty string as the
        # parameter value.
        [ValidateScript( {  if ([string]::IsNullOrEmpty($_)) { $true } else { Test-TodoTxtDate -Date $_ }  } )]
        [Alias('dd', 'done')]
        [string]$DoneDate,

        # The created date to set. This is only validated as a date in the
        # correct format and can be any date past, future or present. As a todo
        # must always have a created date you cannot remove this property value,
        # only change it.
        [ValidateNotNullOrEmpty()]
        [ValidateScript( {  Test-TodoTxtDate -Date $_ } )]
        [Alias('cd', 'created')]
        [string]$CreatedDate,

        # The priority of the todo. To remove this property value from the
        # object pass an empty string as the parameter value.
        [ValidateScript( {  if ([string]::IsNullOrEmpty($_)) { $true } else { Test-TodoTxtPriority -Priority $_ }  } )]
        [Alias('pri', 'u')]
        [string]$Priority,

        # The tasks description of the todo. As a todo must always have a task
        # this property cannot be removed.
        [ValidateNotNullOrEmpty()]
        [Alias('t')]
        [string]$Task,

        # The context(s) of the todo. To remove this property value from the
        # object pass $null or an empty string as the parameter value.
        [ValidateScript( { if ( ($null -eq $_) -or ([string]::IsNullOrEmpty($_)) ) { $true } else { Test-TodoTxtContext -Context $_ }  } )]
        [Alias('c')]
        [string[]]$Context,

        # The project(s) of the todo. To remove this property value from the
        # object pass $null or an empty string as the parameter value.
        [ValidateScript( { if ( ($null -eq $_) -or ([string]::IsNullOrEmpty($_)) ) { $true } else { Test-TodoTxtContext -Context $_ }  } )]
        [Alias('p')]
        [string[]]$Project,

        # The addon key:value pairs of the todo. To remove this property value
        # from the object pass $null as the parameter value.
        [Alias('a')]
        [hashtable]$Addon
    )

    Begin {
        $validParams = @('DoneDate', 'CreatedDate', 'Priority', 'Task', 'Context', 'Project', 'Addon')

        if (-not $PSBoundParameters.ContainsKey('Confirm')) {
            $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference')
        }
        if (-not $PSBoundParameters.ContainsKey('WhatIf')) {
            $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference')
        }
    }

    Process {
        $Todo | ForEach-Object {
            # only check for specific parameters
            $keys = $PsBoundParameters.Keys | Where-Object { $_ -in $validParams }

            # loop through each parameter and set the corresponding property on the todotxt object
            foreach ($key in $keys) {
                if ( ($null -eq $PsBoundParameters.$key) -or ([string]::IsNullOrEmpty($PsBoundParameters.$key)) ) {
                    Write-Verbose "Removing property $key"

                    if ($PSCmdlet.ShouldProcess("ShouldProcess?")) {
                        $_.PSObject.Properties.Remove($key)
                    }
                }
                else {
                    Write-Verbose "Set $key to $($PSBoundParameters.$key)"
                    if ($PSCmdlet.ShouldProcess("ShouldProcess?")) {
                        $_ | Add-Member -MemberType NoteProperty -Name $key -Value $PsBoundParameters.$key -Force
                    } # end if
                } # end else
            } # end foreach

            Write-Debug ($_ | Out-String)
            Write-Output $_
        }
   }

    End {
    }
}
function Test-TodoTxt {
<#
.SYNOPSIS
    Tests a todotxt object.
.DESCRIPTION
    Tests a TodoTxt object properties to ensure they conform to the todotxt
    specification.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)
.LINK
    https://www.github.com/pauby/pstodotxt
.OUTPUTS
    [System.Boolean]
.EXAMPLE
    $obj | Test-TodoTxt

    Tests the properties of the object $obj.
#>


    [CmdletBinding()]
    [OutputType([System.Boolean])]
    Param(
        # The DoneDate property to test.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("dd")]
        [string]$DoneDate,

        # The CreatedDate property to test
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("cd")]
        [string]$CreatedDate,

        # The Priority property to test.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("pri", "u")]
        [string]$Priority,

        # The Tasks property to test.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("t")]
        [string]$Task,

        # The Context property to test.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("c")]
        [string[]]$Context,

        # The Project property to test.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("p")]
        [string[]]$Project,

        # The Addon (key:value pairs) property to test.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("a")]
        [string[]]$Addon
    )

    Process {
        # we didn't mark any parameters mandatory as we didn't want to prompt for them but throw instead
        # test mandatory parameters here
        $mandatoryParams = @( 'CreatedDate', 'Task')
        $keys = @($PsBoundParameters.Keys | Where-Object { $_ -in $mandatoryParams })
        if ($keys.count -ne $mandatoryParams.count) {
            return $false
        }

        # test each parameter passed
        foreach ($key in $PsBoundParameters.Keys) {
            switch ($key) {
                { $_ -in @('DoneDate', 'CreatedDate') } {
                    if (-not (Test-TodoTxtDate $PsBoundParameters.$key)) {
                        return $false
                    }
                    break
                }

                "Priority" {
                    if (-not (Test-TodoTxtPriority $PSBoundParameters.$key)) {
                        return $false
                    }
                    break
                }

                "Task" {
                    if ([string]::IsNullOrEmpty($PSBoundParameters.$key)) {
                        return $false
                    }
                    break
                }

                { $_ -in @( 'Context', 'Project') } {
                    if (-not (Test-TodoTxtContext $PSBoundParameters.$key)) {
                        return $false
                    }
                }
            } #end switch
        } #end foreach

        # if we get here we have passed all Tests
        $true
    } #end process
}
function Test-TodoTxtContext {
<#
.SYNOPSIS
    Tests the todo context.
.DESCRIPTION
    Test the todo context is in the correct format.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)

    TODO : The function should only test a single context string so we know which one if any fail.
                At the moment if any of the contexts fail we fail the whole test.
.LINK
    https://www.github.com/pauby/pstodotxt
.OUTPUTS
    [System.Boolean]
.EXAMPLE
    Test-TodoContext "@computer","@home"

    Tests to see if the contexts "@computer" and "@home" are valid and returns $true or $false.
    #>


    [CmdletBinding()]
    [OutputType([System.Boolean])]
    Param (
        # The context(s) to test. A valid context is a string contains no
        # whitespace and starting with an '@'
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias("Project")]
        [string[]]$Context
    )

    # Context / Project / Tag / List should be a string (or an array of strings)
    $regex = [regex]"^[a-zA-z\d-_]+$"

    foreach ($item in $Context) {
        if (($regex.Match($item)).Success -ne $true) {
            $false
        }
        else {
            $true
        } # end if
    } # end foreach
}
function Test-TodoTxtDate {
<#
.SYNOPSIS
    Tests a date.
.DESCRIPTION
    Tests a date for the format yyy-MM-dd. It does not test to see if the date
    is in the future, past or present.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)

    TODO: Might be easier to this via a regular expression.
.LINK
    https://www.github.com/pauby/pstodotxt
.OUTPUTS
    [System.Boolean]
.EXAMPLE
    Test-TodoDate -TestDate '2015-10-10'

    Tests to ensure the date '2015-10-10' is in the valid todo date format and outputs $true or $false.
    #>


    [CmdletBinding()]
    [OutputType([System.Boolean])]
    Param (
        # The date to test. Note that this is a string and not a date object.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Date
    )

    # what we do here is first of all pass the date to Get-Date and ask it to format itsin yyyy-MM-dd.
    # If it doesn't output the same as the input the date is not in a valid format.
    # also make sure we don't display errors if there is invalid input; instead return $false
    $error.Clear()
    try {
        $result = Get-Date $Date -Format "yyyy-MM-dd" -ErrorAction SilentlyContinue
    }
    catch {
        return $false
    }

    # test if the date returned is not the same as the input or we have an error
    if ($result.CompareTo($Date) -ne 0) {
        $false
    }
    else {
        $true
    }
}
function Test-TodoTxtPriority {
<#
.SYNOPSIS
    Tests a todo priority.
.DESCRIPTION
    Tests to ensure that the priority is valid.
.NOTES
    Author: Paul Broadwith (https://github.com/pauby)
    TODO: Might be easier to this via a regular expression.
.LINK
    https://www.github.com/pauby/pstodotxt
.OUTPUTS
    [System.Boolean]
.EXAMPLE
    Test-TodoPriority "N"

    Tests to see if the priority "N" is valid and outputs $true or $false.
#>


    [CmdletBinding()]
    [OutputType([System.Boolean])]
    Param (
        # The priority to test. A valid priority is a single character string that is between A and Z.
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Priority
    )

    # ensure priority is one character long, is a letter between A and Z
    $Priority = $Priority.ToUpper()
    ($Priority.CompareTo("A") -ge 0) -and ($Priority.CompareTo("Z") -le 0) -and ($Priority.Length -eq 1)
}