JS :: More general use of Array.reduce - partitioning lines or drafts into two groups


#1

In an earlier example, we used Array.reduce to:

  1. Split markdown checkbox lines into two lists (not checked, checked)
  2. 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:

  1. Starting with a seed value [[ ], [ ]] (a pair of empty lists),
  2. working through some list of items (lines, drafts, etc), and
  3. 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 a predicate function), and adding matches to the left list when the function returns true, while adding non-matches to the right list, when the function returns false.

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