1
2def iter_clean_lines(lines):
3    lines = iter(lines)
4    for line in lines:
5        line = line.strip()
6        if line.startswith('# XXX'):
7            continue
8        yield line
9
10
11def parse_table_lines(lines):
12    lines = iter_clean_lines(lines)
13
14    for line in lines:
15        if line.startswith(('####', '#----')):
16            kind = 0 if line[1] == '#' else 1
17            try:
18                line = next(lines).strip()
19            except StopIteration:
20                line = ''
21            if not line.startswith('# '):
22                raise NotImplementedError(line)
23            yield kind, line[2:].lstrip()
24            continue
25
26        maybe = None
27        while line.startswith('#'):
28            if line != '#' and line[1] == ' ':
29                maybe = line[2:].lstrip()
30            try:
31                line = next(lines).strip()
32            except StopIteration:
33                return
34            if not line:
35                break
36        else:
37            if line:
38                if maybe:
39                    yield 2, maybe
40                yield 'row', line
41
42
43def iter_sections(lines):
44    header = None
45    section = []
46    for kind, value in parse_table_lines(lines):
47        if kind == 'row':
48            if not section:
49                if header is None:
50                    header = value
51                    continue
52                raise NotImplementedError(value)
53            yield tuple(section), value
54        else:
55            if header is None:
56                header = False
57            section[kind:] = [value]
58
59
60def collect_sections(lines):
61    sections = {}
62    for section, row in iter_sections(lines):
63        if section not in sections:
64            sections[section] = [row]
65        else:
66            sections[section].append(row)
67    return sections
68
69
70def collate_sections(lines):
71    collated = {}
72    for section, rows in collect_sections(lines).items():
73        parent = collated
74        current = ()
75        for name in section:
76            current += (name,)
77            try:
78                child, secrows, totalrows = parent[name]
79            except KeyError:
80                child = {}
81                secrows = []
82                totalrows = []
83                parent[name] = (child, secrows, totalrows)
84            parent = child
85            if current == section:
86                secrows.extend(rows)
87            totalrows.extend(rows)
88    return collated
89
90
91#############################
92# the commands
93
94def cmd_count_by_section(lines):
95    div = ' ' + '-' * 50
96    total = 0
97    def render_tree(root, depth=0):
98        nonlocal total
99        indent = '    ' * depth
100        for name, data in root.items():
101            subroot, rows, totalrows = data
102            sectotal = f'({len(totalrows)})' if totalrows != rows else ''
103            count = len(rows) if rows else ''
104            if depth == 0:
105                yield div
106            yield f'{sectotal:>7} {count:>4}  {indent}{name}'
107            yield from render_tree(subroot, depth+1)
108            total += len(rows)
109    sections = collate_sections(lines)
110    yield from render_tree(sections)
111    yield div
112    yield f'(total: {total})'
113
114
115#############################
116# the script
117
118def parse_args(argv=None, prog=None):
119    import argparse
120    parser = argparse.ArgumentParser(prog=prog)
121    parser.add_argument('filename')
122
123    args = parser.parse_args(argv)
124    ns = vars(args)
125
126    return ns
127
128
129def main(filename):
130    with open(filename) as infile:
131        for line in cmd_count_by_section(infile):
132            print(line)
133
134
135if __name__ == '__main__':
136    kwargs = parse_args()
137    main(**kwargs)
138