119ea8026Sopenharmony_ci#!/usr/bin/env python3
219ea8026Sopenharmony_ci#
319ea8026Sopenharmony_ci# Script to summarize the outputs of other scripts. Operates on CSV files.
419ea8026Sopenharmony_ci#
519ea8026Sopenharmony_ci# Example:
619ea8026Sopenharmony_ci# ./scripts/code.py lfs.o lfs_util.o -q -o lfs.code.csv
719ea8026Sopenharmony_ci# ./scripts/data.py lfs.o lfs_util.o -q -o lfs.data.csv
819ea8026Sopenharmony_ci# ./scripts/summary.py lfs.code.csv lfs.data.csv -q -o lfs.csv
919ea8026Sopenharmony_ci# ./scripts/summary.py -Y lfs.csv -f code=code_size,data=data_size
1019ea8026Sopenharmony_ci#
1119ea8026Sopenharmony_ci# Copyright (c) 2022, The littlefs authors.
1219ea8026Sopenharmony_ci# SPDX-License-Identifier: BSD-3-Clause
1319ea8026Sopenharmony_ci#
1419ea8026Sopenharmony_ci
1519ea8026Sopenharmony_ciimport collections as co
1619ea8026Sopenharmony_ciimport csv
1719ea8026Sopenharmony_ciimport functools as ft
1819ea8026Sopenharmony_ciimport itertools as it
1919ea8026Sopenharmony_ciimport math as m
2019ea8026Sopenharmony_ciimport os
2119ea8026Sopenharmony_ciimport re
2219ea8026Sopenharmony_ci
2319ea8026Sopenharmony_ci
2419ea8026Sopenharmony_ci# supported merge operations
2519ea8026Sopenharmony_ci#
2619ea8026Sopenharmony_ci# this is a terrible way to express these
2719ea8026Sopenharmony_ci#
2819ea8026Sopenharmony_ciOPS = {
2919ea8026Sopenharmony_ci    'sum':     lambda xs: sum(xs[1:], start=xs[0]),
3019ea8026Sopenharmony_ci    'prod':    lambda xs: m.prod(xs[1:], start=xs[0]),
3119ea8026Sopenharmony_ci    'min':     min,
3219ea8026Sopenharmony_ci    'max':     max,
3319ea8026Sopenharmony_ci    'mean':    lambda xs: Float(sum(float(x) for x in xs) / len(xs)),
3419ea8026Sopenharmony_ci    'stddev':  lambda xs: (
3519ea8026Sopenharmony_ci        lambda mean: Float(
3619ea8026Sopenharmony_ci            m.sqrt(sum((float(x) - mean)**2 for x in xs) / len(xs)))
3719ea8026Sopenharmony_ci        )(sum(float(x) for x in xs) / len(xs)),
3819ea8026Sopenharmony_ci    'gmean':   lambda xs: Float(m.prod(float(x) for x in xs)**(1/len(xs))),
3919ea8026Sopenharmony_ci    'gstddev': lambda xs: (
4019ea8026Sopenharmony_ci        lambda gmean: Float(
4119ea8026Sopenharmony_ci            m.exp(m.sqrt(sum(m.log(float(x)/gmean)**2 for x in xs) / len(xs)))
4219ea8026Sopenharmony_ci            if gmean else m.inf)
4319ea8026Sopenharmony_ci        )(m.prod(float(x) for x in xs)**(1/len(xs))),
4419ea8026Sopenharmony_ci}
4519ea8026Sopenharmony_ci
4619ea8026Sopenharmony_ci
4719ea8026Sopenharmony_ci# integer fields
4819ea8026Sopenharmony_ciclass Int(co.namedtuple('Int', 'x')):
4919ea8026Sopenharmony_ci    __slots__ = ()
5019ea8026Sopenharmony_ci    def __new__(cls, x=0):
5119ea8026Sopenharmony_ci        if isinstance(x, Int):
5219ea8026Sopenharmony_ci            return x
5319ea8026Sopenharmony_ci        if isinstance(x, str):
5419ea8026Sopenharmony_ci            try:
5519ea8026Sopenharmony_ci                x = int(x, 0)
5619ea8026Sopenharmony_ci            except ValueError:
5719ea8026Sopenharmony_ci                # also accept +-∞ and +-inf
5819ea8026Sopenharmony_ci                if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
5919ea8026Sopenharmony_ci                    x = m.inf
6019ea8026Sopenharmony_ci                elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
6119ea8026Sopenharmony_ci                    x = -m.inf
6219ea8026Sopenharmony_ci                else:
6319ea8026Sopenharmony_ci                    raise
6419ea8026Sopenharmony_ci        assert isinstance(x, int) or m.isinf(x), x
6519ea8026Sopenharmony_ci        return super().__new__(cls, x)
6619ea8026Sopenharmony_ci
6719ea8026Sopenharmony_ci    def __str__(self):
6819ea8026Sopenharmony_ci        if self.x == m.inf:
6919ea8026Sopenharmony_ci            return '∞'
7019ea8026Sopenharmony_ci        elif self.x == -m.inf:
7119ea8026Sopenharmony_ci            return '-∞'
7219ea8026Sopenharmony_ci        else:
7319ea8026Sopenharmony_ci            return str(self.x)
7419ea8026Sopenharmony_ci
7519ea8026Sopenharmony_ci    def __int__(self):
7619ea8026Sopenharmony_ci        assert not m.isinf(self.x)
7719ea8026Sopenharmony_ci        return self.x
7819ea8026Sopenharmony_ci
7919ea8026Sopenharmony_ci    def __float__(self):
8019ea8026Sopenharmony_ci        return float(self.x)
8119ea8026Sopenharmony_ci
8219ea8026Sopenharmony_ci    none = '%7s' % '-'
8319ea8026Sopenharmony_ci    def table(self):
8419ea8026Sopenharmony_ci        return '%7s' % (self,)
8519ea8026Sopenharmony_ci
8619ea8026Sopenharmony_ci    diff_none = '%7s' % '-'
8719ea8026Sopenharmony_ci    diff_table = table
8819ea8026Sopenharmony_ci
8919ea8026Sopenharmony_ci    def diff_diff(self, other):
9019ea8026Sopenharmony_ci        new = self.x if self else 0
9119ea8026Sopenharmony_ci        old = other.x if other else 0
9219ea8026Sopenharmony_ci        diff = new - old
9319ea8026Sopenharmony_ci        if diff == +m.inf:
9419ea8026Sopenharmony_ci            return '%7s' % '+∞'
9519ea8026Sopenharmony_ci        elif diff == -m.inf:
9619ea8026Sopenharmony_ci            return '%7s' % '-∞'
9719ea8026Sopenharmony_ci        else:
9819ea8026Sopenharmony_ci            return '%+7d' % diff
9919ea8026Sopenharmony_ci
10019ea8026Sopenharmony_ci    def ratio(self, other):
10119ea8026Sopenharmony_ci        new = self.x if self else 0
10219ea8026Sopenharmony_ci        old = other.x if other else 0
10319ea8026Sopenharmony_ci        if m.isinf(new) and m.isinf(old):
10419ea8026Sopenharmony_ci            return 0.0
10519ea8026Sopenharmony_ci        elif m.isinf(new):
10619ea8026Sopenharmony_ci            return +m.inf
10719ea8026Sopenharmony_ci        elif m.isinf(old):
10819ea8026Sopenharmony_ci            return -m.inf
10919ea8026Sopenharmony_ci        elif not old and not new:
11019ea8026Sopenharmony_ci            return 0.0
11119ea8026Sopenharmony_ci        elif not old:
11219ea8026Sopenharmony_ci            return 1.0
11319ea8026Sopenharmony_ci        else:
11419ea8026Sopenharmony_ci            return (new-old) / old
11519ea8026Sopenharmony_ci
11619ea8026Sopenharmony_ci    def __add__(self, other):
11719ea8026Sopenharmony_ci        return self.__class__(self.x + other.x)
11819ea8026Sopenharmony_ci
11919ea8026Sopenharmony_ci    def __sub__(self, other):
12019ea8026Sopenharmony_ci        return self.__class__(self.x - other.x)
12119ea8026Sopenharmony_ci
12219ea8026Sopenharmony_ci    def __mul__(self, other):
12319ea8026Sopenharmony_ci        return self.__class__(self.x * other.x)
12419ea8026Sopenharmony_ci
12519ea8026Sopenharmony_ci# float fields
12619ea8026Sopenharmony_ciclass Float(co.namedtuple('Float', 'x')):
12719ea8026Sopenharmony_ci    __slots__ = ()
12819ea8026Sopenharmony_ci    def __new__(cls, x=0.0):
12919ea8026Sopenharmony_ci        if isinstance(x, Float):
13019ea8026Sopenharmony_ci            return x
13119ea8026Sopenharmony_ci        if isinstance(x, str):
13219ea8026Sopenharmony_ci            try:
13319ea8026Sopenharmony_ci                x = float(x)
13419ea8026Sopenharmony_ci            except ValueError:
13519ea8026Sopenharmony_ci                # also accept +-∞ and +-inf
13619ea8026Sopenharmony_ci                if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
13719ea8026Sopenharmony_ci                    x = m.inf
13819ea8026Sopenharmony_ci                elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
13919ea8026Sopenharmony_ci                    x = -m.inf
14019ea8026Sopenharmony_ci                else:
14119ea8026Sopenharmony_ci                    raise
14219ea8026Sopenharmony_ci        assert isinstance(x, float), x
14319ea8026Sopenharmony_ci        return super().__new__(cls, x)
14419ea8026Sopenharmony_ci
14519ea8026Sopenharmony_ci    def __str__(self):
14619ea8026Sopenharmony_ci        if self.x == m.inf:
14719ea8026Sopenharmony_ci            return '∞'
14819ea8026Sopenharmony_ci        elif self.x == -m.inf:
14919ea8026Sopenharmony_ci            return '-∞'
15019ea8026Sopenharmony_ci        else:
15119ea8026Sopenharmony_ci            return '%.1f' % self.x
15219ea8026Sopenharmony_ci
15319ea8026Sopenharmony_ci    def __float__(self):
15419ea8026Sopenharmony_ci        return float(self.x)
15519ea8026Sopenharmony_ci
15619ea8026Sopenharmony_ci    none = Int.none
15719ea8026Sopenharmony_ci    table = Int.table
15819ea8026Sopenharmony_ci    diff_none = Int.diff_none
15919ea8026Sopenharmony_ci    diff_table = Int.diff_table
16019ea8026Sopenharmony_ci    diff_diff = Int.diff_diff
16119ea8026Sopenharmony_ci    ratio = Int.ratio
16219ea8026Sopenharmony_ci    __add__ = Int.__add__
16319ea8026Sopenharmony_ci    __sub__ = Int.__sub__
16419ea8026Sopenharmony_ci    __mul__ = Int.__mul__
16519ea8026Sopenharmony_ci
16619ea8026Sopenharmony_ci# fractional fields, a/b
16719ea8026Sopenharmony_ciclass Frac(co.namedtuple('Frac', 'a,b')):
16819ea8026Sopenharmony_ci    __slots__ = ()
16919ea8026Sopenharmony_ci    def __new__(cls, a=0, b=None):
17019ea8026Sopenharmony_ci        if isinstance(a, Frac) and b is None:
17119ea8026Sopenharmony_ci            return a
17219ea8026Sopenharmony_ci        if isinstance(a, str) and b is None:
17319ea8026Sopenharmony_ci            a, b = a.split('/', 1)
17419ea8026Sopenharmony_ci        if b is None:
17519ea8026Sopenharmony_ci            b = a
17619ea8026Sopenharmony_ci        return super().__new__(cls, Int(a), Int(b))
17719ea8026Sopenharmony_ci
17819ea8026Sopenharmony_ci    def __str__(self):
17919ea8026Sopenharmony_ci        return '%s/%s' % (self.a, self.b)
18019ea8026Sopenharmony_ci
18119ea8026Sopenharmony_ci    def __float__(self):
18219ea8026Sopenharmony_ci        return float(self.a)
18319ea8026Sopenharmony_ci
18419ea8026Sopenharmony_ci    none = '%11s %7s' % ('-', '-')
18519ea8026Sopenharmony_ci    def table(self):
18619ea8026Sopenharmony_ci        t = self.a.x/self.b.x if self.b.x else 1.0
18719ea8026Sopenharmony_ci        return '%11s %7s' % (
18819ea8026Sopenharmony_ci            self,
18919ea8026Sopenharmony_ci            '∞%' if t == +m.inf
19019ea8026Sopenharmony_ci            else '-∞%' if t == -m.inf
19119ea8026Sopenharmony_ci            else '%.1f%%' % (100*t))
19219ea8026Sopenharmony_ci
19319ea8026Sopenharmony_ci    diff_none = '%11s' % '-'
19419ea8026Sopenharmony_ci    def diff_table(self):
19519ea8026Sopenharmony_ci        return '%11s' % (self,)
19619ea8026Sopenharmony_ci
19719ea8026Sopenharmony_ci    def diff_diff(self, other):
19819ea8026Sopenharmony_ci        new_a, new_b = self if self else (Int(0), Int(0))
19919ea8026Sopenharmony_ci        old_a, old_b = other if other else (Int(0), Int(0))
20019ea8026Sopenharmony_ci        return '%11s' % ('%s/%s' % (
20119ea8026Sopenharmony_ci            new_a.diff_diff(old_a).strip(),
20219ea8026Sopenharmony_ci            new_b.diff_diff(old_b).strip()))
20319ea8026Sopenharmony_ci
20419ea8026Sopenharmony_ci    def ratio(self, other):
20519ea8026Sopenharmony_ci        new_a, new_b = self if self else (Int(0), Int(0))
20619ea8026Sopenharmony_ci        old_a, old_b = other if other else (Int(0), Int(0))
20719ea8026Sopenharmony_ci        new = new_a.x/new_b.x if new_b.x else 1.0
20819ea8026Sopenharmony_ci        old = old_a.x/old_b.x if old_b.x else 1.0
20919ea8026Sopenharmony_ci        return new - old
21019ea8026Sopenharmony_ci
21119ea8026Sopenharmony_ci    def __add__(self, other):
21219ea8026Sopenharmony_ci        return self.__class__(self.a + other.a, self.b + other.b)
21319ea8026Sopenharmony_ci
21419ea8026Sopenharmony_ci    def __sub__(self, other):
21519ea8026Sopenharmony_ci        return self.__class__(self.a - other.a, self.b - other.b)
21619ea8026Sopenharmony_ci
21719ea8026Sopenharmony_ci    def __mul__(self, other):
21819ea8026Sopenharmony_ci        return self.__class__(self.a * other.a, self.b + other.b)
21919ea8026Sopenharmony_ci
22019ea8026Sopenharmony_ci    def __lt__(self, other):
22119ea8026Sopenharmony_ci        self_t = self.a.x/self.b.x if self.b.x else 1.0
22219ea8026Sopenharmony_ci        other_t = other.a.x/other.b.x if other.b.x else 1.0
22319ea8026Sopenharmony_ci        return (self_t, self.a.x) < (other_t, other.a.x)
22419ea8026Sopenharmony_ci
22519ea8026Sopenharmony_ci    def __gt__(self, other):
22619ea8026Sopenharmony_ci        return self.__class__.__lt__(other, self)
22719ea8026Sopenharmony_ci
22819ea8026Sopenharmony_ci    def __le__(self, other):
22919ea8026Sopenharmony_ci        return not self.__gt__(other)
23019ea8026Sopenharmony_ci
23119ea8026Sopenharmony_ci    def __ge__(self, other):
23219ea8026Sopenharmony_ci        return not self.__lt__(other)
23319ea8026Sopenharmony_ci
23419ea8026Sopenharmony_ci# available types
23519ea8026Sopenharmony_ciTYPES = co.OrderedDict([
23619ea8026Sopenharmony_ci    ('int',   Int),
23719ea8026Sopenharmony_ci    ('float', Float),
23819ea8026Sopenharmony_ci    ('frac',  Frac)
23919ea8026Sopenharmony_ci])
24019ea8026Sopenharmony_ci
24119ea8026Sopenharmony_ci
24219ea8026Sopenharmony_cidef infer(results, *,
24319ea8026Sopenharmony_ci        by=None,
24419ea8026Sopenharmony_ci        fields=None,
24519ea8026Sopenharmony_ci        types={},
24619ea8026Sopenharmony_ci        ops={},
24719ea8026Sopenharmony_ci        renames=[],
24819ea8026Sopenharmony_ci        **_):
24919ea8026Sopenharmony_ci    # if fields not specified, try to guess from data
25019ea8026Sopenharmony_ci    if fields is None:
25119ea8026Sopenharmony_ci        fields = co.OrderedDict()
25219ea8026Sopenharmony_ci        for r in results:
25319ea8026Sopenharmony_ci            for k, v in r.items():
25419ea8026Sopenharmony_ci                if (by is None or k not in by) and v.strip():
25519ea8026Sopenharmony_ci                    types_ = []
25619ea8026Sopenharmony_ci                    for t in fields.get(k, TYPES.values()):
25719ea8026Sopenharmony_ci                        try:
25819ea8026Sopenharmony_ci                            t(v)
25919ea8026Sopenharmony_ci                            types_.append(t)
26019ea8026Sopenharmony_ci                        except ValueError:
26119ea8026Sopenharmony_ci                            pass
26219ea8026Sopenharmony_ci                    fields[k] = types_
26319ea8026Sopenharmony_ci        fields = list(k for k, v in fields.items() if v)
26419ea8026Sopenharmony_ci
26519ea8026Sopenharmony_ci    # deduplicate fields
26619ea8026Sopenharmony_ci    fields = list(co.OrderedDict.fromkeys(fields).keys())
26719ea8026Sopenharmony_ci
26819ea8026Sopenharmony_ci    # if by not specified, guess it's anything not in fields and not a
26919ea8026Sopenharmony_ci    # source of a rename
27019ea8026Sopenharmony_ci    if by is None:
27119ea8026Sopenharmony_ci        by = co.OrderedDict()
27219ea8026Sopenharmony_ci        for r in results:
27319ea8026Sopenharmony_ci            # also ignore None keys, these are introduced by csv.DictReader
27419ea8026Sopenharmony_ci            # when header + row mismatch
27519ea8026Sopenharmony_ci            by.update((k, True) for k in r.keys()
27619ea8026Sopenharmony_ci                if k is not None
27719ea8026Sopenharmony_ci                    and k not in fields
27819ea8026Sopenharmony_ci                    and not any(k == old_k for _, old_k in renames))
27919ea8026Sopenharmony_ci        by = list(by.keys())
28019ea8026Sopenharmony_ci
28119ea8026Sopenharmony_ci    # deduplicate fields
28219ea8026Sopenharmony_ci    by = list(co.OrderedDict.fromkeys(by).keys())
28319ea8026Sopenharmony_ci
28419ea8026Sopenharmony_ci    # find best type for all fields
28519ea8026Sopenharmony_ci    types_ = {}
28619ea8026Sopenharmony_ci    for k in fields:
28719ea8026Sopenharmony_ci        if k in types:
28819ea8026Sopenharmony_ci            types_[k] = types[k]
28919ea8026Sopenharmony_ci        else:
29019ea8026Sopenharmony_ci            for t in TYPES.values():
29119ea8026Sopenharmony_ci                for r in results:
29219ea8026Sopenharmony_ci                    if k in r and r[k].strip():
29319ea8026Sopenharmony_ci                        try:
29419ea8026Sopenharmony_ci                            t(r[k])
29519ea8026Sopenharmony_ci                        except ValueError:
29619ea8026Sopenharmony_ci                            break
29719ea8026Sopenharmony_ci                else:
29819ea8026Sopenharmony_ci                    types_[k] = t
29919ea8026Sopenharmony_ci                    break
30019ea8026Sopenharmony_ci            else:
30119ea8026Sopenharmony_ci                print("error: no type matches field %r?" % k)
30219ea8026Sopenharmony_ci                sys.exit(-1)
30319ea8026Sopenharmony_ci    types = types_
30419ea8026Sopenharmony_ci
30519ea8026Sopenharmony_ci    # does folding change the type?
30619ea8026Sopenharmony_ci    types_ = {}
30719ea8026Sopenharmony_ci    for k, t in types.items():
30819ea8026Sopenharmony_ci        types_[k] = ops.get(k, OPS['sum'])([t()]).__class__
30919ea8026Sopenharmony_ci
31019ea8026Sopenharmony_ci
31119ea8026Sopenharmony_ci    # create result class
31219ea8026Sopenharmony_ci    def __new__(cls, **r):
31319ea8026Sopenharmony_ci        return cls.__mro__[1].__new__(cls,
31419ea8026Sopenharmony_ci            **{k: r.get(k, '') for k in by},
31519ea8026Sopenharmony_ci            **{k: r[k] if k in r and isinstance(r[k], list)
31619ea8026Sopenharmony_ci                else [types[k](r[k])] if k in r
31719ea8026Sopenharmony_ci                else []
31819ea8026Sopenharmony_ci                for k in fields})
31919ea8026Sopenharmony_ci
32019ea8026Sopenharmony_ci    def __add__(self, other):
32119ea8026Sopenharmony_ci        return self.__class__(
32219ea8026Sopenharmony_ci            **{k: getattr(self, k) for k in by},
32319ea8026Sopenharmony_ci            **{k: object.__getattribute__(self, k)
32419ea8026Sopenharmony_ci                + object.__getattribute__(other, k)
32519ea8026Sopenharmony_ci                for k in fields})
32619ea8026Sopenharmony_ci
32719ea8026Sopenharmony_ci    def __getattribute__(self, k):
32819ea8026Sopenharmony_ci        if k in fields:
32919ea8026Sopenharmony_ci            if object.__getattribute__(self, k):
33019ea8026Sopenharmony_ci                return ops.get(k, OPS['sum'])(object.__getattribute__(self, k))
33119ea8026Sopenharmony_ci            else:
33219ea8026Sopenharmony_ci                return None
33319ea8026Sopenharmony_ci        return object.__getattribute__(self, k)
33419ea8026Sopenharmony_ci
33519ea8026Sopenharmony_ci    return type('Result', (co.namedtuple('Result', by + fields),), {
33619ea8026Sopenharmony_ci        '__slots__': (),
33719ea8026Sopenharmony_ci        '__new__': __new__,
33819ea8026Sopenharmony_ci        '__add__': __add__,
33919ea8026Sopenharmony_ci        '__getattribute__': __getattribute__,
34019ea8026Sopenharmony_ci        '_by': by,
34119ea8026Sopenharmony_ci        '_fields': fields,
34219ea8026Sopenharmony_ci        '_sort': fields,
34319ea8026Sopenharmony_ci        '_types': types_,
34419ea8026Sopenharmony_ci    })
34519ea8026Sopenharmony_ci
34619ea8026Sopenharmony_ci
34719ea8026Sopenharmony_cidef fold(Result, results, *,
34819ea8026Sopenharmony_ci        by=None,
34919ea8026Sopenharmony_ci        defines=None,
35019ea8026Sopenharmony_ci        **_):
35119ea8026Sopenharmony_ci    if by is None:
35219ea8026Sopenharmony_ci        by = Result._by
35319ea8026Sopenharmony_ci
35419ea8026Sopenharmony_ci    for k in it.chain(by or [], (k for k, _ in defines or [])):
35519ea8026Sopenharmony_ci        if k not in Result._by and k not in Result._fields:
35619ea8026Sopenharmony_ci            print("error: could not find field %r?" % k)
35719ea8026Sopenharmony_ci            sys.exit(-1)
35819ea8026Sopenharmony_ci
35919ea8026Sopenharmony_ci    # filter by matching defines
36019ea8026Sopenharmony_ci    if defines is not None:
36119ea8026Sopenharmony_ci        results_ = []
36219ea8026Sopenharmony_ci        for r in results:
36319ea8026Sopenharmony_ci            if all(getattr(r, k) in vs for k, vs in defines):
36419ea8026Sopenharmony_ci                results_.append(r)
36519ea8026Sopenharmony_ci        results = results_
36619ea8026Sopenharmony_ci
36719ea8026Sopenharmony_ci    # organize results into conflicts
36819ea8026Sopenharmony_ci    folding = co.OrderedDict()
36919ea8026Sopenharmony_ci    for r in results:
37019ea8026Sopenharmony_ci        name = tuple(getattr(r, k) for k in by)
37119ea8026Sopenharmony_ci        if name not in folding:
37219ea8026Sopenharmony_ci            folding[name] = []
37319ea8026Sopenharmony_ci        folding[name].append(r)
37419ea8026Sopenharmony_ci
37519ea8026Sopenharmony_ci    # merge conflicts
37619ea8026Sopenharmony_ci    folded = []
37719ea8026Sopenharmony_ci    for name, rs in folding.items():
37819ea8026Sopenharmony_ci        folded.append(sum(rs[1:], start=rs[0]))
37919ea8026Sopenharmony_ci
38019ea8026Sopenharmony_ci    return folded
38119ea8026Sopenharmony_ci
38219ea8026Sopenharmony_cidef table(Result, results, diff_results=None, *,
38319ea8026Sopenharmony_ci        by=None,
38419ea8026Sopenharmony_ci        fields=None,
38519ea8026Sopenharmony_ci        sort=None,
38619ea8026Sopenharmony_ci        summary=False,
38719ea8026Sopenharmony_ci        all=False,
38819ea8026Sopenharmony_ci        percent=False,
38919ea8026Sopenharmony_ci        **_):
39019ea8026Sopenharmony_ci    all_, all = all, __builtins__.all
39119ea8026Sopenharmony_ci
39219ea8026Sopenharmony_ci    if by is None:
39319ea8026Sopenharmony_ci        by = Result._by
39419ea8026Sopenharmony_ci    if fields is None:
39519ea8026Sopenharmony_ci        fields = Result._fields
39619ea8026Sopenharmony_ci    types = Result._types
39719ea8026Sopenharmony_ci
39819ea8026Sopenharmony_ci    # fold again
39919ea8026Sopenharmony_ci    results = fold(Result, results, by=by)
40019ea8026Sopenharmony_ci    if diff_results is not None:
40119ea8026Sopenharmony_ci        diff_results = fold(Result, diff_results, by=by)
40219ea8026Sopenharmony_ci
40319ea8026Sopenharmony_ci    # organize by name
40419ea8026Sopenharmony_ci    table = {
40519ea8026Sopenharmony_ci        ','.join(str(getattr(r, k) or '') for k in by): r
40619ea8026Sopenharmony_ci        for r in results}
40719ea8026Sopenharmony_ci    diff_table = {
40819ea8026Sopenharmony_ci        ','.join(str(getattr(r, k) or '') for k in by): r
40919ea8026Sopenharmony_ci        for r in diff_results or []}
41019ea8026Sopenharmony_ci    names = list(table.keys() | diff_table.keys())
41119ea8026Sopenharmony_ci
41219ea8026Sopenharmony_ci    # sort again, now with diff info, note that python's sort is stable
41319ea8026Sopenharmony_ci    names.sort()
41419ea8026Sopenharmony_ci    if diff_results is not None:
41519ea8026Sopenharmony_ci        names.sort(key=lambda n: tuple(
41619ea8026Sopenharmony_ci            types[k].ratio(
41719ea8026Sopenharmony_ci                getattr(table.get(n), k, None),
41819ea8026Sopenharmony_ci                getattr(diff_table.get(n), k, None))
41919ea8026Sopenharmony_ci            for k in fields),
42019ea8026Sopenharmony_ci            reverse=True)
42119ea8026Sopenharmony_ci    if sort:
42219ea8026Sopenharmony_ci        for k, reverse in reversed(sort):
42319ea8026Sopenharmony_ci            names.sort(
42419ea8026Sopenharmony_ci                key=lambda n: tuple(
42519ea8026Sopenharmony_ci                    (getattr(table[n], k),)
42619ea8026Sopenharmony_ci                    if getattr(table.get(n), k, None) is not None else ()
42719ea8026Sopenharmony_ci                    for k in ([k] if k else [
42819ea8026Sopenharmony_ci                        k for k in Result._sort if k in fields])),
42919ea8026Sopenharmony_ci                reverse=reverse ^ (not k or k in Result._fields))
43019ea8026Sopenharmony_ci
43119ea8026Sopenharmony_ci
43219ea8026Sopenharmony_ci    # build up our lines
43319ea8026Sopenharmony_ci    lines = []
43419ea8026Sopenharmony_ci
43519ea8026Sopenharmony_ci    # header
43619ea8026Sopenharmony_ci    header = []
43719ea8026Sopenharmony_ci    header.append('%s%s' % (
43819ea8026Sopenharmony_ci        ','.join(by),
43919ea8026Sopenharmony_ci        ' (%d added, %d removed)' % (
44019ea8026Sopenharmony_ci            sum(1 for n in table if n not in diff_table),
44119ea8026Sopenharmony_ci            sum(1 for n in diff_table if n not in table))
44219ea8026Sopenharmony_ci            if diff_results is not None and not percent else '')
44319ea8026Sopenharmony_ci        if not summary else '')
44419ea8026Sopenharmony_ci    if diff_results is None:
44519ea8026Sopenharmony_ci        for k in fields:
44619ea8026Sopenharmony_ci            header.append(k)
44719ea8026Sopenharmony_ci    elif percent:
44819ea8026Sopenharmony_ci        for k in fields:
44919ea8026Sopenharmony_ci            header.append(k)
45019ea8026Sopenharmony_ci    else:
45119ea8026Sopenharmony_ci        for k in fields:
45219ea8026Sopenharmony_ci            header.append('o'+k)
45319ea8026Sopenharmony_ci        for k in fields:
45419ea8026Sopenharmony_ci            header.append('n'+k)
45519ea8026Sopenharmony_ci        for k in fields:
45619ea8026Sopenharmony_ci            header.append('d'+k)
45719ea8026Sopenharmony_ci    header.append('')
45819ea8026Sopenharmony_ci    lines.append(header)
45919ea8026Sopenharmony_ci
46019ea8026Sopenharmony_ci    def table_entry(name, r, diff_r=None, ratios=[]):
46119ea8026Sopenharmony_ci        entry = []
46219ea8026Sopenharmony_ci        entry.append(name)
46319ea8026Sopenharmony_ci        if diff_results is None:
46419ea8026Sopenharmony_ci            for k in fields:
46519ea8026Sopenharmony_ci                entry.append(getattr(r, k).table()
46619ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
46719ea8026Sopenharmony_ci                    else types[k].none)
46819ea8026Sopenharmony_ci        elif percent:
46919ea8026Sopenharmony_ci            for k in fields:
47019ea8026Sopenharmony_ci                entry.append(getattr(r, k).diff_table()
47119ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
47219ea8026Sopenharmony_ci                    else types[k].diff_none)
47319ea8026Sopenharmony_ci        else:
47419ea8026Sopenharmony_ci            for k in fields:
47519ea8026Sopenharmony_ci                entry.append(getattr(diff_r, k).diff_table()
47619ea8026Sopenharmony_ci                    if getattr(diff_r, k, None) is not None
47719ea8026Sopenharmony_ci                    else types[k].diff_none)
47819ea8026Sopenharmony_ci            for k in fields:
47919ea8026Sopenharmony_ci                entry.append(getattr(r, k).diff_table()
48019ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
48119ea8026Sopenharmony_ci                    else types[k].diff_none)
48219ea8026Sopenharmony_ci            for k in fields:
48319ea8026Sopenharmony_ci                entry.append(types[k].diff_diff(
48419ea8026Sopenharmony_ci                        getattr(r, k, None),
48519ea8026Sopenharmony_ci                        getattr(diff_r, k, None)))
48619ea8026Sopenharmony_ci        if diff_results is None:
48719ea8026Sopenharmony_ci            entry.append('')
48819ea8026Sopenharmony_ci        elif percent:
48919ea8026Sopenharmony_ci            entry.append(' (%s)' % ', '.join(
49019ea8026Sopenharmony_ci                '+∞%' if t == +m.inf
49119ea8026Sopenharmony_ci                else '-∞%' if t == -m.inf
49219ea8026Sopenharmony_ci                else '%+.1f%%' % (100*t)
49319ea8026Sopenharmony_ci                for t in ratios))
49419ea8026Sopenharmony_ci        else:
49519ea8026Sopenharmony_ci            entry.append(' (%s)' % ', '.join(
49619ea8026Sopenharmony_ci                    '+∞%' if t == +m.inf
49719ea8026Sopenharmony_ci                    else '-∞%' if t == -m.inf
49819ea8026Sopenharmony_ci                    else '%+.1f%%' % (100*t)
49919ea8026Sopenharmony_ci                    for t in ratios
50019ea8026Sopenharmony_ci                    if t)
50119ea8026Sopenharmony_ci                if any(ratios) else '')
50219ea8026Sopenharmony_ci        return entry
50319ea8026Sopenharmony_ci
50419ea8026Sopenharmony_ci    # entries
50519ea8026Sopenharmony_ci    if not summary:
50619ea8026Sopenharmony_ci        for name in names:
50719ea8026Sopenharmony_ci            r = table.get(name)
50819ea8026Sopenharmony_ci            if diff_results is None:
50919ea8026Sopenharmony_ci                diff_r = None
51019ea8026Sopenharmony_ci                ratios = None
51119ea8026Sopenharmony_ci            else:
51219ea8026Sopenharmony_ci                diff_r = diff_table.get(name)
51319ea8026Sopenharmony_ci                ratios = [
51419ea8026Sopenharmony_ci                    types[k].ratio(
51519ea8026Sopenharmony_ci                        getattr(r, k, None),
51619ea8026Sopenharmony_ci                        getattr(diff_r, k, None))
51719ea8026Sopenharmony_ci                    for k in fields]
51819ea8026Sopenharmony_ci                if not all_ and not any(ratios):
51919ea8026Sopenharmony_ci                    continue
52019ea8026Sopenharmony_ci            lines.append(table_entry(name, r, diff_r, ratios))
52119ea8026Sopenharmony_ci
52219ea8026Sopenharmony_ci    # total
52319ea8026Sopenharmony_ci    r = next(iter(fold(Result, results, by=[])), None)
52419ea8026Sopenharmony_ci    if diff_results is None:
52519ea8026Sopenharmony_ci        diff_r = None
52619ea8026Sopenharmony_ci        ratios = None
52719ea8026Sopenharmony_ci    else:
52819ea8026Sopenharmony_ci        diff_r = next(iter(fold(Result, diff_results, by=[])), None)
52919ea8026Sopenharmony_ci        ratios = [
53019ea8026Sopenharmony_ci            types[k].ratio(
53119ea8026Sopenharmony_ci                getattr(r, k, None),
53219ea8026Sopenharmony_ci                getattr(diff_r, k, None))
53319ea8026Sopenharmony_ci            for k in fields]
53419ea8026Sopenharmony_ci    lines.append(table_entry('TOTAL', r, diff_r, ratios))
53519ea8026Sopenharmony_ci
53619ea8026Sopenharmony_ci    # find the best widths, note that column 0 contains the names and column -1
53719ea8026Sopenharmony_ci    # the ratios, so those are handled a bit differently
53819ea8026Sopenharmony_ci    widths = [
53919ea8026Sopenharmony_ci        ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1
54019ea8026Sopenharmony_ci        for w, i in zip(
54119ea8026Sopenharmony_ci            it.chain([23], it.repeat(7)),
54219ea8026Sopenharmony_ci            range(len(lines[0])-1))]
54319ea8026Sopenharmony_ci
54419ea8026Sopenharmony_ci    # print our table
54519ea8026Sopenharmony_ci    for line in lines:
54619ea8026Sopenharmony_ci        print('%-*s  %s%s' % (
54719ea8026Sopenharmony_ci            widths[0], line[0],
54819ea8026Sopenharmony_ci            ' '.join('%*s' % (w, x)
54919ea8026Sopenharmony_ci                for w, x in zip(widths[1:], line[1:-1])),
55019ea8026Sopenharmony_ci            line[-1]))
55119ea8026Sopenharmony_ci
55219ea8026Sopenharmony_ci
55319ea8026Sopenharmony_cidef openio(path, mode='r', buffering=-1):
55419ea8026Sopenharmony_ci    # allow '-' for stdin/stdout
55519ea8026Sopenharmony_ci    if path == '-':
55619ea8026Sopenharmony_ci        if mode == 'r':
55719ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
55819ea8026Sopenharmony_ci        else:
55919ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
56019ea8026Sopenharmony_ci    else:
56119ea8026Sopenharmony_ci        return open(path, mode, buffering)
56219ea8026Sopenharmony_ci
56319ea8026Sopenharmony_cidef main(csv_paths, *,
56419ea8026Sopenharmony_ci        by=None,
56519ea8026Sopenharmony_ci        fields=None,
56619ea8026Sopenharmony_ci        defines=None,
56719ea8026Sopenharmony_ci        sort=None,
56819ea8026Sopenharmony_ci        **args):
56919ea8026Sopenharmony_ci    # separate out renames
57019ea8026Sopenharmony_ci    renames = list(it.chain.from_iterable(
57119ea8026Sopenharmony_ci        ((k, v) for v in vs)
57219ea8026Sopenharmony_ci        for k, vs in it.chain(by or [], fields or [])))
57319ea8026Sopenharmony_ci    if by is not None:
57419ea8026Sopenharmony_ci        by = [k for k, _ in by]
57519ea8026Sopenharmony_ci    if fields is not None:
57619ea8026Sopenharmony_ci        fields = [k for k, _ in fields]
57719ea8026Sopenharmony_ci
57819ea8026Sopenharmony_ci    # figure out types
57919ea8026Sopenharmony_ci    types = {}
58019ea8026Sopenharmony_ci    for t in TYPES.keys():
58119ea8026Sopenharmony_ci        for k in args.get(t, []):
58219ea8026Sopenharmony_ci            if k in types:
58319ea8026Sopenharmony_ci                print("error: conflicting type for field %r?" % k)
58419ea8026Sopenharmony_ci                sys.exit(-1)
58519ea8026Sopenharmony_ci            types[k] = TYPES[t]
58619ea8026Sopenharmony_ci    # rename types?
58719ea8026Sopenharmony_ci    if renames:
58819ea8026Sopenharmony_ci        types_ = {}
58919ea8026Sopenharmony_ci        for new_k, old_k in renames:
59019ea8026Sopenharmony_ci            if old_k in types:
59119ea8026Sopenharmony_ci                types_[new_k] = types[old_k]
59219ea8026Sopenharmony_ci        types.update(types_)
59319ea8026Sopenharmony_ci
59419ea8026Sopenharmony_ci    # figure out merge operations
59519ea8026Sopenharmony_ci    ops = {}
59619ea8026Sopenharmony_ci    for o in OPS.keys():
59719ea8026Sopenharmony_ci        for k in args.get(o, []):
59819ea8026Sopenharmony_ci            if k in ops:
59919ea8026Sopenharmony_ci                print("error: conflicting op for field %r?" % k)
60019ea8026Sopenharmony_ci                sys.exit(-1)
60119ea8026Sopenharmony_ci            ops[k] = OPS[o]
60219ea8026Sopenharmony_ci    # rename ops?
60319ea8026Sopenharmony_ci    if renames:
60419ea8026Sopenharmony_ci        ops_ = {}
60519ea8026Sopenharmony_ci        for new_k, old_k in renames:
60619ea8026Sopenharmony_ci            if old_k in ops:
60719ea8026Sopenharmony_ci                ops_[new_k] = ops[old_k]
60819ea8026Sopenharmony_ci        ops.update(ops_)
60919ea8026Sopenharmony_ci
61019ea8026Sopenharmony_ci    # find CSV files
61119ea8026Sopenharmony_ci    results = []
61219ea8026Sopenharmony_ci    for path in csv_paths:
61319ea8026Sopenharmony_ci        try:
61419ea8026Sopenharmony_ci            with openio(path) as f:
61519ea8026Sopenharmony_ci                reader = csv.DictReader(f, restval='')
61619ea8026Sopenharmony_ci                for r in reader:
61719ea8026Sopenharmony_ci                    # rename fields?
61819ea8026Sopenharmony_ci                    if renames:
61919ea8026Sopenharmony_ci                        # make a copy so renames can overlap
62019ea8026Sopenharmony_ci                        r_ = {}
62119ea8026Sopenharmony_ci                        for new_k, old_k in renames:
62219ea8026Sopenharmony_ci                            if old_k in r:
62319ea8026Sopenharmony_ci                                r_[new_k] = r[old_k]
62419ea8026Sopenharmony_ci                        r.update(r_)
62519ea8026Sopenharmony_ci
62619ea8026Sopenharmony_ci                    results.append(r)
62719ea8026Sopenharmony_ci        except FileNotFoundError:
62819ea8026Sopenharmony_ci            pass
62919ea8026Sopenharmony_ci
63019ea8026Sopenharmony_ci    # homogenize
63119ea8026Sopenharmony_ci    Result = infer(results,
63219ea8026Sopenharmony_ci        by=by,
63319ea8026Sopenharmony_ci        fields=fields,
63419ea8026Sopenharmony_ci        types=types,
63519ea8026Sopenharmony_ci        ops=ops,
63619ea8026Sopenharmony_ci        renames=renames)
63719ea8026Sopenharmony_ci    results_ = []
63819ea8026Sopenharmony_ci    for r in results:
63919ea8026Sopenharmony_ci        if not any(k in r and r[k].strip()
64019ea8026Sopenharmony_ci                for k in Result._fields):
64119ea8026Sopenharmony_ci            continue
64219ea8026Sopenharmony_ci        try:
64319ea8026Sopenharmony_ci            results_.append(Result(**{
64419ea8026Sopenharmony_ci                k: r[k] for k in Result._by + Result._fields
64519ea8026Sopenharmony_ci                if k in r and r[k].strip()}))
64619ea8026Sopenharmony_ci        except TypeError:
64719ea8026Sopenharmony_ci            pass
64819ea8026Sopenharmony_ci    results = results_
64919ea8026Sopenharmony_ci
65019ea8026Sopenharmony_ci    # fold
65119ea8026Sopenharmony_ci    results = fold(Result, results, by=by, defines=defines)
65219ea8026Sopenharmony_ci
65319ea8026Sopenharmony_ci    # sort, note that python's sort is stable
65419ea8026Sopenharmony_ci    results.sort()
65519ea8026Sopenharmony_ci    if sort:
65619ea8026Sopenharmony_ci        for k, reverse in reversed(sort):
65719ea8026Sopenharmony_ci            results.sort(
65819ea8026Sopenharmony_ci                key=lambda r: tuple(
65919ea8026Sopenharmony_ci                    (getattr(r, k),) if getattr(r, k) is not None else ()
66019ea8026Sopenharmony_ci                    for k in ([k] if k else Result._sort)),
66119ea8026Sopenharmony_ci                reverse=reverse ^ (not k or k in Result._fields))
66219ea8026Sopenharmony_ci
66319ea8026Sopenharmony_ci    # write results to CSV
66419ea8026Sopenharmony_ci    if args.get('output'):
66519ea8026Sopenharmony_ci        with openio(args['output'], 'w') as f:
66619ea8026Sopenharmony_ci            writer = csv.DictWriter(f, Result._by + Result._fields)
66719ea8026Sopenharmony_ci            writer.writeheader()
66819ea8026Sopenharmony_ci            for r in results:
66919ea8026Sopenharmony_ci                # note we need to go through getattr to resolve lazy fields
67019ea8026Sopenharmony_ci                writer.writerow({
67119ea8026Sopenharmony_ci                    k: getattr(r, k) for k in Result._by + Result._fields})
67219ea8026Sopenharmony_ci
67319ea8026Sopenharmony_ci    # find previous results?
67419ea8026Sopenharmony_ci    if args.get('diff'):
67519ea8026Sopenharmony_ci        diff_results = []
67619ea8026Sopenharmony_ci        try:
67719ea8026Sopenharmony_ci            with openio(args['diff']) as f:
67819ea8026Sopenharmony_ci                reader = csv.DictReader(f, restval='')
67919ea8026Sopenharmony_ci                for r in reader:
68019ea8026Sopenharmony_ci                    # rename fields?
68119ea8026Sopenharmony_ci                    if renames:
68219ea8026Sopenharmony_ci                        # make a copy so renames can overlap
68319ea8026Sopenharmony_ci                        r_ = {}
68419ea8026Sopenharmony_ci                        for new_k, old_k in renames:
68519ea8026Sopenharmony_ci                            if old_k in r:
68619ea8026Sopenharmony_ci                                r_[new_k] = r[old_k]
68719ea8026Sopenharmony_ci                        r.update(r_)
68819ea8026Sopenharmony_ci
68919ea8026Sopenharmony_ci                    if not any(k in r and r[k].strip()
69019ea8026Sopenharmony_ci                            for k in Result._fields):
69119ea8026Sopenharmony_ci                        continue
69219ea8026Sopenharmony_ci                    try:
69319ea8026Sopenharmony_ci                        diff_results.append(Result(**{
69419ea8026Sopenharmony_ci                            k: r[k] for k in Result._by + Result._fields
69519ea8026Sopenharmony_ci                            if k in r and r[k].strip()}))
69619ea8026Sopenharmony_ci                    except TypeError:
69719ea8026Sopenharmony_ci                        pass
69819ea8026Sopenharmony_ci        except FileNotFoundError:
69919ea8026Sopenharmony_ci            pass
70019ea8026Sopenharmony_ci
70119ea8026Sopenharmony_ci        # fold
70219ea8026Sopenharmony_ci        diff_results = fold(Result, diff_results, by=by, defines=defines)
70319ea8026Sopenharmony_ci
70419ea8026Sopenharmony_ci    # print table
70519ea8026Sopenharmony_ci    if not args.get('quiet'):
70619ea8026Sopenharmony_ci        table(Result, results,
70719ea8026Sopenharmony_ci            diff_results if args.get('diff') else None,
70819ea8026Sopenharmony_ci            by=by,
70919ea8026Sopenharmony_ci            fields=fields,
71019ea8026Sopenharmony_ci            sort=sort,
71119ea8026Sopenharmony_ci            **args)
71219ea8026Sopenharmony_ci
71319ea8026Sopenharmony_ci
71419ea8026Sopenharmony_ciif __name__ == "__main__":
71519ea8026Sopenharmony_ci    import argparse
71619ea8026Sopenharmony_ci    import sys
71719ea8026Sopenharmony_ci    parser = argparse.ArgumentParser(
71819ea8026Sopenharmony_ci        description="Summarize measurements in CSV files.",
71919ea8026Sopenharmony_ci        allow_abbrev=False)
72019ea8026Sopenharmony_ci    parser.add_argument(
72119ea8026Sopenharmony_ci        'csv_paths',
72219ea8026Sopenharmony_ci        nargs='*',
72319ea8026Sopenharmony_ci        help="Input *.csv files.")
72419ea8026Sopenharmony_ci    parser.add_argument(
72519ea8026Sopenharmony_ci        '-q', '--quiet',
72619ea8026Sopenharmony_ci        action='store_true',
72719ea8026Sopenharmony_ci        help="Don't show anything, useful with -o.")
72819ea8026Sopenharmony_ci    parser.add_argument(
72919ea8026Sopenharmony_ci        '-o', '--output',
73019ea8026Sopenharmony_ci        help="Specify CSV file to store results.")
73119ea8026Sopenharmony_ci    parser.add_argument(
73219ea8026Sopenharmony_ci        '-d', '--diff',
73319ea8026Sopenharmony_ci        help="Specify CSV file to diff against.")
73419ea8026Sopenharmony_ci    parser.add_argument(
73519ea8026Sopenharmony_ci        '-a', '--all',
73619ea8026Sopenharmony_ci        action='store_true',
73719ea8026Sopenharmony_ci        help="Show all, not just the ones that changed.")
73819ea8026Sopenharmony_ci    parser.add_argument(
73919ea8026Sopenharmony_ci        '-p', '--percent',
74019ea8026Sopenharmony_ci        action='store_true',
74119ea8026Sopenharmony_ci        help="Only show percentage change, not a full diff.")
74219ea8026Sopenharmony_ci    parser.add_argument(
74319ea8026Sopenharmony_ci        '-b', '--by',
74419ea8026Sopenharmony_ci        action='append',
74519ea8026Sopenharmony_ci        type=lambda x: (
74619ea8026Sopenharmony_ci            lambda k,v=None: (k, v.split(',') if v is not None else ())
74719ea8026Sopenharmony_ci            )(*x.split('=', 1)),
74819ea8026Sopenharmony_ci        help="Group by this field. Can rename fields with new_name=old_name.")
74919ea8026Sopenharmony_ci    parser.add_argument(
75019ea8026Sopenharmony_ci        '-f', '--field',
75119ea8026Sopenharmony_ci        dest='fields',
75219ea8026Sopenharmony_ci        action='append',
75319ea8026Sopenharmony_ci        type=lambda x: (
75419ea8026Sopenharmony_ci            lambda k,v=None: (k, v.split(',') if v is not None else ())
75519ea8026Sopenharmony_ci            )(*x.split('=', 1)),
75619ea8026Sopenharmony_ci        help="Show this field. Can rename fields with new_name=old_name.")
75719ea8026Sopenharmony_ci    parser.add_argument(
75819ea8026Sopenharmony_ci        '-D', '--define',
75919ea8026Sopenharmony_ci        dest='defines',
76019ea8026Sopenharmony_ci        action='append',
76119ea8026Sopenharmony_ci        type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
76219ea8026Sopenharmony_ci        help="Only include results where this field is this value. May include "
76319ea8026Sopenharmony_ci            "comma-separated options.")
76419ea8026Sopenharmony_ci    class AppendSort(argparse.Action):
76519ea8026Sopenharmony_ci        def __call__(self, parser, namespace, value, option):
76619ea8026Sopenharmony_ci            if namespace.sort is None:
76719ea8026Sopenharmony_ci                namespace.sort = []
76819ea8026Sopenharmony_ci            namespace.sort.append((value, True if option == '-S' else False))
76919ea8026Sopenharmony_ci    parser.add_argument(
77019ea8026Sopenharmony_ci        '-s', '--sort',
77119ea8026Sopenharmony_ci        nargs='?',
77219ea8026Sopenharmony_ci        action=AppendSort,
77319ea8026Sopenharmony_ci        help="Sort by this field.")
77419ea8026Sopenharmony_ci    parser.add_argument(
77519ea8026Sopenharmony_ci        '-S', '--reverse-sort',
77619ea8026Sopenharmony_ci        nargs='?',
77719ea8026Sopenharmony_ci        action=AppendSort,
77819ea8026Sopenharmony_ci        help="Sort by this field, but backwards.")
77919ea8026Sopenharmony_ci    parser.add_argument(
78019ea8026Sopenharmony_ci        '-Y', '--summary',
78119ea8026Sopenharmony_ci        action='store_true',
78219ea8026Sopenharmony_ci        help="Only show the total.")
78319ea8026Sopenharmony_ci    parser.add_argument(
78419ea8026Sopenharmony_ci        '--int',
78519ea8026Sopenharmony_ci        action='append',
78619ea8026Sopenharmony_ci        help="Treat these fields as ints.")
78719ea8026Sopenharmony_ci    parser.add_argument(
78819ea8026Sopenharmony_ci        '--float',
78919ea8026Sopenharmony_ci        action='append',
79019ea8026Sopenharmony_ci        help="Treat these fields as floats.")
79119ea8026Sopenharmony_ci    parser.add_argument(
79219ea8026Sopenharmony_ci        '--frac',
79319ea8026Sopenharmony_ci        action='append',
79419ea8026Sopenharmony_ci        help="Treat these fields as fractions.")
79519ea8026Sopenharmony_ci    parser.add_argument(
79619ea8026Sopenharmony_ci        '--sum',
79719ea8026Sopenharmony_ci        action='append',
79819ea8026Sopenharmony_ci        help="Add these fields (the default).")
79919ea8026Sopenharmony_ci    parser.add_argument(
80019ea8026Sopenharmony_ci        '--prod',
80119ea8026Sopenharmony_ci        action='append',
80219ea8026Sopenharmony_ci        help="Multiply these fields.")
80319ea8026Sopenharmony_ci    parser.add_argument(
80419ea8026Sopenharmony_ci        '--min',
80519ea8026Sopenharmony_ci        action='append',
80619ea8026Sopenharmony_ci        help="Take the minimum of these fields.")
80719ea8026Sopenharmony_ci    parser.add_argument(
80819ea8026Sopenharmony_ci        '--max',
80919ea8026Sopenharmony_ci        action='append',
81019ea8026Sopenharmony_ci        help="Take the maximum of these fields.")
81119ea8026Sopenharmony_ci    parser.add_argument(
81219ea8026Sopenharmony_ci        '--mean',
81319ea8026Sopenharmony_ci        action='append',
81419ea8026Sopenharmony_ci        help="Average these fields.")
81519ea8026Sopenharmony_ci    parser.add_argument(
81619ea8026Sopenharmony_ci        '--stddev',
81719ea8026Sopenharmony_ci        action='append',
81819ea8026Sopenharmony_ci        help="Find the standard deviation of these fields.")
81919ea8026Sopenharmony_ci    parser.add_argument(
82019ea8026Sopenharmony_ci        '--gmean',
82119ea8026Sopenharmony_ci        action='append',
82219ea8026Sopenharmony_ci        help="Find the geometric mean of these fields.")
82319ea8026Sopenharmony_ci    parser.add_argument(
82419ea8026Sopenharmony_ci        '--gstddev',
82519ea8026Sopenharmony_ci        action='append',
82619ea8026Sopenharmony_ci        help="Find the geometric standard deviation of these fields.")
82719ea8026Sopenharmony_ci    sys.exit(main(**{k: v
82819ea8026Sopenharmony_ci        for k, v in vars(parser.parse_intermixed_args()).items()
82919ea8026Sopenharmony_ci        if v is not None}))
830