f8-engine 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {f8_engine-0.2.0 → f8_engine-0.3.0}/Cargo.lock +2 -2
- {f8_engine-0.2.0 → f8_engine-0.3.0}/Cargo.toml +1 -1
- {f8_engine-0.2.0 → f8_engine-0.3.0}/PKG-INFO +27 -25
- {f8_engine-0.2.0 → f8_engine-0.3.0}/README.md +26 -24
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/model.rs +1 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/resolve.rs +199 -181
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/expand.rs +9 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/symbol.rs +1 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/error.rs +19 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/mod.rs +245 -63
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/address.rs +39 -7
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/mod.rs +30 -34
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/ast.rs +170 -14
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/mod.rs +2 -1
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/parser.rs +114 -187
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/lib.rs +1 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/model/mod.rs +3 -2
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/model/table.rs +113 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/model/theme.rs +18 -7
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-python/src/error.rs +11 -1
- {f8_engine-0.2.0 → f8_engine-0.3.0}/pyproject.toml +4 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/__init__.py +2 -1
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/builder.py +204 -9
- f8_engine-0.3.0/python/f8/skills/f8-modeling/SKILL.md +45 -0
- f8_engine-0.3.0/python/f8/skills/f8-modeling/references/auditing-and-repair.md +170 -0
- f8_engine-0.3.0/python/f8/skills/f8-modeling/references/functions.md +128 -0
- f8_engine-0.3.0/python/f8/skills/f8-modeling/references/syntax-and-grid.md +324 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/types.py +36 -4
- f8_engine-0.2.0/python/f8/skills/f8-modeling/SKILL.md +0 -43
- f8_engine-0.2.0/python/f8/skills/f8-modeling/references/auditing-and-repair.md +0 -367
- f8_engine-0.2.0/python/f8/skills/f8-modeling/references/functions.md +0 -172
- f8_engine-0.2.0/python/f8/skills/f8-modeling/references/syntax-and-grid.md +0 -405
- {f8_engine-0.2.0 → f8_engine-0.3.0}/LICENSE +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/Cargo.toml +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/model.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/format.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/workbook.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/context.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/expr.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/cycle.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/dependency.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/cell.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/id.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/a1.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/common.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/constructors.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/parts.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/serial.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/yearfrac.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/annuity.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/discounting.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/irr.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/boolean.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/branching.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/error_handling.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/index.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/search.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/xlookup.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/xmatch.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/aggregation.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/basic.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/conditional.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/criteria.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/product.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/rounding.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/registry.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/traits.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/model/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/model/format.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/model/id.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/model/manifest.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/value/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/value/format.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-core/src/value/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-python/Cargo.toml +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-python/src/lib.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/crates/f8-python/src/model.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/__main__.py +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/_core.pyi +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/audit.py +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/cli.py +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.0}/python/f8/py.typed +0 -0
|
@@ -37,7 +37,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "f8-core"
|
|
40
|
-
version = "0.
|
|
40
|
+
version = "0.3.0"
|
|
41
41
|
dependencies = [
|
|
42
42
|
"indexmap",
|
|
43
43
|
"petgraph",
|
|
@@ -50,7 +50,7 @@ dependencies = [
|
|
|
50
50
|
|
|
51
51
|
[[package]]
|
|
52
52
|
name = "f8-python"
|
|
53
|
-
version = "0.
|
|
53
|
+
version = "0.3.0"
|
|
54
54
|
dependencies = [
|
|
55
55
|
"f8-core",
|
|
56
56
|
"pyo3",
|
|
@@ -3,7 +3,7 @@ resolver = "3"
|
|
|
3
3
|
members = ["crates/f8-core", "crates/f8-python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.3.0"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
description = "Financial modeling engine with compiler verification and live Excel formulas"
|
|
9
9
|
authors = ["Michael (Chencheng) Hu, CFA"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: f8-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
@@ -26,7 +26,7 @@ Project-URL: Repository, https://github.com/mhcfa/f8
|
|
|
26
26
|
|
|
27
27
|
# f8
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Author, verify, and automate financial models with code and AI agents, compiled into live Excel workbooks.
|
|
30
30
|
|
|
31
31
|
## Who is f8 for
|
|
32
32
|
|
|
@@ -71,35 +71,36 @@ f8 guide --all # streams the full DSL, function, and auditing guide to stdout
|
|
|
71
71
|
```python
|
|
72
72
|
from f8 import Model
|
|
73
73
|
|
|
74
|
+
years = ["2025A", "2026E", "2027E"]
|
|
75
|
+
capex = {"2025A": 25.0, "2026E": 30.0, "2027E": 35.0}
|
|
76
|
+
depr = {"2025A": -15.0, "2026E": -18.0, "2027E": -22.0}
|
|
77
|
+
|
|
74
78
|
model = Model("Operating Forecast")
|
|
75
79
|
sheet = model.add_sheet("Summary")
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for year in
|
|
80
|
-
|
|
81
|
+
ppe = sheet.add_table("PPE", "PP&E Corkscrew Schedule", "($ in millions)")
|
|
82
|
+
ppe.add_column("item", role="descriptor", header="Line Item")
|
|
83
|
+
for year in years:
|
|
84
|
+
ppe.add_column(year, header=year, format="currency", decimals=1)
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
cells={"item": "
|
|
85
|
-
expr='
|
|
86
|
-
)
|
|
87
|
-
pnl.add_row(
|
|
88
|
-
"cogs",
|
|
89
|
-
cells={"item": "Cost of Revenue"},
|
|
90
|
-
expr='PnL["revenue", @col] * 0.30',
|
|
86
|
+
ppe.add_row(
|
|
87
|
+
"beg_balance",
|
|
88
|
+
cells={"item": "Beginning Balance", "2025A": 100.0},
|
|
89
|
+
expr='PPE["end_balance", col.prev]',
|
|
91
90
|
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
ppe.add_row("capex", cells={"item": "Capital Expenditures", **capex})
|
|
92
|
+
ppe.add_row("depreciation", cells={"item": "Depreciation", **depr})
|
|
93
|
+
ppe.add_row(
|
|
94
|
+
"end_balance",
|
|
95
|
+
cells={"item": "Ending Balance"},
|
|
96
|
+
expr='SUM(PPE["beg_balance".."depreciation", @col])',
|
|
96
97
|
style="total",
|
|
97
98
|
)
|
|
98
99
|
|
|
99
100
|
compiled = model.compile()
|
|
100
101
|
|
|
101
102
|
# Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
|
|
102
|
-
print(compiled.table("
|
|
103
|
+
print(compiled.table("PPE").to_markdown())
|
|
103
104
|
|
|
104
105
|
# Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
|
|
105
106
|
compiled.save("forecast.xlsx")
|
|
@@ -108,11 +109,12 @@ compiled.save("forecast.xlsx")
|
|
|
108
109
|
Output:
|
|
109
110
|
|
|
110
111
|
```markdown
|
|
111
|
-
| Line Item
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
112
|
+
| Line Item | 2025A | 2026E | 2027E |
|
|
113
|
+
| :------------------- | ---------: | ---------: | ---------: |
|
|
114
|
+
| Beginning Balance | $100.0 | $110.0 | $122.0 |
|
|
115
|
+
| Capital Expenditures | $25.0 | $30.0 | $35.0 |
|
|
116
|
+
| Depreciation | ($15.0) | ($18.0) | ($22.0) |
|
|
117
|
+
| **Ending Balance** | **$110.0** | **$122.0** | **$135.0** |
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
## Sample deliverables
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# f8
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Author, verify, and automate financial models with code and AI agents, compiled into live Excel workbooks.
|
|
4
4
|
|
|
5
5
|
## Who is f8 for
|
|
6
6
|
|
|
@@ -45,35 +45,36 @@ f8 guide --all # streams the full DSL, function, and auditing guide to stdout
|
|
|
45
45
|
```python
|
|
46
46
|
from f8 import Model
|
|
47
47
|
|
|
48
|
+
years = ["2025A", "2026E", "2027E"]
|
|
49
|
+
capex = {"2025A": 25.0, "2026E": 30.0, "2027E": 35.0}
|
|
50
|
+
depr = {"2025A": -15.0, "2026E": -18.0, "2027E": -22.0}
|
|
51
|
+
|
|
48
52
|
model = Model("Operating Forecast")
|
|
49
53
|
sheet = model.add_sheet("Summary")
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for year in
|
|
54
|
-
|
|
55
|
+
ppe = sheet.add_table("PPE", "PP&E Corkscrew Schedule", "($ in millions)")
|
|
56
|
+
ppe.add_column("item", role="descriptor", header="Line Item")
|
|
57
|
+
for year in years:
|
|
58
|
+
ppe.add_column(year, header=year, format="currency", decimals=1)
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
cells={"item": "
|
|
59
|
-
expr='
|
|
60
|
-
)
|
|
61
|
-
pnl.add_row(
|
|
62
|
-
"cogs",
|
|
63
|
-
cells={"item": "Cost of Revenue"},
|
|
64
|
-
expr='PnL["revenue", @col] * 0.30',
|
|
60
|
+
ppe.add_row(
|
|
61
|
+
"beg_balance",
|
|
62
|
+
cells={"item": "Beginning Balance", "2025A": 100.0},
|
|
63
|
+
expr='PPE["end_balance", col.prev]',
|
|
65
64
|
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
ppe.add_row("capex", cells={"item": "Capital Expenditures", **capex})
|
|
66
|
+
ppe.add_row("depreciation", cells={"item": "Depreciation", **depr})
|
|
67
|
+
ppe.add_row(
|
|
68
|
+
"end_balance",
|
|
69
|
+
cells={"item": "Ending Balance"},
|
|
70
|
+
expr='SUM(PPE["beg_balance".."depreciation", @col])',
|
|
70
71
|
style="total",
|
|
71
72
|
)
|
|
72
73
|
|
|
73
74
|
compiled = model.compile()
|
|
74
75
|
|
|
75
76
|
# Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
|
|
76
|
-
print(compiled.table("
|
|
77
|
+
print(compiled.table("PPE").to_markdown())
|
|
77
78
|
|
|
78
79
|
# Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
|
|
79
80
|
compiled.save("forecast.xlsx")
|
|
@@ -82,11 +83,12 @@ compiled.save("forecast.xlsx")
|
|
|
82
83
|
Output:
|
|
83
84
|
|
|
84
85
|
```markdown
|
|
85
|
-
| Line Item
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
86
|
+
| Line Item | 2025A | 2026E | 2027E |
|
|
87
|
+
| :------------------- | ---------: | ---------: | ---------: |
|
|
88
|
+
| Beginning Balance | $100.0 | $110.0 | $122.0 |
|
|
89
|
+
| Capital Expenditures | $25.0 | $30.0 | $35.0 |
|
|
90
|
+
| Depreciation | ($15.0) | ($18.0) | ($22.0) |
|
|
91
|
+
| **Ending Balance** | **$110.0** | **$122.0** | **$135.0** |
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
## Sample deliverables
|
|
@@ -4,7 +4,9 @@ use super::error::ResolveError;
|
|
|
4
4
|
use crate::compiler::ir::cell::{CellGrid, CellPayload};
|
|
5
5
|
use crate::compiler::ir::id::CellId;
|
|
6
6
|
use crate::compiler::ir::symbol::{SymbolTable, TableMetadata};
|
|
7
|
-
use crate::expr::ast::{
|
|
7
|
+
use crate::expr::ast::{
|
|
8
|
+
ColEndpoint, ColSelector, Expr, RelativeOffset, RowEndpoint, RowSelector, TableRef,
|
|
9
|
+
};
|
|
8
10
|
use crate::model::id::{ColId, RowId};
|
|
9
11
|
|
|
10
12
|
/// Recursively collects all table references present within a formula expression AST.
|
|
@@ -32,6 +34,70 @@ fn collect_refs_recursive<'a>(expr: &'a Expr, out: &mut Vec<&'a TableRef>) {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/// Resolves an explicit row key to its 0-based index and canonical [`RowId`] in `target_table.rows`.
|
|
38
|
+
fn resolve_row_key<'a>(
|
|
39
|
+
origin: &CellId,
|
|
40
|
+
target_table: &'a TableMetadata,
|
|
41
|
+
row_id: &RowId,
|
|
42
|
+
) -> Result<(usize, &'a RowId), ResolveError> {
|
|
43
|
+
target_table.rows.get_full(row_id).ok_or_else(|| {
|
|
44
|
+
ResolveError::unknown_row(origin, target_table.table_id.clone(), row_id.clone())
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Resolves a relative row offset (`row.prev`, `row.next`) to its 0-based index and [`RowId`] in `target_table.rows`.
|
|
49
|
+
fn resolve_row_offset<'a>(
|
|
50
|
+
origin: &CellId,
|
|
51
|
+
target_table: &'a TableMetadata,
|
|
52
|
+
origin_table: &TableMetadata,
|
|
53
|
+
offset: RelativeOffset,
|
|
54
|
+
) -> Result<(usize, &'a RowId), ResolveError> {
|
|
55
|
+
let origin_idx = origin_table.rows.get_index_of(&origin.row).ok_or_else(|| {
|
|
56
|
+
ResolveError::unknown_row(origin, origin_table.table_id.clone(), origin.row.clone())
|
|
57
|
+
})?;
|
|
58
|
+
|
|
59
|
+
let signed_offset = match offset {
|
|
60
|
+
RelativeOffset::Prev(k) => -(k as i32),
|
|
61
|
+
RelativeOffset::Next(k) => k as i32,
|
|
62
|
+
};
|
|
63
|
+
let target_idx = match offset {
|
|
64
|
+
RelativeOffset::Prev(k) => origin_idx
|
|
65
|
+
.checked_sub(k as usize)
|
|
66
|
+
.ok_or_else(|| ResolveError::temporal_offset_out_of_bounds(origin, signed_offset))?,
|
|
67
|
+
RelativeOffset::Next(k) => origin_idx
|
|
68
|
+
.checked_add(k as usize)
|
|
69
|
+
.ok_or_else(|| ResolveError::temporal_offset_out_of_bounds(origin, signed_offset))?,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let target_row_id = origin_table
|
|
73
|
+
.rows
|
|
74
|
+
.get_index(target_idx)
|
|
75
|
+
.ok_or_else(|| ResolveError::temporal_offset_out_of_bounds(origin, signed_offset))?;
|
|
76
|
+
resolve_row_key(origin, target_table, target_row_id)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Resolves a single row endpoint to its 0-based index and canonical [`RowId`] in `target_table.rows`.
|
|
80
|
+
fn resolve_row_endpoint<'a>(
|
|
81
|
+
origin: &CellId,
|
|
82
|
+
target_table: &'a TableMetadata,
|
|
83
|
+
origin_table: &TableMetadata,
|
|
84
|
+
endpoint: &RowEndpoint,
|
|
85
|
+
) -> Result<(usize, &'a RowId), ResolveError> {
|
|
86
|
+
match endpoint {
|
|
87
|
+
RowEndpoint::Active => target_table.rows.get_full(&origin.row).ok_or_else(|| {
|
|
88
|
+
ResolveError::active_row_mismatch(
|
|
89
|
+
origin,
|
|
90
|
+
target_table.table_id.clone(),
|
|
91
|
+
origin.row.clone(),
|
|
92
|
+
)
|
|
93
|
+
}),
|
|
94
|
+
RowEndpoint::Key(row_id) => resolve_row_key(origin, target_table, row_id),
|
|
95
|
+
RowEndpoint::Offset(offset) => {
|
|
96
|
+
resolve_row_offset(origin, target_table, origin_table, *offset)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
35
101
|
/// Resolves a row selector against target and origin table metadata.
|
|
36
102
|
pub(crate) fn resolve_row_selector(
|
|
37
103
|
origin: &CellId,
|
|
@@ -41,110 +107,134 @@ pub(crate) fn resolve_row_selector(
|
|
|
41
107
|
) -> Result<Vec<RowId>, ResolveError> {
|
|
42
108
|
match row_selector {
|
|
43
109
|
RowSelector::Active => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
target_table.table_id.clone(),
|
|
48
|
-
origin.row.clone(),
|
|
49
|
-
));
|
|
50
|
-
}
|
|
51
|
-
Ok(vec![origin.row.clone()])
|
|
110
|
+
let (_, id) =
|
|
111
|
+
resolve_row_endpoint(origin, target_table, origin_table, &RowEndpoint::Active)?;
|
|
112
|
+
Ok(vec![id.clone()])
|
|
52
113
|
}
|
|
53
114
|
RowSelector::Key(row_id) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
Ok(vec![row_id.clone()])
|
|
115
|
+
let (_, id) = resolve_row_key(origin, target_table, row_id)?;
|
|
116
|
+
Ok(vec![id.clone()])
|
|
117
|
+
}
|
|
118
|
+
RowSelector::Offset(offset) => {
|
|
119
|
+
let (_, id) = resolve_row_offset(origin, target_table, origin_table, *offset)?;
|
|
120
|
+
Ok(vec![id.clone()])
|
|
62
121
|
}
|
|
63
122
|
RowSelector::Range(start, end) => {
|
|
64
|
-
let start_idx
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let end_idx = target_table.rows.get_index_of(end).ok_or_else(|| {
|
|
68
|
-
ResolveError::unknown_row(origin, target_table.table_id.clone(), end.clone())
|
|
69
|
-
})?;
|
|
123
|
+
let (start_idx, start_id) =
|
|
124
|
+
resolve_row_endpoint(origin, target_table, origin_table, start)?;
|
|
125
|
+
let (end_idx, end_id) = resolve_row_endpoint(origin, target_table, origin_table, end)?;
|
|
70
126
|
|
|
71
127
|
if start_idx > end_idx {
|
|
72
128
|
return Err(ResolveError::inverted_range(
|
|
73
129
|
origin,
|
|
74
130
|
target_table.table_id.clone(),
|
|
75
|
-
|
|
76
|
-
|
|
131
|
+
start_id.to_string(),
|
|
132
|
+
end_id.to_string(),
|
|
77
133
|
));
|
|
78
134
|
}
|
|
79
135
|
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
RowSelector::Offset(offset) => match offset {
|
|
86
|
-
RelativeOffset::Prev(k) => {
|
|
87
|
-
let origin_idx = origin_table.rows.get_index_of(&origin.row).ok_or_else(|| {
|
|
88
|
-
ResolveError::unknown_row(
|
|
136
|
+
let slice = target_table
|
|
137
|
+
.rows
|
|
138
|
+
.get_range(start_idx..=end_idx)
|
|
139
|
+
.ok_or_else(|| {
|
|
140
|
+
ResolveError::inverted_range(
|
|
89
141
|
origin,
|
|
90
|
-
|
|
91
|
-
|
|
142
|
+
target_table.table_id.clone(),
|
|
143
|
+
start_id.to_string(),
|
|
144
|
+
end_id.to_string(),
|
|
92
145
|
)
|
|
93
146
|
})?;
|
|
94
147
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-(*k as i32),
|
|
100
|
-
));
|
|
101
|
-
}
|
|
148
|
+
Ok(slice.iter().cloned().collect())
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
102
152
|
|
|
103
|
-
|
|
104
|
-
|
|
153
|
+
/// Resolves an explicit column key to its 0-based index and canonical [`ColId`] in `target_table.columns`.
|
|
154
|
+
fn resolve_col_key<'a>(
|
|
155
|
+
origin: &CellId,
|
|
156
|
+
target_table: &'a TableMetadata,
|
|
157
|
+
col_id: &ColId,
|
|
158
|
+
) -> Result<(usize, &'a ColId), ResolveError> {
|
|
159
|
+
let idx = target_table
|
|
160
|
+
.column_index
|
|
161
|
+
.get(col_id)
|
|
162
|
+
.copied()
|
|
163
|
+
.ok_or_else(|| {
|
|
164
|
+
ResolveError::unknown_column(origin, target_table.table_id.clone(), col_id.clone())
|
|
165
|
+
})?;
|
|
166
|
+
let col = target_table.columns.get(idx).ok_or_else(|| {
|
|
167
|
+
ResolveError::unknown_column(origin, target_table.table_id.clone(), col_id.clone())
|
|
168
|
+
})?;
|
|
169
|
+
Ok((idx, &col.id))
|
|
170
|
+
}
|
|
105
171
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
172
|
+
/// Resolves a relative column offset (`col.prev`, `col.next`) to its 0-based index and [`ColId`] in `target_table.columns`.
|
|
173
|
+
fn resolve_col_offset<'a>(
|
|
174
|
+
origin: &CellId,
|
|
175
|
+
target_table: &'a TableMetadata,
|
|
176
|
+
origin_table: &TableMetadata,
|
|
177
|
+
offset: RelativeOffset,
|
|
178
|
+
) -> Result<(usize, &'a ColId), ResolveError> {
|
|
179
|
+
let signed_offset = match offset {
|
|
180
|
+
RelativeOffset::Prev(k) => -(k as i32),
|
|
181
|
+
RelativeOffset::Next(k) => k as i32,
|
|
182
|
+
};
|
|
183
|
+
let origin_measure_idx = origin_table
|
|
184
|
+
.measure_columns
|
|
185
|
+
.iter()
|
|
186
|
+
.position(|c| c == &origin.col)
|
|
187
|
+
.ok_or_else(|| ResolveError::temporal_offset_out_of_bounds(origin, signed_offset))?;
|
|
188
|
+
|
|
189
|
+
let target_measure_idx = match offset {
|
|
190
|
+
RelativeOffset::Prev(k) => origin_measure_idx
|
|
191
|
+
.checked_sub(k as usize)
|
|
192
|
+
.ok_or_else(|| ResolveError::temporal_offset_out_of_bounds(origin, signed_offset))?,
|
|
193
|
+
RelativeOffset::Next(k) => origin_measure_idx
|
|
194
|
+
.checked_add(k as usize)
|
|
195
|
+
.ok_or_else(|| ResolveError::temporal_offset_out_of_bounds(origin, signed_offset))?,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let target_col_id = origin_table
|
|
199
|
+
.measure_columns
|
|
200
|
+
.get(target_measure_idx)
|
|
201
|
+
.ok_or_else(|| ResolveError::temporal_offset_out_of_bounds(origin, signed_offset))?;
|
|
202
|
+
resolve_col_key(origin, target_table, target_col_id)
|
|
203
|
+
}
|
|
113
204
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
205
|
+
/// Resolves a single column endpoint to its 0-based index and canonical [`ColId`] in `target_table.columns`.
|
|
206
|
+
fn resolve_col_endpoint<'a>(
|
|
207
|
+
origin: &CellId,
|
|
208
|
+
target_table: &'a TableMetadata,
|
|
209
|
+
origin_table: &TableMetadata,
|
|
210
|
+
endpoint: &ColEndpoint,
|
|
211
|
+
) -> Result<(usize, &'a ColId), ResolveError> {
|
|
212
|
+
match endpoint {
|
|
213
|
+
ColEndpoint::Active => {
|
|
214
|
+
let idx = target_table
|
|
215
|
+
.column_index
|
|
216
|
+
.get(&origin.col)
|
|
217
|
+
.copied()
|
|
218
|
+
.ok_or_else(|| {
|
|
219
|
+
ResolveError::active_column_mismatch(
|
|
119
220
|
origin,
|
|
120
|
-
|
|
121
|
-
origin.
|
|
221
|
+
target_table.table_id.clone(),
|
|
222
|
+
origin.col.clone(),
|
|
122
223
|
)
|
|
123
224
|
})?;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if !target_table.rows.contains(&target_row_id) {
|
|
138
|
-
return Err(ResolveError::unknown_row(
|
|
139
|
-
origin,
|
|
140
|
-
target_table.table_id.clone(),
|
|
141
|
-
target_row_id,
|
|
142
|
-
));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
Ok(vec![target_row_id])
|
|
146
|
-
}
|
|
147
|
-
},
|
|
225
|
+
let col = target_table.columns.get(idx).ok_or_else(|| {
|
|
226
|
+
ResolveError::active_column_mismatch(
|
|
227
|
+
origin,
|
|
228
|
+
target_table.table_id.clone(),
|
|
229
|
+
origin.col.clone(),
|
|
230
|
+
)
|
|
231
|
+
})?;
|
|
232
|
+
Ok((idx, &col.id))
|
|
233
|
+
}
|
|
234
|
+
ColEndpoint::Key(col_id) => resolve_col_key(origin, target_table, col_id),
|
|
235
|
+
ColEndpoint::Offset(offset) => {
|
|
236
|
+
resolve_col_offset(origin, target_table, origin_table, *offset)
|
|
237
|
+
}
|
|
148
238
|
}
|
|
149
239
|
}
|
|
150
240
|
|
|
@@ -157,119 +247,46 @@ pub(crate) fn resolve_col_selector(
|
|
|
157
247
|
) -> Result<Vec<ColId>, ResolveError> {
|
|
158
248
|
match col_selector {
|
|
159
249
|
ColSelector::Active => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
target_table.table_id.clone(),
|
|
164
|
-
origin.col.clone(),
|
|
165
|
-
));
|
|
166
|
-
}
|
|
167
|
-
Ok(vec![origin.col.clone()])
|
|
250
|
+
let (_, id) =
|
|
251
|
+
resolve_col_endpoint(origin, target_table, origin_table, &ColEndpoint::Active)?;
|
|
252
|
+
Ok(vec![id.clone()])
|
|
168
253
|
}
|
|
169
254
|
ColSelector::Key(col_id) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
Ok(vec![col_id.clone()])
|
|
255
|
+
let (_, id) = resolve_col_key(origin, target_table, col_id)?;
|
|
256
|
+
Ok(vec![id.clone()])
|
|
257
|
+
}
|
|
258
|
+
ColSelector::Offset(offset) => {
|
|
259
|
+
let (_, id) = resolve_col_offset(origin, target_table, origin_table, *offset)?;
|
|
260
|
+
Ok(vec![id.clone()])
|
|
178
261
|
}
|
|
179
262
|
ColSelector::Range(start, end) => {
|
|
180
|
-
let start_idx =
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.copied()
|
|
184
|
-
.ok_or_else(|| {
|
|
185
|
-
ResolveError::unknown_column(
|
|
186
|
-
origin,
|
|
187
|
-
target_table.table_id.clone(),
|
|
188
|
-
start.clone(),
|
|
189
|
-
)
|
|
190
|
-
})?;
|
|
191
|
-
let end_idx = target_table.column_index.get(end).copied().ok_or_else(|| {
|
|
192
|
-
ResolveError::unknown_column(origin, target_table.table_id.clone(), end.clone())
|
|
193
|
-
})?;
|
|
263
|
+
let (start_idx, start_id) =
|
|
264
|
+
resolve_col_endpoint(origin, target_table, origin_table, start)?;
|
|
265
|
+
let (end_idx, end_id) = resolve_col_endpoint(origin, target_table, origin_table, end)?;
|
|
194
266
|
|
|
195
267
|
if start_idx > end_idx {
|
|
196
268
|
return Err(ResolveError::inverted_range(
|
|
197
269
|
origin,
|
|
198
270
|
target_table.table_id.clone(),
|
|
199
|
-
|
|
200
|
-
|
|
271
|
+
start_id.to_string(),
|
|
272
|
+
end_id.to_string(),
|
|
201
273
|
));
|
|
202
274
|
}
|
|
203
275
|
|
|
204
|
-
let
|
|
205
|
-
.
|
|
206
|
-
.
|
|
207
|
-
.
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
ColSelector::Offset(offset) => match offset {
|
|
211
|
-
RelativeOffset::Prev(k) => {
|
|
212
|
-
let origin_measure_idx = origin_table
|
|
213
|
-
.measure_columns
|
|
214
|
-
.iter()
|
|
215
|
-
.position(|c| c == &origin.col)
|
|
216
|
-
.ok_or_else(|| {
|
|
217
|
-
ResolveError::temporal_offset_out_of_bounds(origin, -(*k as i32))
|
|
218
|
-
})?;
|
|
219
|
-
|
|
220
|
-
let k_usize = *k as usize;
|
|
221
|
-
if origin_measure_idx < k_usize {
|
|
222
|
-
return Err(ResolveError::temporal_offset_out_of_bounds(
|
|
223
|
-
origin,
|
|
224
|
-
-(*k as i32),
|
|
225
|
-
));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
let target_measure_idx = origin_measure_idx - k_usize;
|
|
229
|
-
let target_col_id = origin_table.measure_columns[target_measure_idx].clone();
|
|
230
|
-
|
|
231
|
-
if !target_table.column_index.contains_key(&target_col_id) {
|
|
232
|
-
return Err(ResolveError::unknown_column(
|
|
233
|
-
origin,
|
|
234
|
-
target_table.table_id.clone(),
|
|
235
|
-
target_col_id,
|
|
236
|
-
));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
Ok(vec![target_col_id])
|
|
240
|
-
}
|
|
241
|
-
RelativeOffset::Next(k) => {
|
|
242
|
-
let origin_measure_idx = origin_table
|
|
243
|
-
.measure_columns
|
|
244
|
-
.iter()
|
|
245
|
-
.position(|c| c == &origin.col)
|
|
246
|
-
.ok_or_else(|| {
|
|
247
|
-
ResolveError::temporal_offset_out_of_bounds(origin, *k as i32)
|
|
248
|
-
})?;
|
|
249
|
-
|
|
250
|
-
let k_usize = *k as usize;
|
|
251
|
-
let target_measure_idx = match origin_measure_idx.checked_add(k_usize) {
|
|
252
|
-
Some(idx) if idx < origin_table.measure_columns.len() => idx,
|
|
253
|
-
_ => {
|
|
254
|
-
return Err(ResolveError::temporal_offset_out_of_bounds(
|
|
255
|
-
origin, *k as i32,
|
|
256
|
-
));
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
let target_col_id = origin_table.measure_columns[target_measure_idx].clone();
|
|
261
|
-
|
|
262
|
-
if !target_table.column_index.contains_key(&target_col_id) {
|
|
263
|
-
return Err(ResolveError::unknown_column(
|
|
276
|
+
let slice = target_table
|
|
277
|
+
.columns
|
|
278
|
+
.get(start_idx..=end_idx)
|
|
279
|
+
.ok_or_else(|| {
|
|
280
|
+
ResolveError::inverted_range(
|
|
264
281
|
origin,
|
|
265
282
|
target_table.table_id.clone(),
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
283
|
+
start_id.to_string(),
|
|
284
|
+
end_id.to_string(),
|
|
285
|
+
)
|
|
286
|
+
})?;
|
|
269
287
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
},
|
|
288
|
+
Ok(slice.iter().map(|c| c.id.clone()).collect())
|
|
289
|
+
}
|
|
273
290
|
}
|
|
274
291
|
}
|
|
275
292
|
|
|
@@ -425,6 +442,7 @@ mod tests {
|
|
|
425
442
|
subtitle: None,
|
|
426
443
|
currency: None,
|
|
427
444
|
collapsible: false,
|
|
445
|
+
placement: crate::model::table::PlacementSpec::default(),
|
|
428
446
|
columns: vec![ColumnSpec::new("2024E")],
|
|
429
447
|
rows: vec![RowSpec::new("rev")],
|
|
430
448
|
};
|