Finding duplicates?

Is there a good outline path that will look for duplicates? By duplicates I mean rows that contain exactly the same text, without specifying what that text is.

I don’t think you can do this with just a path, but this script should do it I think:

tell application "Bike"
	tell front document
		repeat with each in rows
			set theText to name of each
			set thePath to "//@text = \"" & theText & "\""
			set theMatches to query outline path thePath
			if (count of theMatches) > 1 then
				display dialog theText
			end if
		end repeat
	end tell
end tell
3 Likes

and with a script you could, of course, also define various kinds of listing.

Here, for example, a list of labelled bike links (to duplicate lines) is displayed and copied to the clipboard.

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

    ObjC.import("AppKit");

    // Copy list of links to duplicated lines
    // in the front Bike document.

    // Rob Trew @2023
    // Ver 0.02

    // main :: IO ()
    const main = () => {
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return either(
            alert("Duplicated lines in Bike document")
        )(
            compose(
                alert("Links to duplicates copied to clipboard"),
                copyText
            )
        )(
            doc.exists()
                ? duplicateRowsLR(doc)
                : Left("No document open in Bike")
        );
    };

    // duplicateRowsLR :: Bike Doc -> Either String String
    const duplicateRowsLR = doc => {
        // Either a message or list of Bike links
        // to duplicated rows in the document.
        const
            docID = doc.id(),
            rows = doc.rows,
            duplicates = groupOn(fst)(
                sortOn(fst)(
                    zip(
                        rows.name()
                        .map(x => x.trim())
                    )(
                        rows.id()
                    )
                )
            )
            .flatMap(
                xs => 1 < xs.length
                    ? xs.map(
                        ([txt, id]) =>
                            `[${txt}](bike://${docID}/#${id})`
                    )
                    : []
            );

        return 0 < duplicates.length
            ? Right(
                duplicates.join("\n")
            )
            : Left("No duplicates found");
    };

    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

    // copyText :: String -> IO String
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;

        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
    // A function defined by the right-to-left
    // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // comparing :: Ord a => (b -> a) -> b -> b -> Ordering
    const comparing = f =>
    // The ordering of f(x) and f(y) as a value
    // drawn from {-1, 0, 1}, representing {LT, EQ, GT}.
        x => y => {
            const
                a = f(x),
                b = f(y);

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


    // fst :: (a, b) -> a
    const fst = tpl =>
    // First member of a pair.
        tpl[0];


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = eqOp =>
    // A list of lists, each containing only elements
    // equal under the given equality operator, such
    // that the concatenation of these lists is xs.
        xs => 0 < xs.length ? (() => {
            const [h, ...t] = xs;
            const [groups, g] = t.reduce(
                ([gs, a], x) => eqOp(a[0])(x) ? (
                    [gs, [...a, x]]
                ) : [[...gs, a], [x]],
                [[], [h]]
            );

            return [...groups, g];
        })() : [];


    // groupOn :: (a -> b) -> [a] -> [[a]]
    const groupOn = f =>
    // A list of lists, each containing only elements
    // which return equal values for f,
    // such that the concatenation of these lists is xs.
        xs => 0 < xs.length
            ? groupBy(a => b => a[0] === b[0])(
                xs.map(x => [f(x), x])
            )
            .map(gp => gp.map(ab => ab[1]))
            : [];


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
    // A copy of xs sorted by the comparator function f.
        xs => xs.slice()
        .sort((a, b) => f(a)(b));


    // 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 => sortBy(
            comparing(x => x[0])
        )(
            xs.map(x => [f(x), x])
        )
        .map(x => x[1]);

    // zip :: [a] -> [b] -> [(a, b)]
    const zip = xs =>
    // The paired members of xs and ys, up to
    // the length of the shorter of the two lists.
        ys => Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => [xs[i], ys[i]]);


    return main();
})();

To test in Script Editor, set the language selector at top left to JavaScript rather than AppleScript

See: Using Scripts - Bike

2 Likes

Thank to both. My goal was to automate the creation of a master task list within a document, where the list would be populated by “mirrors” of tasks in the rest of the outline. A “mirror” is simply a task type row with the name and link to the original task.
I wanted the automation to omit tasks that are already mirrored on the master list.

The solution was to run two separate queries, one querying the outline for tasks (except the master task list) and the other querying the master task list, then checking to see if the first query includes items already in the second query. If there’s a task missing on the master list, the automation creates a mirror to the missing tasks inside the master task list.

I’m attaching the demo shortcut if anyone wants to play with it.

Bike Create master todo list.shortcut.zip (13.3 KB)

Some improvements that could be made:

  • the Master task list needs to have at least one task item inside for the script to work. I’m sure there’s a way to fix that;
  • a better way to compare lists would be by checking if links in the master task list include row IDs from the rest of the outline, instead of checking the row name. This would allow the inclusion of tasks that share the same name. I’m guessing there’s a way to do that with //*/run::@link but haven’t the time to follow this through.
2 Likes