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).
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();
})();