DueDates Script for Taskpaper 3?

I do plan to add ISO-date creation, just having decided how best to do it. In simplest case you could just select some text and issue a command convert-relative-date-to-absolute. So that would turn “next monday” into “2016-02-15”. Let me know if you’ve any ideas on better UI for this.

Adding ISO-date creation would be great!

Concerning the UI: I don’t really like the need to mark text for conversion first. Would it be possible to have a nothing-selected-fallback that tries to convert all non-ISO due-tags? So that all tags like @due{next monday} (or even shorter @due{mon}) anywhere in the text are converted to @due{2016-02-15}?

In the next preview release I’ll include api for converting a date string to a TaskPaper style ISO date. It will then be quick to write a script to search for and convert all @due tag relative dates to ISO dates. (Let me know if not quick for you and I can help). That can get you started working with the feature, later if it makes sense can decide how best to build it in.

1 Like

Sounds great - will try to get my workflow working!

Thanks,
k.

Wow, that is just awesome

Jesse’s new API will make these things much easier, but in the meanwhile, an interim Keyboard Maestro macro which translates informal date times (using TP3’s date language) when you close the final parenthesis on a tag in TaskPaper 3 Preview:

1 Like

(Minor update to ver 0.10 of the script above, using the new API in build 169)

I finally found the time to try your script - this is really nice! I set up my workflow to switch to tp3 now and will try that in the next days!

Thanks again,
k.

1 Like

Are times on today’s date supported?

Starts today @search(* except //@start>[d] today)

  • starts today @start(2016-02-18)
  • starts today 9am (in the past) @start(2016-02-18 09:00)
  • starts today 9pm (in the future) @start(2016-02-18 21:00)

This saved search only shows the first line for me (when I run this at 11:25am, and the second line should be included).

What am I missing?

I think the mapping is:

 `today` -> midnight at start of today

`2016-02-18` -> 2016-02-18 00:00

So line one is not excluded by your except clause (00:00 is not more that 00:00)

but the other two are excluded:

  • 09:00 > 00:00
  • 21:00 > 00:00

Perhaps your filter is really intended to exclude things which start beyond midnight (or end of office hours) ?

( or perhaps which start beyond the current value of the now keyword ?)

1 Like

@complexpoint Yes, the now keyword is exactly what I want. Thank you!

1 Like

@complexpoint Thank you for the awesome macro!

1 Like

I’m surprised I can’t find more discussion or instruction about workflow with recurring tasks. I also used to use DueDates, and that made sense. Now, it looks like I either have to setup tasks that recur, but cannot mark them as complete (unsatisfying, and no record of completion), or manually recreate them every time.

Regardless, I attempted setting up recurring tasks that I just wouldn’t make as complete. I took a big list of dated entries, many of which have today’s date. I can use something like @due(Wednesday) (assuming that means it will be due every Wednesday…today is Thursday), then I try to query for items due today. I tried:

@due = [d] today (shows nothing)
@due = [d] today (shows nothing)
@due <= [d] today (works correctly, but shows the task I set for Wednesday…apparently it shows tasks from earlier in the week)

I can’t figure out how to get just today’s tasks.

Are there any guides out for recurring workflows/recommendations?

The thing to remember is that any dateTime value has a time as well as a day. If no time is shown, then its value is precisely midnight.

That means that the = operator is not likely to be what you want – it’s only going to find things that precisely, down to milliseconds, share the time as well as a day.

To match the period between the start and end of a calendar or working day, < and > are what you need. For example, from midnight precisely this morning, to a millisecond before midnight tonight:

@due >=[d] today and @due <[d] tomorrow

( and of course you can adjust that for a watershed at the start or end of the working day, rather than midnight)

I wrote a primer some time ago. Read it and if you have some specific questions, just create a new post with the question and I or others will help you out.

Primer on using dates

That helped. Thanks. With DueDates, I had a clean slate at the end of the day. With this method, recurring items just have to stay there in the “today” query. Maybe a script that just duplicates items in the archive tagged @recurring and resets their status. That would solve it. You check off tasks as you finish them, they go into the archive, then you run the script every morning and get fresh tasks. I’m going to look into this.

Should not be too difficult, I think, to write something for TaskPaper 3 which uses the same @repeat tag format as that earlier script, and creates or updates @due dates in tasks which:

  • have a @repeat(unit:n) tag,
  • and are marked @done,
  • or don’t yet have a @due tag.

Is that more or less the spec ?

Something to be run intermittently from Hazel, or manually launched as an update script ?

Yep, I think that’s it.

Here’s a first rough draft, so there may be some glitches to discover.

It aims to handle @repeat tags in the style of the earlier script at:

You could test it initially from Script Editor, using dummy data, and ⌘Z Undo,

and later assign to a keystroke, or put it in the TaskPaper script menu

JavaScript for Automation Source:
(make sure that you scroll down to fully copy all of the lines)

// First ROUGH draft of @repeat tag handling on the model of an earlier script at:
// http://www.hogbaysoftware.com/wiki/DueDates

// TEST with a combination of dummy data and the UNDO key (Command-Z)

// Rob Trew @complexpoint 2016-05-06

(function () {
    'use strict';

    // TASKPAPER CONTEXT

    function TaskPaperContext(editor, options) {

        // What @repeat(UNIT:Quantity) pair do we have here ?
        // unitQuantParsed :: String -> maybe String
        //                -> {unit: maybe String, quant: maybe Integer}
        function unitQuantParsed(strSerial, strMaybeDelimiter) {
            var lstParts = strSerial.split(strMaybeDelimiter || ':'),
                lngParts = lstParts.length,
                strUnit = lngParts > 0 ? lstParts[0] : undefined;

            return {
                unit: strUnit,
                nameInterval: strUnit ? nameInterval(strUnit) : undefined,
                quant: !isNaN(lngParts > 1 ? (
                    lstParts[1]
                ) : undefined) ? (
                    parseInt(lstParts[1], 10)
                ) : undefined
            };
        }

        // Is this a unit like day, month, year etc
        // or a calendar Name like Friday, May etc ?

        // Weekday names -> 7, Month names -> c. 365
        // nameInterval :: String -> Integer
        function nameInterval(strDayOrMonthName) {
            return isNaN(DateTime.parse(strDayOrMonthName)) ? (
                undefined
            ) : (
                DateTime.parse('next ' + strDayOrMonthName) -
                DateTime.parse('last ' + strDayOrMonthName)
            ) / 172800000; // 2 days of milliSeconds
        }

        // Parseable expression for dateTime of next recurrence ?
        // nextExpression :: Item -> String
        function nextExpression(item) {
            var strRepeat = item.getAttribute('data-repeat'),
                dctRepeat = unitQuantParsed(strRepeat),

                // If the task has a @due tag, then all calculations are done
                // relative to that date.
                // Otherwise, they will be calculated relative to today.
                dteAnchor = DateTime.parse(
                    item.getAttribute('data-due') || 'today'
                );

            if (dteAnchor && dctRepeat.unit) {
                var lngInterval = dctRepeat.nameInterval;

                if (lngInterval) {
                    var dteNamed = DateTime.parse(dctRepeat.unit),
                        strBase = (dteNamed < dteAnchor) ? (
                            'next ' + dctRepeat.unit
                        ) : DateTime.format(dteNamed),

                        lngSkipped = dctRepeat.quant ? (
                            dctRepeat.quant - 1
                        ) : 0;

                    return lngSkipped ? (
                        (strBase + ' +' + lngSkipped.toString()) +
                        (lngInterval === 7 ? ' weeks' : (
                            lngInterval > 360 ? ' years' : ' days'
                        ))
                    ) : strBase;

                } else return DateTime.format(dteAnchor) + ' +' +
                    (dctRepeat.quant || 1) + ' ' + dctRepeat.unit

            } else return undefined;
        }


        // MAIN

        var outline = editor.outline,
            lstUpdateable = outline.evaluateItemPath(
                '//@repeat and (@done or not @due)');


        if (lstUpdateable.length > 0) {
            var result = undefined;

            outline.groupUndoAndChanges(function () {
                result = lstUpdateable
                    .map(function (item) {
                        var strNext = nextExpression(item);

                        if (strNext) {
                            var dteNext = DateTime.parse(
                                strNext
                            );

                            if (!isNaN(dteNext)) {
                                item.removeAttribute(
                                    'data-done'
                                );
                                item.setAttribute(
                                    'data-due',
                                    DateTime.format(
                                        dteNext
                                    )
                                );

                            } else return 'Not parsed: ' + item.bodyString;
                        } else return 'Not parsed: ' + item.bodyString;

                        return strNext;
                    });

            });
            return result;
        }
    };

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    var tp3 = Application('com.hogbaysoftware.TaskPaper3'),
        ds = tp3.documents,
        d = ds.length ? ds[0] : (
            ds.push(tp3.Document()), ds[0]
        );

    return d.evaluate({
        script: TaskPaperContext.toString(),
        withOptions: {
            delimiter: ':'
        }
    });
})();

This seems to work perfectly.

2 Likes