f8-engine 0.2.0__tar.gz → 0.3.1__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.1}/Cargo.lock +2 -2
- {f8_engine-0.2.0 → f8_engine-0.3.1}/Cargo.toml +3 -1
- {f8_engine-0.2.0 → f8_engine-0.3.1}/PKG-INFO +27 -25
- {f8_engine-0.2.0 → f8_engine-0.3.1}/README.md +26 -24
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/emission/workbook.rs +41 -6
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/eval/model.rs +1 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/graph/resolve.rs +199 -181
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/ir/expand.rs +9 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/ir/symbol.rs +1 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/layout/error.rs +19 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/layout/mod.rs +245 -63
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/lowering/address.rs +39 -7
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/lowering/mod.rs +30 -34
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/expr/ast.rs +170 -14
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/expr/mod.rs +2 -1
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/expr/parser.rs +114 -187
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/lib.rs +1 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/model/mod.rs +3 -2
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/model/table.rs +113 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/model/theme.rs +18 -7
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-python/src/error.rs +11 -1
- {f8_engine-0.2.0 → f8_engine-0.3.1}/pyproject.toml +4 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/python/f8/__init__.py +2 -1
- {f8_engine-0.2.0 → f8_engine-0.3.1}/python/f8/builder.py +204 -9
- f8_engine-0.3.1/python/f8/skills/f8-modeling/SKILL.md +45 -0
- f8_engine-0.3.1/python/f8/skills/f8-modeling/references/auditing-and-repair.md +170 -0
- f8_engine-0.3.1/python/f8/skills/f8-modeling/references/functions.md +128 -0
- f8_engine-0.3.1/python/f8/skills/f8-modeling/references/syntax-and-grid.md +324 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/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.1}/LICENSE +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/Cargo.toml +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/compiled/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/compiled/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/compiled/model.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/emission/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/emission/format.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/emission/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/eval/context.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/eval/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/eval/expr.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/eval/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/graph/cycle.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/graph/dependency.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/graph/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/graph/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/ir/cell.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/ir/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/ir/id.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/ir/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/layout/a1.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/lowering/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/compiler/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/expr/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/common.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/date/constructors.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/date/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/date/parts.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/date/serial.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/date/yearfrac.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/financial/annuity.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/financial/discounting.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/financial/irr.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/financial/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/logical/boolean.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/logical/branching.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/logical/error_handling.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/logical/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/lookup/index.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/lookup/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/lookup/search.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/lookup/xlookup.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/lookup/xmatch.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/math/aggregation.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/math/basic.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/math/conditional.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/math/criteria.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/math/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/math/product.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/math/rounding.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/registry.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/functions/traits.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/model/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/model/format.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/model/id.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/model/manifest.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/value/error.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/value/format.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-core/src/value/mod.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-python/Cargo.toml +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-python/src/lib.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/crates/f8-python/src/model.rs +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/python/f8/__main__.py +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/python/f8/_core.pyi +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/python/f8/audit.py +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/python/f8/cli.py +0 -0
- {f8_engine-0.2.0 → f8_engine-0.3.1}/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.1"
|
|
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.1"
|
|
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.1"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
description = "Financial modeling engine with compiler verification and live Excel formulas"
|
|
9
9
|
authors = ["Michael (Chencheng) Hu, CFA"]
|
|
@@ -25,3 +25,5 @@ pythonize = "0.24"
|
|
|
25
25
|
[workspace.lints.clippy]
|
|
26
26
|
unwrap_used = "deny"
|
|
27
27
|
expect_used = "deny"
|
|
28
|
+
wildcard_imports = "deny"
|
|
29
|
+
fn_params_excessive_bools = "warn"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: f8-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -12,8 +12,9 @@ use super::format::{CellFormatKey, FormatCache, parse_hex_color};
|
|
|
12
12
|
use crate::compiler::eval::EvaluatedModel;
|
|
13
13
|
use crate::compiler::ir::cell::{CellGrid, CellPayload};
|
|
14
14
|
use crate::compiler::ir::id::CellId;
|
|
15
|
-
use crate::compiler::layout::LayoutMap;
|
|
15
|
+
use crate::compiler::layout::{LayoutMap, StyleRole};
|
|
16
16
|
use crate::compiler::lowering::LoweredFormulaMap;
|
|
17
|
+
use crate::model::format::SemanticStyle;
|
|
17
18
|
use crate::model::id::SheetId;
|
|
18
19
|
use crate::model::manifest::ProjectManifest;
|
|
19
20
|
use crate::model::table::{ColumnSpec, RowSpec, TableSpec, ValueLiteral};
|
|
@@ -274,7 +275,15 @@ impl WorkbookEmitter {
|
|
|
274
275
|
let col_idx = (placement.start_col + c_idx as u32) as u16;
|
|
275
276
|
match col_spec {
|
|
276
277
|
ColumnSpec::Gutter => {
|
|
277
|
-
// Gutter column:
|
|
278
|
+
// Gutter column: continue header bottom border and fill across gutter
|
|
279
|
+
if is_bottom_tier || format_cache.has_col_header_fill() {
|
|
280
|
+
let fmt = if is_bottom_tier {
|
|
281
|
+
format_cache.col_header_format(false)
|
|
282
|
+
} else {
|
|
283
|
+
format_cache.col_header_upper_format(false)
|
|
284
|
+
};
|
|
285
|
+
worksheet.write_blank(tier_row, col_idx, &fmt)?;
|
|
286
|
+
}
|
|
278
287
|
}
|
|
279
288
|
ColumnSpec::Data(d) => {
|
|
280
289
|
if t == 0 && d.group.is_some() {
|
|
@@ -349,10 +358,36 @@ impl WorkbookEmitter {
|
|
|
349
358
|
}
|
|
350
359
|
}
|
|
351
360
|
RowSpec::Data(row_data) => {
|
|
352
|
-
for col_spec in
|
|
353
|
-
let
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
for (c_idx, col_spec) in table.columns.iter().enumerate() {
|
|
362
|
+
let col_data = match col_spec {
|
|
363
|
+
ColumnSpec::Data(d) => d,
|
|
364
|
+
ColumnSpec::Gutter => {
|
|
365
|
+
// Gutter column: continue row formatting (horizontal borders, shade, highlight)
|
|
366
|
+
let role = StyleRole {
|
|
367
|
+
horizontal: row_data
|
|
368
|
+
.style
|
|
369
|
+
.unwrap_or(SemanticStyle::Data),
|
|
370
|
+
vertical: SemanticStyle::Data,
|
|
371
|
+
is_highlight: row_data.highlight.unwrap_or(false),
|
|
372
|
+
shade: row_data.shade.filter(|s| !s.is_none()),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
if role != StyleRole::data() {
|
|
376
|
+
let format_key = CellFormatKey::new(
|
|
377
|
+
role, None, None, None, false, false, 0,
|
|
378
|
+
);
|
|
379
|
+
let cell_format =
|
|
380
|
+
format_cache.get_cell_format(&format_key)?;
|
|
381
|
+
let phys_col =
|
|
382
|
+
(placement.start_col + c_idx as u32) as u16;
|
|
383
|
+
worksheet.write_blank(
|
|
384
|
+
current_phys_row,
|
|
385
|
+
phys_col,
|
|
386
|
+
&cell_format,
|
|
387
|
+
)?;
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
356
391
|
};
|
|
357
392
|
|
|
358
393
|
let cell_id = CellId::new(
|