Skip to content

Commit f3c9847

Browse files
committed
Add enumerable grouped template blocks with .NET parity
1 parent 6c74aa3 commit f3c9847

10 files changed

Lines changed: 426 additions & 20 deletions

File tree

‎.github/workflows/rust.yml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,14 @@ jobs:
344344
with:
345345
dotnet-version: 10.0.x
346346
- name: Run Rust async template tests
347-
run: cargo +1.85.0 test -p miniexcel --test template_async --features async --locked
347+
run: |
348+
cargo +1.85.0 test -p miniexcel --test template --locked
349+
cargo +1.85.0 test -p miniexcel --test template_async --features async --locked
348350
- name: Run scoped MiniExcel .NET async template tests
349351
run: >-
350352
dotnet test dotnet-source/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj
351353
--framework net10.0
352-
--filter "FullyQualifiedName~MiniExcelTemplateAsyncTests.TemplateAsyncBasicTest|FullyQualifiedName~MiniExcelTemplateAsyncTests.SaveAsByTemplateAsync_TakeCancel_Throws_TaskCanceledException|FullyQualifiedName~MiniExcelTemplateAsyncTests.TestIEnumerableConditional"
354+
--filter "FullyQualifiedName~MiniExcelTemplateAsyncTests.TemplateAsyncBasicTest|FullyQualifiedName~MiniExcelTemplateAsyncTests.SaveAsByTemplateAsync_TakeCancel_Throws_TaskCanceledException|FullyQualifiedName~MiniExcelTemplateAsyncTests.TestIEnumerableConditional|FullyQualifiedName~MiniExcelTemplateTests.GroupTemplateTest|FullyQualifiedName~MiniExcelTemplateAsyncTests.TestIEnumerableGrouped"
353355
--verbosity minimal
354356
355357
msrv:

‎README.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,14 +598,29 @@ markers and bodies each occupy one line; every item still emits one row. Malform
598598
missing fields return template errors. Nested blocks, logical expressions, and conditional formula
599599
branches are not supported.
600600

601+
Multirow blocks can be repeated for each array item with exact marker rows:
602+
603+
```text
604+
@group
605+
@header{{items.name}}
606+
{{items.name}} | {{items.department}}
607+
@endgroup
608+
```
609+
610+
The marker rows are removed. Every item repeats the rows between them in source order, while an
611+
`@header{{root.field}}` row is skipped only when its rendered key equals the immediately preceding
612+
header key. Group bodies may use the conditional blocks above and retain row/cell styles. Rust
613+
rejects empty arrays, nested/unmatched groups, multiple array roots, formulas, and merged cells in
614+
grouped worksheets instead of emitting ambiguous or invalid references.
615+
601616
With the optional `async` feature, `save_as_template_async()` and
602617
`save_as_template_async_with_cancellation()` run blocking template ZIP/XML work on a worker thread
603618
and atomically publish the validated path output. Pre-cancellation, rendering errors, dropped
604619
futures, and cancellation before commit preserve an existing destination byte-for-byte or leave a
605620
missing destination absent. This does not make ZIP/filesystem operations asynchronous and retains
606621
the current in-memory template rendering model.
607622

608-
Version 1 does not implement `@group`, parametrized sheet cloning, `$=` formula templates, or formula recalculation.
623+
Version 1 does not implement parametrized sheet cloning, `$=` formula templates, or formula recalculation.
609624

610625
## Important Semantics
611626

‎README.zh-CN.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,13 +586,28 @@ Top {{items.name}}
586586
和正文各占一行;每个 item 仍输出一行。Malformed block 与缺失 field 会返回 template error。
587587
Nested block、逻辑表达式和 conditional formula branch 尚不支持。
588588

589+
可使用精确 marker row,让 multirow block 按 array item 重复:
590+
591+
```text
592+
@group
593+
@header{{items.name}}
594+
{{items.name}} | {{items.department}}
595+
@endgroup
596+
```
597+
598+
Marker row 会被移除。每个 item 按源顺序重复 marker 之间的 row;仅当
599+
`@header{{root.field}}` 的 rendered key 与紧邻的前一个 header key 相同时,才跳过该 header
600+
row。Group body 可使用上述 conditional block,并保留 row/cell style。Rust 会拒绝空 array、
601+
nested/unmatched group、多 array root、formula 和 grouped worksheet 中的 merged cell,避免
602+
生成含歧义或无效 reference 的 workbook。
603+
589604
启用可选 `async` feature 后,`save_as_template_async()` 与
590605
`save_as_template_async_with_cancellation()` 会在 worker thread 执行 blocking template
591606
ZIP/XML 工作,并原子发布通过验证的 path output。Pre-cancellation、render error、drop future
592607
以及 commit 前 cancellation 会保留已有 destination 的原始字节,或让缺失 destination 继续
593608
不存在。该 API 不会把 ZIP/filesystem 操作变成 async,并保留当前模板内存渲染模型。
594609

595-
版本 1 尚不支持 `@group`、参数化 sheet 克隆、`$=` 公式模板或公式重算。
610+
版本 1 尚不支持参数化 sheet 克隆、`$=` 公式模板或公式重算。
596611

597612
## 重要语义
598613

‎docs/compatibility.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ The contract covers only the current common surface: dynamic/typed path queries,
182182
| New-workbook `SaveAs`, including multiple sheets | Implemented and roundtrip-tested | Not yet |
183183
| Basic `SaveAsTemplate` scalar/list fill | Implemented and roundtrip-tested | Not yet |
184184
| Enumerable-cell conditional template blocks | Implemented for string/number/bool comparisons | Rust sync/async tests; scoped .NET conditional test |
185+
| Enumerable grouped template blocks | Implemented for JSON arrays with adjacent header suppression | Rust sync/async tests; scoped .NET sync/async group tests |
185186
| Byte-array query/write for WASM | Implemented | Rust/browser tests |
186187
| Versioned grouped analytics | Rust research extension | No |
187188
| Addressed JSONL/Markdown/manifest RAG export | Rust research extension | No |
@@ -203,4 +204,4 @@ This matrix is the coverage claim: Rust does not yet provide complete API parity
203204

204205
## Deferred Work
205206

206-
SQL text parsing, `HAVING`, `ORDER BY`, joins, windows, pivots, disk-spill aggregation, vector indexing, model calls, old Excel formats, template grouping, nested/logical conditions, formula templates and sheet cloning, image authoring, merged-cell APIs, formula calculation/dependency expansion, formula authoring, general styling, async template streams, inferred/typed async export sources, async borrowed readers/writers, and borrowed XLSX lazy readers require separate design and acceptance milestones. CSV DataReader/DataTable adapters are intentionally replaced by Rust iterators, and a one-call CSV/XLSX converter is not exposed; callers compose query and save APIs. See the [Insert migration guide](insert-v1-migration.md) for supported workflows and deliberate differences.
207+
SQL text parsing, `HAVING`, `ORDER BY`, joins, windows, pivots, disk-spill aggregation, vector indexing, model calls, old Excel formats, nested/logical template conditions, grouped formulas/merges, formula templates and sheet cloning, image authoring, merged-cell APIs, formula calculation/dependency expansion, formula authoring, general styling, async template streams, inferred/typed async export sources, async borrowed readers/writers, and borrowed XLSX lazy readers require separate design and acceptance milestones. CSV DataReader/DataTable adapters are intentionally replaced by Rust iterators, and a one-call CSV/XLSX converter is not exposed; callers compose query and save APIs. See the [Insert migration guide](insert-v1-migration.md) for supported workflows and deliberate differences.

‎docs/compatibility.zh-CN.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Rust workflow 会在 Linux 和 Windows 上运行 Rust 契约。其 .NET parity j
182182
| 新 workbook `SaveAs`(含多工作表) | 已实现并完成 roundtrip 测试 | 尚未 |
183183
| 基础 `SaveAsTemplate` 标量/列表填充 | 已实现并完成 roundtrip 测试 | 尚未 |
184184
| Enumerable-cell conditional template block | 已支持 string/number/bool 比较 | Rust sync/async 测试;scoped .NET conditional test |
185+
| Enumerable grouped template block | 已支持 JSON array 与相邻 header suppression | Rust sync/async 测试;scoped .NET sync/async group 测试 |
185186
| 用于 WASM 的字节数组 query/write | 已实现 | Rust/browser 测试 |
186187
| 版本化分组分析 | Rust 研究扩展 | 否 |
187188
| 带地址 JSONL/Markdown/manifest RAG 导出 | Rust 研究扩展 | 否 |
@@ -203,4 +204,4 @@ Rust workflow 会在 Linux 和 Windows 上运行 Rust 契约。其 .NET parity j
203204

204205
## 延后工作
205206

206-
SQL 文本解析、`HAVING`、`ORDER BY`、join、window、pivot、磁盘 spill 聚合、向量索引、模型调用、旧 Excel 格式、template grouping、nested/logical condition、formula template 与 sheet cloning、image authoring、merged-cell API、公式计算/依赖展开、公式编写、通用 style、async template stream、推断/类型化 async export source、async borrowed reader/writer,以及 borrowed XLSX lazy reader,都需要独立的设计与验收里程碑。CSV DataReader/DataTable adapter 已有意替换为 Rust iterator;当前不提供一步式 CSV/XLSX converter,调用方可组合 query 与 save API。支持工作流与有意保留的差异见 [Insert 迁移说明](insert-v1-migration.zh-CN.md)。
207+
SQL 文本解析、`HAVING`、`ORDER BY`、join、window、pivot、磁盘 spill 聚合、向量索引、模型调用、旧 Excel 格式、nested/logical template condition、grouped formula/merge、formula template 与 sheet cloning、image authoring、merged-cell API、公式计算/依赖展开、公式编写、通用 style、async template stream、推断/类型化 async export source、async borrowed reader/writer,以及 borrowed XLSX lazy reader,都需要独立的设计与验收里程碑。CSV DataReader/DataTable adapter 已有意替换为 Rust iterator;当前不提供一步式 CSV/XLSX converter,调用方可组合 query 与 save API。支持工作流与有意保留的差异见 [Insert 迁移说明](insert-v1-migration.zh-CN.md)。

‎docs/dotnet-feature-gaps.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This report compares observable public capabilities in the local checkouts below
88

99
| Project | Revision |
1010
| --- | --- |
11-
| MiniExcel-Rust | working tree based on `6a7ea00` (`0.3.0`) |
11+
| MiniExcel-Rust | working tree based on `6c74aa3` (`0.3.0`) |
1212
| MiniExcel .NET | `b9a76d7af62142e0e38545b6905b01a06e8d160e` |
1313

1414
The comparison uses the .NET public APIs, their controlling implementations, and focused tests under the sibling `../MiniExcel` checkout. Rust status is based on the public `MiniExcel` facade, options, integration tests, and [compatibility boundary](compatibility.md).
@@ -35,7 +35,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
3535
| General save inputs | Partial | Export from general objects/enumerables, dictionaries, `DataTable`, `IDataReader`, and async enumerables, with progress. Rust accepts dynamic or same-type Serde slices and reports per-sheet row counts. |
3636
| Multi-sheet export | Partial | Rust creates ordered visible, hidden, and very-hidden worksheets, but does not yet accept heterogeneous Serde row types in one call. |
3737
| Existing-workbook operations | Implemented | Rust atomically appends, strictly replaces, renames, changes visibility, reorders, and performs .NET-style source-workbook copy-and-add while preserving unrelated package parts and worksheet identity. Rename preserves formula text; visibility rejects hiding the final visible sheet; reorder remaps active/view/local-name indices; copy-and-add preserves the source and atomically publishes a separate destination. |
38-
| Templates | Partial | Rust fills path/byte templates with scalars, arrays, and enumerable-cell conditional blocks while preserving package parts; path output also has a cancellable async wrapper. Streams, grouping, nested/logical conditions, parametrized sheets, `$=` formulas, formula-reference updates, and calculation-chain handling remain unsupported. |
38+
| Templates | Partial | Rust fills path/byte templates with scalars, arrays, conditional blocks, and validated multirow groups with adjacent header suppression; path output also has a cancellable async wrapper. Streams, nested/logical conditions, grouped formulas/merges, parametrized sheets, `$=` formulas, formula-reference updates, and calculation-chain handling remain unsupported. |
3939
| Pictures and merge processing | Missing | Add anchored pictures and merge adjacent identical cells through the templater surface. Structured reads do not provide authoring parity. |
4040
| CSV | Implemented | Dynamic/Serde path, byte, and borrowed query/save APIs; column discovery; inferred/explicit-schema append; delimiter/newline/encoding/BOM/null/quoting configuration; and `query-csv` CLI. DataReader/DataTable are replaced by Rust iterators. Async/progress APIs and a one-call CSV/XLSX converter are not exposed. |
4141
| Comments and notes | Implemented | Path/bytes/borrowed APIs return threaded roots, replies, unresolved person IDs, people/provider/user IDs, resolution state, typed timestamps, and legacy notes. |
@@ -55,6 +55,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
5555
| Async export | `MiniExcel::save_as_with_schema_async*`; Rust focused rollback/cancellation/cleanup tests | `OpenXmlExporter.ExportAsync`; async-enumerable exporter tests |
5656
| Async template | `MiniExcel::save_as_template_async*`; Rust focused rollback/cancellation/cleanup tests | `OpenXmlTemplater.SaveAsByTemplateAsync`; scoped basic/cancellation tests |
5757
| Template conditions | Enumerable-cell `@if`/`@elseif`/`@else` blocks; Rust sync/async branch/error/style tests | `TestIEnumerableConditional` |
58+
| Template groups | `@group`/`@header`/`@endgroup` multirow blocks; Rust sync/async order/error/style tests | `GroupTemplateTest`; `TestIEnumerableGrouped` |
5859
| Tables | `MiniExcel::query_table*`; Rust focused tests use the exact `TestQueryTable.xlsx` fixture (SHA-256 `04F719BF9F9E99D9B437A8FB32F8111FD92580A1D29ACAD10B6ED128C0564501`) | `OpenXmlImporter.QueryTableAsync`; `tests/MiniExcel.OpenXml.Tests/Tables/` |
5960
| Comments | `MiniExcel::get_comments*`; Rust focused tests use `TestCommentsAndNotes.xlsx` (SHA-256 `3A855CE896ED62DC27C91797432DD89EE081F07CD03AB05BF1B0CD745543A3FC`) | `OpenXmlImporter.RetrieveCommentsAsync`; `tests/MiniExcel.OpenXml.Tests/Comments/` |
6061
| DataReader/DataTable | Rust iterators and borrowed visitors are the native abstraction; no literal .NET tabular adapter is planned | `OpenXmlImporter.GetDataReader`, `GetAsyncDataReader`, `QueryAsDataTableAsync`; `tests/MiniExcel.OpenXml.Tests/DataReader/` |
@@ -70,7 +71,7 @@ The .NET APIs marked with `Async` also have generated synchronous counterparts t
7071
## Suggested Implementation Order
7172

7273
1. **Broader async export/stream APIs**: extend runtime-neutral cancellation to typed/inferred producers and borrowed streams without presenting blocking ZIP work as async I/O.
73-
2. **Advanced templates and Fluent Mapping**: add grouped templates, richer conditional expressions, parametrized sheets, and mapping through separate compatibility milestones.
74+
2. **Advanced templates and Fluent Mapping**: add formula/merge-aware groups, richer conditional expressions, parametrized sheets, and mapping through separate compatibility milestones.
7475
3. **Selected-sheet cloning**: not part of .NET `CopyAndAddSheet`; add only for a concrete Rust use case with a relationship-closure cloning contract.
7576

7677
DataReader/DataTable are .NET ecosystem abstractions and are intentionally not literal Rust parity requirements. A Rust-native record-batch or tabular adapter is appropriate only when a concrete integration requires it.

0 commit comments

Comments
 (0)