Draft script – frequently used words

A rough sketch of something which shows frequently repeated words in the active Bike document.

A bit Anglo-centric, I’m afraid – it assumes that words are space-delimited strings of Alpha characters, and filters out a list of very common Anglo words (to which I find I am occasionally adding additional items by hand).

sample
Bike word count and frequencies.kmmacros.zip (5.1 KB)

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

    // Rob Trew @2022

    // Rough word count and top frequencies for Bike.app
    // Ver 0.2

    // main :: IO()
    const main = () =>
        either(
            alert("Word count and frequencies")
        )(
            ([fp, ps]) => {
                const
                    ws = words(unwords(ps)).flatMap(w => {
                        const
                            x = toLower(
                                [...w].filter(isAlpha)
                                .join("")
                            );

                        return Boolean(x.length) && (
                            !enHiFreq.includes(x)
                        ) ? (
                            [x]
                        ) : [];
                    }),
                    freqs = wordFreqReport(ws).join("\n");

                alert("Word count and frequencies")(
                    [
                        `${ws.length} words`,
                        0 < freqs.length ? (
                            `Frequent:\n${freqs}`
                        ) : "",
                        tildePath(fp)
                    ]
                    .join("\n\n")
                );
            }
        )(
            frontBikeDocFilePathAndParasLR(
                Application("Bike").documents
            )
        );


    // --------------------- ENGLISH ---------------------

    const enHiFreq = [
        "the", "be", "and", "a", "of", "to", "in", "i",
        "you", "it", "have", "to", "that", "for", "do",
        "he", "with", "on", "this", "we", "that", "not",
        "but", "they", "say", "at", "what", "his", "from",
        "go", "or", "by", "get", "she", "my", "can", "as",
        "is", "when", "an", "are"
    ];

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

    // frontBikeDocFilePathAndParasLR :: Documents ->
    // Either String (FilePath, [String])
    const frontBikeDocFilePathAndParasLR = docs =>
        bindLR(
            0 < docs.length ? (() => {
                const fp = `${docs.at(0).file()}`;

                return bindLR(
                    readFileLR(fp)
                )(
                    compose(Right, Tuple(fp))
                );
            })() : Left("No documents open in Bike.")
        )(
            ([fp, xml]) => bindLR(
                xQueryLR(
                    "for $p in //p return string($p)"
                )(xml)
            )(
                compose(Right, Tuple(fp))
            )
        );

    // ---------------- WORD FREQUENCIES -----------------

    // wordFreqReport :: [String] -> String
    const wordFreqReport = ws =>
        groupBy(
            on(eq)(snd)
        )(
            sortBy(
                flip(comparing(snd))
            )(
                Object.entries(wordCounts(ws))
            )
        )
        .flatMap(gp => {
            const n = gp[0][1];

            return 1 < n ? [
                hangingIndent(`${n} *`)(
                    wordWrap(50)(
                        sort(gp.map(fst))
                    )
                    .map(row => words(row).join(", "))
                )
            ] : [];
        });

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


    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        // Either a message or the contents of any
        // text file at the given filepath.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );

        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };


    // xQueryLR :: String -> String -> Either String String
    const xQueryLR = xquery =>
        xml => {
            const
                uw = ObjC.unwrap,
                error = $(),
                node = $.NSXMLDocument.alloc
                .initWithXMLStringOptionsError(
                    xml, 0, error
                );

            return bindLR(
                node.isNil() ? (
                    Left(uw(error.localizedDescription))
                ) : Right(node)
            )(
                oNode => {
                    const
                        err = $(),
                        xs = oNode.objectsForXQueryError(
                            xquery, err
                        );

                    return xs.isNil() ? (
                        Left(uw(err.localizedDescription))
                    ) : Right(uw(xs).map(uw));
                }
            );
        };


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


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


    // append (<>) :: [a] -> [a] -> [a]
    const append = xs =>
        // Two lists joined into one.
        ys => xs.concat(ys);


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr ? (
            lr
        ) : mf(lr.Right);


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


    // 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 => "Left" in e ? (
            fl(e.Left)
        ) : fr(e.Right);


    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
        // True when a and b are equivalent in the terms
        // defined below for their shared data type.
        b => a === b;


    // 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)
        ) : (a => b => op(b)(a));


    // 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(x)(a[0]) ? (
                    Tuple(gs)([...a, x])
                ) : Tuple([...gs, a])([x]),
                Tuple([])([h])
            );

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


    // hangingIndent :: String -> [String] -> String
    const hangingIndent = pfx =>
        rows => [`${pfx}\t${rows[0]}`]
        .concat(
            rows.slice(1)
            .map(x => `\t${x}`)
        )
        .join("\n");


    // insertWith :: Ord k => (a -> a -> a) ->
    // k -> a -> Map k a -> Map k a
    const insertWith = f =>
        // A new dictionary updated with a (k, f(v)(x)) pair.
        // Where there is no existing v for k, the supplied
        // x is used directly.
        k => x => dict => Object.assign({},
            dict, {
                [k]: k in dict ? (
                    f(dict[k])(x)
                ) : x
            });


    // isAlpha :: Char -> Bool
    const isAlpha = c =>
        (/[A-Za-z\u00C0-\u00FF]/u).test(c);


    // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
    const on = f =>
        // e.g. groupBy(on(eq)(length))
        g => a => b => f(g(a))(g(b));


    // snd :: (a, b) -> b
    const snd = tpl =>
        // Second member of a pair.
        tpl[1];


    // sort :: Ord a => [a] -> [a]
    const sort = xs =>
        // An A-Z sorted copy of xs.
        xs.slice().sort(
            (a, b) => a < b ? (
                -1
            ) : (
                a > b ? (
                    1
                ) : 0
            )
        );


    // tildePath :: FilePath -> FilePath
    const tildePath = fp =>
        // A filepath in which any initial component
        // leading to the user's home directory
        // is abbreviated as `~`.
        ObjC.unwrap(
            $(fp)
            .stringByAbbreviatingWithTildeInPath
        );


    // toLower :: String -> String
    const toLower = s =>
        // Lower-case version of string.
        s.toLocaleLowerCase();


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


    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (...args) => {
            const [x, y] = Boolean(args.length % 2) ? (
                args[0]
            ) : args;

            return f(x)(y);
        };


    // unwords :: [String] -> String
    const unwords = xs =>
        // A space-separated string derived
        // from a list of words.
        xs.join(" ");


    // wordCounts :: [String] -> Dict String Int
    const wordCounts = wordList =>
        // Dictionary of all words in wordList,
        // with the frequency of each.
        wordList.reduce(
            (a, w) => insertWith(
                x => y => x + y
            )(w)(1)(a), {}
        );


    // words :: String -> [String]
    const words = s =>
        // List of space-delimited sub-strings.
        s.split(/\s+/u);


    // wordWrap :: Int -> [String] -> [String]
    const wordWrap = n =>
        // A list of lines containing the words of ws
        // breaking at or before width n.
        ws => uncurry(append)(
            ws.reduce(
                ([rows, row], w) =>
                n <= (1 + row.length + w.length) ? (
                    [rows.concat(row), w]
                ) : [
                    rows,
                    0 < row.length ? (
                        `${row} ${w}`
                    ) : w
                ], [
                    [], ""
                ]
            )
        );

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

Scripting, Scripting … I can’t wait to implement a better scripting API :slight_smile:

Busy working on infrastructure stuff this last week. Payment processing, better software update process. Hope to have another release in a day or two.

2 Likes