Skip to content

Commit 436f1bf

Browse files
committed
Add exact-cell Serde mapping with .NET parity
1 parent 14ad5ec commit 436f1bf

14 files changed

Lines changed: 469 additions & 8 deletions

File tree

.github/workflows/rust.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,37 @@ jobs:
354354
--filter "FullyQualifiedName~MiniExcelTemplateAsyncTests.TemplateAsyncBasicTest|FullyQualifiedName~MiniExcelTemplateAsyncTests.SaveAsByTemplateAsync_TakeCancel_Throws_TaskCanceledException|FullyQualifiedName~MiniExcelTemplateAsyncTests.TestIEnumerableConditional|FullyQualifiedName~MiniExcelTemplateTests.GroupTemplateTest|FullyQualifiedName~MiniExcelTemplateAsyncTests.TestIEnumerableGrouped|FullyQualifiedName~MiniExcelTemplateAsyncTests.TestIEnumerableWithFormulas|FullyQualifiedName~CalcChainTests|FullyQualifiedName~CalcChainAsyncTests"
355355
--verbosity minimal
356356
357+
cell-mapping-parity:
358+
strategy:
359+
matrix:
360+
os: [ubuntu-latest, windows-latest]
361+
runs-on: ${{ matrix.os }}
362+
steps:
363+
- uses: actions/checkout@v4
364+
- name: Checkout MiniExcel .NET
365+
uses: actions/checkout@v4
366+
with:
367+
repository: mini-software/MiniExcel
368+
ref: b9a76d7af62142e0e38545b6905b01a06e8d160e
369+
path: dotnet-source
370+
- name: Install Rust 1.85.0
371+
uses: dtolnay/rust-toolchain@1.85.0
372+
- uses: Swatinem/rust-cache@v2
373+
with:
374+
key: cell-mapping-parity-${{ matrix.os }}
375+
- name: Setup .NET
376+
uses: actions/setup-dotnet@v4
377+
with:
378+
dotnet-version: 10.0.x
379+
- name: Run Rust exact-cell mapping tests
380+
run: cargo +1.85.0 test -p miniexcel --test cell_mapping --locked
381+
- name: Run scoped MiniExcel .NET FluentMapping tests
382+
run: >-
383+
dotnet test dotnet-source/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj
384+
--framework net10.0
385+
--filter "FullyQualifiedName~MiniExcelMappingTests.MappingReader_ReadBasicData_Success|FullyQualifiedName~MiniExcelMappingTests.Mapping_WithComplexCellAddresses_ShouldMapCorrectly"
386+
--verbosity minimal
387+
357388
msrv:
358389
runs-on: ubuntu-latest
359390
steps:

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,29 @@ let rows = MiniExcel::query_as::<Release>("book.xlsx")?
334334

335335
Serde `rename`, `alias`, `default`, `skip`, and `Option` semantics are supported. MiniExcel-specific column-index attributes are not supported.
336336

337+
### Exact Cell Mapping
338+
339+
Use `CellMap` when one object is stored in fixed, non-tabular worksheet cells:
340+
341+
```rust
342+
use miniexcel::{CellMap, MiniExcel};
343+
344+
let mapping = CellMap::new()
345+
.with_sheet_name("Profile")
346+
.with_cell("name", "B3".parse()?)
347+
.with_cell("age", "F2".parse()?)
348+
.with_cell("joined", "D9".parse()?);
349+
350+
let profile: Profile = MiniExcel::read_mapped_as("book.xlsx", &mapping)?;
351+
```
352+
353+
Field names are Serde input names and bindings retain insertion order. Path, byte, and borrowed
354+
reader APIs share the same behavior. Duplicate fields/cells and empty maps fail before workbook
355+
I/O. Missing cells deserialize as empty values, so `Option` and Serde defaults work naturally.
356+
Formula cells expose their cached value to the mapped object; use structured reads when formula or
357+
number-format provenance is required. This Rust-native API replaces the useful exact-cell subset of
358+
.NET FluentMapping without reflection or class-map registries.
359+
337360
## Dynamic Writing
338361

339362
```rust

README.zh-CN.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,29 @@ let rows = MiniExcel::query_as::<Release>("book.xlsx")?
329329

330330
支持 Serde 的 `rename``alias``default``skip``Option` 语义。暂不支持 MiniExcel 专用的列索引 Attribute。
331331

332+
### 精确 Cell Mapping
333+
334+
当一个对象存储在固定、非表格化 worksheet cell 中时,可使用 `CellMap`
335+
336+
```rust
337+
use miniexcel::{CellMap, MiniExcel};
338+
339+
let mapping = CellMap::new()
340+
.with_sheet_name("Profile")
341+
.with_cell("name", "B3".parse()?)
342+
.with_cell("age", "F2".parse()?)
343+
.with_cell("joined", "D9".parse()?);
344+
345+
let profile: Profile = MiniExcel::read_mapped_as("book.xlsx", &mapping)?;
346+
```
347+
348+
Field name 是 Serde input name,binding 保持插入顺序。Path、byte 和 borrowed reader API
349+
行为一致。Duplicate field/cell 与空 map 会在 workbook I/O 前失败。缺失 cell 按 empty value
350+
反序列化,因此可自然使用 `Option` 与 Serde default。Formula cell 会把 cached value 提供给
351+
mapped object;需要 formula 或 number-format provenance 时应使用 structured read。该
352+
Rust-native API 覆盖 .NET FluentMapping 中有实际价值的 exact-cell 子集,不引入 reflection
353+
或 class-map registry。
354+
332355
## 动态写入
333356

334357
```rust

docs/compatibility.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The latest `calamine 0.36` and `rust_xlsxwriter 0.97` require Rust 1.88. The MVP
4444
| `GetSheetDimensions` | `MiniExcel::get_sheet_dimensions()` | Returns used ranges in workbook order with 1-based indices |
4545
| `GetColumns` | `MiniExcel::get_columns()` | Returns selected dynamic keys or an empty vector |
4646
| `QueryTable` | `query_table()` / `query_table_as()` / byte and borrowed-reader variants | Case-insensitive table-name lookup, metadata headers, and inclusive table bounds |
47+
| Exact-cell object mapping | `CellMap` / `read_mapped_as*()` | Ordered Serde field-to-A1 bindings; path, bytes, and borrowed readers |
4748
| Retrieve comments and notes | `get_comments()` / bytes / borrowed-reader variants | Thread roots, replies, people, resolution/timestamps, and legacy notes |
4849
| `startCell` | `ReadOptions::with_start_cell()` | A1 start coordinate |
4950
| `IgnoreEmptyRows` | `ReadOptions::with_ignore_empty_rows()` | Defaults to `false` for MiniExcel compatibility |
@@ -172,6 +173,7 @@ The contract covers only the current common surface: dynamic/typed path queries,
172173
| .NET surface | Rust status | Shared contract |
173174
| --- | --- | --- |
174175
| Dynamic and typed XLSX query | Implemented | Yes |
176+
| Exact-cell Serde object mapping | Implemented | Rust path/bytes/borrowed parity tests; scoped .NET FluentMapping baseline |
175177
| `QueryRange` with A1 coordinates | Implemented | Yes |
176178
| `GetSheetNames` and `GetColumns` | Implemented | Yes |
177179
| `GetSheetInformations` ID/index/name/type/visibility/active | Implemented | Rust tests against .NET fixtures |

docs/compatibility.zh-CN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Rust MVP 在统一的 `MiniExcel` facade 后实现最小但实用的 MiniExcel
4444
| `GetSheetDimensions` | `MiniExcel::get_sheet_dimensions()` | 按 workbook 顺序返回使用范围,index 从 1 开始 |
4545
| `GetColumns` | `MiniExcel::get_columns()` | 返回选中的动态 key,或空 vector |
4646
| `QueryTable` | `query_table()` / `query_table_as()` / byte 和 borrowed-reader variants | 大小写不敏感 table-name lookup、metadata header 与包含端点的 table bounds |
47+
| Exact-cell object mapping | `CellMap` / `read_mapped_as*()` | 有序 Serde field-to-A1 binding;path、byte 与 borrowed reader |
4748
| 读取 comments 与 notes | `get_comments()` / bytes / borrowed-reader variants | Thread root、reply、person、resolved/timestamp 与 legacy note |
4849
| `startCell` | `ReadOptions::with_start_cell()` | A1 起始坐标 |
4950
| `IgnoreEmptyRows` | `ReadOptions::with_ignore_empty_rows()` | 为兼容 MiniExcel,默认值为 `false` |
@@ -172,6 +173,7 @@ Rust workflow 会在 Linux 和 Windows 上运行 Rust 契约。其 .NET parity j
172173
| .NET 接口 | Rust 状态 | 共享契约 |
173174
| --- | --- | --- |
174175
| 动态和类型化 XLSX query | 已实现 ||
176+
| Exact-cell Serde object mapping | 已实现 | Rust path/byte/borrowed parity 测试;scoped .NET FluentMapping 基准 |
175177
| 使用 A1 坐标的 `QueryRange` | 已实现 ||
176178
| `GetSheetNames``GetColumns` | 已实现 ||
177179
| `GetSheetInformations` ID/index/name/type/visibility/active | 已实现 | Rust 使用 .NET fixture 测试 |

docs/dotnet-feature-gaps.md

Lines changed: 4 additions & 4 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 `f3c9847` (`0.3.0`) |
11+
| MiniExcel-Rust | working tree based on `14ad5ec` (`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).
@@ -39,7 +39,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
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. |
42-
| Fluent mapping | Missing | Address-based object mapping, formula/format mappings, collection start cells and spacing, nested collections, and mapped import/export/template APIs. |
42+
| Fluent mapping | Partial | Rust-native `CellMap` supports ordered exact-cell Serde reads from paths, bytes, and borrowed readers. Collection start cells/spacing, nested collections, and mapped export/template APIs remain unsupported. Formula/format provenance remains available through structured reads rather than mapping metadata. |
4343
| Attribute-based mapping | Partial | Column index/name attributes, localized headers, formula metadata, custom dynamic formatters, field mapping, and dynamic column ordering/filtering remain. Serde covers rename, alias, defaults, skips, options, and custom serializers; `WriteOptions` covers width/hidden layout by final header name. |
4444
| Read configuration | Partial | Culture-aware conversion, buffer/fast modes, and some null/empty-string behavior. Merged-cell filling and shared-string disk caching are implemented. |
4545
| Write configuration and style | Partial | OOXML tables, shared versus inline strings, and broader cell styling remain. Rust exposes default/minimal cell style modes, header output/style, AutoFilter, right-to-left views, frozen rows/columns, bounded AutoWidth, body wrapping/alignment, and number formats. |
@@ -63,7 +63,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
6363
| Multi-sheet and workbook edits | Writer creates multiple sheets; existing workbooks support append, strict replacement, every independent `AlterSheet` metadata operation, and source-to-destination `CopyAndAddSheet` with atomic package preservation | `OpenXmlExporter.InsertSheetAsync`, `CopyAndAddSheetAsync`, `AlterSheetAsync`; exporter, `MultipleSheets/`, and `AlterSheets/` tests |
6464
| Templates/pictures/merges | Basic template fill implemented; advanced directives and authoring remain deferred | `src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs`; `tests/MiniExcel.OpenXml.Tests/Templates/` |
6565
| CSV/conversion | `MiniExcel::query_csv*`, `save_csv*`, and `append_csv*`; Rust tests use exact `TestHeader.csv` (`6C2FC27FCA2876F1ECCA17061B8EE23E133ECDB726F8E0B84167E58D86234432`) and GB2312 (`BA8A2505AB271D5575C58CC1FCBE5A5002CEB9E2F43CB95412246E25A50E8B5A`) fixtures | `src/MiniExcel.Csv/Api/`; `src/MiniExcel/MiniExcelConverter.cs`; `tests/MiniExcel.Csv.Tests/` |
66-
| Mapping | Serde mapping only | `src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs`; `src/MiniExcel.OpenXml.FluentMapping/`; mapping tests |
66+
| Mapping | Serde row mapping plus explicit `CellMap` exact-cell object reads | `src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs`; `src/MiniExcel.OpenXml.FluentMapping/`; scoped basic/complex-address tests |
6767
| Comments | `MiniExcel::get_comments*`; Rust focused tests use `TestCommentsAndNotes.xlsx` (SHA-256 `3A855CE896ED62DC27C91797432DD89EE081F07CD03AB05BF1B0CD745543A3FC`) | `OpenXmlImporter.RetrieveCommentsAsync`; `src/MiniExcel.OpenXml/Models/Comments.cs`; comment tests |
6868
| Configuration/style | Narrow `ReadOptions` and `WriteOptions` | `MiniExcelBaseConfiguration`, `OpenXmlConfiguration`, `OpenXmlStyleOptions`; exporter tests |
6969

@@ -72,7 +72,7 @@ The .NET APIs marked with `Async` also have generated synchronous counterparts t
7272
## Suggested Implementation Order
7373

7474
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.
75-
2. **Advanced templates and Fluent Mapping**: add formula/merge-aware groups, richer conditional expressions, parametrized sheets, and mapping through separate compatibility milestones.
75+
2. **Advanced templates and collection mapping**: add formula/merge-aware groups, richer conditional expressions, parametrized sheets, and deterministic collection layouts through separate compatibility milestones.
7676
3. **Selected-sheet cloning**: not part of .NET `CopyAndAddSheet`; add only for a concrete Rust use case with a relationship-closure cloning contract.
7777

7878
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.

docs/dotnet-feature-gaps.zh-CN.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
| 项目 | 版本基线 |
1010
| --- | --- |
11-
| MiniExcel-Rust | 基于 `f3c9847` 的工作区(`0.3.0`|
11+
| MiniExcel-Rust | 基于 `14ad5ec` 的工作区(`0.3.0`|
1212
| MiniExcel .NET | `b9a76d7af62142e0e38545b6905b01a06e8d160e` |
1313

1414
比对依据包括同级目录 `../MiniExcel` 中的 .NET 公开 API、控制实现和聚焦测试,以及 Rust 的公开 `MiniExcel` 门面、选项、集成测试和[兼容性边界](compatibility.zh-CN.md)
@@ -39,7 +39,7 @@ Rust 已支持动态及 Serde 强类型 XLSX 路径查询、闭区间 A1 范围
3939
| 图片与合并处理 | 未实现 | 添加锚定图片,以及通过模板 API 合并相邻相同单元格。结构化读取不等于具备写入能力。 |
4040
| CSV | 已实现 | 动态/Serde path、byte、borrowed query/save API;column discovery;推断/显式 schema append;delimiter/newline/encoding/BOM/null/quoting 配置,以及 `query-csv` CLI。DataReader/DataTable 由 Rust iterator 替代;未暴露 async/progress API 和一步式 CSV/XLSX converter。 |
4141
| 批注与注释 | 已实现 | Path/bytes/borrowed API 返回 threaded root、reply、未解析 person ID、person/provider/user ID、resolved state、typed timestamp 与 legacy note。 |
42-
| Fluent Mapping | 未实现 | 基于地址的对象映射、公式/格式映射、集合起始单元格与间距、嵌套集合,以及映射式导入/导出/模板 API。 |
42+
| Fluent Mapping | 部分实现 | Rust-native `CellMap` 支持从 path、byte 和 borrowed reader 执行有序 exact-cell Serde 读取。Collection start cell/spacing、nested collection 与 mapped export/template API 仍不支持。Formula/format provenance 继续通过 structured read 提供,而不是 mapping metadata|
4343
| 特性式字段映射 | 部分实现 | 仍缺列索引/名称特性、本地化表头、公式元数据、自定义动态格式器、字段映射,以及动态列排序/过滤。Serde 可覆盖重命名、别名、默认值、跳过、可选值和自定义序列化;`WriteOptions` 可按最终 header name 配置 width/hidden layout。 |
4444
| 读取配置 | 部分实现 | 区域文化感知转换、缓冲/快速模式,以及部分 null/空字符串行为。合并单元格填充和 shared-string 磁盘 cache 已实现。 |
4545
| 写入配置与样式 | 部分实现 | 仍缺 OOXML table、共享字符串与内联字符串选择,以及更广泛的单元格样式。Rust 已暴露默认/最小 cell style 模式、header output/style、AutoFilter、从右到左 view、冻结行列、有界 AutoWidth、body 换行/对齐和数字格式。 |
@@ -63,7 +63,7 @@ Rust 已支持动态及 Serde 强类型 XLSX 路径查询、闭区间 A1 范围
6363
| 多工作表与工作簿修改 | Writer 可创建多个工作表;现有 workbook 支持 append、严格 replacement、全部独立 `AlterSheet` metadata 操作,以及 source-to-destination `CopyAndAddSheet`,并提供原子 package preservation | `OpenXmlExporter.InsertSheetAsync``CopyAndAddSheetAsync``AlterSheetAsync`;exporter、`MultipleSheets/``AlterSheets/` 测试 |
6464
| 模板/图片/合并 | 已实现基础模板填充;高级指令与 authoring 仍延期 | `src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs``tests/MiniExcel.OpenXml.Tests/Templates/` |
6565
| CSV/转换 | `MiniExcel::query_csv*``save_csv*``append_csv*`;Rust 测试使用完全相同的 `TestHeader.csv``6C2FC27FCA2876F1ECCA17061B8EE23E133ECDB726F8E0B84167E58D86234432`)和 GB2312(`BA8A2505AB271D5575C58CC1FCBE5A5002CEB9E2F43CB95412246E25A50E8B5A`)fixture | `src/MiniExcel.Csv/Api/``src/MiniExcel/MiniExcelConverter.cs``tests/MiniExcel.Csv.Tests/` |
66-
| 映射 | Serde 映射 | `src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs``src/MiniExcel.OpenXml.FluentMapping/`映射测试 |
66+
| 映射 | Serde row mapping 加显式 `CellMap` exact-cell object read | `src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs``src/MiniExcel.OpenXml.FluentMapping/`scoped basic/complex-address 测试 |
6767
| 批注 | `MiniExcel::get_comments*`;Rust focused test 使用 `TestCommentsAndNotes.xlsx`(SHA-256 `3A855CE896ED62DC27C91797432DD89EE081F07CD03AB05BF1B0CD745543A3FC`| `OpenXmlImporter.RetrieveCommentsAsync``src/MiniExcel.OpenXml/Models/Comments.cs`;comment test |
6868
| 配置/样式 | 较窄的 `ReadOptions``WriteOptions` | `MiniExcelBaseConfiguration``OpenXmlConfiguration``OpenXmlStyleOptions`;导出测试 |
6969

@@ -72,7 +72,7 @@ Rust 已支持动态及 Serde 强类型 XLSX 路径查询、闭区间 A1 范围
7272
## 建议实现顺序
7373

7474
1. **更广泛 async export/stream API**:把 runtime-neutral cancellation 扩展到类型化/推断 producer 和 borrowed stream,不把 blocking ZIP 工作描述成 async I/O。
75-
2. **高级模板与 Fluent Mapping**:通过独立兼容里程碑增加 formula/merge-aware group、更丰富 conditional expression、参数化 sheet 和 mapping
75+
2. **高级模板与 collection mapping**:通过独立兼容里程碑增加 formula/merge-aware group、更丰富 conditional expression、参数化 sheet 和确定性 collection layout
7676
3. **选定 worksheet cloning**:不属于 .NET `CopyAndAddSheet`;仅在有具体 Rust 使用场景时,通过 relationship closure cloning contract 增加。
7777

7878
DataReader/DataTable 属于 .NET 生态抽象,明确不作为逐字 Rust parity 要求。只有出现具体集成需求时,才应设计 Rust-native record-batch 或 table adapter。

miniexcel/src/error.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ enum ErrorKind {
5151
#[error("invalid comments for worksheet '{sheet}': {reason}")]
5252
InvalidComments { sheet: String, reason: String },
5353

54+
#[error("invalid cell mapping: {0}")]
55+
InvalidCellMap(String),
56+
57+
#[error("failed to deserialize mapped cells from worksheet '{sheet}' ({mapping}): {source}")]
58+
MappedDeserialize {
59+
sheet: String,
60+
mapping: String,
61+
#[source]
62+
source: calamine::DeError,
63+
},
64+
5465
#[error("the workbook does not contain any worksheets")]
5566
NoWorksheets,
5667

@@ -178,6 +189,18 @@ impl Error {
178189
ErrorKind::InvalidComments { sheet: sheet.into(), reason: reason.into() }.into()
179190
}
180191

192+
pub(crate) fn invalid_cell_map(message: impl Into<String>) -> Self {
193+
ErrorKind::InvalidCellMap(message.into()).into()
194+
}
195+
196+
pub(crate) fn mapped_deserialize(
197+
sheet: impl Into<String>,
198+
mapping: impl Into<String>,
199+
source: calamine::DeError,
200+
) -> Self {
201+
ErrorKind::MappedDeserialize { sheet: sheet.into(), mapping: mapping.into(), source }.into()
202+
}
203+
181204
pub(crate) fn no_worksheets() -> Self {
182205
ErrorKind::NoWorksheets.into()
183206
}

0 commit comments

Comments
 (0)