In an earlier example, we used Array.reduce to:
- Split markdown checkbox lines into two lists (not checked, checked)
- Join the two lists back together, so that completed items were moved to the end.
We can generalise this pattern of partitioning things (lines, drafts, anything) into two groups – those that do, and those that don’t match some criterion.
// partition :: Predicate -> List -> (Matches, nonMatches)
// partition :: (a -> Bool) -> [a] -> ([a], [a])
const partition = (p, xs) =>
xs.reduce(
(a, x) =>
p(x) ? (
[a[0].concat(x), a[1]]
) : [a[0], a[1].concat(x)],
[[], []]
);
The heart of this more general and reusable function is still reduce:
- Starting with a seed value
[[ ], [ ]]
(a pair of empty lists), - working through some list of items (lines, drafts, etc), and
-
updating the seed value step by step – in this case, testing each item in the list with a supplied criterion (an
item -> Bool
function, sometimes called apredicate
function), and adding matches to the left list when the function returnstrue
, while adding non-matches to the right list, when the function returnsfalse
.
We could use this more general approach either, for example:
- to divide Markdown lines into [unchecked] and [checked] lists, or
- to divide Taskpaper lines into [no @done tag] and [@done tag found] lists.
For the Markdown check-boxes, we could write something like:
partition(x => !/^\s*-\s*\[[x\-\*\+]\]/.exec(x), allLines)
(lines which are not checked vs lines that are – the ! is a logical negator)
and for TaskPaper @done tags
, something like:
partition(x => !x.includes('@done'), allLines)
For a flexible action, which chooses selects the matching criterion according to the value of the draft.languageGrammar
setting, we might write a sketch like:
https://actions.getdrafts.com/a/1HP
// Completed items moved to bottom of draft
(() => {
// main :: () -> IO Bool
const main = () => {
// completedItemsToEnd :: Draft -> String
const completedItemsToEnd = d =>
concat( // Two lists rejoined,
partition( // after partition.
d.languageGrammar === 'Taskpaper' ? (
x => !x.includes('@done')
) : x => !/^\s*-\s*\[[x\-\*\+]\]/.exec(x),
d.content.split('\n')
)
).join('\n');
draft.content = (
completedItemsToEnd(draft)
);
draft.update();
console.log(true);
return true;
};
// General reusable helper functions ------------------
// concat :: [[a]] -> [a]
// concat :: [String] -> String
const concat = xs =>
xs.length > 0 ? (() => {
const unit = typeof xs[0] === 'string' ? '' : [];
return unit.concat.apply(unit, xs);
})() : [];
// partition :: Predicate -> List -> (Matches, nonMatches)
// partition :: (a -> Bool) -> [a] -> ([a], [a])
const partition = (p, xs) =>
xs.reduce(
(a, x) =>
p(x) ? (
[a[0].concat(x), a[1]]
) : [a[0], a[1].concat(x)], [
[],
[]
]
);
// MAIN -----------------------------------------------
return main();
})()