DueDates

Introduction to the DueDates script

Please also see the variant for managing start and due dates here which is based on this script.

There has been some discussion about managing due dates on tasks in TaskPaper, and this is a problem that I too need to solve. Thanks to the terrific job Jesse did on the object model and AppleScript support TaskPaper 2, I was able to write an AppleScript that provides the basic functionality for managing due dates that I need. So, I thought I would post the script for anyone who might find it useful either as is or modified to suit your needs.

I had 5 main goals for managing tasks with due dates:

  1. Keep track of when certain tasks are due.
  2. Find tasks due today.
  3. Find tasks that are over due.
  4. Find tasks that are due soon.
  5. Manage tasks that need to be done on a regular basis (recurring tasks).

My basic approach to doing this in TaskPaper is to add @due tags with a value that is either a date or a word that describes a date (e.g. "wednesday"). I then run the AppleScript and it handles adding tags to tasks that are due today (@today), tasks that are past due (@overdue), and tasks that will be due in the next 5 days (@soon). For tasks that need to be done on a regular basis, I add a @repeat tag that describes how often it needs to happen, then after the task is marked @done, running the script will remove the @done task and update the date in the @due tag. (Details on using the script are below.)

I created queries in TaskPaper that allow me to find everything due today (@due, @overdue) and upcoming items (@upcoming). I also use the Themes to add a color to items by tag. I have @overdue items appear in red, for example.

Before I get into the details of the script, I should make a couple of disclaimers. It does add and remove tags on tasks, and I can't promise it won't wreck your document either due to a bug in the script, or a bug in the AppleScript support in TaskPaper 2. I also wrote it mainly late at night, and don't have much time to support it.

All that said, this script has worked fine for me now for a few weeks. If you have questions or suggestions, feel free to post them here and I'll see what I can do. I'm relatively new to the wacky world of AppleScript, so if you have suggestions on how to do stuff better, please do share.

How to use the script

Anyway, here are some details on using the script, including the tags the script uses and the valid values for each.

Entering Due Dates

To associate a due date with a task, you add the @due tag with a date value. The date must be in the format YYYY-MM-DD.

Example:

- This task has a due date @due(2008-05-31)

Instead of the date, you can also enter one of the following values and they will be converted to a date in the YYYY-MM-DD format when the script is run:

Examples:

- This will be due today @due(today)  
- This will be due tomorrow @due(tomorrow)  
- This will be due next Tuesday @due(tuesday)  
- This will be due today @due

Managing Repeating Tasks

You can specify that a task should be done at a regular interval by using the @repeat tag. The following are valid values for the @repeat tag.

Any of these values can be followed by a ":" and a number that specifies a count (this will make more sense when you see the examples below). If no count is specified, 1 is assumed.

If the task has a @due tag, then all calculations are done relative to that date. Otherwise, they will be calculated relative to today.

The @repeat tag is only evaluated on items that are marked done, or items that have no @due tag. So when you finish a repeating task and mark it done, the next time you run the script, the the @done tag will be removed, and the @due tag will have its date value changed to a new date based on the @repeat tag value. When the new date is calculated, the script will always generate a new due date that is on or after the current date. So if you have a task that repeats every day, and you skip 2 days, the next due date will be today, not yesterday.

Examples:

- This will repeat every 3 days, starting 3 days from today @repeat(day:3)
- This will repeat every other monday @due(2008-05-12) @repeat(monday:2) @done(2008-05-12)
- This will repeat every month on the 15th, but when the script is run nothing will change on this item because it is not marked done @due(2008-05-15) @repeat(month)

Note that this script will not currently resurrect done items from the Archive project. That should be doable, I just didn't write the code to do it. So you should run the script before archiving done tasks.

What Happens When You Run the Script

When you run the script, first thing it does is give all @due tags with a descriptive value (e.g. @due(tomorrow)) an actual date. Then it updates @due dates on all the tasks with @repeat tags that are done, or missing a @due tag. Finally, it adds the @today, @overdue and @upcoming tags as appropriate.

The script also has a list of tags that will get removed from tasks marked @done. The main ones that get stripped are the @today, @upcoming and @overdue tags. I did this because it simplifies queries if I don't have to ignore tasks with @done tags. If you don't want that to happen then you can...

Customize the Script

The properties at the beginning of the script allow you to customize some behaviors without digging into the code. For the most part, you are able customize all the tag names used, the number of days into to future to look for "upcoming" items (default is 5), and the tags that get stripped from @done tasks.

Error Handling

There is some basic error handling in the script for things like invalid date formats and unknown repeat values. If the script encounters a problem, it will add an @error tag to the task where it found the problem.

What It Will Not Do

There are probably a lot of things you might want that this script doesn't currently have, but here are a couple I can think of:

The Script

To use this script, copy it and paste it into Script Editor. You may need to change the name of the TaskPaper application in the first "tell" block. You can also edit any of the properties at the beginning to suit your tastes.

Finally, compile the script and save it in the TaskPaper Scripts folder. I also suggest adding a keyboard equivalent using them method described in this tip.


   (
    A basic script for managing due dates.
)

property dueTag : "due" property repeatTag : "repeat" property todayTag : "today" property overdueTag : "overdue" property upcomingTag : "upcoming" property doneTag : "done" property inProgressTag : "inprogress" property errorTag : "error" property removeTags : {upcomingTag, todayTag, overdueTag, inProgressTag} property upcomingDays : 5

property numberSet : {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}

global vToday set vToday to current date set time of vToday to 0

tell front document of application "TaskPaper" --Start by converting any due tags with a text description of the date --to an actual date repeat with tpDueTag in every tag of (every entry) whose name is dueTag tell tpDueTag set vDueVal to value if vDueVal is missing value then set vDueVal to "today" end if if character 1 of vDueVal is not in numberSet then set vDateString to null tell me to set vDate to getDateForDueValue(vDueVal) if vDate is not null then tell me to set vDateString to getTextFromDate(vDate) else tell entry of tpDueTag make new tag with properties {name:errorTag, value:"Unknown value for due tag"} end tell end if if vDateString is not null then set value to vDateString end if end if end tell end repeat

--Next handle repeat tags. A due tag is not required on these.
repeat with tpRptTag in every tag of (every entry) whose name is repeatTag
    tell entry of tpRptTag
        set tagList to (every tag whose name is dueTag)
        set needDue to false
        if tagList is {} then
            --no due tag, create one, and remember to fill in a date below.
            --use today as the start date
            set tpDueTag to make new tag with properties {name:dueTag}
            copy vToday to vDate
            set needDue to true
        else
            --have a due tag, use that as the start date
            set tpDueTag to first item of tagList
            set dateString to (get value of tpDueTag)
            tell me to set vDate to getDateFromText(dateString)
        end if
        if not needDue then
            set tagList to (every tag whose name is doneTag)
        end if
        --if there was no due tag, or the entry is marked done,
        --the calculate and set the date of the due tag.
        if needDue or tagList is not {} then
            --Delete all extra tags
            repeat with delTag in removeTags
                delete (every tag whose name is delTag)
            end repeat

            --Create a date from the repeat value
            set rInfo to (get value of tpRptTag)
            if (vDate is not null) then 
                tell me to set vDate to getRepeatDate(rInfo, vDate)
            end if

            if vDate is not null then
                --fill in the date, make sure it is not marked done
                tell me to set vDateText to getTextFromDate(vDate)
                set value of tpDueTag to vDateText
                delete (every tag whose name is doneTag)
            else
                --flag unknown repeat value with an error tag
                make new tag with properties {name:errorTag, value:"unknown repeat type"}
            end if
        end if
    end tell
end repeat

--Add/update the extra tags based on the due date of items that are not done
repeat with tpDueTag in (every tag of (every entry) whose name is dueTag)
    tell entry of tpDueTag
        set tagList to (every tag whose name is doneTag)
        set errTagList to (every tag whose name is errorTag)
        if (tagList is {}) and (errTagList is {}) then
            --Not marked done, see if any tags need to be added
            set dateString to (get value of tpDueTag)
            tell me to set vDate to getDateFromText(dateString)
            if vDate is not null
                set diffDays to (vDate - vToday) / days
                if (diffDays < 0) then
                    set tagList to (every tag whose name is overdueTag)
                    if (tagList is {}) then
                        make new tag with properties {name:overdueTag}
                    end if
                    delete (every tag whose name is upcomingTag)
                    delete (every tag whose name is todayTag)
                else if (diffDays = 0) then
                    set tagList to (every tag whose name is todayTag)
                    if (tagList is {}) then
                        make new tag with properties {name:todayTag}
                    end if
                    delete (every tag whose name is upcomingTag)
                else if (diffDays <= upcomingDays) then
                    set tagList to (every tag whose name is upcomingTag)
                    if (tagList is {}) then
                        make new tag with properties {name:upcomingTag}
                        delete (every tag whose name is todayTag)
                    end if
                end if
            else
                --invalid date format
                make new tag with properties {name:"error", value:"invalid date format"}
            end if
        else
            --Marked done, delete all extra tags
            repeat with delTag in removeTags
                delete (every tag whose name is delTag)
            end repeat
        end if
    end tell
end repeat

end tell

on getDateFromText(dateText) set vDate to null set AppleScript's text item delimiters to {"-"} if (count of text item of dateText) is 3 then set vYear to text item 1 of dateText set vMonth to text item 2 of dateText set vDay to text item 3 of dateText if (vYear > 1000) and (vMonth > 0 and vMonth < 13) and (vDay > 0 and vDay < 32) then set vDate to current date set year of vDate to (vYear as integer) set month of vDate to vMonth as integer set day of vDate to vDay as integer set time of vDate to 0 end if end if return vDate end getDateFromText

on getTextFromDate(vDate) set dText to ((year of vDate) as text) & "-" set dayText to (month of vDate as number) as text if length of dayText is 1 then set dayText to "0" & dayText end if set dText to dText & dayText & "-" set dayText to (day of vDate as number) as text if length of dayText is 1 then set dayText to "0" & dayText end if return dText & dayText end getTextFromDate

on getDateForDueValue(dueValue) set vDate to null if character 1 of dueValue is in numberSet then set vDate to getDateFromText(dueValue) else --Special cases for due date not handled in getRepeatDate if "today" is dueValue then copy vToday to vDate else if "tomorrow" starts with dueValue then copy vToday to vDate set vDate to vDate + 1 * days else set vDate to getRepeatDate(dueValue, vToday) end if end if return vDate end getDateForDueValue

on getRepeatDate(repeatDesc, fromDate) copy fromDate to vDate

set AppleScript's text item delimiters to {":"}
set repeatType to text item 1 of repeatDesc
if (count of text items in repeatDesc) > 1 then
    set repeatCount to text item 2 of repeatDesc
else
    set repeatCount to 1
end if

if repeatType is "day" or repeatType is "week" then
    if repeatType is "day" then
        set vInterval to days
    else
        set vInterval to weeks
    end if
    set vDate to vDate + repeatCount * vInterval
    repeat while vDate comes before vToday
        set vDate to vDate + repeatCount * vInterval
    end repeat
else if repeatType is "month" then
    set month of vDate to (month of vDate) + repeatCount
    repeat while vDate comes before vToday
        set month of vDate to (month of vDate) + repeatCount
    end repeat
else if repeatType is "year" then
    set year of vDate to (year of vDate) + repeatCount
    Log "vDate is " & vDate
    repeat while vDate comes before vToday
        set year of vDate to (year of vDate) + repeatCount
    end repeat
else
    set rDay to 0
    if "sunday" starts with repeatType then
        set rDay to Sunday
    else if "monday" starts with repeatType then
        set rDay to Monday
    else if "tuesday" starts with repeatType then
        set rDay to Tuesday
    else if "wednesday" starts with repeatType then
        set rDay to Wednesday
    else if "thursday" starts with repeatType then
        set rDay to Thursday
    else if "friday" starts with repeatType then
        set rDay to Friday
    else if "saturday" starts with repeatType then
        set rDay to Saturday
    end if
    if rDay > 0 then
        --Handle case where vDate is not currect day of week.
        set vOffset to rDay - (weekday of vDate)
        if vOffset is not 0 then
            set vDate to vDate + vOffset * days
            if vOffset > 0 then
                --If we move forward, count that as 1
                set repeatCount to repeatCount - 1
            end if
        end if

        --Find next date after vDate
        set vOffset to 7 * repeatCount
        set vDate to vDate + vOffset * days
        repeat while vDate comes before vToday
            set vDate to vDate + vOffset * days
        end repeat
    else
        --Unknown type, return nothing
        set vDate to null
    end if
end if

return vDate

end getRepeatDate

Download Parse Due Dates.applescript

Testing the Script

Finally, if you want to play with the script and see what it does, here is text of the TaskPaper document I have been using to test the script. Just paste this into a new TaskPaper document and then run the script.


       Assigning due dates :
       Items due today will get the today tag.
       Items due in the next 5 days get the upcoming tag.
       Items due prior to today that are not marked done get the overdue tag.
       - Enter due dates in YYYY-MM-DD format
           - This due 5/01/08, should be marked overdue @due(2008-05-01) 
           - Use today and tomorrow as values.  Script will replace them with actual dates.
               - this is due today @due(today)
               - this is due tomorrow @due(tomorrow)
       - Use same specifiers as repeat tags.  Script will replace them with actual dates.
           - Do this tomorrow @due(day)
           - Do this in 3 days @due(day:3)
           - Do this next wednesday @due(wednesday)
           - Do this next week @due(week)
           - Do this in 2 weeks @due(week:2)
           - Do this next month @due(month)
           - Do this next year @due(year)
       - Error Handling
           - Invalid date @due(5/25)
               - Works, generates error tag
           - No date @due
               - Works, defaults to today
           - Invalid specifier @due(yesterday)
               - Works, generates error tag.
       Repeating Items : 
       The repeat tag is only processed if the item is marked done, or does not have a due date assigned.
       - day
           - With number, every x days.  Example, every 4 days @due(2008-05-21) @repeat(day:4) @done(2008-05-26)
       - day name
           - With number, repeat every x day names.  Example, every other tuesday @repeat(tuesday:2) @due(2008-05-20) @done(2008-05-26)
           - Allow abbreviations
               - Every 2 Mondays @repeat(mon:2) @due(2008-05-25) @done(2008-05-26)
               - Repeat every Friday starting this Friday @repeat(fri)
           - Add "-month" to get "of the month".  Example, every 3rd thursday of month:@repeat(thursday-month:3)
               - This one is not implemented yet
       - week
           - With number, every x weeks. Example, every other week @due(2008-05-15) @repeat(week:2) @done(2008-05-26)
       - month
           - With number, every x months. Example, every six months @due(2008-05-05) @repeat(month:6) @done(2008-05-26)
       - year
           - With number, every x years.  Example, a year later @due(2008-04-15) @repeat(year:1) @done(2008-05-26)
       - If no due tag present, add one, calculating from today
           - Repeat every day, starting tomorrow @repeat(day) 
       - Error Handling
           - Bad specifier with due date @due(2008-05-21) @repeat(yesterday) @done(2008-05-26)
               - Works, generates error tag.
           - Bad specifier @repeat(yesterday)
               - Mostly works, generates error tag, but also empty due tag.
           - Bad date with repeat @due(5/25) @repeat(month)
               - Works, generates error tag.