BIKE Outliner – Sum of numeric rows

A macro for Jesse Grosjean's Bike Outliner.

SUM of the numeric rows in the extended selection.

OR (if the selection is not extended)
Sum of all visible numeric rows descending
from (i.e. indented below) the current line.

(Any descendant rows hidden by outline folding are excluded from the sum)

( The sum is copied to the clipboard, and displayed in a dialog )

BIKE -- Sum of some numeric rows.kmmacros (11 KB)


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

    ObjC.import("AppKit");

    // SUM of the numeric rows in the extended selection
    // OR
    //  (if the selection is not extended)
    //  Sum of all visible numeric rows descending
    //  from the current line.

    // Copied to the clipboard, and displayed in a dialog.

    // Rob Trew @2022
    // Ver 0.03

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

        return either(
            alert("Sum of numbers in Bike")
        )(
            compose(
                alert("Copied to clipboard"),
                copyText
            )
        )(
            doc.exists() ? Right(
                (
                    Boolean(doc.selectedText()) ? (
                        selectedRowsForest(doc)
                    ) : [
                        visibleSubTreeOfRow(
                            doc.selectionRow()
                        )
                    ]
                )
                .reduce(treeSum, 0)
                .toFixed(2)
            ) : Left("No document open in Bike.")
        );
    };

    // ---------------------- BIKE -----------------------

    // forestFromIndentedLines :: [(Int, String)] ->
    // [Tree {text:String, body:Int}]
    const forestFromIndentedLines = tuples => {
        const go = xs =>
            0 < xs.length ? (() => {
                // First line and its sub-tree,
                const
                    [depth, body] = xs[0],
                    [tree, rest] = span(x => depth < x[0])(
                        xs.slice(1)
                    );

                return [
                    Node({
                        text: body,
                        level: depth
                    })(go(tree))
                ]
                .concat(go(rest));
            })() : [];

        return go(tuples);
    };


    // selectedRowsForest :: Bike Doc -> IO [Tree String]
    const selectedRowsForest = doc => {
    // Forest of currently selected rows.
        const
            rows = doc.rows.where({
                _and: [
                    {selected: true},
                    {_not: [{
                        name: ""
                    }]}
                ]
            });

        return forestFromIndentedLines(
            zip(
                rows.level()
            )(
                rows.name()
            )
        );
    };


    // treeSum :: (Float, {text::String}) -> Float
    const treeSum = (floatSum, tree) =>
        // Float accumulator updated by any
        // numeric node texts in the tree.
        floatSum + foldTree(
            x => ns => ns.reduce(add, 0) + (
                parseFloat(x.text, 10) || 0
            )
        )(tree);


    // visibleSubTreeOfRow :: Bike Row -> IO Tree String
    const visibleSubTreeOfRow = row => {
    // Tree of the given row and all its
    // visible descendants.
        const go = r =>
            Node(
                {text: r.name()}
            )(
                r.containsRows() ? (
                    r.rows.where({visible: true})()
                    .map(go)
                ) : []
            );

        return go(row);
    };


    // ----------------------- 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
    });


    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
    // Constructor for a Tree node which connects a
    // value of some kind to a list of zero or
    // more child trees.
        xs => ({
            type: "Node",
            root: v,
            nest: xs || []
        });


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
    // A pair of values, possibly of
    // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // add (+) :: Num a => (a, a) -> a
    const add = (a, b) =>
    // Curried addition.
        a + b;


    // 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);


    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = f => {
    // The catamorphism on trees. A summary
    // value obtained by a depth-first fold.
        const go = tree => f(
            root(tree)
        )(
            nest(tree).map(go)
        );

        return go;
    };


    // nest :: Tree a -> [a]
    const nest = tree => {
    // Allowing for lazy (on-demand) evaluation.
    // If the nest turns out to be a function –
    // rather than a list – that function is applied
    // here to the root, and returns a list.
        const xs = tree.nest;

        return "function" !== typeof xs ? (
            xs
        ) : xs(root(tree));
    };


    // root :: Tree a -> a
    const root = tree =>
    // The value attached to a tree node.
        tree.root;


    // span :: (a -> Bool) -> [a] -> ([a], [a])
    const span = p =>
    // Longest prefix of xs consisting of elements which
    // all satisfy p, tupled with the remainder of xs.
        xs => {
            const i = xs.findIndex(x => !p(x));

            return -1 !== i ? (
                Tuple(xs.slice(0, i))(
                    xs.slice(i)
                )
            ) : Tuple(xs)([]);
        };


    // 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]]);


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


Other Keyboard Maestro macros for BIKE Outliner

1 Like