"""Built-in formula functions for the bitable formula engine. v1 implements: SUM, AVG, COUNT, MIN, MAX, CONCAT, ABS, ROUND, IF, LEN. Aggregate functions (SUM/AVG/COUNT/MIN/MAX) accept a list of values (the entire column) and return a scalar. Non-aggregate functions (ABS/ROUND/IF/LEN/CONCAT) operate on scalars. The engine determines whether to pass a column (list) or scalar (row value) based on the calling context — see :mod:`agentkit.bitable.formula.engine`. """ from __future__ import annotations from typing import Callable, TypeAlias # A formula evaluates to a scalar primitive: text, number, or nothing. # bool is intentionally excluded — comparisons live in the parser layer # and never reach the function registry. FormulaResult: TypeAlias = str | int | float | None # ── Aggregate functions (operate on lists) ──────────────── def _sum(values: list[FormulaResult]) -> float | int: """Sum of numeric values, ignoring None/empty.""" total = 0 for v in values: if v is None or v == "": continue total += v return total def _avg(values: list[FormulaResult]) -> float: """Average of numeric values, ignoring None/empty.""" nums = [v for v in values if v is not None and v != ""] if not nums: return 0.0 return sum(nums) / len(nums) def _count(values: list[FormulaResult]) -> int: """Count of non-empty values.""" return sum(1 for v in values if v is not None and v != "") def _min(values: list[FormulaResult]) -> FormulaResult: """Minimum of numeric values, ignoring None/empty.""" nums = [v for v in values if v is not None and v != ""] if not nums: return 0 return min(nums) def _max(values: list[FormulaResult]) -> FormulaResult: """Maximum of numeric values, ignoring None/empty.""" nums = [v for v in values if v is not None and v != ""] if not nums: return 0 return max(nums) # ── Scalar functions ────────────────────────────────────── def _abs(value: FormulaResult) -> FormulaResult: return abs(value) def _round(value: FormulaResult, digits: int = 0) -> float: return round(value, digits) def _if( condition: FormulaResult, true_val: FormulaResult, false_val: FormulaResult = None, ) -> FormulaResult: return true_val if condition else false_val def _len(value: FormulaResult) -> int: if value is None: return 0 return len(str(value)) def _concat(*args: FormulaResult) -> str: """Concatenate all arguments as strings.""" return "".join(str(a) for a in args if a is not None) # ── Registry ────────────────────────────────────────────── # Functions that aggregate a column (receive a list of all column values) AGGREGATE_FUNCTIONS: frozenset[str] = frozenset({"SUM", "AVG", "COUNT", "MIN", "MAX"}) FUNCTION_REGISTRY: dict[str, Callable[..., FormulaResult]] = { "SUM": _sum, "AVG": _avg, "COUNT": _count, "MIN": _min, "MAX": _max, "ABS": _abs, "ROUND": _round, "IF": _if, "LEN": _len, "CONCAT": _concat, }