scripts: csv.py: Started adding -g/--accumulate

The idea here is to add some sort of accumulate operation to csv.py, so
we can stop cumulative-result clunkiness. It would also be immensely
useful as a general function, and -i/--enumerate already sets a
precedent for this sort of cross-row behavior.

But I'm starting to think using flags here is not the best way, maybe
this would be better as a field expr?
This commit is contained in:
Christopher Haster
2026-01-21 22:23:49 -06:00
parent 45f1850028
commit 3978a32156
+63 -10
View File
@@ -1306,6 +1306,7 @@ class CsvExpr:
file=sys.stderr) file=sys.stderr)
sys.exit(3) sys.exit(3)
# SI-prefix formatter # SI-prefix formatter
def si(x): def si(x):
if x == 0: if x == 0:
@@ -1409,6 +1410,15 @@ def punescape_help():
'%(field)[fFeEgG]', 'An existing field formatted as a float')) '%(field)[fFeEgG]', 'An existing field formatted as a float'))
# a couple marker classes
class CsvEnumerate:
pass
class CsvAccumulate:
def __init__(self, expr):
self.expr = expr
# open with '-' for stdin/stdout # open with '-' for stdin/stdout
def openio(path, mode='r', buffering=-1): def openio(path, mode='r', buffering=-1):
import os import os
@@ -1646,11 +1656,18 @@ def compile(fields_, results,
def homogenize(Result, results, *, def homogenize(Result, results, *,
enumerates=None, enumerates=None,
accumulates=None,
defines=[], defines=[],
depth=1, depth=1,
**_): **_):
# this just converts all (possibly recursive) results to our # convert all (possibly recursive) results to our result type
# result type
# prepare accumulators
accumulators = {
k: {'i': CsvInt(0), 'a': CsvInt(0), 'b': CsvInt(0), 'i': CsvInt(0)}
for k, v in accumulates}
# homogenize results
results_ = [] results_ = []
for r in results: for r in results:
# filter by matching defines # filter by matching defines
@@ -1671,11 +1688,14 @@ def homogenize(Result, results, *,
| ({e: len(results_) for e in enumerates} | ({e: len(results_) for e in enumerates}
if enumerates is not None if enumerates is not None
else {}) else {})
# accumulate?
# TODO
# recurse? # recurse?
| ({Result._children: homogenize( | ({Result._children: homogenize(
Result, r[Result._children], Result, r[Result._children],
# only filter defines at the top level! # only filter defines at the top level!
enumerates=enumerates, enumerates=enumerates,
accumulates=accumulates,
depth=depth-1)} depth=depth-1)}
if hasattr(Result, '_children') if hasattr(Result, '_children')
and Result._children in r and Result._children in r
@@ -2346,20 +2366,25 @@ def main(csv_paths, *,
# separate out enumerates/mods/exprs # separate out enumerates/mods/exprs
# #
# enumerate enumerates: -ia # enumerate enumerates: -ia
# accumulate supports exprs: -ga=0.99*g+0.01*b
# by supports mods: -ba=%(b)s # by supports mods: -ba=%(b)s
# fields/sort/etc supports exprs: -fa=b+c # fields/sort/etc supports exprs: -fa=b+c
# #
enumerates = [k enumerates = [k
for (k, v), hidden in (by or []) for (k, v), hidden in (by or [])
if v == enumerate] if isinstance(v, CsvEnumerate)]
accumulates = [(k, v)
for (k, v), hidden in (fields or [])
if isinstance(v, CsvAccumulate)]
mods = [(k, v) mods = [(k, v)
for k, v in it.chain( for k, v in it.chain(
((k, v) for (k, v), hidden in (by or []) ((k, v) for (k, v), hidden in (by or [])
if v != enumerate)) if not isinstance(v, CsvEnumerate)))
if v is not None] if v is not None]
exprs = [(k, v) exprs = [(k, v)
for k, v in it.chain( for k, v in it.chain(
((k, v) for (k, v), hidden in (fields or [])), ((k, v) for (k, v), hidden in (fields or [])
if not isinstance(v, CsvAccumulate)),
((k, v) for (k, v), reverse in (sort or [])), ((k, v) for (k, v), reverse in (sort or [])),
((k, v) for (k, v), reverse in (hot or []))) ((k, v) for (k, v), reverse in (hot or [])))
if v is not None] if v is not None]
@@ -2432,6 +2457,7 @@ def main(csv_paths, *,
# homogenize # homogenize
results = homogenize(Result, results, results = homogenize(Result, results,
enumerates=enumerates, enumerates=enumerates,
accumulates=accumulates,
defines=defines, defines=defines,
depth=depth) depth=depth)
@@ -2469,6 +2495,7 @@ def main(csv_paths, *,
# homogenize # homogenize
diff_results = homogenize(Result, diff_results, diff_results = homogenize(Result, diff_results,
enumerates=enumerates, enumerates=enumerates,
accumulates=accumulates,
defines=defines, defines=defines,
depth=depth) depth=depth)
@@ -2567,16 +2594,16 @@ if __name__ == "__main__":
'-i', '--enumerate', '-i', '--enumerate',
action=AppendBy, action=AppendBy,
nargs='?', nargs='?',
type=lambda x: (x, enumerate), type=lambda x: (x, CsvEnumerate()),
const=('i', enumerate), const=('i', CsvEnumerate()),
help="Enumerate results with this field. This will prevent " help="Enumerate results with this field. This will prevent "
"result folding.") "result folding.")
parser.add_argument( parser.add_argument(
'-I', '--hidden-enumerate', '-I', '--hidden-enumerate',
action=AppendBy, action=AppendBy,
nargs='?', nargs='?',
type=lambda x: (x, enumerate), type=lambda x: (x, CsvEnumerate()),
const=('i', enumerate), const=('i', CsvEnumerate()),
help="Like -i/--enumerate, but hidden from the table renderer, " help="Like -i/--enumerate, but hidden from the table renderer, "
"and doesn't affect -b/--by defaults.") "and doesn't affect -b/--by defaults.")
parser.add_argument( parser.add_argument(
@@ -2604,7 +2631,8 @@ if __name__ == "__main__":
if namespace.fields is None: if namespace.fields is None:
namespace.fields = [] namespace.fields = []
namespace.fields.append((value, option in { namespace.fields.append((value, option in {
'-F', '--hidden-field'})) '-F', '--hidden-field',
'-G', '--hidden-accumulate'}))
parser.add_argument( parser.add_argument(
'-f', '--field', '-f', '--field',
dest='fields', dest='fields',
@@ -2627,6 +2655,31 @@ if __name__ == "__main__":
)(*x.split('=', 1)), )(*x.split('=', 1)),
help="Like -f/--field, but hidden from the table renderer, " help="Like -f/--field, but hidden from the table renderer, "
"and doesn't affect -f/--field defaults.") "and doesn't affect -f/--field defaults.")
parser.add_argument(
'-g', '--accumulate',
dest='fields',
action=AppendField,
type=lambda x: (
lambda k, v=None: (
k.strip(),
CsvAccumulate(CsvExpr(v) if v is not None else None))
)(*x.split('=', 1)),
help="Accumulate this field. Note accumulation is dependent "
"on input row order, and may need a second pass after "
"sorting. Can include an expression, but the expression "
"is evaluated early with i, a, b, and g as arguments. "
"Default behavior matches the expression (i==0)?b:g+b.")
parser.add_argument(
'-G', '--hidden-accumulate',
dest='fields',
action=AppendField,
type=lambda x: (
lambda k, v=None: (
k.strip(),
CsvAccumulate(CsvExpr(v) if v is not None else None))
)(*x.split('=', 1)),
help="Like -g/--accumulate, but hidden from the table renderer, "
"and doesn't affect -f/--field defaults.")
parser.add_argument( parser.add_argument(
'-D', '--define', '-D', '--define',
dest='defines', dest='defines',