-
Notifications
You must be signed in to change notification settings - Fork 0
feat(calc): formula evaluation engine with recursive expression parser #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,10 +8,13 @@ | |
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from typing import Any | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from wolfxl._worksheet import Worksheet | ||
|
|
||
| if TYPE_CHECKING: | ||
| from wolfxl.calc._protocol import RecalcResult | ||
|
|
||
|
|
||
| class Workbook: | ||
| """openpyxl-compatible workbook backed by Rust.""" | ||
|
|
@@ -23,6 +26,7 @@ def __init__(self) -> None: | |
| self._rust_writer: Any = _rust.RustXlsxWriterBook() | ||
| self._rust_reader: Any = None | ||
| self._rust_patcher: Any = None | ||
| self._evaluator: Any = None | ||
| self._sheet_names: list[str] = ["Sheet"] | ||
| self._sheets: dict[str, Worksheet] = {} | ||
| self._sheets["Sheet"] = Worksheet(self, "Sheet") | ||
|
|
@@ -36,6 +40,7 @@ def _from_reader(cls, path: str) -> Workbook: | |
| wb = object.__new__(cls) | ||
| wb._rust_writer = None | ||
| wb._rust_patcher = None | ||
| wb._evaluator = None | ||
| wb._rust_reader = _rust.CalamineStyledBook.open(path) | ||
| names = [str(n) for n in wb._rust_reader.sheet_names()] | ||
| wb._sheet_names = names | ||
|
|
@@ -51,6 +56,7 @@ def _from_patcher(cls, path: str) -> Workbook: | |
|
|
||
| wb = object.__new__(cls) | ||
| wb._rust_writer = None | ||
| wb._evaluator = None | ||
| wb._rust_reader = _rust.CalamineStyledBook.open(path) | ||
| wb._rust_patcher = _rust.XlsxPatcher.open(path) | ||
| names = [str(n) for n in wb._rust_reader.sheet_names()] | ||
|
|
@@ -118,6 +124,50 @@ def save(self, filename: str | os.PathLike[str]) -> None: | |
| else: | ||
| raise RuntimeError("save requires write or modify mode") | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Formula evaluation (requires wolfxl.calc) | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def calculate(self) -> dict[str, Any]: | ||
| """Evaluate all formulas in the workbook. | ||
|
|
||
| Returns a dict of cell_ref -> computed value for all formula cells. | ||
| Requires the ``wolfxl.calc`` module (install via ``pip install wolfxl[calc]``). | ||
|
|
||
| The internal evaluator is cached so that a subsequent | ||
| :meth:`recalculate` call can reuse it without rescanning. | ||
| """ | ||
| from wolfxl.calc._evaluator import WorkbookEvaluator | ||
|
|
||
| ev = WorkbookEvaluator() | ||
| ev.load(self) | ||
| result = ev.calculate() | ||
| self._evaluator = ev # cache for recalculate() | ||
| return result | ||
|
|
||
| def recalculate( | ||
| self, | ||
| perturbations: dict[str, float | int], | ||
| tolerance: float = 1e-10, | ||
| ) -> RecalcResult: | ||
| """Perturb input cells and recompute affected formulas. | ||
|
Comment on lines
148
to
153
|
||
|
|
||
| Returns a ``RecalcResult`` describing which cells changed. | ||
| Requires the ``wolfxl.calc`` module. | ||
|
|
||
| If :meth:`calculate` was called first, the cached evaluator is | ||
| reused (avoiding a full rescan + recalculate). | ||
| """ | ||
| ev = self._evaluator | ||
| if ev is None: | ||
| from wolfxl.calc._evaluator import WorkbookEvaluator | ||
|
|
||
| ev = WorkbookEvaluator() | ||
| ev.load(self) | ||
| ev.calculate() | ||
| self._evaluator = ev | ||
| return ev.recalculate(perturbations, tolerance) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Context manager + cleanup | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """wolfxl.calc - Formula evaluation engine for wolfxl workbooks.""" | ||
|
|
||
| from wolfxl.calc._evaluator import WorkbookEvaluator | ||
| from wolfxl.calc._functions import FUNCTION_WHITELIST_V1, FunctionRegistry, is_supported | ||
| from wolfxl.calc._graph import DependencyGraph | ||
| from wolfxl.calc._parser import FormulaParser, all_references, expand_range | ||
| from wolfxl.calc._protocol import CalcEngine, CellDelta, RecalcResult | ||
|
|
||
| __all__ = [ | ||
| "CalcEngine", | ||
| "CellDelta", | ||
| "DependencyGraph", | ||
| "FUNCTION_WHITELIST_V1", | ||
| "FormulaParser", | ||
| "FunctionRegistry", | ||
| "RecalcResult", | ||
| "WorkbookEvaluator", | ||
| "all_references", | ||
| "expand_range", | ||
| "is_supported", | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workbook.calculate()assignsself._evaluator, but_evaluatoris never declared/initialized on the class. With[tool.mypy] strict = true, this typically triggers an "attribute-defined" error, and it also means reader/patcher constructors won't have the attribute. Initializeself._evaluator(e.g., toNone) in__init__and also set it on instances created via_from_reader/_from_patcher.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 668fd6e. _evaluator is now initialized to None in init, _from_reader, and _from_patcher. Strict mypy should be happy.