Filter by date then sort by tag

Varieties of this question have been asked before I think but not in this form so I created a new topic, Apologies for possible overlaps.

I have saved searches for my tasks for every day of the week so that I can quickly see what’s on my plate for any given day. What I would love to do is be able to sort the tasks for a given day further by non-date tags (for instance @mail, @writing, @reading, @discuss etc); this way I can more easily plan my day in blocks since tasks of a certain type are all grouped together.

This is the kind of thing that is possible with agenda views in emacs + orgmode. I have been experimenting a bit with emacs already but wonder if this could be somehow scripted?

Sorting is the tricky part… TaskPaper doesn’t support sorted views directly. With a script your options are:

  1. You could write a script to sort your view in place… but that means it stays sorted, so you can’t go back to your original order.

  2. You could write a script to generate a new sorted view by copying items out of your existing document. Then the new view is sorted, and original view is left as is. But now you have two views, so they won’t sync. I don’t know to much about org-mode… but I just read a paragraph about Agenda views in org-mode and it says they are (read only, in separate buffer) which sounds a lot like what I’m describing in case 2 here.

1 Like

If you have some experience with Emacs, you can give the TaskPaper mode for Emacs a try. It has an Agenda view, very similar to Org Agenda view, but works with TaskPaper files.

Thanks a lot for both replies.

@jessegrosjean use case 2 is indeed what agenda views in orgmode do.

@saf-dmitry I just discovered TP mode and it looks good; I have some trouble getting the agenda view to work but still getting used to emacs… would you mind if I DM for some help getting it going?

@gerbenzaagsma If you need help with TP mode, just DM me.

Are there any examples of scripts that do something like @jessegrosjean 's No. 2 above?

I want to write a script that filters and sorts my tasks and dumps them into a new (temporary) TaskPaper document. Despite a decent amount of experience with Python, I’ve never managed to write a successful JXA script. I’m hoping a simple example like the above would help me get going…

Thanks!

Hi there, brand new to Taskpaper and wondering if there are any scripts for @jessegrosjean’s #1 above? I have only 1 tag that I’m wanting to sort by, but it has different values: @status(next), @status(later), @status(someday).

Thanks in advance!

1 Like

Did you find the extensions wiki yet?

There’s a section of sorting scripts.

yup - it seems that all the sorting scripts there are either alphabetical or by due date and not by a tag value. Unless I missed something

Due date is a tag value like any other – should prove transferable, I think.

See under Item.getAttribute

(Item · GitBook)

oh wow - never thought about it like that, thanks - I’ll give that a try.

EDIT: Now thinking about this a bit more, what I would actually need is a multi-level sort. So sort all projects by @ProjStatus values and sort all tasks by @ActionStatus values. What complicates matters is that I’d want these statuses not to be sorted alphabetically, but rather in a predefined order:

  • @ProjStatus(now)@ProjStatus(next)@ProjStatus(later)@ProjStatus(waiting)@ProjStatus(someday)

You just need to write a custom comparator (compare function) for each tag:

Array.prototype.sort() - JavaScript | MDN

and that in turn can make use of a custom dictionary mapping each value string to an ordinal number:

const statusOrderMap = {
    now: 0,
    next: 1,
    later: 2,
    waiting: 3,
    someday: 4
};

There is a distinction, of course between:

  • obtaining a sorted copy of a list of projects/tasks/notes
  • updating the editor to contain the sorted version

but narrowing focus to:

  • obtaining a sorted list (without updating the editor state)
  • of just top-level projects
  • by using a custom comparator and a dictionary of ordinal values

here is a sketch of one approach:

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // main :: IO ()
    const main = () => {
        const windows = Application("TaskPaper").windows;

        return 0 < windows.length ? (
            windows.at(0).document.evaluate({
                script: `${TaskPaperContext}`,
                withOptions: {}
            })
        ) : "No document windows open in TaskPaper";
    };

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const statusOrder = {
            now: 0,
            next: 1,
            later: 2,
            waiting: 3,
            someday: 4
        };

        const tpMain = () => {
            const
                outline = editor.outline,
                projects = outline.evaluateItemPath(
                    "/@type=project"
                );

            // Just a reordered list of top-level projects
            // the editor status is not yet updated.
            return sortedNodeArray(
                x => statusOrder[
                    x.getAttribute("data-ProjStatus")
                ]
            )(false)(projects)
            .map(x => x.bodyString);
        };

        // ----------- FUNCTIONS FOR TASKPAPER -----------

        // sortedNodeArray :: (Node -> a) -> Bool ->
        // [TPNode] -> [TPNode]
        const sortedNodeArray = propertyAccessor =>
            // A sorted array of Taskpaper 3 nodes,
            // sorted by the property fetched from each node
            // by the propertyAccessor function,
            // and descending if blnDesc is true.
            blnDesc => sortBy(
                // (flip reverses the arguments, and
                //  thus derives DESCending from ASCending)
                (blnDesc ? flip : identity)(
                    comparing(propertyAccessor)
                )
            );

        // ----------- GENERICS FOR TASKPAPER ------------

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            x => y => {
                const
                    a = f(x),
                    b = f(y);

                return a < b ? -1 : (a > b ? 1 : 0);
            };


        // identity :: a -> a
        const identity = x =>
            // The identity function. No change.
            x;


        // flip :: (a -> b -> c) -> b -> a -> c
        const flip = op =>
            // The binary function op with
            // its arguments reversed.
            1 < op.length ? (
                (a, b) => op(b, a)
            ) : (x => y => op(y)(x));


        // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
        const sortBy = f =>
            xs => xs.slice()
            .sort((a, b) => f(a)(b));


        // MAIN ---
        return tpMain();
    };

    // --------------------- GENERIC ---------------------


    return main();
})();
2 Likes

If your file were large enough for performance to lag perceptibly on sort, you could eliminate repeated fetches of sortable properties by using a slightly different pattern:

// sortOn :: Ord b => (a -> b) -> [a] -> [a]
const sortOn = f =>
    // Equivalent to sortBy(comparing(f)), but with f(x)
    // evaluated only once for each x in xs.
    // ('Schwartzian' decorate-sort-undecorate).
    xs => xs.map(
        x => Tuple(f(x))(x)
    )
    .sort(uncurry(comparing(fst)))
    .map(snd);

but that entails a few extra dependencies, and is only really worthwhile at largish scale.

The extra odds and ends are at:

2 Likes

Hey @complexpoint - thanks so much for taking the time in generating these examples. My scripting knowledge is basically zero so these really help a lot! I’ll take a look at what you shared over the next few days and see what I can put together. For the record I was looking to sort the list in place, which would be your 2nd usecase (“updating the editor to contain the sorted version”).

Thanks again for the help!

Hey @complexpoint, I’ve finally had the chance to give your script a go, but it doesn’t seem to do anything :thinking:. I changed the main tag’s name from @projStatus@ps but made sure to update it in your script. When I run the script in ScriptEditor I get a result that seems to be an array (my guess), but that hasn’t been sorted.

Any ideas?

You would need to show me:

  1. The exact source of the script that you are using,
  2. a sample TaskPaper text
  3. The output you are seeing
  4. The output you expected

Hey @complexpoint - thanks so much for the help

In summary what I’m trying to achieve is a 2 level sort (projects & tasks), where projects are sorted by one tag’s values (@ps) and tasks another tag’s values (@as). And when I mean sort I am referring to updating the document to the sorted version.

The tags that I mentioned above have the following values and sort order (not alphabetic):
Projects:

@ps(urgent) → @ps(perpetual) → @ps(current) → @ps(following) → @ps(future) → @ps(someday)

Tasks:

@as(now) → @as(next) → @as(later) → @as(waiting) → @as(someday)

Regarding your message I just tried to use the script that you posted above to get a feel if I could tweak it for my usecase described above, but tbh didn’t know where to start :see_no_evil:

Very happy to help, but times presses so I do need:

(The previous illustrative snippet wasn’t a complete script, and used different tag names from those you are suggesting here).

Up to you, however. to provide the 1.-4. (above) samples, if I am going to have time to take a look.

Ok no problem.

  1. So the script that I tried was this one, but as you mentioned it wasn’t a complete script.
Source script
(() => {
    "use strict";

    // main :: IO ()
    const main = () => {
        const windows = Application("TaskPaper").windows;

        return 0 < windows.length ? (
            windows.at(0).document.evaluate({
                script: `${TaskPaperContext}`,
                withOptions: {}
            })
        ) : "No document windows open in TaskPaper";
    };

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const statusOrder = {
            urgent: 0,
            perpetual: 1,
            current: 2,
            following: 3,
            future: 4,
			someday: 5
        };

        const tpMain = () => {
            const
                outline = editor.outline,
                projects = outline.evaluateItemPath(
                    "/@type=project"
                );

            // Just a reordered list of top-level projects
            // the editor status is not yet updated.
            return sortedNodeArray(
                x => statusOrder[
                    x.getAttribute("data-ps")
                ]
            )(false)(projects)
            .map(x => x.bodyString);
        };

        // ----------- FUNCTIONS FOR TASKPAPER -----------

        // sortedNodeArray :: (Node -> a) -> Bool ->
        // [TPNode] -> [TPNode]
        const sortedNodeArray = propertyAccessor =>
            // A sorted array of Taskpaper 3 nodes,
            // sorted by the property fetched from each node
            // by the propertyAccessor function,
            // and descending if blnDesc is true.
            blnDesc => sortBy(
                // (flip reverses the arguments, and
                //  thus derives DESCending from ASCending)
                (blnDesc ? flip : identity)(
                    comparing(propertyAccessor)
                )
            );

        // ----------- GENERICS FOR TASKPAPER ------------

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            x => y => {
                const
                    a = f(x),
                    b = f(y);

                return a < b ? -1 : (a > b ? 1 : 0);
            };


        // identity :: a -> a
        const identity = x =>
            // The identity function. No change.
            x;


        // flip :: (a -> b -> c) -> b -> a -> c
        const flip = op =>
            // The binary function op with
            // its arguments reversed.
            1 < op.length ? (
                (a, b) => op(b, a)
            ) : (x => y => op(y)(x));


        // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
        const sortBy = f =>
            xs => xs.slice()
            .sort((a, b) => f(a)(b));


        // MAIN ---
        return tpMain();
    };

    // --------------------- GENERIC ---------------------


    return main();
})();
  1. Here’s a sample .taskpaper snippet that I used to test it
Unsorted sample
Buy a boat: @ps(someday)
	- 🔵 Subscribe to a boating magazine | @as(someday)
	- 🟠 Do my skippers license | @as(next)
	- 🟡 Go down to the boat shop and ask for advice | @as(later)
	- 🔴 Do reasearch on boats | @as(now)
Go to the grocery store: @ps(perpetual)
	- 🟠 But apples | @as(next)
	- 🔴 Find out when they have salmon in stock | @as(now)
Paint the house: @ps(following)
	- 🟤  Waiting to hear back from Steve to help out | @as(waiting)
	- 🟠 Start sanding the house | @as(next)
	- 🔴 Go to the shop and buy paint | @as(now)
Buy birthday gift for James: @ps(urgent)
	- 🟠 Go to the mall | @as(next)
	- 🟡 Wrap the gift | @as(later)
	- 🔴 Speak to Marie about what James likes | @as(now)
	- 🟡 Write a card | @as(later)
	- 🟤 Give gift to James | @as(waiting)
Clean the pool: @ps(current)
	- 🟠 Put chlorine into the pool | @as(next)
	- 🟡 Run pool pump | @as(later)
	- 🔴 Buy chlorine | @as(now)
	- 🔵 Swim | @as(someday)
Go bowling: @ps(future)
	- 🔴 Learn how to bowl | @as(now)
	- 🔵 Buy bowling shoes  | @as(someday)
	- 🔴 Find a local club | @as(now)
  1. So the output that I see is in the Script editor’s result window and returns a sorted array of the projects which is awesome! I must have done something wrong earlier as I didn’t see this behavior before :see_no_evil:
Result
["Buy birthday gift for James: @ps(urgent)", 
"Go to the grocery store: @ps(perpetual)", 
"Clean the pool: @ps(current)", 
"Paint the house: @ps(following)", 
"Go bowling: @ps(future)", 
"Buy a boat: @ps(someday)"]
  1. I’d expect the editor to update with the sorted version (so your option 2 above) and also for all the children tasks to also be sorted (but the value of @as tag) under each project. So as an example for the @ps(urgent) project “Buy birthday gift for James” the task order would be as follows:
Expected result
Buy birthday gift for James: @ps(urgent)
	- 🔴 Speak to Marie about what James likes | @as(now)
	- 🟠 Go to the mall | @as(next)
	- 🟡 Wrap the gift | @as(later)
	- 🟡 Write a card | @as(later)
	- 🟤 Give gift to James | @as(waiting)

I hope that this helps!