RosettaCodeData/Task/Display-an-outline-as-a-nested-table/Python/display-an-outline-as-a-nested-table-2.py
2023-07-01 13:44:08 -04:00

364 lines
9.3 KiB
Python

'''Display an outline as a nested table'''
from itertools import chain, cycle, takewhile
from functools import reduce
from operator import add
# wikiTablesFromOutline :: [String] -> String -> String
def wikiTablesFromOutline(colorSwatch):
'''Wikitable markup for (colspan) tables representing
the indentation of a given outline.
Each key-line point (child of a tree root) has a
distinct color, inherited by all its descendants.
The first color in the swatch is for the root node.
A sequence of tables is generated where the outline
represents a forest rather than a singly-rooted tree.
'''
def go(outline):
return '\n\n'.join([
wikiTableFromTree(colorSwatch)(tree) for tree in
forestFromLevels(
indentLevelsFromLines(
outline.splitlines()
)
)
])
return go
# wikiTableFromTree :: [String] -> Tree String -> String
def wikiTableFromTree(colorSwatch):
'''A wikitable rendered from a single tree.
'''
return compose(
wikiTableFromRows,
levels,
paintedTree(colorSwatch),
widthMeasuredTree,
ap(paddedTree(""))(treeDepth)
)
# ------------------------- TEST -------------------------
# main :: IO ()
def main():
'''A colored wikitable rendering of a given outline'''
outline = '''Display an outline as a nested table.
Parse the outline to a tree,
measuring the indent of each line,
translating the indentation to a nested structure,
and padding the tree to even depth.
count the leaves descending from each node,
defining the width of a leaf as 1,
and the width of a parent node as a sum.
(The sum of the widths of its children)
and write out a table with 'colspan' values
either as a wiki table,
or as HTML.'''
print(
wikiTablesFromOutline([
"#ffffe6",
"#ffebd2",
"#f0fff0",
"#e6ffff",
"#ffeeff"
])(outline)
)
# ------------------ TREE FROM OUTLINE -------------------
# indentLevelsFromLines :: [String] -> [(Int, String)]
def indentLevelsFromLines(xs):
'''Each input line stripped of leading
white space, and tupled with a preceding integer
giving its level of indentation from 0 upwards.
'''
indentTextPairs = [
(n, s[n:]) for (n, s)
in (
(len(list(takewhile(isSpace, x))), x)
for x in xs
)
]
indentUnit = len(next(
x for x in indentTextPairs if x[0]
)) or 1
return [
(x[0] // indentUnit, x[1])
for x in indentTextPairs
]
# forestFromLevels :: [(Int, String)] -> [Tree a]
def forestFromLevels(levelValuePairs):
'''A list of trees derived from a list of values paired
with integers giving their levels of indentation.
'''
def go(xs):
if xs:
level, v = xs[0]
children, rest = span(
lambda x: level < x[0]
)(xs[1:])
return [Node(v)(go(children))] + go(rest)
else:
return []
return go(levelValuePairs)
# -------------- TREE PADDED TO EVEN DEPTH ---------------
# paddedTree :: a -> (Int, Node a) -> Node a
def paddedTree(padValue):
'''A tree vertically padded to a given depth,
with additional nodes, containing padValue,
where needed.
'''
def go(tree):
def pad(n):
prev = n - 1
return Node(tree.get('root'))([
go(x)(prev) for x in (
tree.get('nest') or [Node(padValue)([])]
)
]) if prev else tree
return pad
return go
# treeDepth :: Tree a -> Int
def treeDepth(tree):
'''Maximum number of distinct levels in the tree.
'''
def go(_, xs):
return 1 + max(xs) if xs else 1
return foldTree(go)(tree)
# ------------ SPANNING WIDTH OF EACH SUBTREE ------------
# widthMeasuredTree :: Tree a -> Tree (a, Int)
def widthMeasuredTree(tree):
'''A tree in which each node value is tupled
with the width of the subtree.
'''
def go(x, xs):
return Node((x, 1))([]) if not xs else (
Node((x, reduce(
lambda a, child: a + (
child.get('root')[1]
),
xs,
0
)))(xs)
)
return foldTree(go)(tree)
# ----------------- COLOR SWATCH APPLIED -----------------
# paintedTree :: [String] -> Tree a -> Tree (String, a)
def paintedTree(swatch):
'''A tree in which every node value is tupled with
a hexadecimal color string taken from a swatch list.
The first colour is used for the root node.
The next n colours paint the root's n children.
All descendants of those children are painted with
the same color as their non-root ancestor.
'''
colors = cycle(swatch)
def go(tree):
return fmapTree(
lambda x: ("", x)
)(tree) if not swatch else (
Node(
(next(colors), tree.get('root'))
)(
list(map(
lambda k, child: fmapTree(
lambda v: (k, v)
)(child),
colors,
tree.get('nest')
))
)
)
return go
# ---------------- GENERIC TREE FUNCTIONS ----------------
# Node :: a -> [Tree a] -> Tree a
def Node(v):
'''Constructor for a Tree node which connects a
value of some kind to a list of zero or
more child trees.
'''
return lambda xs: {'root': v, 'nest': xs}
# fmapTree :: (a -> b) -> Tree a -> Tree b
def fmapTree(f):
'''A new tree holding the results of
an application of f to each root in
the existing tree.
'''
def go(x):
return Node(
f(x.get('root'))
)([go(v) for v in x.get('nest')])
return go
# foldTree :: (a -> [b] -> b) -> Tree a -> b
def foldTree(f):
'''The catamorphism on trees. A summary
value defined by a depth-first fold.
'''
def go(node):
return f(
node.get('root'),
[go(x) for x in node.get('nest')]
)
return go
# levels :: Tree a -> [[a]]
def levels(tree):
'''A list of lists, grouping the root
values of each level of the tree.
'''
return [[tree.get('root')]] + list(
reduce(
zipWithLong(add),
map(levels, tree.get('nest')),
[]
)
)
# ----------------- WIKITABLE RENDERING ------------------
# wikiTableFromRows :: [[(String, (String, Int))]] -> String
def wikiTableFromRows(rows):
'''A wiki table rendering of rows in which each cell
has the form (hexColorString, (text, colspan))
'''
def cw(color, width):
def go(w):
return f' colspan={w}' if 1 < w else ''
return f'style="background: {color}; "{go(width)}'
def cellText(cell):
color, (txt, width) = cell
return f'| {cw(color,width) if txt else ""} | {txt}'
def go(row):
return '\n'.join([cellText(cell) for cell in row])
return '{| class="wikitable" ' + (
'style="text-align: center;"\n|-\n'
) + '\n|-\n'.join([go(row) for row in rows]) + '\n|}'
# ----------------------- GENERIC ------------------------
# ap :: (a -> b -> c) -> (a -> b) -> a -> c
def ap(f):
'''Applicative instance for functions.
'''
def go(g):
return lambda x: f(x)(g(x))
return go
# compose :: ((a -> a), ...) -> (a -> a)
def compose(*fs):
'''Composition, from right to left,
of a series of functions.
'''
def go(f, g):
def fg(x):
return f(g(x))
return fg
return reduce(go, fs, lambda x: x)
# head :: [a] -> a
def head(xs):
'''The first element of a non-empty list.
'''
return xs[0] if isinstance(xs, list) else next(xs)
# isSpace :: Char -> Bool
# isSpace :: String -> Bool
def isSpace(s):
'''True if s is not empty, and
contains only white space.
'''
return s.isspace()
# span :: (a -> Bool) -> [a] -> ([a], [a])
def span(p):
'''The longest (possibly empty) prefix of xs that
contains only elements satisfying p, tupled with the
remainder of xs. span p xs is equivalent to
(takeWhile p xs, dropWhile p xs).
'''
def match(ab):
b = ab[1]
return not b or not p(b[0])
def f(ab):
a, b = ab
return a + [b[0]], b[1:]
def go(xs):
return until(match)(f)(([], xs))
return go
# until :: (a -> Bool) -> (a -> a) -> a -> a
def until(p):
'''The result of repeatedly applying f until p holds.
The initial seed value is x.
'''
def go(f):
def g(x):
v = x
while not p(v):
v = f(v)
return v
return g
return go
# zipWithLong :: ((a, a) -> a) -> ([a], [a]) -> [a]
def zipWithLong(f):
'''Analogous to map(f, xs, ys)
but returns a list with the length of the *longer*
of xs and ys, taking any surplus values unmodified.
'''
def go(xs, ys):
lxs = list(xs)
lys = list(ys)
i = min(len(lxs), len(lys))
return chain.from_iterable([
map(f, lxs, lys),
lxs[i:],
lys[i:]
])
return go
# MAIN ---
if __name__ == '__main__':
main()