Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Chsword.Excel2Object.Tests/Chsword.Excel2Object.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
<ProjectReference Include="..\Chsword.Excel2Object\Chsword.Excel2Object.csproj" />
</ItemGroup>

<!-- 流式导入的用例要拆看 .xlsx 里的部件,用到 ZipArchive;.NET Framework 下该程序集不在默认引用之列 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
</ItemGroup>

<!-- 命令行工具只发布 net8.0,.NET Framework 无法引用,故该目标框架下不含其测试 -->
<ItemGroup Condition="'$(TargetFramework)' != 'net472'">
<ProjectReference Include="..\Chsword.Excel2Object.Cli\Chsword.Excel2Object.Cli.csproj" />
Expand Down
379 changes: 379 additions & 0 deletions Chsword.Excel2Object.Tests/StreamingImportTest.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Chsword.Excel2Object/Chsword.Excel2Object.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Product>Chsword.Excel2Object Library</Product>
<Authors>Zou Jian</Authors>
<Version>2.10.0</Version>
<Version>2.11.0</Version>
<Copyright>Copyright ? 2014-2025</Copyright>
<PackageProjectUrl>https://github.com/chsword/Excel2Object/</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
8 changes: 8 additions & 0 deletions Chsword.Excel2Object/ExcelHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ public static void ObjectToExcel<TModel>(IEnumerable<TModel> data, string path,
return excelExporter.ObjectToExcelBytes(data, optionsAction);
}

/// <inheritdoc cref="ExcelImporter.ExcelStreamToObject{TModel}(Stream, Action{ExcelImporterOptions})" />
public static IEnumerable<TModel> ExcelStreamToObject<TModel>(Stream input,
Action<ExcelImporterOptions>? optionAction = null)
where TModel : class, new()
{
return new ExcelImporter().ExcelStreamToObject<TModel>(input, optionAction);
}

/// <inheritdoc cref="ExcelExporter.ObjectToExcelStream{TModel}(IEnumerable{TModel}, Stream, Action{ExcelExporterOptions})" />
public static void ObjectToExcelStream<TModel>(IEnumerable<TModel> data, Stream output,
Action<ExcelExporterOptions>? optionsAction = null)
Expand Down
381 changes: 215 additions & 166 deletions Chsword.Excel2Object/ExcelImporter.cs

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions Chsword.Excel2Object/Internal/CellData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Globalization;
using NPOI.SS.UserModel;

namespace Chsword.Excel2Object.Internal;

/// <summary>
/// 一格读到的东西,与它从哪里来无关:可以是 NPOI 的单元格,也可以是流式读取解出的一段 XML。
/// </summary>
/// <remarks>
/// 导入的类型转换原本直接写在 <see cref="ICell" /> 之上,而流式读取拿到的是原始的 XML,并没有
/// 单元格对象。把两者都归到这个结构上,转换那一套便只有一份。
/// </remarks>
internal readonly struct CellData
{
/// <summary>该格不存在——空单元格并不占位。</summary>
public static readonly CellData None = default;

/// <summary>
/// 这一格读取失败,且已上报。与「不存在」不同:不存在读作空白,读取失败则应取该类型的默认值,
/// 不再按空字符串继续转换,否则真正的原因会被转换时抛出的异常掩盖。
/// </summary>
public static readonly CellData Failure = new(true);

/// <summary>
/// 文本以可空字段存放而非自动属性:结构的默认值(不存在的那一格)本就没有文本,让它读作空串,
/// 各处便无须为此判空。
/// </summary>
private readonly string? _text;

private CellData(bool failed)
{
Failed = failed;
Exists = false;
Kind = CellType.Blank;
_text = null;
Number = 0;
Flag = false;
FormatIndex = 0;
Format = null;
Date1904 = false;
}

private CellData(CellType kind, string? text, double number, bool flag, short formatIndex, string? format,
bool date1904)
{
Failed = false;
Exists = true;
Kind = kind;
_text = text;
Number = number;
Flag = flag;
FormatIndex = formatIndex;
Format = format;
Date1904 = date1904;
}

public bool Exists { get; }

public bool Failed { get; }

public CellType Kind { get; }

/// <summary>文本格即其文本;数值、布尔与错误格则是其渲染出的样子。</summary>
public string Text => _text ?? string.Empty;

public double Number { get; }

public bool Flag { get; }

/// <summary>数字格式的序号与格式串,用于判断这一格是不是日期。</summary>
public short FormatIndex { get; }

public string? Format { get; }

/// <summary>该工作簿是否采用 1904 日期系统;序列号换算成日期时须据此而定。</summary>
public bool Date1904 { get; }

public static CellData Blank(short formatIndex = 0, string? format = null)
{
return new CellData(CellType.Blank, null, 0, false, formatIndex, format, false);
}

public static CellData OfNumber(double value, short formatIndex, string? format, bool date1904)
{
return new CellData(CellType.Numeric, null, value, false, formatIndex, format, date1904);
}

public static CellData OfText(string? text)
{
return new CellData(CellType.String, text, 0, false, 0, null, false);
}

public static CellData OfBoolean(bool value)
{
// 与 NPOI 的单元格渲染一致
return new CellData(CellType.Boolean, value ? "TRUE" : "FALSE", 0, value, 0, null, false);
}

public static CellData OfError(string? text)
{
return new CellData(CellType.Error, text, 0, false, 0, null, false);
}

/// <summary>数值格的文本表示;必须能原样解析回同一个 double,数值列正是经由这段文本转换的。</summary>
public string NumberText()
{
var text = Number.ToString("R", CultureInfo.InvariantCulture);
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var back) &&
back.Equals(Number))
return text;

// .NET Framework 的 "R" 对少数值无法往返(已知缺陷),此时退回 17 位有效数字
return Number.ToString("G17", CultureInfo.InvariantCulture);
}

/// <summary>该格作为日期的取值;序列号超出 Excel 日历时为 null。</summary>
public DateTime? Date()
{
if (Kind != CellType.Numeric || !DateUtil.IsValidExcelDate(Number)) return null;

return DateUtil.GetJavaDate(Number, Date1904);
}
}
3 changes: 2 additions & 1 deletion Chsword.Excel2Object/Internal/ImportContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public void Report(IRow? row, int columnIndex, Exception exception)
Report(row?.Sheet?.SheetName, row?.RowNum ?? -1, columnIndex, exception);
}

private void Report(string? sheetTitle, int rowIndex, int columnIndex, Exception exception)
/// <summary>位置由调用方给出:流式读取没有单元格对象,只有行列号。</summary>
public void Report(string? sheetTitle, int rowIndex, int columnIndex, Exception exception)
{
if (_options.OnCellError == null) return;

Expand Down
91 changes: 91 additions & 0 deletions Chsword.Excel2Object/Internal/ImportRow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using NPOI.SS.UserModel;

namespace Chsword.Excel2Object.Internal;

/// <summary>
/// 一行数据,与它从哪里来无关:整份读入内存的工作簿,或流式读取解出的一行。
/// </summary>
internal interface IImportRow
{
string? SheetTitle { get; }

int RowIndex { get; }

/// <summary>该行存在的各格,按列序号给出。</summary>
IEnumerable<KeyValuePair<int, CellData>> Cells { get; }

/// <summary>该行有几格。整行为空的行不参与导入,判断这一点不必把每格都取出来。</summary>
int CellCount { get; }

/// <summary>某一列上的那一格;不存在时为 <see cref="CellData.None" />。</summary>
CellData Cell(int columnIndex);
}

/// <summary>整份读入内存时的一行,取值经由 NPOI 的单元格。</summary>
internal sealed class NpoiRow : IImportRow
{
private readonly ImportContext _context;
private readonly bool _date1904;
private readonly IRow _row;

public NpoiRow(IRow row, ImportContext context, bool date1904)
{
_row = row;
_context = context;
_date1904 = date1904;
}

public string? SheetTitle => _row.Sheet?.SheetName;

public int RowIndex => _row.RowNum;

public int CellCount => _row.Cells.Count;

public IEnumerable<KeyValuePair<int, CellData>> Cells
{
get
{
foreach (var cell in _row.Cells)
yield return new KeyValuePair<int, CellData>(cell.ColumnIndex, Of(cell, _context, _date1904));
}
}

public CellData Cell(int columnIndex)
{
var cell = _row.GetCell(columnIndex);
return cell == null ? CellData.None : Of(cell, _context, _date1904);
}

/// <summary>
/// 把 NPOI 的单元格取成与来源无关的一格。公式在此求值:求值器按工作簿创建一次,其结果的类型
/// 与取值即为这一格的类型与取值。
/// </summary>
private static CellData Of(ICell cell, ImportContext context, bool date1904)
{
try
{
switch (cell.CellType)
{
case CellType.Numeric:
var style = cell.CellStyle;
return CellData.OfNumber(cell.NumericCellValue, style?.DataFormat ?? 0,
style?.GetDataFormatString(), date1904);
case CellType.String:
return CellData.OfText(cell.StringCellValue);
case CellType.Boolean:
return CellData.OfBoolean(cell.BooleanCellValue);
case CellType.Blank:
return CellData.Blank();
case CellType.Formula:
return Of(context.Evaluator(cell.Sheet.Workbook).EvaluateInCell(cell), context, date1904);
default:
return CellData.OfError(cell.ToString() ?? string.Empty);
}
}
catch (Exception e)
{
context.Report(cell.Sheet?.SheetName, cell.RowIndex, cell.ColumnIndex, e);
return CellData.Failure;
}
}
}
Loading