From 3b6edd116f046fc175bed25118805602f40966df Mon Sep 17 00:00:00 2001 From: GabrielMarquezMatte <61604367+GabrielMarquezMatte@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:29:36 +0000 Subject: [PATCH 1/7] chore: promote public API for v7.0.0 --- .../PublicAPI/PublicAPI.Shipped.txt | 23 +++ .../PublicAPI/PublicAPI.Unshipped.txt | 23 --- .../PublicAPI/PublicAPI.Shipped.txt | 191 ++++++++++++++++-- .../PublicAPI/PublicAPI.Unshipped.txt | 163 --------------- 4 files changed, 200 insertions(+), 200 deletions(-) diff --git a/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Shipped.txt b/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Shipped.txt index a10d55c4..2b6ce440 100644 --- a/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Shipped.txt +++ b/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Shipped.txt @@ -1,3 +1,26 @@ #nullable enable ExcelReader.Arrow.ArrowConversionExtensions +ExcelReader.Arrow.ArrowReadOptions +ExcelReader.Arrow.ArrowReadOptions.ArrowReadOptions() -> void +ExcelReader.Arrow.ArrowReadOptions.HeaderRow.get -> int +ExcelReader.Arrow.ArrowReadOptions.HeaderRow.init -> void +ExcelReader.Arrow.ArrowReadOptions.Schema.get -> System.Collections.Generic.IReadOnlyList? +ExcelReader.Arrow.ArrowReadOptions.Schema.init -> void ExcelReader.Arrow.ArrowWriteExtensions +ExcelReader.Arrow.ArrowWriteOptions +ExcelReader.Arrow.ArrowWriteOptions.ArrowWriteOptions() -> void +ExcelReader.Arrow.ArrowWriteOptions.SheetName.get -> string! +ExcelReader.Arrow.ArrowWriteOptions.SheetName.init -> void +ExcelReader.Arrow.ArrowWriteOptions.WriteHeader.get -> bool +ExcelReader.Arrow.ArrowWriteOptions.WriteHeader.init -> void +static ExcelReader.Arrow.ArrowConversionExtensions.ToArrowRecordBatch(this ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Arrow.ArrowReadOptions? options = null) -> Apache.Arrow.RecordBatch! +static ExcelReader.Arrow.ArrowReadOptions.Default.get -> ExcelReader.Arrow.ArrowReadOptions! +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Csv.CsvWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Xls.XlsWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Xlsb.XlsbWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Xlsx.XlsxWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Csv.CsvWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Xls.XlsWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Xlsb.XlsbWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Xlsx.XlsxWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Arrow.ArrowWriteOptions.Default.get -> ExcelReader.Arrow.ArrowWriteOptions! diff --git a/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Unshipped.txt b/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Unshipped.txt index 854fae10..7dc5c581 100644 --- a/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/ExcelReader.Arrow/PublicAPI/PublicAPI.Unshipped.txt @@ -1,24 +1 @@ #nullable enable -ExcelReader.Arrow.ArrowReadOptions -ExcelReader.Arrow.ArrowReadOptions.ArrowReadOptions() -> void -ExcelReader.Arrow.ArrowReadOptions.HeaderRow.get -> int -ExcelReader.Arrow.ArrowReadOptions.HeaderRow.init -> void -ExcelReader.Arrow.ArrowReadOptions.Schema.get -> System.Collections.Generic.IReadOnlyList? -ExcelReader.Arrow.ArrowReadOptions.Schema.init -> void -ExcelReader.Arrow.ArrowWriteOptions -ExcelReader.Arrow.ArrowWriteOptions.ArrowWriteOptions() -> void -ExcelReader.Arrow.ArrowWriteOptions.SheetName.get -> string! -ExcelReader.Arrow.ArrowWriteOptions.SheetName.init -> void -ExcelReader.Arrow.ArrowWriteOptions.WriteHeader.get -> bool -ExcelReader.Arrow.ArrowWriteOptions.WriteHeader.init -> void -static ExcelReader.Arrow.ArrowConversionExtensions.ToArrowRecordBatch(this ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Arrow.ArrowReadOptions? options = null) -> Apache.Arrow.RecordBatch! -static ExcelReader.Arrow.ArrowReadOptions.Default.get -> ExcelReader.Arrow.ArrowReadOptions! -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Csv.CsvWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Xls.XlsWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Xlsb.XlsbWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatch(this ExcelReader.Core.Writer.Xlsx.XlsxWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null) -> void -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Csv.CsvWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Xls.XlsWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Xlsb.XlsbWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Arrow.ArrowWriteExtensions.WriteRecordBatchAsync(this ExcelReader.Core.Writer.Xlsx.XlsxWorkbookWriter! workbook, Apache.Arrow.RecordBatch! batch, ExcelReader.Arrow.ArrowWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Arrow.ArrowWriteOptions.Default.get -> ExcelReader.Arrow.ArrowWriteOptions! diff --git a/src/ExcelReader.Core/PublicAPI/PublicAPI.Shipped.txt b/src/ExcelReader.Core/PublicAPI/PublicAPI.Shipped.txt index 64ceb60b..88dfd9a2 100644 --- a/src/ExcelReader.Core/PublicAPI/PublicAPI.Shipped.txt +++ b/src/ExcelReader.Core/PublicAPI/PublicAPI.Shipped.txt @@ -109,14 +109,6 @@ ExcelReader.Core.Reader.CellType.Empty = 0 -> ExcelReader.Core.Reader.CellType ExcelReader.Core.Reader.CellType.Error = 6 -> ExcelReader.Core.Reader.CellType ExcelReader.Core.Reader.CellType.ExcelString = 1 -> ExcelReader.Core.Reader.CellType ExcelReader.Core.Reader.CellType.Number = 2 -> ExcelReader.Core.Reader.CellType -ExcelReader.Core.Reader.RowAggregation -ExcelReader.Core.Reader.RowAggregation.Accumulate.get -> ExcelReader.Core.Reader.RowAction! -ExcelReader.Core.Reader.RowAggregation.Accumulate.init -> void -ExcelReader.Core.Reader.RowAggregation.Combine.get -> System.Func! -ExcelReader.Core.Reader.RowAggregation.Combine.init -> void -ExcelReader.Core.Reader.RowAggregation.RowAggregation() -> void -ExcelReader.Core.Reader.RowAggregation.Seed.get -> System.Func! -ExcelReader.Core.Reader.RowAggregation.Seed.init -> void ExcelReader.Core.Reader.Csv.CsvDialect ExcelReader.Core.Reader.Csv.CsvDialect.CsvDialect() -> void ExcelReader.Core.Reader.Csv.CsvDialect.Delimiter.get -> byte @@ -139,7 +131,9 @@ ExcelReader.Core.Reader.Csv.CsvReader.FirstSheet.get -> ExcelReader.Core.Reader. ExcelReader.Core.Reader.Csv.CsvReader.IsDate1904.get -> bool ExcelReader.Core.Reader.Csv.CsvReader.SheetCount.get -> int ExcelReader.Core.Reader.Csv.CsvReader.Sheets.get -> ExcelReader.Core.Reader.ExcelSheetList! +ExcelReader.Core.Reader.Csv.CsvReader.Tables.get -> System.Collections.Generic.IReadOnlyList! ExcelReader.Core.Reader.Csv.CsvReader.TryGetSheet(System.ReadOnlySpan name, out ExcelReader.Core.Reader.Csv.CsvSheet sheet) -> bool +ExcelReader.Core.Reader.Csv.CsvReader.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool ExcelReader.Core.Reader.Csv.CsvReaderOptions ExcelReader.Core.Reader.Csv.CsvReaderOptions.$() -> ExcelReader.Core.Reader.Csv.CsvReaderOptions! ExcelReader.Core.Reader.Csv.CsvReaderOptions.CsvReaderOptions() -> void @@ -155,7 +149,6 @@ ExcelReader.Core.Reader.Csv.CsvReaderOptions.Quote.init -> void ExcelReader.Core.Reader.Csv.CsvReaderOptions.SniffDialect.get -> bool ExcelReader.Core.Reader.Csv.CsvReaderOptions.SniffDialect.init -> void ExcelReader.Core.Reader.Csv.CsvReaderOptions.WithDialect(ExcelReader.Core.Reader.Csv.CsvDialect dialect) -> ExcelReader.Core.Reader.Csv.CsvReaderOptions! -ExcelReader.Core.Reader.RowAction ExcelReader.Core.Reader.Csv.CsvSheet ExcelReader.Core.Reader.Csv.CsvSheet.CsvSheet() -> void ExcelReader.Core.Reader.Csv.CsvSheet.Equals(ExcelReader.Core.Reader.Csv.CsvSheet other) -> bool @@ -176,11 +169,6 @@ ExcelReader.Core.Reader.Csv.CsvSnifferOptions.CsvSnifferOptions() -> void ExcelReader.Core.Reader.Csv.CsvSnifferOptions.Equals(ExcelReader.Core.Reader.Csv.CsvSnifferOptions? other) -> bool ExcelReader.Core.Reader.Csv.CsvSnifferOptions.MaxSampleLines.get -> int ExcelReader.Core.Reader.Csv.CsvSnifferOptions.MaxSampleLines.init -> void -ExcelReader.Core.Reader.IRowAccumulator -ExcelReader.Core.Reader.IRowAccumulator.Add(TModel model) -> void -ExcelReader.Core.Reader.IRowAccumulator.Merge(TSelf following) -> void -ExcelReader.Core.Reader.IRowRecord -ExcelReader.Core.Reader.IRowRecord.TryParse(ExcelReader.Core.Reader.Row row, out TSelf record) -> bool ExcelReader.Core.Reader.Excel ExcelReader.Core.Reader.ExcelEncryptionException ExcelReader.Core.Reader.ExcelEncryptionException.ExcelEncryptionException() -> void @@ -237,10 +225,25 @@ ExcelReader.Core.Reader.ExcelReaderOptions.PrefetchDecompression.get -> bool ExcelReader.Core.Reader.ExcelReaderOptions.PrefetchDecompression.init -> void ExcelReader.Core.Reader.ExcelReaderOptions.VerifyEncryptedIntegrity.get -> bool ExcelReader.Core.Reader.ExcelReaderOptions.VerifyEncryptedIntegrity.init -> void +ExcelReader.Core.Reader.ExcelReaderOptions.VerifyZipChecksums.get -> bool +ExcelReader.Core.Reader.ExcelReaderOptions.VerifyZipChecksums.init -> void ExcelReader.Core.Reader.ExcelSheetList ExcelReader.Core.Reader.ExcelSheetList.Count.get -> int ExcelReader.Core.Reader.ExcelSheetList.GetEnumerator() -> System.ArraySegment.Enumerator ExcelReader.Core.Reader.ExcelSheetList.this[int index].get -> TSheet +ExcelReader.Core.Reader.ExcelTable +ExcelReader.Core.Reader.ExcelTable.AsSheet() -> ExcelReader.Core.Reader.IExcelSheet! +ExcelReader.Core.Reader.ExcelTable.ColumnNames.get -> System.Collections.Generic.IReadOnlyList! +ExcelReader.Core.Reader.ExcelTable.FirstColumn.get -> int +ExcelReader.Core.Reader.ExcelTable.FirstRow.get -> int +ExcelReader.Core.Reader.ExcelTable.HeaderRowCount.get -> int +ExcelReader.Core.Reader.ExcelTable.LastColumn.get -> int +ExcelReader.Core.Reader.ExcelTable.LastRow.get -> int +ExcelReader.Core.Reader.ExcelTable.Name.get -> string! +ExcelReader.Core.Reader.ExcelTable.Ref.get -> string! +ExcelReader.Core.Reader.ExcelTable.Sheet.get -> ExcelReader.Core.Reader.IExcelSheet! +ExcelReader.Core.Reader.ExcelTable.StyleName.get -> string? +ExcelReader.Core.Reader.ExcelTable.TotalsRowCount.get -> int ExcelReader.Core.Reader.IExcelRowEnumerator ExcelReader.Core.Reader.IExcelRowEnumerator.Current.get -> ExcelReader.Core.Reader.Row ExcelReader.Core.Reader.IExcelRowEnumerator.MoveNext() -> bool @@ -258,7 +261,14 @@ ExcelReader.Core.Reader.IExcelWorkbook.FirstSheet.get -> ExcelReader.Core.Reader ExcelReader.Core.Reader.IExcelWorkbook.IsDate1904.get -> bool ExcelReader.Core.Reader.IExcelWorkbook.SheetAt(int index) -> ExcelReader.Core.Reader.IExcelSheet! ExcelReader.Core.Reader.IExcelWorkbook.SheetCount.get -> int +ExcelReader.Core.Reader.IExcelWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! ExcelReader.Core.Reader.IExcelWorkbook.TryGetSheet(System.ReadOnlySpan name, out ExcelReader.Core.Reader.IExcelSheet! sheet) -> bool +ExcelReader.Core.Reader.IExcelWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool +ExcelReader.Core.Reader.IRowAccumulator +ExcelReader.Core.Reader.IRowAccumulator.Add(TModel model) -> void +ExcelReader.Core.Reader.IRowAccumulator.Merge(TSelf following) -> void +ExcelReader.Core.Reader.IRowRecord +ExcelReader.Core.Reader.IRowRecord.TryParse(ExcelReader.Core.Reader.Row row, out TSelf record) -> bool ExcelReader.Core.Reader.PooledStreamRowEnumerator ExcelReader.Core.Reader.PooledStreamRowEnumerator.Dispose() -> void ExcelReader.Core.Reader.PooledStreamRowEnumerator.DisposeAsync() -> System.Threading.Tasks.ValueTask @@ -267,6 +277,15 @@ ExcelReader.Core.Reader.Row.Cells.get -> ExcelReader.Core.Reader.RowCellEnumerat ExcelReader.Core.Reader.Row.ColumnCount.get -> int ExcelReader.Core.Reader.Row.Row() -> void ExcelReader.Core.Reader.Row.this[int column].get -> ExcelReader.Core.Reader.Cell +ExcelReader.Core.Reader.RowAction +ExcelReader.Core.Reader.RowAggregation +ExcelReader.Core.Reader.RowAggregation.Accumulate.get -> ExcelReader.Core.Reader.RowAction! +ExcelReader.Core.Reader.RowAggregation.Accumulate.init -> void +ExcelReader.Core.Reader.RowAggregation.Combine.get -> System.Func! +ExcelReader.Core.Reader.RowAggregation.Combine.init -> void +ExcelReader.Core.Reader.RowAggregation.RowAggregation() -> void +ExcelReader.Core.Reader.RowAggregation.Seed.get -> System.Func! +ExcelReader.Core.Reader.RowAggregation.Seed.init -> void ExcelReader.Core.Reader.RowCell ExcelReader.Core.Reader.RowCell.ColumnIndex.get -> int ExcelReader.Core.Reader.RowCell.RowCell() -> void @@ -296,6 +315,60 @@ ExcelReader.Core.Reader.Schema.ExcelColumnType.Int64Column = 1 -> ExcelReader.Co ExcelReader.Core.Reader.Schema.ExcelColumnType.StringColumn = 0 -> ExcelReader.Core.Reader.Schema.ExcelColumnType ExcelReader.Core.Reader.Schema.ExcelColumnType.TimeColumn = 5 -> ExcelReader.Core.Reader.Schema.ExcelColumnType ExcelReader.Core.Reader.Schema.ExcelColumnType.TimestampColumn = 6 -> ExcelReader.Core.Reader.Schema.ExcelColumnType +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.HeaderRow.get -> int +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.HeaderRow.init -> void +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.ParseText.get -> bool +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.ParseText.init -> void +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.SampleSize.get -> int +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.SampleSize.init -> void +ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.SchemaInferenceOptions() -> void +ExcelReader.Core.Reader.SheetDataReader +ExcelReader.Core.Reader.SheetDataReader.Close() -> void +ExcelReader.Core.Reader.SheetDataReader.Depth.get -> int +ExcelReader.Core.Reader.SheetDataReader.Dispose() -> void +ExcelReader.Core.Reader.SheetDataReader.FieldCount.get -> int +ExcelReader.Core.Reader.SheetDataReader.GetBoolean(int i) -> bool +ExcelReader.Core.Reader.SheetDataReader.GetByte(int i) -> byte +ExcelReader.Core.Reader.SheetDataReader.GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) -> long +ExcelReader.Core.Reader.SheetDataReader.GetChar(int i) -> char +ExcelReader.Core.Reader.SheetDataReader.GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) -> long +ExcelReader.Core.Reader.SheetDataReader.GetData(int i) -> System.Data.IDataReader! +ExcelReader.Core.Reader.SheetDataReader.GetDataTypeName(int i) -> string! +ExcelReader.Core.Reader.SheetDataReader.GetDateTime(int i) -> System.DateTime +ExcelReader.Core.Reader.SheetDataReader.GetDecimal(int i) -> decimal +ExcelReader.Core.Reader.SheetDataReader.GetDouble(int i) -> double +ExcelReader.Core.Reader.SheetDataReader.GetFieldType(int i) -> System.Type! +ExcelReader.Core.Reader.SheetDataReader.GetFloat(int i) -> float +ExcelReader.Core.Reader.SheetDataReader.GetGuid(int i) -> System.Guid +ExcelReader.Core.Reader.SheetDataReader.GetInt16(int i) -> short +ExcelReader.Core.Reader.SheetDataReader.GetInt32(int i) -> int +ExcelReader.Core.Reader.SheetDataReader.GetInt64(int i) -> long +ExcelReader.Core.Reader.SheetDataReader.GetName(int i) -> string! +ExcelReader.Core.Reader.SheetDataReader.GetOrdinal(string! name) -> int +ExcelReader.Core.Reader.SheetDataReader.GetSchemaTable() -> System.Data.DataTable! +ExcelReader.Core.Reader.SheetDataReader.GetString(int i) -> string! +ExcelReader.Core.Reader.SheetDataReader.GetValue(int i) -> object! +ExcelReader.Core.Reader.SheetDataReader.GetValues(object![]! values) -> int +ExcelReader.Core.Reader.SheetDataReader.IsClosed.get -> bool +ExcelReader.Core.Reader.SheetDataReader.IsDBNull(int i) -> bool +ExcelReader.Core.Reader.SheetDataReader.NextResult() -> bool +ExcelReader.Core.Reader.SheetDataReader.Read() -> bool +ExcelReader.Core.Reader.SheetDataReader.RecordsAffected.get -> int +ExcelReader.Core.Reader.SheetDataReader.SheetDataReader(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.SheetDataReaderOptions? options = null) -> void +ExcelReader.Core.Reader.SheetDataReader.this[int i].get -> object! +ExcelReader.Core.Reader.SheetDataReader.this[string! name].get -> object! +ExcelReader.Core.Reader.SheetDataReaderOptions +ExcelReader.Core.Reader.SheetDataReaderOptions.HeaderRow.get -> int +ExcelReader.Core.Reader.SheetDataReaderOptions.HeaderRow.init -> void +ExcelReader.Core.Reader.SheetDataReaderOptions.SheetDataReaderOptions() -> void +ExcelReader.Core.Reader.SheetParallel +ExcelReader.Core.Reader.SheetParallelOptions +ExcelReader.Core.Reader.SheetParallelOptions.DegreeOfParallelism.get -> int +ExcelReader.Core.Reader.SheetParallelOptions.DegreeOfParallelism.init -> void +ExcelReader.Core.Reader.SheetParallelOptions.HeaderRow.get -> int +ExcelReader.Core.Reader.SheetParallelOptions.HeaderRow.init -> void +ExcelReader.Core.Reader.SheetParallelOptions.SheetParallelOptions() -> void ExcelReader.Core.Reader.Xls.XlsSheet ExcelReader.Core.Reader.Xls.XlsSheet.Equals(ExcelReader.Core.Reader.Xls.XlsSheet other) -> bool ExcelReader.Core.Reader.Xls.XlsSheet.GetAsyncEnumerator(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> ExcelReader.Core.Reader.Xls.XlsWorkbook.Enumerator! @@ -318,7 +391,9 @@ ExcelReader.Core.Reader.Xls.XlsWorkbook.FirstSheet.get -> ExcelReader.Core.Reade ExcelReader.Core.Reader.Xls.XlsWorkbook.IsDate1904.get -> bool ExcelReader.Core.Reader.Xls.XlsWorkbook.SheetCount.get -> int ExcelReader.Core.Reader.Xls.XlsWorkbook.Sheets.get -> ExcelReader.Core.Reader.ExcelSheetList! +ExcelReader.Core.Reader.Xls.XlsWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! ExcelReader.Core.Reader.Xls.XlsWorkbook.TryGetSheet(System.ReadOnlySpan name, out ExcelReader.Core.Reader.Xls.XlsSheet sheet) -> bool +ExcelReader.Core.Reader.Xls.XlsWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool ExcelReader.Core.Reader.Xlsb.XlsbSheet ExcelReader.Core.Reader.Xlsb.XlsbSheet.Equals(ExcelReader.Core.Reader.Xlsb.XlsbSheet other) -> bool ExcelReader.Core.Reader.Xlsb.XlsbSheet.GetAsyncEnumerator(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.Enumerator! @@ -339,7 +414,9 @@ ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.FirstSheet.get -> ExcelReader.Core.Rea ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.IsDate1904.get -> bool ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.SheetCount.get -> int ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.Sheets.get -> ExcelReader.Core.Reader.ExcelSheetList! +ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.TryGetSheet(System.ReadOnlySpan name, out ExcelReader.Core.Reader.Xlsb.XlsbSheet sheet) -> bool +ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool ExcelReader.Core.Reader.Xlsx.XlsxSheet ExcelReader.Core.Reader.Xlsx.XlsxSheet.Equals(ExcelReader.Core.Reader.Xlsx.XlsxSheet other) -> bool ExcelReader.Core.Reader.Xlsx.XlsxSheet.GetAsyncEnumerator(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.Enumerator! @@ -360,7 +437,9 @@ ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.FirstSheet.get -> ExcelReader.Core.Rea ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.IsDate1904.get -> bool ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.SheetCount.get -> int ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.Sheets.get -> ExcelReader.Core.Reader.ExcelSheetList! +ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.TryGetSheet(System.ReadOnlySpan name, out ExcelReader.Core.Reader.Xlsx.XlsxSheet sheet) -> bool +ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool ExcelReader.Core.Writer.CellStyle ExcelReader.Core.Writer.CellStyle.Bold.get -> bool ExcelReader.Core.Writer.CellStyle.Bold.init -> void @@ -429,6 +508,22 @@ ExcelReader.Core.Writer.ExcelRecordLayout ExcelReader.Core.Writer.ExcelRecordMapBuilder ExcelReader.Core.Writer.ExcelRecordMapBuilder.Column(string! header, System.Action! write) -> ExcelReader.Core.Writer.ExcelRecordMapBuilder! ExcelReader.Core.Writer.ExcelRecordMapBuilder.ExcelRecordMapBuilder() -> void +ExcelReader.Core.Writer.ExcelTableOptions +ExcelReader.Core.Writer.ExcelTableOptions.$() -> ExcelReader.Core.Writer.ExcelTableOptions! +ExcelReader.Core.Writer.ExcelTableOptions.Equals(ExcelReader.Core.Writer.ExcelTableOptions? other) -> bool +ExcelReader.Core.Writer.ExcelTableOptions.ExcelTableOptions() -> void +ExcelReader.Core.Writer.ExcelTableOptions.FirstColumn.get -> int +ExcelReader.Core.Writer.ExcelTableOptions.FirstColumn.init -> void +ExcelReader.Core.Writer.ExcelTableOptions.ShowColumnStripes.get -> bool +ExcelReader.Core.Writer.ExcelTableOptions.ShowColumnStripes.init -> void +ExcelReader.Core.Writer.ExcelTableOptions.ShowFirstColumn.get -> bool +ExcelReader.Core.Writer.ExcelTableOptions.ShowFirstColumn.init -> void +ExcelReader.Core.Writer.ExcelTableOptions.ShowLastColumn.get -> bool +ExcelReader.Core.Writer.ExcelTableOptions.ShowLastColumn.init -> void +ExcelReader.Core.Writer.ExcelTableOptions.ShowRowStripes.get -> bool +ExcelReader.Core.Writer.ExcelTableOptions.ShowRowStripes.init -> void +ExcelReader.Core.Writer.ExcelTableOptions.StyleName.get -> string? +ExcelReader.Core.Writer.ExcelTableOptions.StyleName.init -> void ExcelReader.Core.Writer.IExcelCellWriter ExcelReader.Core.Writer.IExcelCellWriter.Write(ExcelReader.Core.Writer.IRowWriter! row, T value) -> void ExcelReader.Core.Writer.IExcelRecordMap @@ -456,6 +551,10 @@ ExcelReader.Core.Writer.ISheetWriter.StartRow() -> TRow ExcelReader.Core.Writer.ISheetWriter.StartRow(int styleId) -> TRow ExcelReader.Core.Writer.ISheetWriter.StartRowAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask ExcelReader.Core.Writer.ISheetWriter.StartRowAsync(int styleId, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +ExcelReader.Core.Writer.ITableSheetWriter +ExcelReader.Core.Writer.ITableSheetWriter.BeginTable(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null) -> void +ExcelReader.Core.Writer.ITableSheetWriter.BeginTableAsync(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +ExcelReader.Core.Writer.ITableSheetWriter.EndTable() -> void ExcelReader.Core.Writer.IWorkbookWriter ExcelReader.Core.Writer.IWorkbookWriter.AddSheet(string! name) -> TSheet ExcelReader.Core.Writer.IWorkbookWriter.AddSheet(string! name, ExcelReader.Core.ExcelSheetVisibility visibility) -> TSheet @@ -464,6 +563,13 @@ ExcelReader.Core.Writer.IWorkbookWriter.End() -> void ExcelReader.Core.Writer.IWorkbookWriter.EndAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask ExcelReader.Core.Writer.IWorkbookWriter.Flush() -> void ExcelReader.Core.Writer.IWorkbookWriter.FlushAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +ExcelReader.Core.Writer.ParallelSheetWriterExtensions +ExcelReader.Core.Writer.ParallelWriteOptions +ExcelReader.Core.Writer.ParallelWriteOptions.BatchRows.get -> int +ExcelReader.Core.Writer.ParallelWriteOptions.BatchRows.init -> void +ExcelReader.Core.Writer.ParallelWriteOptions.DegreeOfParallelism.get -> int +ExcelReader.Core.Writer.ParallelWriteOptions.DegreeOfParallelism.init -> void +ExcelReader.Core.Writer.ParallelWriteOptions.ParallelWriteOptions() -> void ExcelReader.Core.Writer.SheetWriterExtensions ExcelReader.Core.Writer.Xls.XlsRowWriter ExcelReader.Core.Writer.Xls.XlsRowWriter.Dispose() -> void @@ -502,6 +608,12 @@ ExcelReader.Core.Writer.Xls.XlsWorkbookWriter.End() -> void ExcelReader.Core.Writer.Xls.XlsWorkbookWriter.EndAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask ExcelReader.Core.Writer.Xls.XlsWorkbookWriter.Flush() -> void ExcelReader.Core.Writer.Xls.XlsWorkbookWriter.FlushAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +ExcelReader.Core.Writer.Xls.XlsWriterOptions +ExcelReader.Core.Writer.Xls.XlsWriterOptions.$() -> ExcelReader.Core.Writer.Xls.XlsWriterOptions! +ExcelReader.Core.Writer.Xls.XlsWriterOptions.Date1904.get -> bool +ExcelReader.Core.Writer.Xls.XlsWriterOptions.Date1904.init -> void +ExcelReader.Core.Writer.Xls.XlsWriterOptions.Equals(ExcelReader.Core.Writer.Xls.XlsWriterOptions? other) -> bool +ExcelReader.Core.Writer.Xls.XlsWriterOptions.XlsWriterOptions() -> void ExcelReader.Core.Writer.Xlsb.XlsbCell ExcelReader.Core.Writer.Xlsb.XlsbCell.Equals(ExcelReader.Core.Writer.Xlsb.XlsbCell other) -> bool ExcelReader.Core.Writer.Xlsb.XlsbCell.XlsbCell() -> void @@ -522,10 +634,13 @@ ExcelReader.Core.Writer.Xlsb.XlsbRowWriter.Write(T value) -> void ExcelReader.Core.Writer.Xlsb.XlsbRowWriter.Write(T? value) -> void ExcelReader.Core.Writer.Xlsb.XlsbRowWriter.WriteUtf8(System.ReadOnlySpan utf8) -> void ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter +ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.BeginTable(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null) -> void +ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.BeginTableAsync(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.Dispose() -> void ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.DisposeAsync() -> System.Threading.Tasks.ValueTask ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.End() -> void ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.EndAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.EndTable() -> void ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.SetColumnStyle(int columnIndex, int styleId) -> void ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.SetColumnWidth(int columnIndex, double width) -> void ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.StartRow() -> ExcelReader.Core.Writer.Xlsb.XlsbRowWriter! @@ -572,10 +687,13 @@ ExcelReader.Core.Writer.Xlsx.XlsxRowWriter.Write(T value) -> void ExcelReader.Core.Writer.Xlsx.XlsxRowWriter.Write(T? value) -> void ExcelReader.Core.Writer.Xlsx.XlsxRowWriter.WriteUtf8(System.ReadOnlySpan utf8) -> void ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter +ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.BeginTable(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null) -> void +ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.BeginTableAsync(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.Dispose() -> void ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.DisposeAsync() -> System.Threading.Tasks.ValueTask ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.End() -> void ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.EndAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.EndTable() -> void ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.SetColumnStyle(int columnIndex, int styleId) -> void ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.SetColumnWidth(int columnIndex, double width) -> void ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.StartRow() -> ExcelReader.Core.Writer.Xlsx.XlsxRowWriter! @@ -596,6 +714,8 @@ ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.$() -> ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions! ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.Compression.get -> System.IO.Compression.CompressionLevel ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.Compression.init -> void +ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.Date1904.get -> bool +ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.Date1904.init -> void ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.Equals(ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions? other) -> bool ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.PrefetchWrite.get -> bool ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.PrefetchWrite.init -> void @@ -625,6 +745,12 @@ override ExcelReader.Core.Writer.CellStyle.GetHashCode() -> int override ExcelReader.Core.Writer.Csv.CsvWriterOptions.Equals(object? obj) -> bool override ExcelReader.Core.Writer.Csv.CsvWriterOptions.GetHashCode() -> int override ExcelReader.Core.Writer.Csv.CsvWriterOptions.ToString() -> string! +override ExcelReader.Core.Writer.ExcelTableOptions.Equals(object? obj) -> bool +override ExcelReader.Core.Writer.ExcelTableOptions.GetHashCode() -> int +override ExcelReader.Core.Writer.ExcelTableOptions.ToString() -> string! +override ExcelReader.Core.Writer.Xls.XlsWriterOptions.Equals(object? obj) -> bool +override ExcelReader.Core.Writer.Xls.XlsWriterOptions.GetHashCode() -> int +override ExcelReader.Core.Writer.Xls.XlsWriterOptions.ToString() -> string! override ExcelReader.Core.Writer.Xlsb.XlsbCell.GetHashCode() -> int override ExcelReader.Core.Writer.Xlsb.XlsbWriterOptions.Equals(object? obj) -> bool override ExcelReader.Core.Writer.Xlsb.XlsbWriterOptions.GetHashCode() -> int @@ -665,6 +791,11 @@ static ExcelReader.Core.Reader.Excel.EncryptPackage(System.IO.Stream! package, S static ExcelReader.Core.Reader.Excel.EncryptPackage(string! packagePath, string! destinationPath, ExcelReader.Core.Reader.ExcelPassword! password) -> void static ExcelReader.Core.Reader.Excel.EncryptPackageAsync(System.IO.Stream! package, System.IO.Stream! destination, ExcelReader.Core.Reader.ExcelPassword! password, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static ExcelReader.Core.Reader.Excel.EncryptPackageAsync(string! packagePath, string! destinationPath, ExcelReader.Core.Reader.ExcelPassword! password, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Reader.Excel.FromCsv(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Csv.CsvReader! +static ExcelReader.Core.Reader.Excel.FromCsv(System.ReadOnlyMemory data, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Csv.CsvReader! +static ExcelReader.Core.Reader.Excel.FromCsvAsync(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Reader.Excel.FromCsvFile(string! path, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Csv.CsvReader! +static ExcelReader.Core.Reader.Excel.FromCsvFileAsync(string! path, ExcelReader.Core.Reader.ExcelReaderOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static ExcelReader.Core.Reader.Excel.FromXls(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Xls.XlsWorkbook! static ExcelReader.Core.Reader.Excel.FromXls(System.ReadOnlyMemory data, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Xls.XlsWorkbook! static ExcelReader.Core.Reader.Excel.FromXlsAsync(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -680,6 +811,7 @@ static ExcelReader.Core.Reader.Excel.FromXlsx(System.ReadOnlyMemory data, static ExcelReader.Core.Reader.Excel.FromXlsxAsync(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static ExcelReader.Core.Reader.Excel.FromXlsxFile(string! path, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Xlsx.XlsxWorkbook! static ExcelReader.Core.Reader.Excel.FromXlsxFileAsync(string! path, ExcelReader.Core.Reader.ExcelReaderOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Reader.Excel.InferSchema(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.Schema.SchemaInferenceOptions? options = null) -> System.Collections.Generic.IReadOnlyList! static ExcelReader.Core.Reader.Excel.Open(System.IO.Stream! stream, ExcelReader.Core.Reader.ExcelFileFormat format, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.IExcelWorkbook! static ExcelReader.Core.Reader.Excel.Open(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.IExcelWorkbook! static ExcelReader.Core.Reader.Excel.Open(System.ReadOnlyMemory data, ExcelReader.Core.Reader.ExcelFileFormat format, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.IExcelWorkbook! @@ -697,6 +829,15 @@ static ExcelReader.Core.Reader.ExcelReaderOptions.operator !=(ExcelReader.Core.R static ExcelReader.Core.Reader.ExcelReaderOptions.operator ==(ExcelReader.Core.Reader.ExcelReaderOptions? left, ExcelReader.Core.Reader.ExcelReaderOptions? right) -> bool static ExcelReader.Core.Reader.Schema.ExcelColumnSchema.operator !=(ExcelReader.Core.Reader.Schema.ExcelColumnSchema left, ExcelReader.Core.Reader.Schema.ExcelColumnSchema right) -> bool static ExcelReader.Core.Reader.Schema.ExcelColumnSchema.operator ==(ExcelReader.Core.Reader.Schema.ExcelColumnSchema left, ExcelReader.Core.Reader.Schema.ExcelColumnSchema right) -> bool +static ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.Default.get -> ExcelReader.Core.Reader.Schema.SchemaInferenceOptions! +static ExcelReader.Core.Reader.SheetDataReaderOptions.Default.get -> ExcelReader.Core.Reader.SheetDataReaderOptions! +static ExcelReader.Core.Reader.SheetParallel.AggregateAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Parser.ExcelParser! parser, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static ExcelReader.Core.Reader.SheetParallel.AggregateAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static ExcelReader.Core.Reader.SheetParallel.AggregateAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.RowAggregation! aggregation, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static ExcelReader.Core.Reader.SheetParallel.ForEachAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Parser.ExcelParser! parser, System.Action! body, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static ExcelReader.Core.Reader.SheetParallel.ForEachAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, System.Action! body, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static ExcelReader.Core.Reader.SheetParallel.ParseAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Parser.ExcelParser! parser, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! +static ExcelReader.Core.Reader.SheetParallelOptions.Default.get -> ExcelReader.Core.Reader.SheetParallelOptions! static ExcelReader.Core.Reader.Xls.XlsSheet.operator !=(ExcelReader.Core.Reader.Xls.XlsSheet left, ExcelReader.Core.Reader.Xls.XlsSheet right) -> bool static ExcelReader.Core.Reader.Xls.XlsSheet.operator ==(ExcelReader.Core.Reader.Xls.XlsSheet left, ExcelReader.Core.Reader.Xls.XlsSheet right) -> bool static ExcelReader.Core.Reader.Xlsb.XlsbSheet.operator !=(ExcelReader.Core.Reader.Xlsb.XlsbSheet left, ExcelReader.Core.Reader.Xlsb.XlsbSheet right) -> bool @@ -711,11 +852,33 @@ static ExcelReader.Core.Writer.Csv.CsvWriterOptions.operator !=(ExcelReader.Core static ExcelReader.Core.Writer.Csv.CsvWriterOptions.operator ==(ExcelReader.Core.Writer.Csv.CsvWriterOptions? left, ExcelReader.Core.Writer.Csv.CsvWriterOptions? right) -> bool static ExcelReader.Core.Writer.ExcelRecordLayout.FromAttributes() -> ExcelReader.Core.Writer.ExcelRecordLayout! static ExcelReader.Core.Writer.ExcelRecordLayout.Generated() -> ExcelReader.Core.Writer.ExcelRecordLayout! +static ExcelReader.Core.Writer.ExcelTableOptions.Default.get -> ExcelReader.Core.Writer.ExcelTableOptions! +static ExcelReader.Core.Writer.ExcelTableOptions.operator !=(ExcelReader.Core.Writer.ExcelTableOptions? left, ExcelReader.Core.Writer.ExcelTableOptions? right) -> bool +static ExcelReader.Core.Writer.ExcelTableOptions.operator ==(ExcelReader.Core.Writer.ExcelTableOptions? left, ExcelReader.Core.Writer.ExcelTableOptions? right) -> bool +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.ParallelWriteOptions.Default.get -> ExcelReader.Core.Writer.ParallelWriteOptions! static ExcelReader.Core.Writer.SheetWriterExtensions.WriteRecordsAsync(this ExcelReader.Core.Writer.ISheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static ExcelReader.Core.Writer.SheetWriterExtensions.WriteRecordsAsync(this ExcelReader.Core.Writer.ISheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, System.Action! writeRow, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static ExcelReader.Core.Writer.SheetWriterExtensions.WriteRecordsAsync(this ExcelReader.Core.Writer.ISheetWriter! sheet, System.Collections.Generic.IEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static ExcelReader.Core.Writer.SheetWriterExtensions.WriteRecordsAsync(this ExcelReader.Core.Writer.ISheetWriter! sheet, System.Collections.Generic.IEnumerable! records, System.Action! writeRow, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static ExcelReader.Core.Writer.SheetWriterExtensions.WriteRecordsAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, System.Action! writeRow, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.SheetWriterExtensions.WriteTableAsync(this ExcelReader.Core.Writer.ITableSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.SheetWriterExtensions.WriteTableAsync(this ExcelReader.Core.Writer.ITableSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static ExcelReader.Core.Writer.Xls.XlsWorkbookWriter.Create(System.IO.Stream! stream, bool leaveOpen = false, ExcelReader.Core.Writer.Xls.XlsWriterOptions? options = null) -> ExcelReader.Core.Writer.Xls.XlsWorkbookWriter! +static ExcelReader.Core.Writer.Xls.XlsWriterOptions.Default.get -> ExcelReader.Core.Writer.Xls.XlsWriterOptions! +static ExcelReader.Core.Writer.Xls.XlsWriterOptions.operator !=(ExcelReader.Core.Writer.Xls.XlsWriterOptions? left, ExcelReader.Core.Writer.Xls.XlsWriterOptions? right) -> bool +static ExcelReader.Core.Writer.Xls.XlsWriterOptions.operator ==(ExcelReader.Core.Writer.Xls.XlsWriterOptions? left, ExcelReader.Core.Writer.Xls.XlsWriterOptions? right) -> bool static ExcelReader.Core.Writer.Xlsb.XlsbCell.Create(System.DateTime value) -> ExcelReader.Core.Writer.Xlsb.XlsbCell static ExcelReader.Core.Writer.Xlsb.XlsbCell.Create(System.DateTime? value) -> ExcelReader.Core.Writer.Xlsb.XlsbCell static ExcelReader.Core.Writer.Xlsb.XlsbCell.Create(bool value) -> ExcelReader.Core.Writer.Xlsb.XlsbCell diff --git a/src/ExcelReader.Core/PublicAPI/PublicAPI.Unshipped.txt b/src/ExcelReader.Core/PublicAPI/PublicAPI.Unshipped.txt index 5d3628e0..7dc5c581 100644 --- a/src/ExcelReader.Core/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/ExcelReader.Core/PublicAPI/PublicAPI.Unshipped.txt @@ -1,164 +1 @@ #nullable enable -ExcelReader.Core.Reader.Csv.CsvReader.Tables.get -> System.Collections.Generic.IReadOnlyList! -ExcelReader.Core.Reader.Csv.CsvReader.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool -ExcelReader.Core.Reader.ExcelTable -ExcelReader.Core.Reader.ExcelTable.ColumnNames.get -> System.Collections.Generic.IReadOnlyList! -ExcelReader.Core.Reader.ExcelTable.FirstColumn.get -> int -ExcelReader.Core.Reader.ExcelTable.FirstRow.get -> int -ExcelReader.Core.Reader.ExcelTable.HeaderRowCount.get -> int -ExcelReader.Core.Reader.ExcelTable.LastColumn.get -> int -ExcelReader.Core.Reader.ExcelTable.LastRow.get -> int -ExcelReader.Core.Reader.ExcelTable.Name.get -> string! -ExcelReader.Core.Reader.ExcelTable.Ref.get -> string! -ExcelReader.Core.Reader.ExcelTable.Sheet.get -> ExcelReader.Core.Reader.IExcelSheet! -ExcelReader.Core.Reader.ExcelTable.StyleName.get -> string? -ExcelReader.Core.Reader.ExcelTable.TotalsRowCount.get -> int -ExcelReader.Core.Reader.IExcelWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! -ExcelReader.Core.Reader.IExcelWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool -ExcelReader.Core.Reader.Xls.XlsWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! -ExcelReader.Core.Reader.Xls.XlsWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool -ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! -ExcelReader.Core.Reader.Xlsb.XlsbWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool -ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.Tables.get -> System.Collections.Generic.IReadOnlyList! -ExcelReader.Core.Reader.Xlsx.XlsxWorkbook.TryGetTable(System.ReadOnlySpan name, out ExcelReader.Core.Reader.ExcelTable! table) -> bool -ExcelReader.Core.Reader.ExcelTable.AsSheet() -> ExcelReader.Core.Reader.IExcelSheet! -ExcelReader.Core.Writer.ExcelTableOptions -ExcelReader.Core.Writer.ExcelTableOptions.$() -> ExcelReader.Core.Writer.ExcelTableOptions! -ExcelReader.Core.Writer.ExcelTableOptions.Equals(ExcelReader.Core.Writer.ExcelTableOptions? other) -> bool -ExcelReader.Core.Writer.ExcelTableOptions.ExcelTableOptions() -> void -ExcelReader.Core.Writer.ExcelTableOptions.FirstColumn.get -> int -ExcelReader.Core.Writer.ExcelTableOptions.FirstColumn.init -> void -ExcelReader.Core.Writer.ExcelTableOptions.ShowColumnStripes.get -> bool -ExcelReader.Core.Writer.ExcelTableOptions.ShowColumnStripes.init -> void -ExcelReader.Core.Writer.ExcelTableOptions.ShowFirstColumn.get -> bool -ExcelReader.Core.Writer.ExcelTableOptions.ShowFirstColumn.init -> void -ExcelReader.Core.Writer.ExcelTableOptions.ShowLastColumn.get -> bool -ExcelReader.Core.Writer.ExcelTableOptions.ShowLastColumn.init -> void -ExcelReader.Core.Writer.ExcelTableOptions.ShowRowStripes.get -> bool -ExcelReader.Core.Writer.ExcelTableOptions.ShowRowStripes.init -> void -ExcelReader.Core.Writer.ExcelTableOptions.StyleName.get -> string? -ExcelReader.Core.Writer.ExcelTableOptions.StyleName.init -> void -ExcelReader.Core.Writer.ITableSheetWriter -ExcelReader.Core.Writer.ITableSheetWriter.BeginTable(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null) -> void -ExcelReader.Core.Writer.ITableSheetWriter.BeginTableAsync(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -ExcelReader.Core.Writer.ITableSheetWriter.EndTable() -> void -override ExcelReader.Core.Writer.ExcelTableOptions.Equals(object? obj) -> bool -override ExcelReader.Core.Writer.ExcelTableOptions.GetHashCode() -> int -override ExcelReader.Core.Writer.ExcelTableOptions.ToString() -> string! -static ExcelReader.Core.Writer.ExcelTableOptions.Default.get -> ExcelReader.Core.Writer.ExcelTableOptions! -static ExcelReader.Core.Writer.ExcelTableOptions.operator !=(ExcelReader.Core.Writer.ExcelTableOptions? left, ExcelReader.Core.Writer.ExcelTableOptions? right) -> bool -static ExcelReader.Core.Writer.ExcelTableOptions.operator ==(ExcelReader.Core.Writer.ExcelTableOptions? left, ExcelReader.Core.Writer.ExcelTableOptions? right) -> bool -ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.BeginTable(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null) -> void -ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.BeginTableAsync(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter.EndTable() -> void -ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.BeginTable(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null) -> void -ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.BeginTableAsync(string! name, System.Collections.Generic.IReadOnlyList! columns, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter.EndTable() -> void -static ExcelReader.Core.Writer.SheetWriterExtensions.WriteTableAsync(this ExcelReader.Core.Writer.ITableSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.SheetWriterExtensions.WriteTableAsync(this ExcelReader.Core.Writer.ITableSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -ExcelReader.Core.Writer.ParallelWriteOptions -ExcelReader.Core.Writer.ParallelWriteOptions.BatchRows.get -> int -ExcelReader.Core.Writer.ParallelWriteOptions.BatchRows.init -> void -ExcelReader.Core.Writer.ParallelWriteOptions.DegreeOfParallelism.get -> int -ExcelReader.Core.Writer.ParallelWriteOptions.DegreeOfParallelism.init -> void -ExcelReader.Core.Writer.ParallelWriteOptions.ParallelWriteOptions() -> void -static ExcelReader.Core.Writer.ParallelWriteOptions.Default.get -> ExcelReader.Core.Writer.ParallelWriteOptions! -ExcelReader.Core.Writer.ParallelSheetWriterExtensions -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsx.XlsxSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteRecordsParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, System.Action! writeRow, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IAsyncEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Writer.ParallelSheetWriterExtensions.WriteTableParallelAsync(this ExcelReader.Core.Writer.Xlsb.XlsbSheetWriter! sheet, System.Collections.Generic.IEnumerable! records, string! name, ExcelReader.Core.Writer.ExcelRecordLayout! layout, ExcelReader.Core.Writer.ExcelTableOptions? tableOptions = null, ExcelReader.Core.Writer.ParallelWriteOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -ExcelReader.Core.Reader.SheetParallel -ExcelReader.Core.Reader.SheetParallelOptions -ExcelReader.Core.Reader.SheetParallelOptions.DegreeOfParallelism.get -> int -ExcelReader.Core.Reader.SheetParallelOptions.DegreeOfParallelism.init -> void -ExcelReader.Core.Reader.SheetParallelOptions.HeaderRow.get -> int -ExcelReader.Core.Reader.SheetParallelOptions.HeaderRow.init -> void -ExcelReader.Core.Reader.SheetParallelOptions.SheetParallelOptions() -> void -static ExcelReader.Core.Reader.SheetParallel.AggregateAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Parser.ExcelParser! parser, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static ExcelReader.Core.Reader.SheetParallel.AggregateAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static ExcelReader.Core.Reader.SheetParallel.AggregateAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.RowAggregation! aggregation, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static ExcelReader.Core.Reader.SheetParallel.ForEachAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Parser.ExcelParser! parser, System.Action! body, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static ExcelReader.Core.Reader.SheetParallel.ForEachAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, System.Action! body, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static ExcelReader.Core.Reader.SheetParallel.ParseAsync(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Parser.ExcelParser! parser, ExcelReader.Core.Reader.SheetParallelOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable! -static ExcelReader.Core.Reader.SheetParallelOptions.Default.get -> ExcelReader.Core.Reader.SheetParallelOptions! -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.HeaderRow.get -> int -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.HeaderRow.init -> void -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.ParseText.get -> bool -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.ParseText.init -> void -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.SampleSize.get -> int -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.SampleSize.init -> void -ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.SchemaInferenceOptions() -> void -static ExcelReader.Core.Reader.Schema.SchemaInferenceOptions.Default.get -> ExcelReader.Core.Reader.Schema.SchemaInferenceOptions! -ExcelReader.Core.Writer.Xls.XlsWriterOptions -ExcelReader.Core.Writer.Xls.XlsWriterOptions.$() -> ExcelReader.Core.Writer.Xls.XlsWriterOptions! -ExcelReader.Core.Writer.Xls.XlsWriterOptions.Date1904.get -> bool -ExcelReader.Core.Writer.Xls.XlsWriterOptions.Date1904.init -> void -ExcelReader.Core.Writer.Xls.XlsWriterOptions.Equals(ExcelReader.Core.Writer.Xls.XlsWriterOptions? other) -> bool -ExcelReader.Core.Writer.Xls.XlsWriterOptions.XlsWriterOptions() -> void -ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.Date1904.get -> bool -ExcelReader.Core.Writer.Xlsx.XlsxWriterOptions.Date1904.init -> void -override ExcelReader.Core.Writer.Xls.XlsWriterOptions.Equals(object? obj) -> bool -override ExcelReader.Core.Writer.Xls.XlsWriterOptions.GetHashCode() -> int -override ExcelReader.Core.Writer.Xls.XlsWriterOptions.ToString() -> string! -static ExcelReader.Core.Writer.Xls.XlsWorkbookWriter.Create(System.IO.Stream! stream, bool leaveOpen = false, ExcelReader.Core.Writer.Xls.XlsWriterOptions? options = null) -> ExcelReader.Core.Writer.Xls.XlsWorkbookWriter! -static ExcelReader.Core.Writer.Xls.XlsWriterOptions.Default.get -> ExcelReader.Core.Writer.Xls.XlsWriterOptions! -static ExcelReader.Core.Writer.Xls.XlsWriterOptions.operator !=(ExcelReader.Core.Writer.Xls.XlsWriterOptions? left, ExcelReader.Core.Writer.Xls.XlsWriterOptions? right) -> bool -static ExcelReader.Core.Writer.Xls.XlsWriterOptions.operator ==(ExcelReader.Core.Writer.Xls.XlsWriterOptions? left, ExcelReader.Core.Writer.Xls.XlsWriterOptions? right) -> bool -static ExcelReader.Core.Reader.Excel.FromCsv(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Csv.CsvReader! -static ExcelReader.Core.Reader.Excel.FromCsv(System.ReadOnlyMemory data, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Csv.CsvReader! -static ExcelReader.Core.Reader.Excel.FromCsvAsync(System.IO.Stream! stream, bool leaveOpen = true, ExcelReader.Core.Reader.ExcelReaderOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Reader.Excel.FromCsvFile(string! path, ExcelReader.Core.Reader.ExcelReaderOptions? options = null) -> ExcelReader.Core.Reader.Csv.CsvReader! -static ExcelReader.Core.Reader.Excel.FromCsvFileAsync(string! path, ExcelReader.Core.Reader.ExcelReaderOptions? options = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static ExcelReader.Core.Reader.Excel.InferSchema(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.Schema.SchemaInferenceOptions? options = null) -> System.Collections.Generic.IReadOnlyList! -ExcelReader.Core.Reader.SheetDataReader -ExcelReader.Core.Reader.SheetDataReader.Close() -> void -ExcelReader.Core.Reader.SheetDataReader.Depth.get -> int -ExcelReader.Core.Reader.SheetDataReader.Dispose() -> void -ExcelReader.Core.Reader.SheetDataReader.FieldCount.get -> int -ExcelReader.Core.Reader.SheetDataReader.GetBoolean(int i) -> bool -ExcelReader.Core.Reader.SheetDataReader.GetByte(int i) -> byte -ExcelReader.Core.Reader.SheetDataReader.GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) -> long -ExcelReader.Core.Reader.SheetDataReader.GetChar(int i) -> char -ExcelReader.Core.Reader.SheetDataReader.GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) -> long -ExcelReader.Core.Reader.SheetDataReader.GetData(int i) -> System.Data.IDataReader! -ExcelReader.Core.Reader.SheetDataReader.GetDataTypeName(int i) -> string! -ExcelReader.Core.Reader.SheetDataReader.GetDateTime(int i) -> System.DateTime -ExcelReader.Core.Reader.SheetDataReader.GetDecimal(int i) -> decimal -ExcelReader.Core.Reader.SheetDataReader.GetDouble(int i) -> double -ExcelReader.Core.Reader.SheetDataReader.GetFieldType(int i) -> System.Type! -ExcelReader.Core.Reader.SheetDataReader.GetFloat(int i) -> float -ExcelReader.Core.Reader.SheetDataReader.GetGuid(int i) -> System.Guid -ExcelReader.Core.Reader.SheetDataReader.GetInt16(int i) -> short -ExcelReader.Core.Reader.SheetDataReader.GetInt32(int i) -> int -ExcelReader.Core.Reader.SheetDataReader.GetInt64(int i) -> long -ExcelReader.Core.Reader.SheetDataReader.GetName(int i) -> string! -ExcelReader.Core.Reader.SheetDataReader.GetOrdinal(string! name) -> int -ExcelReader.Core.Reader.SheetDataReader.GetSchemaTable() -> System.Data.DataTable! -ExcelReader.Core.Reader.SheetDataReader.GetString(int i) -> string! -ExcelReader.Core.Reader.SheetDataReader.GetValue(int i) -> object! -ExcelReader.Core.Reader.SheetDataReader.GetValues(object![]! values) -> int -ExcelReader.Core.Reader.SheetDataReader.IsClosed.get -> bool -ExcelReader.Core.Reader.SheetDataReader.IsDBNull(int i) -> bool -ExcelReader.Core.Reader.SheetDataReader.NextResult() -> bool -ExcelReader.Core.Reader.SheetDataReader.Read() -> bool -ExcelReader.Core.Reader.SheetDataReader.RecordsAffected.get -> int -ExcelReader.Core.Reader.SheetDataReader.SheetDataReader(ExcelReader.Core.Reader.IExcelSheet! sheet, ExcelReader.Core.Reader.SheetDataReaderOptions? options = null) -> void -ExcelReader.Core.Reader.SheetDataReader.this[int i].get -> object! -ExcelReader.Core.Reader.SheetDataReader.this[string! name].get -> object! -ExcelReader.Core.Reader.SheetDataReaderOptions -ExcelReader.Core.Reader.SheetDataReaderOptions.HeaderRow.get -> int -ExcelReader.Core.Reader.SheetDataReaderOptions.HeaderRow.init -> void -ExcelReader.Core.Reader.SheetDataReaderOptions.SheetDataReaderOptions() -> void -static ExcelReader.Core.Reader.SheetDataReaderOptions.Default.get -> ExcelReader.Core.Reader.SheetDataReaderOptions! -ExcelReader.Core.Reader.ExcelReaderOptions.VerifyZipChecksums.get -> bool -ExcelReader.Core.Reader.ExcelReaderOptions.VerifyZipChecksums.init -> void From 40ce4060a61f9aa3086171d2060ba06db525fc70 Mon Sep 17 00:00:00 2001 From: Gabriel Matte Date: Thu, 15 Oct 2026 11:33:11 -0300 Subject: [PATCH 2/7] chore: update package validation baseline version to 7.0.0 --- src/ExcelReader.Arrow/ExcelReader.Arrow.csproj | 2 +- src/ExcelReader.Core/ExcelReader.Core.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ExcelReader.Arrow/ExcelReader.Arrow.csproj b/src/ExcelReader.Arrow/ExcelReader.Arrow.csproj index db16a3c1..12562a0c 100644 --- a/src/ExcelReader.Arrow/ExcelReader.Arrow.csproj +++ b/src/ExcelReader.Arrow/ExcelReader.Arrow.csproj @@ -8,7 +8,7 @@ true - 7.0.0-rc.2 + 7.0.0 diff --git a/src/ExcelReader.Core/ExcelReader.Core.csproj b/src/ExcelReader.Core/ExcelReader.Core.csproj index 8bfd9514..82943fb7 100644 --- a/src/ExcelReader.Core/ExcelReader.Core.csproj +++ b/src/ExcelReader.Core/ExcelReader.Core.csproj @@ -11,7 +11,7 @@ true - 7.0.0-rc.2 + 7.0.0 From 23725ebad5369ef853c500464c42942cd6653e87 Mon Sep 17 00:00:00 2001 From: Gabriel Matte Date: Thu, 15 Oct 2026 15:22:48 -0300 Subject: [PATCH 3/7] feat: add FastDecimal class for efficient decimal parsing and implement tests --- src/ExcelReader.Core/Reader/Cell.cs | 5 + .../Reader/Internal/FastDecimal.cs | 59 ++++++++++++ .../Reader/FastDecimalTests.cs | 92 +++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 src/ExcelReader.Core/Reader/Internal/FastDecimal.cs create mode 100644 tests/ExcelReader.Tests/Reader/FastDecimalTests.cs diff --git a/src/ExcelReader.Core/Reader/Cell.cs b/src/ExcelReader.Core/Reader/Cell.cs index 92e6d66e..e599c2b6 100644 --- a/src/ExcelReader.Core/Reader/Cell.cs +++ b/src/ExcelReader.Core/Reader/Cell.cs @@ -100,6 +100,11 @@ public bool TryParse(IFormatProvider? provider, [MaybeNullWhen(false)] out T result = Unsafe.As(ref fast); return true; } + if (typeof(T) == typeof(decimal) && UsesDotDecimalSeparator(provider) && FastDecimal.TryParse(Value, out decimal fastDecimal)) + { + result = Unsafe.As(ref fastDecimal); + return true; + } if (TryParseAsciiDigits(Value, provider, out result)) { return true; diff --git a/src/ExcelReader.Core/Reader/Internal/FastDecimal.cs b/src/ExcelReader.Core/Reader/Internal/FastDecimal.cs new file mode 100644 index 00000000..c96568c6 --- /dev/null +++ b/src/ExcelReader.Core/Reader/Internal/FastDecimal.cs @@ -0,0 +1,59 @@ +using System.Runtime.CompilerServices; + +namespace ExcelReader.Core.Reader.Internal +{ + internal static class FastDecimal + { + private const int MaxMantissaDigits = 19; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(ReadOnlySpan s, out decimal value) + { + value = 0; + if (s.IsEmpty) + { + return false; + } + int i = 0; + bool neg = s[0] == (byte)'-'; + if (neg || s[0] == (byte)'+') + { + i = 1; + } + + ulong mantissa = 0; + int integerStart = i; + for (; i < s.Length; i++) + { + uint d = (uint)(s[i] - (byte)'0'); + if (d > 9) + { + break; + } + mantissa = (mantissa * 10) + d; + } + int integerDigits = i - integerStart; + int scale = 0; + if (i < s.Length && s[i] == (byte)'.') + { + int fractionStart = ++i; + for (; i < s.Length; i++) + { + uint d = (uint)(s[i] - (byte)'0'); + if (d > 9) + { + break; + } + mantissa = (mantissa * 10) + d; + } + scale = i - fractionStart; + } + if (i != s.Length || integerDigits + scale is 0 or > MaxMantissaDigits) + { + return false; + } + value = new decimal((int)mantissa, (int)(mantissa >> 32), 0, neg, (byte)scale); + return true; + } + } +} diff --git a/tests/ExcelReader.Tests/Reader/FastDecimalTests.cs b/tests/ExcelReader.Tests/Reader/FastDecimalTests.cs new file mode 100644 index 00000000..8eeb4b74 --- /dev/null +++ b/tests/ExcelReader.Tests/Reader/FastDecimalTests.cs @@ -0,0 +1,92 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using ExcelReader.Core.Reader.Internal; + +namespace ExcelReader.Tests.Reader +{ + public class FastDecimalTests + { + private static void AssertBitEqualToDecimalParse(string text) + { + byte[] utf8 = Encoding.ASCII.GetBytes(text); + Assert.True(FastDecimal.TryParse(utf8, out decimal actual), $"Expected FastDecimal to accept \"{text}\"."); + decimal expected = decimal.Parse(utf8, CultureInfo.InvariantCulture); + Assert.Equal(decimal.GetBits(expected), decimal.GetBits(actual)); + } + + [Theory] + [InlineData("0")] + [InlineData("-0")] + [InlineData("-0.00")] + [InlineData("42")] + [InlineData("-42")] + [InlineData("+42")] + [InlineData("3.14")] + [InlineData("1.50")] + [InlineData("1.")] + [InlineData(".5")] + [InlineData("-.5")] + [InlineData("00042")] + [InlineData("0.000000000000000001")] + [InlineData("0000000000000000001")] + [InlineData("9999999999999999999")] + [InlineData("1844674407.370955161")] + [InlineData("12345678901234567.89")] + public void AcceptsAndMatchesDecimalParseBitExactly(string text) + { + AssertBitEqualToDecimalParse(text); + } + + [Theory] + [InlineData("")] + [InlineData("-")] + [InlineData("+")] + [InlineData(".")] + [InlineData("-.")] + [InlineData("1.2.3")] + [InlineData("1,000")] + [InlineData(" 1")] + [InlineData("1 ")] + [InlineData("1x")] + [InlineData("--5")] + [InlineData("1e5")] + [InlineData("99999999999999999999")] + [InlineData("0.0000000000000000001")] + public void RejectsUnsupportedForms(string text) + { + Assert.False(FastDecimal.TryParse(Encoding.ASCII.GetBytes(text), out _), $"Expected FastDecimal to reject \"{text}\"."); + } + + [Fact] + [SuppressMessage("Security", "CA5394:Do not use insecure randomness", + Justification = "Needs a reproducible seeded PRNG for deterministic test content, not cryptographic randomness.")] + public void MatchesDecimalParseOnRandomValues() + { + var rng = new Random(24680); + StringBuilder text = new(); + for (int i = 0; i < 200_000; i++) + { + text.Clear(); + if (rng.Next(4) == 0) + { + text.Append('-'); + } + int integerDigits = rng.Next(1, 12); + for (int d = integerDigits; d > 0; d--) + { + text.Append((char)('0' + rng.Next(10))); + } + if (rng.Next(3) != 0) + { + text.Append('.'); + for (int d = rng.Next(19 - integerDigits); d > 0; d--) + { + text.Append((char)('0' + rng.Next(10))); + } + } + AssertBitEqualToDecimalParse(text.ToString()); + } + } + } +} From 4c963e127bc7b07090f12f2d6938a9612a1a0c1e Mon Sep 17 00:00:00 2001 From: GabrielMarquezMatte Date: Fri, 16 Oct 2026 01:16:58 -0300 Subject: [PATCH 4/7] docs: refresh benchmark numbers from the 2026-10-15 run Short run of the whole suite on the idle Ryzen 7 5700X (.NET 10.0.12), with the rows whose Short interval exceeded 25% of the mean rerun on MediumRun. OfficeIMO.Excel 3.4.5 reads XLSX several times faster than the figures published before; the conversion-heavy CSV parallel path gained from FastDecimal but is allocator-bound at dop 16. --- docs/guide/encryption.md | 4 +- docs/guide/reading.md | 22 +-- docs/guide/writing.md | 4 +- docs/performance/benchmarks.md | 264 ++++++++++++++++----------------- 4 files changed, 147 insertions(+), 147 deletions(-) diff --git a/docs/guide/encryption.md b/docs/guide/encryption.md index ad0cf358..fe4208b3 100644 --- a/docs/guide/encryption.md +++ b/docs/guide/encryption.md @@ -59,8 +59,8 @@ The in-memory path (`Excel.Open(ReadOnlyMemory)` and friends) has already by the time it returns, so it always verifies regardless of this setting. Standard encryption has no HMAC field, so the setting does not apply to it. -Opening an encrypted workbook costs a fixed ~26 ms for key derivation, whatever the file size. After -that, reading is ~10% slower than reading the same workbook unencrypted — see +Opening an encrypted workbook costs a fixed ~25 ms for key derivation, whatever the file size. After +that, reading is ~3% slower than reading the same workbook unencrypted — see [Encrypted workbooks](../performance/benchmarks.md#encrypted-workbooks) in the benchmarks. Writing an encrypted workbook is a second step: build the package with any writer, then wrap it with diff --git a/docs/guide/reading.md b/docs/guide/reading.md index dcacb011..c02d2186 100644 --- a/docs/guide/reading.md +++ b/docs/guide/reading.md @@ -127,7 +127,7 @@ A callback that writes to a database or sends messages must be idempotent on CSV [`ExcelParseException.Row`](xref:ExcelReader.Core.Parser.ExcelParseException.Row) on `ParseAsync` counts the rows emitted so far. On `ForEachAsync` and `AggregateAsync` it is the exact row when the read is sequential and `0` when it was partitioned. -See the [parallel sheet benchmarks](../performance/benchmarks.md#parallel-sheet-reading) and the [parallel CSV benchmarks](../performance/benchmarks.md#parallel-csv) for what this buys. On a real-data XLSX the sheet stage drops from 29 ms (prefetched, sequential) to about 18 ms, and on XLSB from 16 ms to about 13 ms; the floor is inflate, which runs on one thread. CSV has no such floor: its `AggregateAsync` at dop 16 is ~2.8x faster than the typed path while allocating ~410x less, with zero garbage collections. +See the [parallel sheet benchmarks](../performance/benchmarks.md#parallel-sheet-reading) and the [parallel CSV benchmarks](../performance/benchmarks.md#parallel-csv) for what this buys. On a real-data XLSX the sheet stage drops from 28 ms (prefetched, sequential) to about 17 ms, and on XLSB from 17 ms to about 13 ms; the floor is inflate, which runs on one thread. CSV has no such floor: its `AggregateAsync` at dop 4 is ~1.8x faster than the typed path while allocating ~450x less, with zero garbage collections. Properties need setters. A get-only property is skipped during binding and silently receives nothing. @@ -355,16 +355,16 @@ Measured across both read benchmarks (see [Real data reads](../performance/bench | Workload | Default | `PrefetchDecompression = true` | Gain | |---|---:|---:|---:| -| XLSX, real data | 49.7 ms | 27.8 ms | 44% | -| XLSM, real data | 50.3 ms | 28.1 ms | 44% | -| XLSB, real data | 27.7 ms | 16.9 ms | 39% | -| XLSX, string-heavy | 41.2 ms | 27.4 ms | 34% | -| XLSB, string-heavy | 39.5 ms | 26.0 ms | 34% | - -The gain tracks how much of a read is decompression rather than parsing, so it is largest -on XLSX and XLSM with numeric data (where inflate dominates). Allocations rise from the producer task and the -pooled decompression buffers — on the real-data corpus, roughly 8 KB to 25 KB for XLSX -and 7 KB to 16 KB for XLSB — and neither path triggers a garbage collection. +| XLSX, real data | 41.2 ms | 27.8 ms | 32% | +| XLSM, real data | 43.5 ms | 28.4 ms | 35% | +| XLSB, real data | 25.8 ms | 16.7 ms | 35% | +| XLSX, string-heavy | 33.1 ms | 23.0 ms | 31% | +| XLSB, string-heavy | 32.6 ms | 19.7 ms | 40% | + +The gain tracks how much of a read is decompression rather than parsing, so it is smallest +on XLSX real data (about a third) and largest on string-heavy XLSB (40%). Allocations rise from the producer task and the +pooled decompression buffers — on the real-data corpus, roughly 7 KB to 24 KB for XLSX +and 7 KB to 15 KB for XLSB — and neither path triggers a garbage collection. Do **not** enable it for concurrent server workloads: a caller already reading many files in parallel is CPU-saturated, and an extra background thread per read only doubles thread diff --git a/docs/guide/writing.md b/docs/guide/writing.md index b3e52711..1dd68ecb 100644 --- a/docs/guide/writing.md +++ b/docs/guide/writing.md @@ -263,8 +263,8 @@ Writing a 50,000-row workbook (`WriteBenchmark`, both figures from one run): | Workload | Default | `PrefetchWrite = true` | Gain | |---|---:|---:|---:| -| XLSX | 8.337 ms | 5.175 ms | 38% | -| XLSB | 5.572 ms | 3.419 ms | 39% | +| XLSX | 8.527 ms | 5.245 ms | 38% | +| XLSB | 5.786 ms | 3.234 ms | 44% | Allocations are unchanged (4.02 MB either way) — the background writer hands over buffers the row writer already owns rather than copying them. diff --git a/docs/performance/benchmarks.md b/docs/performance/benchmarks.md index 67bce0fd..4e836ba7 100644 --- a/docs/performance/benchmarks.md +++ b/docs/performance/benchmarks.md @@ -5,7 +5,9 @@ Throwaway numbers go stale; these are regenerated from the benchmark suite in ## Benchmarks -Benchmarks were run with BenchmarkDotNet v0.15.8 on Windows 10 (22H2), AMD Ryzen 7 5700X, .NET 10.0.11 (SDK 10.0.400). Generated-data benchmarks use 50,000 rows, except the string-heavy reads, which use 65,536. Raw results: [GitHub Pages benchmark dashboard](https://gabrielmarquezmatte.github.io/ExcelReader/dev/bench/) — `tests/ExcelReader.Benchmarks/BenchmarkDotNet.Artifacts/` is `.gitignore`-excluded and never actually reaches GitHub, so a link to it 404s for every reader. +Benchmarks were run with BenchmarkDotNet v0.15.8 on Windows 10 (22H2), AMD Ryzen 7 5700X, .NET 10.0.12 (SDK 10.0.401). Generated-data benchmarks use 50,000 rows, except the string-heavy reads, which use 65,536. Raw results: [GitHub Pages benchmark dashboard](https://gabrielmarquezmatte.github.io/ExcelReader/dev/bench/) — `tests/ExcelReader.Benchmarks/BenchmarkDotNet.Artifacts/` is `.gitignore`-excluded and never actually reaches GitHub, so a link to it 404s for every reader. + +Every BenchmarkDotNet table is a `--job Short` run (1 launch, 3 warmup and 3 measured iterations) on an otherwise idle machine. Rows whose 99.9% confidence interval came out wider than 25% of the mean were run again with `--job Medium` (2 launches, 15 iterations) and carry those figures instead: the parallel CSV tables, the CSV cell-by-cell read, the Sylvan XLS read, the Sylvan string-heavy reads, the string-heavy XLSX matched-work read, the generated-workbook matched-work read, `DataTable.Load` and the large encrypted read. The cold-start rows set their own launch count, and the parallel record writes, the Python/C++/Rust figures and the native-library comparison come from other harnesses that were not re-run. **Benchmark methodology.** In the Excel-format tables below, the **"Cell-by-cell read"** rows (including the per-format rows in "Real data reads" / "String-heavy reads") read ExcelReader's `cell.Value` — a zero-copy `ReadOnlySpan`, no decode or allocation — against each competitor's own idiomatic read call. For Sylvan.Data.Excel, that's the ADO.NET-style `GetString(i)`, which is forced to materialize a UTF-16 `string`; its API has no zero-copy accessor, so it cannot avoid that cost. These rows are therefore not matched work: part of the reported gap is "we parse faster" and part is "we skipped an allocation you were never offered a way to skip." Each affected benchmark class also has a `*_Materialized` sibling (calling `cell.GetString()`, the same UTF-16 materialization Sylvan pays) so the matched-work number is measurable — see `tests/ExcelReader.Benchmarks/Shared/BenchmarkAccumulators.cs`. **Those results are published under [Matched-work reads](#matched-work-reads-_materialized), and reading them is the honest way to judge the comparison:** treat the Excel cell-by-cell ratios in the tables below as an upper bound, not a like-for-like number. The CSV cell-by-cell rows are matched work: Sep and Sylvan.Data.Csv both expose span accessors (`row[i].Span`, `GetFieldSpan(i)`), and the benchmarks use them. The **"Typed row parsing"** / **"Typed record writing"** rows are unaffected — both sides already materialize real objects/strings there, so those comparisons are matched work as published. @@ -15,54 +17,54 @@ Compares ExcelReader against established XLSX libraries on the same generated wo | Scenario | ExcelReader | Sylvan | OfficeIMO | SpreadCheetah | |---|---:|---:|---:|---:| -| Cell-by-cell read | 6.847 ms, 3.57 KB | 36.131 ms, 1.89 MB | 106.861 ms, 6.84 MB | - | -| Cell-by-cell read async | 7.391 ms, 3.64 KB | - | - | - | -| Typed row parsing | 10.668 ms, 3.87 MB | 56.150 ms, 10.48 MB | 107.805 ms, 5.70 MB | - | -| Typed row parsing async | 10.833 ms, 3.87 MB | 59.036 ms, 10.48 MB | - | - | -| Typed row parsing, shared strings | 9.216 ms, 2.29 MB | - | - | - | -| Workbook writing | 8.337 ms, 4.02 MB | - | 18.437 ms, 4.02 MB | 15.332 ms, 15.84 MB | -| Workbook writing, shared strings | 8.358 ms, 4.06 MB | - | - | - | +| Cell-by-cell read | 6.780 ms, 3.63 KB | 35.084 ms, 1.89 MB | 14.447 ms, 3.51 MB | - | +| Cell-by-cell read async | 6.953 ms, 3.70 KB | - | - | - | +| Typed row parsing | 10.979 ms, 3.87 MB | 56.677 ms, 10.48 MB | 11.802 ms, 2.36 MB | - | +| Typed row parsing async | 10.958 ms, 3.87 MB | 58.753 ms, 10.48 MB | - | - | +| Typed row parsing, shared strings | 9.896 ms, 2.29 MB | - | - | - | +| Workbook writing | 8.527 ms, 4.02 MB | - | 18.100 ms, 4.04 MB | 15.393 ms, 15.84 MB | +| Workbook writing, shared strings | 8.416 ms, 4.06 MB | - | - | - | -ExcelReader is ~5.3x faster than Sylvan for raw XLSX reads, allocating ~543x less, and ~15.6x faster than OfficeIMO. For typed parsing it is ~5.3x faster than Sylvan (~5.4x async against async). For XLSX writing, ExcelReader is ~1.8x faster than SpreadCheetah while allocating ~3.9x less memory, and ~2.2x faster than OfficeIMO. +ExcelReader is ~5.2x faster than Sylvan for raw XLSX reads, allocating ~534x less, and ~2.1x faster than OfficeIMO. For typed parsing it is ~5.2x faster than Sylvan (~5.4x async against async) and ~1.1x faster than OfficeIMO, which allocates less (2.36 MB against 3.87 MB). For XLSX writing, ExcelReader is ~1.8x faster than SpreadCheetah while allocating ~3.9x less memory, and ~2.1x faster than OfficeIMO. The OfficeIMO rows are OfficeIMO.Excel 3.4.5: that release made its XLSX read and typed parse several times faster than the 3.4.4 figures published before (106.861 ms and 107.805 ms). OfficeIMO.Excel's typed parsing is hand-mapped from its `OpenDataReader`, because `RowsAs` needs an `r` attribute on every row and cell, which the spec makes optional and ExcelReader's writer omits. -Reading a shared-strings XLSX workbook with typed parsing is ~14% faster than the inline-string sheet above (9.216 ms vs. 10.668 ms) and allocates ~41% less (2.29 MB vs. 3.87 MB) — each distinct string decodes once into the shared-string cache instead of once per cell occurrence. +Reading a shared-strings XLSX workbook with typed parsing is ~10% faster than the inline-string sheet above (9.896 ms vs. 10.979 ms) and allocates ~41% less (2.29 MB vs. 3.87 MB) — each distinct string decodes once into the shared-string cache instead of once per cell occurrence. ### XLSB (BIFF12) | Scenario | ExcelReader | OfficeIMO | |---|---:|---:| -| Cell-by-cell read | 4.798 ms, 3.82 KB | 11.123 ms, 6.70 MB | -| Cell-by-cell read async | 4.845 ms, 3.89 KB | - | -| Typed row parsing | 7.819 ms, 3.87 MB | - | -| Typed row parsing async | 8.061 ms, 3.87 MB | - | -| Workbook writing | 5.572 ms, 4.02 MB | - | -| Workbook writing, shared strings | 5.595 ms, 4.05 MB | - | +| Cell-by-cell read | 4.881 ms, 3.89 KB | 10.928 ms, 5.12 MB | +| Cell-by-cell read async | 5.220 ms, 3.96 KB | - | +| Typed row parsing | 7.851 ms, 3.87 MB | - | +| Typed row parsing async | 8.014 ms, 3.87 MB | - | +| Workbook writing | 5.786 ms, 4.02 MB | - | +| Workbook writing, shared strings | 5.734 ms, 4.05 MB | - | -XLSB is the fastest generated Excel format in these results: raw reads are ~1.4x faster than XLSX reads, typed parsing is ~1.4x faster than XLSX parsing, and writing is ~1.5x faster than XLSX writing. Against OfficeIMO's XLSB reader, ExcelReader is ~2.3x faster while allocating ~1,800x less. The XLSB writer is also ~2.8x faster than SpreadCheetah's XLSX writer on this benchmark while allocating ~75% less memory. +XLSB is the fastest generated Excel format in these results: raw reads are ~1.4x faster than XLSX reads, typed parsing is ~1.4x faster than XLSX parsing, and writing is ~1.5x faster than XLSX writing. Against OfficeIMO's XLSB reader, ExcelReader is ~2.2x faster while allocating ~1,350x less. The XLSB writer is also ~2.7x faster than SpreadCheetah's XLSX writer on this benchmark while allocating ~75% less memory. ### XLS (BIFF8) | Scenario | ExcelReader | Sylvan | OfficeIMO | |---|---:|---:|---:| -| Cell-by-cell read | 3.916 ms, 2.84 KB | 5.173 ms, 1,717.73 KB | 6.856 ms, 10,499.25 KB | -| Cell-by-cell read async | 3.236 ms, 2.91 KB | - | - | -| Workbook writing | 4.859 ms, 16.31 MB | - | - | +| Cell-by-cell read | 3.961 ms, 2.84 KB | 5.177 ms, 1,717.73 KB | 7.788 ms, 10,499.26 KB | +| Cell-by-cell read async | 3.253 ms, 2.91 KB | - | - | +| Workbook writing | 4.753 ms, 16.31 MB | - | - | -ExcelReader is ~1.3x faster than Sylvan for generated XLS reads while allocating ~605x less memory, and ~1.8x faster than OfficeIMO. The XLS writer is ~1.7x faster than the XLSX writer in this benchmark (`XlsWriteBenchmark`, 4.859 ms vs 8.375 ms in the same run). Most of its 16.31 MB is the benchmark's pre-sized 16 MB destination `MemoryStream`; the typed-record table below measures it at 4.31 MB, close to the other formats. +ExcelReader is ~1.3x faster than Sylvan for generated XLS reads while allocating ~605x less memory, and ~2.0x faster than OfficeIMO. The XLS writer is ~1.8x faster than the XLSX writer in this benchmark (`XlsWriteBenchmark`, 4.753 ms vs 8.600 ms in the same run). Most of its 16.31 MB is the benchmark's pre-sized 16 MB destination `MemoryStream`; the typed-record table below measures it at 4.31 MB, close to the other formats. ### CSV | Scenario | ExcelReader | Sep | Sylvan.Data.Csv | |---|---:|---:|---:| -| Cell-by-cell read | 3.053 ms, 848 B | 8.012 ms, 3.93 KB | 4.359 ms, 37.78 KB | -| Cell-by-cell read async | 2.941 ms, 920 B | - | - | -| Typed row parsing | 4.631 ms, 3.86 MB | 8.454 ms, 3.87 MB | 11.832 ms, 10.95 MB | -| Typed row parsing async | 4.987 ms, 3.86 MB | - | - | -| Row writing | 4.749 ms, 4.00 MB | 7.030 ms, 4.01 MB | 7.276 ms, 4.04 MB | +| Cell-by-cell read | 2.499 ms, 888 B | 7.827 ms, 3.93 KB | 4.044 ms, 37.78 KB | +| Cell-by-cell read async | 2.935 ms, 960 B | - | - | +| Typed row parsing | 4.889 ms, 3.86 MB | 8.736 ms, 3.87 MB | 12.161 ms, 10.95 MB | +| Typed row parsing async | 4.915 ms, 3.86 MB | - | - | +| Row writing | 4.551 ms, 4.00 MB | 6.880 ms, 4.01 MB | 6.983 ms, 4.04 MB | -For raw CSV reads, all three libraries read text through spans. ExcelReader is ~2.6x faster than Sep while allocating ~5x less, and ~1.4x faster than Sylvan.Data.Csv while allocating ~46x less. For typed CSV parsing (the more common case — building actual records), ExcelReader is ~1.8x faster than Sep and ~2.6x faster than Sylvan.Data.Csv, with the lowest allocation of the group. For CSV writing, ExcelReader is ~1.5x faster than both Sep and Sylvan.Data.Csv, which are tied; the ~4 MB shown across all three is primarily the benchmark's pre-sized destination `MemoryStream`, not per-row writer state. +For raw CSV reads, all three libraries read text through spans. ExcelReader is ~3.1x faster than Sep while allocating ~4.5x less, and ~1.6x faster than Sylvan.Data.Csv while allocating ~44x less. For typed CSV parsing (the more common case — building actual records), ExcelReader is ~1.8x faster than Sep and ~2.5x faster than Sylvan.Data.Csv, with the lowest allocation of the group. For CSV writing, ExcelReader is ~1.5x faster than both Sep and Sylvan.Data.Csv, which are tied; the ~4 MB shown across all three is primarily the benchmark's pre-sized destination `MemoryStream`, not per-row writer state. ### Parallel CSV @@ -70,28 +72,26 @@ For raw CSV reads, all three libraries read text through spans. ExcelReader is ~ | Dop | Conversion-heavy, typed | Narrow ints, typed | Conversion-heavy, `ref struct` aggregate | Narrow ints, `ref struct` aggregate | |---:|---:|---:|---:|---:| -| 1 | 904.0 ms, 671.42 MB | 518.6 ms, 245.76 MB | 784.1 ms, 1.20 MB | 478.1 ms, 1.20 MB | -| 2 | 623.7 ms, 680.13 MB | 382.9 ms, 254.68 MB | 405.4 ms, 1.26 MB | 262.9 ms, 1.28 MB | -| 4 | 343.0 ms, 680.24 MB | 217.2 ms, 254.72 MB | 218.3 ms, 1.37 MB | 137.6 ms, 1.44 MB | -| 8 | 285.2 ms, 680.15 MB | 204.7 ms, 254.63 MB | 158.3 ms, 1.54 MB | 94.6 ms, 1.62 MB | -| 16 | 324.9 ms, 680.32 MB | 189.0 ms, 255.15 MB | 114.4 ms, 1.66 MB | 73.0 ms, 1.69 MB | - -These numbers were measured before the rename to `SheetParallel`; the rename did not touch the CSV engine, and a scratch harness comparing the two builds on a 156 MB CSV showed no regression. +| 1 | 587.8 ms, 671.42 MB | 516.9 ms, 245.76 MB | 472.0 ms, 1.20 MB | 463.9 ms, 1.20 MB | +| 2 | 461.6 ms, 680.28 MB | 385.8 ms, 254.80 MB | 267.3 ms, 1.35 MB | 265.6 ms, 1.34 MB | +| 4 | 256.9 ms, 680.39 MB | 217.8 ms, 254.81 MB | 145.2 ms, 1.50 MB | 140.1 ms, 1.51 MB | +| 8 | 210.5 ms, 680.31 MB | 187.8 ms, 254.77 MB | 96.3 ms, 1.69 MB | 93.1 ms, 1.68 MB | +| 16 | 471.2 ms, 680.33 MB | 187.0 ms, 255.23 MB | 74.0 ms, 1.79 MB | 73.3 ms, 1.76 MB | -The typed path scales ~2.8x on the conversion-heavy corpus (bimodal at dop 16: 86.7 ms standard deviation, median 275.8 ms) and ~2.7x on narrow ints, where it flattens out after dop 4 (217.2 ms vs. 189.0 ms at dop 16). Its ~680 MB is one materialized row object per record, and at dop 16 that costs 42,000 Gen0 plus 41,000 Gen1 collections. +The typed path scales ~2.3x by dop 4 on the conversion-heavy corpus (587.8 to 256.9 ms) and ~2.4x on narrow ints (516.9 to 217.8 ms), then flattens out: narrow ints reach only 187.0 ms at dop 16, and the conversion-heavy corpus gets *slower* past dop 8 (471.2 ms at dop 16 against 210.5 ms at dop 8, standard deviation 23.2 ms). Its ~680 MB is one materialized row object per record, and at dop 16 that costs 42,000 Gen0 plus 41,000 Gen1 collections. -The aggregate columns fold each record into a per-partition accumulator through a `ref struct` model, so text columns stay as `ReadOnlySpan` and no row object is ever materialized. They scale better than the typed path — ~6.9x (conversion-heavy) and ~6.5x (narrow) at dop 16 — because they are not fighting the allocator: at dop 16 the conversion-heavy aggregate is ~2.8x faster than typed while allocating ~410x less (1.66 MB against 680.32 MB), with **zero** garbage collections at every degree. The remaining allocation is per-partition bookkeeping, not per-row. +The aggregate columns fold each record into a per-partition accumulator through a `ref struct` model, so text columns stay as `ReadOnlySpan` and no row object is ever materialized. They scale better than the typed path — ~4.9x (conversion-heavy) and ~5.0x (narrow) at dop 8, ~6.4x and ~6.3x at dop 16 — because they are not fighting the allocator: at dop 4 the conversion-heavy aggregate is ~1.8x faster than typed while allocating ~450x less (1.50 MB against 680.39 MB), with **zero** garbage collections at every degree. The remaining allocation is per-partition bookkeeping, not per-row. -Type conversion is the floor under both. A single-threaded pass that allocates nothing still costs 784.1 ms, so parsing bytes into `DateTime`/`decimal`/`int` — not allocation and not I/O — is what the parallelism is actually buying down. +Type conversion is the floor under both. A single-threaded pass that allocates nothing still costs 472.0 ms, so parsing bytes into `DateTime`/`decimal`/`int` — not allocation and not I/O — is what the parallelism is actually buying down. Against [Sep](https://github.com/nietras/Sep)'s own `ParallelEnumerate` (all cores; 8,000,000 narrow rows, 3,000,000 conversion-heavy rows): | Corpus | ExcelReader sequential | ExcelReader parallel | Sep sequential | Sep parallel | |---|---:|---:|---:|---:| -| Narrow ints | 490.6 ms, 244.15 MB | 187.6 ms, 255.14 MB | 521.7 ms, 244.15 MB | 429.3 ms, 247.77 MB | -| Conversion-heavy | 626.6 ms, 467.31 MB | 184.9 ms, 474.82 MB | 817.5 ms, 467.31 MB | 605.5 ms, 468.84 MB | +| Narrow ints | 479.4 ms, 244.15 MB | 189.8 ms, 255.24 MB | 515.1 ms, 244.15 MB | 434.8 ms, 245.60 MB | +| Conversion-heavy | 397.9 ms, 467.31 MB | 330.4 ms, 474.83 MB | 808.7 ms, 467.31 MB | 619.0 ms, 469.93 MB | -Sequentially, ExcelReader's typed parser is ~1.1x faster than Sep on narrow ints (490.6 ms vs. 521.7 ms), and ~1.3x faster on the conversion-heavy corpus. In parallel ExcelReader is ~2.3x (narrow) and ~3.3x (conversion-heavy) faster than Sep's, while yielding rows in file order. +Sequentially, ExcelReader's typed parser is ~1.1x faster than Sep on narrow ints (479.4 ms vs. 515.1 ms), and ~2.0x faster on the conversion-heavy corpus, where decimal parsing dominates. In parallel ExcelReader is ~2.3x (narrow) and ~1.9x (conversion-heavy) faster than Sep's, while yielding rows in file order. The conversion-heavy parallel run gains less than the narrow one (330.4 ms against 397.9 ms sequential) because it is bound by the allocator, like the typed dop 16 row above: 28 Gen1 collections per run. ### Parallel sheet reading @@ -99,12 +99,12 @@ Sequentially, ExcelReader's typed parser is ~1.1x faster than Sep on narrow ints | Dop | XLSX | XLSB | |---:|---:|---:| -| 1 | 52.60 ms, 8.02 MB | 35.77 ms, 8.02 MB | -| 2 | 36.30 ms, 8.13 MB | 24.48 ms, 8.07 MB | -| 4 | 16.89 ms, 8.11 MB | 13.32 ms, 8.07 MB | -| 8 | 17.06 ms, 8.16 MB | 13.67 ms, 8.09 MB | +| 1 | 52.76 ms, 8.02 MB | 33.82 ms, 8.02 MB | +| 2 | 36.63 ms, 8.13 MB | 25.35 ms, 8.07 MB | +| 4 | 16.64 ms, 8.11 MB | 13.25 ms, 8.07 MB | +| 8 | 16.73 ms, 8.16 MB | 13.25 ms, 8.09 MB | -At dop 4 XLSX reads ~3.1x faster than sequentially and XLSB ~2.7x. Dop 2 is a single parse worker running alongside the inflating thread. Past dop 4 nothing improves: the one inflating thread is the floor (~14 ms for XLSX, ~10 ms for XLSB, see [Inflate](#inflate), plus the XLSB record walk that finds row boundaries). Allocation barely moves with dop (8.02 to 8.16 MB): it is the typed model's strings. +At dop 4 XLSX reads ~3.2x faster than sequentially and XLSB ~2.6x. Dop 2 is a single parse worker running alongside the inflating thread. Past dop 4 nothing improves: the one inflating thread is the floor (~14 ms for XLSX, ~9 ms for XLSB, see [Inflate](#inflate), plus the XLSB record walk that finds row boundaries). Allocation barely moves with dop (8.02 to 8.16 MB): it is the typed model's strings. ### Real data reads @@ -112,13 +112,13 @@ This benchmark reads a real workbook exported in multiple formats. | Format | ExcelReader | ExcelReader, prefetch | Sylvan | |---|---:|---:|---:| -| XLSX | 41.275 ms, 7.31 KB | 29.616 ms, 23.52 KB | 197.561 ms, 644.23 KB | -| XLSM | 43.649 ms, 7.31 KB | 29.347 ms, 23.48 KB | 194.102 ms, 644.30 KB | -| XLSB | 25.303 ms, 6.87 KB | 17.588 ms, 15.20 KB | 29.771 ms, 338.54 KB | -| XLS | 11.280 ms, 12.02 KB | n/a | 18.379 ms, 185.90 KB | -| CSV | 5.072 ms, 888 B | n/a | 4.585 ms, 40.26 KB | +| XLSX | 41.206 ms, 7.38 KB | 27.823 ms, 23.54 KB | 191.883 ms, 644.25 KB | +| XLSM | 43.502 ms, 7.38 KB | 28.364 ms, 23.55 KB | 197.799 ms, 644.33 KB | +| XLSB | 25.775 ms, 6.94 KB | 16.722 ms, 15.28 KB | 29.536 ms, 338.54 KB | +| XLS | 10.750 ms, 12.02 KB | n/a | 18.124 ms, 185.90 KB | +| CSV | 4.992 ms, 888 B | n/a | 4.482 ms, 40.26 KB | -On this real-data workload, ExcelReader is ~4.8x faster than Sylvan for XLSX and ~4.4x for XLSM, ~1.2x faster for XLSB, and ~1.6x faster for XLS — allocating ~88x less for XLSX and XLSM, ~49x less for XLSB and ~15x less for XLS. On CSV, where both sides read text through spans, Sylvan.Data.Csv is ~11% faster, while ExcelReader allocates ~46x less. The prefetch column is the opt-in [`PrefetchDecompression`](../guide/reading.md#prefetch-decompression-xlsxxlsb) option; XLS and CSV are uncompressed, so it does not apply to them. +On this real-data workload, ExcelReader is ~4.7x faster than Sylvan for XLSX and ~4.5x for XLSM, ~1.1x faster for XLSB, and ~1.7x faster for XLS — allocating ~87x less for XLSX and XLSM, ~49x less for XLSB and ~15x less for XLS. On CSV, where both sides read text through spans, Sylvan.Data.Csv is ~11% faster, while ExcelReader allocates ~46x less. The prefetch column is the opt-in [`PrefetchDecompression`](../guide/reading.md#prefetch-decompression-xlsxxlsb) option; XLS and CSV are uncompressed, so it does not apply to them. ### In-memory real-data reads @@ -126,18 +126,18 @@ The real-data benchmark also measures the in-memory path for workbook content lo | Method | Mean | StdDev | Allocated | |---|---:|---:|---:| -| Xlsx_ExcelReader_Memory | 42.521 ms | 0.905 ms | 7.05 KB | -| Xlsx_ExcelReader_Memory_Prefetch | 29.167 ms | 0.353 ms | 23.03 KB | -| Xlsm_ExcelReader_Memory | 40.925 ms | 0.140 ms | 7.05 KB | -| Xlsm_ExcelReader_Memory_Prefetch | 30.345 ms | 1.268 ms | 23.22 KB | -| Xlsb_ExcelReader_Memory | 27.084 ms | 1.294 ms | 8.55 KB | -| Xlsb_ExcelReader_Memory_Prefetch | 18.140 ms | 0.703 ms | 16.79 KB | -| Xls_ExcelReader_Memory | 10.704 ms | 0.139 ms | 11.88 KB | -| Csv_ExcelReader_Memory | 4.759 ms | 0.022 ms | 616 B | +| Xlsx_ExcelReader_Memory | 41.625 ms | 0.079 ms | 7.10 KB | +| Xlsx_ExcelReader_Memory_Prefetch | 28.699 ms | 0.022 ms | 23.26 KB | +| Xlsm_ExcelReader_Memory | 41.559 ms | 0.042 ms | 7.10 KB | +| Xlsm_ExcelReader_Memory_Prefetch | 28.687 ms | 0.011 ms | 23.23 KB | +| Xlsb_ExcelReader_Memory | 24.465 ms | 0.008 ms | 8.61 KB | +| Xlsb_ExcelReader_Memory_Prefetch | 17.235 ms | 0.015 ms | 16.93 KB | +| Xls_ExcelReader_Memory | 10.574 ms | 0.026 ms | 11.88 KB | +| Csv_ExcelReader_Memory | 4.762 ms | 0.003 ms | 616 B | -`Csv_ExcelReader_Memory` is both faster (4.759 ms vs. 5.072 ms for `Csv_ExcelReader`) and allocates less (616 B vs. 888 B) than its stream twin. +`Csv_ExcelReader_Memory` is both faster (4.762 ms vs. 4.992 ms for `Csv_ExcelReader`) and allocates less (616 B vs. 888 B) than its stream twin. -`Xls_ExcelReader_Memory` is ~5% faster than its stream twin (10.704 ms vs. 11.280 ms for `Xls_ExcelReader`), with near-identical allocation (11.88 KB vs. 12.02 KB). In earlier runs the in-memory path was ~33% faster, thanks to `BiffCursor` caching the enclosing contiguous sector run; the stream path has since mostly caught up. +`Xls_ExcelReader_Memory` is ~2% faster than its stream twin (10.574 ms vs. 10.750 ms for `Xls_ExcelReader`), with near-identical allocation (11.88 KB vs. 12.02 KB). In earlier runs the in-memory path was ~33% faster, thanks to `BiffCursor` caching the enclosing contiguous sector run; the stream path has since mostly caught up. ### String-heavy reads @@ -145,22 +145,22 @@ The real-data corpus above is mostly numbers and dates — its shared-string tab | Format | ExcelReader | ExcelReader, prefetch | Sylvan | |---|---:|---:|---:| -| XLSX | 32.84 ms, 2.18 MB | 24.03 ms, 2.21 MB | 186.05 ms, 17.41 MB | -| XLSB | 32.44 ms, 2.18 MB | 21.12 ms, 2.20 MB | 65.22 ms, 17.38 MB | +| XLSX | 33.14 ms, 2.18 MB | 22.97 ms, 2.21 MB | 176.85 ms, 17.41 MB | +| XLSB | 32.56 ms, 2.18 MB | 19.68 ms, 2.20 MB | 59.78 ms, 17.38 MB | -Both formats handle this well: ~5.7x and ~2.0x faster than Sylvan for XLSX and XLSB respectively, at roughly ~8x less memory in both cases, with no garbage collections in either configuration. XLSB's shared-string path previously materialized its table eagerly (~27 MB here); `ParseSharedStreaming` brought it in line with the XLSX streaming/pooling path, cutting allocation by ~12x on this workload. +Both formats handle this well: ~5.3x and ~1.8x faster than Sylvan for XLSX and XLSB respectively, at roughly ~8x less memory in both cases, with no garbage collections in either configuration. XLSB's shared-string path previously materialized its table eagerly (~27 MB here); `ParseSharedStreaming` brought it in line with the XLSX streaming/pooling path, cutting allocation by ~12x on this workload. `XlsxSharedStringHotPathBenchmark` splits the XLSX read into stages on the same workbook. The stored rows re-zip it without compression, so they show what parsing costs once inflate is gone: | Stage | Mean | Allocated | |---|---:|---:| -| Whole read | 33.87 ms | 2.18 MB | -| Whole read, prefetch | 23.36 ms | 2.21 MB | -| Shared-string table only (open + first row) | 9.31 ms | 747 KB | -| Whole read, stored | 20.23 ms | 2.18 MB | -| Shared-string table only, stored | 3.42 ms | 746 KB | +| Whole read | 33.06 ms | 2.18 MB | +| Whole read, prefetch | 23.00 ms | 2.21 MB | +| Shared-string table only (open + first row) | 9.37 ms | 747 KB | +| Whole read, stored | 20.69 ms | 2.18 MB | +| Shared-string table only, stored | 3.35 ms | 746 KB | -About 40% of the default read is inflate: without it the whole read drops from 33.87 ms to 20.23 ms, and the shared-string table from 9.31 ms to 3.42 ms. What remains, ~17 ms, is the worksheet scan. +About 37% of the default read is inflate: without it the whole read drops from 33.06 ms to 20.69 ms, and the shared-string table from 9.37 ms to 3.35 ms. What remains, ~17 ms, is the worksheet scan. ### Inflate @@ -168,9 +168,9 @@ About 40% of the default read is inflate: without it the whole read drops from 3 | Entry | `DeflateStream` | `InflateStream` | Ratio | |---|---:|---:|---:| -| XLSX `sheet1.xml` (6.7 MB → 34.4 MB) | 23.789 ms, 280 B | 13.741 ms, 200 B | 0.58 | -| XLSB `sheet1.bin` (3.7 MB → 15.9 MB) | 13.143 ms, 280 B | 9.734 ms, 200 B | 0.74 | -| String-heavy `sharedStrings.xml` | 10.571 ms, 281 B | 6.106 ms, 200 B | 0.58 | +| XLSX `sheet1.xml` (6.7 MB → 34.4 MB) | 22.232 ms, 280 B | 13.523 ms, 200 B | 0.61 | +| XLSB `sheet1.bin` (3.7 MB → 15.9 MB) | 12.695 ms, 280 B | 9.147 ms, 200 B | 0.72 | +| String-heavy `sharedStrings.xml` | 10.375 ms, 280 B | 5.915 ms, 200 B | 0.57 | Without prefetch a read is inflate plus parse, so the faster inflate converts directly; with prefetch the two overlap and the read is bounded by the slower stage, which is why the prefetch columns above moved little. @@ -230,30 +230,30 @@ Real-data workbook, per format: | Format | Mean | StdDev | Allocated | |---|---:|---:|---:| -| XLSX | 43.03 ms | 0.15 ms | 16.78 KB | -| XLSM | 43.64 ms | 0.28 ms | 16.78 KB | -| XLSB | 28.01 ms | 0.90 ms | 16.34 KB | -| XLS | 12.95 ms | 0.25 ms | 21.48 KB | -| CSV | 19.31 ms | 0.22 ms | 35.71 MB | +| XLSX | 44.42 ms | 0.04 ms | 16.85 KB | +| XLSM | 44.96 ms | 0.06 ms | 16.85 KB | +| XLSB | 26.61 ms | 0.01 ms | 16.41 KB | +| XLS | 12.31 ms | 0.04 ms | 21.48 KB | +| CSV | 18.63 ms | 0.18 ms | 35.71 MB | Generated 50,000-row XLSX workbook: | Scenario | Mean | StdDev | Allocated | |---|---:|---:|---:| -| Cell-by-cell read, materialized | 7.89 ms | 0.03 ms | 1.58 MB | +| Cell-by-cell read, materialized | 8.43 ms | 0.15 ms | 1.58 MB | String-heavy workbook (65,536 rows, ~190,000 distinct shared strings): | Format | Mean | StdDev | Gen0 | Gen1 | Gen2 | Allocated | |---|---:|---:|---:|---:|---:|---:| -| XLSX | 55.51 ms | 0.58 ms | 1,111.1 | 1,000.0 | 333.3 | 15.25 MB | -| XLSB | 55.55 ms | 0.95 ms | 1,111.1 | 1,000.0 | 333.3 | 15.25 MB | +| XLSX | 50.93 ms | 1.98 ms | 1,100.0 | 1,000.0 | 300.0 | 15.25 MB | +| XLSB | 53.12 ms | 0.67 ms | 1,100.0 | 1,000.0 | 300.0 | 15.25 MB | Reading the allocation columns against the tables above gives the honest shape of the tradeoff: - **CSV real data:** 35.71 MB materialized against 888 B for the span-based read of the same file. Every CSV field is a distinct string, so nothing dedupes — this is where zero-copy reading earns its keep outright. -- **XLSB real data:** 16.34 KB, essentially cheap. That corpus repeats a small set of values, so the shared-string table dedupes and the reader's string cache materializes each distinct value once. -- **String-heavy XLSX:** 15.25 MB materialized, against **Sylvan's 17.41 MB on the same workload** — doing matched work here, ExcelReader allocates ~12% *less* than Sylvan, with 333 Gen2 collections, and is still ~3.4x faster (55.51 ms vs. 186.05 ms). The same allocation holds for XLSB (15.25 MB vs. Sylvan's 17.38 MB, also ~12% less, 333 Gen2 collections), where matched-work time is ~17% ahead of Sylvan (55.55 ms vs. 65.22 ms). This was previously an inversion (ExcelReader allocated ~1.75x *more* than Sylvan here): the per-reader shared-string dedup cache was an unpresized `Dictionary`, and at ~190,000 distinct values its resize/rehash churn (several of the largest resizes landing on the LOH) accounted for the entire gap — the strings themselves were never the problem, since both readers retain the same ~190,000 distinct instances. Replacing the dictionary with a `string?[]` indexed by shared-string index (sized exactly from the table's known count, no resizing) cut the allocation in half and cut wall-clock time by 14-16% on this benchmark too, since the churn was costing cycles, not just memory. +- **XLSB real data:** 16.41 KB, essentially cheap. That corpus repeats a small set of values, so the shared-string table dedupes and the reader's string cache materializes each distinct value once. +- **String-heavy XLSX:** 15.25 MB materialized, against **Sylvan's 17.41 MB on the same workload** — doing matched work here, ExcelReader allocates ~12% *less* than Sylvan and is still ~3.5x faster (50.93 ms vs. 176.85 ms). The same allocation holds for XLSB (15.25 MB vs. Sylvan's 17.38 MB, also ~12% less), where matched-work time is ~13% ahead of Sylvan (53.12 ms vs. 59.78 ms). This was previously an inversion (ExcelReader allocated ~1.75x *more* than Sylvan here): the per-reader shared-string dedup cache was an unpresized `Dictionary`, and at ~190,000 distinct values its resize/rehash churn (several of the largest resizes landing on the LOH) accounted for the entire gap — the strings themselves were never the problem, since both readers retain the same ~190,000 distinct instances. Replacing the dictionary with a `string?[]` indexed by shared-string index (sized exactly from the table's known count, no resizing) cut the allocation in half and cut wall-clock time by 14-16% on this benchmark too, since the churn was costing cycles, not just memory. The takeaway is not that one column beats the other: it is that ExcelReader's headline read numbers come from a zero-copy path competitors do not expose, and when it does the same work as them, the gap narrows — and here, with the dedup cache fixed, no longer inverts even at high shared-string cardinality. @@ -263,12 +263,12 @@ The takeaway is not that one column beats the other: it is that ExcelReader's he | Format | Mean | Allocated | |---|---:|---:| -| XLSX | 8.922 ms | 4.02 MB | -| XLSB | 6.113 ms | 4.02 MB | -| XLS | 6.292 ms | 4.31 MB | -| CSV | 4.824 ms | 4.00 MB | +| XLSX | 8.978 ms | 4.02 MB | +| XLSB | 6.170 ms | 4.02 MB | +| XLS | 6.167 ms | 4.31 MB | +| CSV | 4.688 ms | 4.00 MB | -Relative ordering: CSV at the front, XLSB and XLS close behind it, then XLSX — the record-mapping layer adds little overhead over hand-written cell-by-cell writes (XLSB 6.113 ms vs. 5.572 ms, XLSX 8.922 ms vs. 8.337 ms). +Relative ordering: CSV at the front, XLS and XLSB close behind it, then XLSX — the record-mapping layer adds little overhead over hand-written cell-by-cell writes (XLSB 6.170 ms vs. 5.786 ms, XLSX 8.978 ms vs. 8.527 ms). ### Native and Arrow string writes @@ -276,10 +276,10 @@ Relative ordering: CSV at the front, XLSB and XLS close behind it, then XLSX — | Scenario | Mean | Allocated | |---|---:|---:| -| Native string columns → XLSX | 14.559 ms | 15.74 KB | -| Native string columns → CSV | 5.098 ms | 416 B | -| Arrow `StringArray` → XLSX | 20.343 ms | 15.79 KB | -| Styled rows → XLSX | 5.411 ms | 15.92 KB | +| Native string columns → XLSX | 15.598 ms | 15.93 KB | +| Native string columns → CSV | 4.963 ms | 416 B | +| Arrow `StringArray` → XLSX | 21.837 ms | 15.98 KB | +| Styled rows → XLSX | 6.318 ms | 16.10 KB | Text arrives through `IRowWriter.WriteUtf8`, which the XLSX and CSV writers copy through as bytes whenever the text needs no escaping, so none of these allocate per cell: the ~16 KB is per-workbook ZIP and part setup, which CSV does not have. @@ -289,11 +289,11 @@ A `ref struct` model (see [Parse into a ref struct](../guide/parsing.md#parse-in | Target | Mean | Allocated | |---|---:|---:| -| `class` (`ExcelParser`) | 11.22 ms | 3.87 MB | -| `struct` (`ExcelParser`) | 10.11 ms | 1.58 MB | -| `ref struct` + span binding (`ExcelParser`) | 9.65 ms | 4.12 KB | +| `class` (`ExcelParser`) | 10.98 ms | 3.87 MB | +| `struct` (`ExcelParser`) | 10.32 ms | 1.58 MB | +| `ref struct` + span binding (`ExcelParser`) | 9.67 ms | 4.17 KB | -Parsing into a `ref struct` with a `ReadOnlySpan` text column removes essentially all per-row allocation — ~99.9% less than the `class` baseline — and is ~5% faster than the `struct` target, since there's no per-row `string` allocation for the text column. It is not AOT/trim-safe (reflection-based, same tradeoff as `ExcelParser.FromAttributes`). It can be consumed with `foreach` or `await foreach` but not through `IEnumerable`/`IAsyncEnumerable`/LINQ — a `ref struct` element can't be boxed through those interfaces. +Parsing into a `ref struct` with a `ReadOnlySpan` text column removes essentially all per-row allocation — ~99.9% less than the `class` baseline — and is ~6% faster than the `struct` target, since there's no per-row `string` allocation for the text column. It is not AOT/trim-safe (reflection-based, same tradeoff as `ExcelParser.FromAttributes`). It can be consumed with `foreach` or `await foreach` but not through `IEnumerable`/`IAsyncEnumerable`/LINQ — a `ref struct` element can't be boxed through those interfaces. ### ADO.NET bridge (`IDataReader`) @@ -301,13 +301,13 @@ Parsing into a `ref struct` with a `ReadOnlySpan` text column removes esse | Access | Mean | Ratio | Allocated | |---|---:|---:|---:| -| Raw rows (`row[i].Value`, baseline) | 6.845 ms | 1.00 | 3.62 KB | -| `GetBytes` (text column only) | 7.039 ms | 1.03 | 4.31 KB | -| `GetValue` (every column) | 9.479 ms | 1.39 | 5.01 MB | -| Typed getters (`GetString`/`GetInt32`/`GetDateTime`/`GetDouble`) | 9.094 ms | 1.33 | 1.58 MB | -| `DataTable.Load` | 60.509 ms | 8.85 | 22.84 MB | +| Raw rows (`row[i].Value`, baseline) | 6.545 ms | 1.00 | 3.67 KB | +| `GetBytes` (text column only) | 6.766 ms | 1.03 | 4.37 KB | +| `GetValue` (every column) | 9.215 ms | 1.41 | 5.01 MB | +| Typed getters (`GetString`/`GetInt32`/`GetDateTime`/`GetDouble`) | 8.954 ms | 1.37 | 1.58 MB | +| `DataTable.Load` | 62.767 ms | 9.59 | 22.84 MB | -The bridge costs ~1.3–1.4x over reading rows directly. `GetValue` boxes every value (the 5 MB), and the typed getters allocate only the text column's `string`s. `GetBytes` copies UTF-8 without making a `string`, so it stays within ~3% of the raw read. `DataTable.Load` is dominated by `DataTable`'s own row storage: the same values cost 9.479 ms to read through `GetValue` alone. +The bridge costs ~1.4x over reading rows directly. `GetValue` boxes every value (the 5 MB), and the typed getters allocate only the text column's `string`s. `GetBytes` copies UTF-8 without making a `string`, so it stays within ~3% of the raw read. `DataTable.Load` is dominated by `DataTable`'s own row storage: the same values cost 9.215 ms to read through `GetValue` alone. ### Arrow conversion @@ -315,12 +315,12 @@ The bridge costs ~1.3–1.4x over reading rows directly. `GetValue` boxes every | Scenario | Mean | Allocated | |---|---:|---:| -| CSV, typed, explicit schema | 10.76 ms | 8.13 MB | -| CSV, typed, inferred schema | 10.77 ms | 14.89 MB | -| XLSB, typed, explicit schema | 15.85 ms | 8.14 MB | -| CSV, 8 text columns, explicit schema | 17.22 ms | 16.27 MB | +| CSV, typed, explicit schema | 11.94 ms | 8.13 MB | +| CSV, typed, inferred schema | 12.51 ms | 14.89 MB | +| XLSB, typed, explicit schema | 16.40 ms | 8.14 MB | +| CSV, 8 text columns, explicit schema | 22.22 ms | 16.27 MB | -On the same file, letting `Excel.InferSchema` guess the schema costs about the same time (10.77 ms vs. 10.76 ms) and ~1.8x the allocation of passing an explicit `ExcelColumnSchema[]`. The allocation grows with the sheet, because the batch holds every row at once and there is no streaming variant. +On the same file, letting `Excel.InferSchema` guess the schema costs ~5% more time (12.51 ms vs. 11.94 ms) and ~1.8x the allocation of passing an explicit `ExcelColumnSchema[]`. The allocation grows with the sheet, because the batch holds every row at once and there is no streaming variant. ### Encrypted workbooks @@ -328,16 +328,16 @@ On the same file, letting `Excel.InferSchema` guess the schema costs about the s | Scenario | Mean | Allocated | |---|---:|---:| -| Small, plain | 0.208 ms | 8.84 KB | -| Small, encrypted, stream | 25.880 ms | 38.07 KB | -| Small, encrypted, stream, `VerifyEncryptedIntegrity` | 26.150 ms | 40.16 KB | -| Small, encrypted, in memory | 26.600 ms | 53.24 KB | -| Small, encrypted, open only | 26.378 ms | 32.54 KB | -| 300,000 rows, plain | 38.283 ms | 3.62 KB | -| 300,000 rows, encrypted | 65.485 ms | 29.53 KB | -| 300,000 rows, encrypted, open only | 26.235 ms | 28.92 KB | +| Small, plain | 0.215 ms | 8.91 KB | +| Small, encrypted, stream | 26.414 ms | 38.13 KB | +| Small, encrypted, stream, `VerifyEncryptedIntegrity` | 25.961 ms | 40.29 KB | +| Small, encrypted, in memory | 25.546 ms | 53.28 KB | +| Small, encrypted, open only | 25.244 ms | 32.52 KB | +| 300,000 rows, plain | 36.634 ms | 3.67 KB | +| 300,000 rows, encrypted | 63.432 ms | 29.59 KB | +| 300,000 rows, encrypted, open only | 25.605 ms | 28.92 KB | -Nearly all of the cost is a fixed ~26 ms paid when the workbook is opened: deriving the key through 100,000 SHA-512 iterations, which the format requires so that passwords are slow to brute-force. It does not depend on file size (open-only is 26.38 ms for 15 KB and 26.24 ms for 300,000 rows). After that, decrypting while reading costs ~3% over a plain read: 65.49 ms − 26.24 ms = 39.25 ms, against 38.28 ms unencrypted. On the small fixture, integrity verification and the in-memory path stay within ~3% of the plain encrypted read, inside the spread of the key derivation itself. The fixture is too small to show the extra pass that verification makes over a large stream. +Nearly all of the cost is a fixed ~25 ms paid when the workbook is opened: deriving the key through 100,000 SHA-512 iterations, which the format requires so that passwords are slow to brute-force. It does not depend on file size (open-only is 25.24 ms for 15 KB and 25.61 ms for 300,000 rows). After that, decrypting while reading costs ~3% over a plain read: 63.43 ms − 25.61 ms = 37.83 ms, against 36.63 ms unencrypted. On the small fixture, integrity verification and the in-memory path stay within ~3% of the plain encrypted read, inside the spread of the key derivation itself. The fixture is too small to show the extra pass that verification makes over a large stream. ### Cold start @@ -345,8 +345,8 @@ First use of `ExcelParser.FromAttributes`/`ExcelRecordLayout.FromAttributes`) | 40.69 ms | 21.09 KB | -| First fluent parse (`ExcelParser.Build`) | 36.11 ms | 24.03 KB | -| First fluent parse (`BuildWithAttributeFallback`) | 46.15 ms | 26.16 KB | +| First typed parse (`ExcelParser.FromAttributes`) | 46.64 ms | 20.91 KB | +| First fluent parse (`ExcelParser.Build`) | 42.14 ms | 23.84 KB | +| First fluent parse (`BuildWithAttributeFallback`) | 49.63 ms | 25.97 KB | -`Build` is ~11% faster than the reflection-based parser here; `BuildWithAttributeFallback` is slower than reflection (~13%, with a 5.4 ms standard deviation), since it runs the same `TypeMapper.GetInfo()` path plus the fluent build on top. +`Build` is ~10% faster than the reflection-based parser here; `BuildWithAttributeFallback` is slower than reflection (~6%, with a 0.4 ms standard deviation), since it runs the same `TypeMapper.GetInfo()` path plus the fluent build on top. Run the benchmarks locally: @@ -372,25 +372,25 @@ dotnet run --project tests/ExcelReader.Benchmarks/ExcelReader.Benchmarks.csproj | Entry point | Shape handed back | Mean | Managed allocated | |---|---|---:|---:| -| `xl_rows_next` | one row per call, serialized into a caller buffer | 37.93 ms | 3.71 MB | -| `xl_rows_read_all_blob` | whole sheet in one buffer | 42.74 ms | 21.06 MB | -| `xl_rows_read_all_decoded` | whole sheet as native row structs | 56.73 ms | 5.71 MB | -| `xl_parse_typed` | typed native columns | 35.73 ms | 3.71 MB | -| `xl_parse_arrow` | Arrow C Data Interface arrays | 36.93 ms | 3.72 MB | +| `xl_rows_next` | one row per call, serialized into a caller buffer | 37.72 ms | 3.71 MB | +| `xl_rows_read_all_blob` | whole sheet in one buffer | 40.31 ms | 21.06 MB | +| `xl_rows_read_all_decoded` | whole sheet as native row structs | 57.21 ms | 5.71 MB | +| `xl_parse_typed` | typed native columns | 34.30 ms | 3.71 MB | +| `xl_parse_arrow` | Arrow C Data Interface arrays | 35.19 ms | 3.72 MB | The allocation column is **managed memory only**. `MemoryDiagnoser` cannot see the native blocks that `xl_rows_read_all_decoded`, `xl_parse_typed` and `xl_parse_arrow` return their data in. -Row by row, `xl_rows_next` costs ~1.4x the managed in-memory read of the same file (37.93 ms vs. 27.084 ms for `Xlsb_ExcelReader_Memory` under [In-memory real-data reads](#in-memory-real-data-reads)). Every cell crosses the ABI as text, so each binary XLSB number is formatted to UTF-8 on the way out. `xl_rows_read_all_blob` adds ~13% over it, because the whole sheet is buffered before it is copied out. `xl_rows_read_all_decoded` adds ~50%, because it allocates a native block per row. `xl_parse_typed` and `xl_parse_arrow` are slightly faster than a row-by-row read (~3–6%) and return columnar native buffers with numbers still binary, so they are the path to use for bulk typed loads. +Row by row, `xl_rows_next` costs ~1.5x the managed in-memory read of the same file (37.72 ms vs. 24.465 ms for `Xlsb_ExcelReader_Memory` under [In-memory real-data reads](#in-memory-real-data-reads)). Every cell crosses the ABI as text, so each binary XLSB number is formatted to UTF-8 on the way out. `xl_rows_read_all_blob` adds ~7% over it, because the whole sheet is buffered before it is copied out. `xl_rows_read_all_decoded` adds ~52%, because it allocates a native block per row. `xl_parse_typed` and `xl_parse_arrow` are slightly faster than a row-by-row read (~7–9%) and return columnar native buffers with numbers still binary, so they are the path to use for bulk typed loads. `ChunkedParseBenchmark` compares `xl_parse_typed` on a whole sheet against draining the same sheet in batches through `xl_typed_reader_open`/`xl_typed_reader_next`. It uses the generated 50,000-row XLSX file, read from disk, with three nullable columns (string, int64, float64): | Mode | Mean | Managed allocated | |---|---:|---:| -| Whole sheet (`xl_parse_typed`) | 8.93 ms | 10 KB | -| Batches of 10,000 rows | 8.36 ms | 16.95 KB | -| Batches of 1,000 rows | 8.53 ms | 81.29 KB | +| Whole sheet (`xl_parse_typed`) | 8.55 ms | 10.06 KB | +| Batches of 10,000 rows | 8.09 ms | 17.00 KB | +| Batches of 1,000 rows | 7.86 ms | 81.34 KB | -Batching costs nothing in time: both batch sizes are slightly faster than one whole-sheet table (~4–6%). A caller can bound its native memory to one batch at a time without paying for it. +Batching costs nothing in time: both batch sizes are slightly faster than one whole-sheet table (~5–8%). A caller can bound its native memory to one batch at a time without paying for it. ### C++ and Rust bindings From d1a7e2b6d4f1977c673faa9548840f0ee37dcc45 Mon Sep 17 00:00:00 2001 From: GabrielMarquezMatte Date: Fri, 16 Oct 2026 02:11:14 -0300 Subject: [PATCH 5/7] feat: add benchmarks for large sheet parallel reading and writing --- README.md | 2 +- docs/guide/writing.md | 15 +- docs/performance/benchmarks.md | 55 +++++- .../Read/LargeSheetParallelBenchmark.cs | 153 +++++++++++++++ .../Shared/CsvGenerator.cs | 19 +- .../Write/ParallelRecordWriteBenchmark.cs | 177 ++++++++++++++++++ .../benchmark-groups.json | 4 +- 7 files changed, 403 insertions(+), 22 deletions(-) create mode 100644 tests/ExcelReader.Benchmarks/Read/LargeSheetParallelBenchmark.cs create mode 100644 tests/ExcelReader.Benchmarks/Write/ParallelRecordWriteBenchmark.cs diff --git a/README.md b/README.md index 9e48640f..5f745b20 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ errors to stderr, so `convert` is safe to pipe. - Each ZIP entry of an XLSX or XLSB package is checked against its CRC-32 as it is read, so a damaged file throws `InvalidDataException` instead of returning wrong cells. `ExcelReaderOptions.VerifyZipChecksums = false` turns the check off. - The XLSX writer emits a compact workbook with strings, numbers, booleans, dates, and blank cells; shared strings are opt-in. - The XLSB writer emits BIFF12 workbook parts inside the standard XLSB ZIP package; shared strings are opt-in. -- `WriteRecordsParallelAsync` and `WriteTableParallelAsync` format and compress batches of records on several cores and write them in order, for XLSX and XLSB. On a Ryzen 7 5700X a 1,000,000-row sheet writes in 50 ms as XLSX with 4 batches in flight and 36 ms with 8, against 187 ms serially; XLSB takes 41 ms and 28 ms against 124 ms. Memory stays at about 3 MB per batch in flight. See [Write records in parallel](docs/guide/writing.md#write-records-in-parallel-xlsxxlsb) and the [benchmarks](docs/performance/benchmarks.md#parallel-record-writes). +- `WriteRecordsParallelAsync` and `WriteTableParallelAsync` format and compress batches of records on several cores and write them in order, for XLSX and XLSB. On a Ryzen 7 5700X a 1,000,000-row sheet writes in 46 ms as XLSX with 4 batches in flight and 31 ms with 8, against 172 ms serially; XLSB takes 31 ms and 21 ms against 115 ms. Memory stays at about 3 MB per batch in flight. See [Write records in parallel](docs/guide/writing.md#write-records-in-parallel-xlsxxlsb) and the [benchmarks](docs/performance/benchmarks.md#parallel-record-writes). - The XLS writer buffers records in memory and assembles the OLE container at `EndAsync`; choose it when write throughput matters more than peak allocation. - `Excel.EncryptPackage`/`EncryptPackageAsync` wrap a written XLSX/XLSB package in agile ECMA-376 encryption; see [Encrypted workbooks](docs/guide/encryption.md). diff --git a/docs/guide/writing.md b/docs/guide/writing.md index 1dd68ecb..dd82c515 100644 --- a/docs/guide/writing.md +++ b/docs/guide/writing.md @@ -324,16 +324,17 @@ defaults each batch in flight holds about 3 MB (its formatted rows, the compress compressed result), whatever the size of the sheet. Writing 1,000,000 rows of four columns (text, integer, date, number) to a `MemoryStream` at -`Fastest`, on a Ryzen 7 5700X, median of 9 runs: +`Fastest`, on a Ryzen 7 5700X (`ParallelRecordWriteBenchmark`): | Write | XLSX | XLSB | |---|---:|---:| -| Serial | 187.3 ms | 123.6 ms | -| Serial, `PrefetchWrite` | 117.8 ms | 67.6 ms | -| Parallel, 1 batch in flight | 172.7 ms | 120.6 ms | -| Parallel, 2 | 92.1 ms | 63.2 ms | -| Parallel, 4 | 49.9 ms | 40.9 ms | -| Parallel, 8 | 35.7 ms | 27.7 ms | +| Serial | 171.57 ms | 115.39 ms | +| Serial, `PrefetchWrite` | 104.92 ms | 60.02 ms | +| Parallel, 1 batch in flight | 165.22 ms | 107.81 ms | +| Parallel, 2 | 89.69 ms | 57.94 ms | +| Parallel, 4 | 46.29 ms | 30.62 ms | +| Parallel, 8 | 30.56 ms | 21.04 ms | +| Parallel, 16 | 27.93 ms | 18.25 ms | The file comes out 0.18% (XLSX) or 0.04% (XLSB) larger, because each batch is compressed on its own. As with `PrefetchWrite`, this is for single-file batch work: a server already writing many files at diff --git a/docs/performance/benchmarks.md b/docs/performance/benchmarks.md index 4e836ba7..43d74e0c 100644 --- a/docs/performance/benchmarks.md +++ b/docs/performance/benchmarks.md @@ -7,7 +7,7 @@ Throwaway numbers go stale; these are regenerated from the benchmark suite in Benchmarks were run with BenchmarkDotNet v0.15.8 on Windows 10 (22H2), AMD Ryzen 7 5700X, .NET 10.0.12 (SDK 10.0.401). Generated-data benchmarks use 50,000 rows, except the string-heavy reads, which use 65,536. Raw results: [GitHub Pages benchmark dashboard](https://gabrielmarquezmatte.github.io/ExcelReader/dev/bench/) — `tests/ExcelReader.Benchmarks/BenchmarkDotNet.Artifacts/` is `.gitignore`-excluded and never actually reaches GitHub, so a link to it 404s for every reader. -Every BenchmarkDotNet table is a `--job Short` run (1 launch, 3 warmup and 3 measured iterations) on an otherwise idle machine. Rows whose 99.9% confidence interval came out wider than 25% of the mean were run again with `--job Medium` (2 launches, 15 iterations) and carry those figures instead: the parallel CSV tables, the CSV cell-by-cell read, the Sylvan XLS read, the Sylvan string-heavy reads, the string-heavy XLSX matched-work read, the generated-workbook matched-work read, `DataTable.Load` and the large encrypted read. The cold-start rows set their own launch count, and the parallel record writes, the Python/C++/Rust figures and the native-library comparison come from other harnesses that were not re-run. +Every BenchmarkDotNet table is a `--job Short` run (1 launch, 3 warmup and 3 measured iterations) on an otherwise idle machine. Rows whose 99.9% confidence interval came out wider than 25% of the mean were run again with `--job Medium` (2 launches, 15 iterations) and carry those figures instead: the parallel CSV tables, the CSV cell-by-cell read, the Sylvan XLS read, the Sylvan string-heavy reads, the string-heavy XLSX matched-work read, the generated-workbook matched-work read, `DataTable.Load` and the large encrypted read. The cold-start rows set their own launch count, and the file-size and working-set figures under the parallel record writes, the Python/C++/Rust figures and the native-library comparison come from other harnesses that were not re-run. The large-sheet and parallel-write tables are `--job Medium` runs. **Benchmark methodology.** In the Excel-format tables below, the **"Cell-by-cell read"** rows (including the per-format rows in "Real data reads" / "String-heavy reads") read ExcelReader's `cell.Value` — a zero-copy `ReadOnlySpan`, no decode or allocation — against each competitor's own idiomatic read call. For Sylvan.Data.Excel, that's the ADO.NET-style `GetString(i)`, which is forced to materialize a UTF-16 `string`; its API has no zero-copy accessor, so it cannot avoid that cost. These rows are therefore not matched work: part of the reported gap is "we parse faster" and part is "we skipped an allocation you were never offered a way to skip." Each affected benchmark class also has a `*_Materialized` sibling (calling `cell.GetString()`, the same UTF-16 materialization Sylvan pays) so the matched-work number is measurable — see `tests/ExcelReader.Benchmarks/Shared/BenchmarkAccumulators.cs`. **Those results are published under [Matched-work reads](#matched-work-reads-_materialized), and reading them is the honest way to judge the comparison:** treat the Excel cell-by-cell ratios in the tables below as an upper bound, not a like-for-like number. The CSV cell-by-cell rows are matched work: Sep and Sylvan.Data.Csv both expose span accessors (`row[i].Span`, `GetFieldSpan(i)`), and the benchmarks use them. The **"Typed row parsing"** / **"Typed record writing"** rows are unaffected — both sides already materialize real objects/strings there, so those comparisons are matched work as published. @@ -82,6 +82,8 @@ The typed path scales ~2.3x by dop 4 on the conversion-heavy corpus (587.8 to 25 The aggregate columns fold each record into a per-partition accumulator through a `ref struct` model, so text columns stay as `ReadOnlySpan` and no row object is ever materialized. They scale better than the typed path — ~4.9x (conversion-heavy) and ~5.0x (narrow) at dop 8, ~6.4x and ~6.3x at dop 16 — because they are not fighting the allocator: at dop 4 the conversion-heavy aggregate is ~1.8x faster than typed while allocating ~450x less (1.50 MB against 680.39 MB), with **zero** garbage collections at every degree. The remaining allocation is per-partition bookkeeping, not per-row. +The typed path's slowdown past dop 8 is the garbage collector. Every degree allocates the same ~680 MB and runs 42 gen0 collections, but at dop 16 each of them also collects gen1 (41 gen1 collections against 24 at dop 8), and the pauses add up to 100-200 ms against 39 ms. In a console harness on the 4.3 million row corpus, with the default workstation GC, dop 16 took 365 ms; with `DOTNET_gcServer=1` it took 182 ms, faster than dop 8. The aggregate path allocates no row objects and is unaffected. + Type conversion is the floor under both. A single-threaded pass that allocates nothing still costs 472.0 ms, so parsing bytes into `DateTime`/`decimal`/`int` — not allocation and not I/O — is what the parallelism is actually buying down. Against [Sep](https://github.com/nietras/Sep)'s own `ParallelEnumerate` (all cores; 8,000,000 narrow rows, 3,000,000 conversion-heavy rows): @@ -106,6 +108,38 @@ Sequentially, ExcelReader's typed parser is ~1.1x faster than Sep on narrow ints At dop 4 XLSX reads ~3.2x faster than sequentially and XLSB ~2.6x. Dop 2 is a single parse worker running alongside the inflating thread. Past dop 4 nothing improves: the one inflating thread is the floor (~14 ms for XLSX, ~9 ms for XLSB, see [Inflate](#inflate), plus the XLSB record walk that finds row boundaries). Allocation barely moves with dop (8.02 to 8.16 MB): it is the typed model's strings. +### Large sheets + +`LargeSheetParallelBenchmark` reads sheets generated in memory at the largest sizes the formats allow. XLSX and XLSB hold 1,000,000 rows by four columns (text, integer, date, number), the limit being 1,048,576 rows per sheet; CSV holds 4,000,000 conversion-heavy rows (two strings, a date, two decimals and an int), and has no such limit. "Aggregate" folds every row into a per-partition sum; "typed parse" is the ordered `ParseAsync` into a class (`ref struct` for the CSV aggregate). + +| XLSX, Dop | Aggregate | Typed parse | +|---:|---:|---:| +| 1 | 215.91 ms, 77.25 MB | 243.27 ms, 77.25 MB | +| 2 | 169.84 ms, 77.62 MB | 185.00 ms, 106.81 MB | +| 4 | 64.81 ms, 77.58 MB | 74.01 ms, 106.75 MB | +| 8 | 51.11 ms, 77.52 MB | 55.44 ms, 106.78 MB | +| 16 | 44.51 ms, 77.94 MB | 57.32 ms, 107.11 MB | + +| XLSB, Dop | Aggregate | Typed parse | +|---:|---:|---:| +| 1 | 160.91 ms, 77.25 MB | 189.08 ms, 77.25 MB | +| 2 | 119.50 ms, 77.53 MB | 127.86 ms, 99.69 MB | +| 4 | 48.76 ms, 77.46 MB | 58.04 ms, 99.59 MB | +| 8 | 46.92 ms, 77.61 MB | 53.51 ms, 99.74 MB | +| 16 | 49.19 ms, 77.88 MB | 54.30 ms, 100.00 MB | + +| CSV, Dop | Aggregate | Typed parse | +|---:|---:|---:| +| 1 | 426.07 ms, 1.99 KB | 543.94 ms, 623.08 MB | +| 2 | 226.22 ms, 6.77 KB | 338.08 ms, 626.87 MB | +| 4 | 118.30 ms, 10.60 KB | 191.11 ms, 626.71 MB | +| 8 | 90.67 ms, 18.32 KB | 189.98 ms, 626.43 MB | +| 16 | 64.16 ms, 34.29 KB | 207.63 ms, 626.57 MB | + +The XLSX and XLSB aggregates and typed parses each materialize a `Record` and its name string per row, which is where their ~77-107 MB comes from; the CSV aggregate binds a `ref struct` and allocates kilobytes, while the CSV typed parse allocates ~626 MB at every degree, one row object and two strings per record. + +XLSX and XLSB reach ~3.3x at dop 4 and then barely improve, as in the real-data table above (the XLSX aggregate gets to 4.9x at dop 16, the XLSB aggregate to 3.4x at dop 8). The CSV aggregate keeps scaling because nothing but the parser runs on the workers: 4.7x at dop 8 and 6.6x at dop 16. The typed CSV parse reaches 2.9x at dop 8 and then loses 9% at dop 16 to the collector (see [Parallel CSV](#parallel-csv)), so for a 4-million-row file the aggregate at dop 16 is ~3.2x faster than the typed parse and allocates no row objects. + ### Real data reads This benchmark reads a real workbook exported in multiple formats. @@ -205,20 +239,21 @@ The encoder's limits are deliberate: ### Parallel record writes -`WriteRecordsParallelAsync` formats and compresses batches of 8,192 records on pool threads and appends them to the sheet in order. Writing 1,000,000 rows by four columns (text, integer, date, number) into a pre-sized `MemoryStream` at `Fastest`, on the same Ryzen 7 5700X. Each figure is the median of 9 writes after 3 warm-up writes, from a console harness rather than BenchmarkDotNet: +`WriteRecordsParallelAsync` formats and compresses batches of 8,192 records on pool threads and appends them to the sheet in order. `ParallelRecordWriteBenchmark` writes 1,000,000 rows by four columns (text, integer, date, number) into a pre-sized `MemoryStream` at `Fastest`, on the same Ryzen 7 5700X, with a number of batches in flight from 1 to 16: | Write | XLSX | vs serial | XLSB | vs serial | |---|---:|---:|---:|---:| -| Serial | 187.3 ms | 1.0x | 123.6 ms | 1.0x | -| Serial, `PrefetchWrite` | 117.8 ms | 1.6x | 67.6 ms | 1.8x | -| Parallel, 1 batch in flight | 172.7 ms | 1.1x | 120.6 ms | 1.0x | -| Parallel, 2 | 92.1 ms | 2.0x | 63.2 ms | 2.0x | -| Parallel, 4 | 49.9 ms | 3.8x | 40.9 ms | 3.0x | -| Parallel, 8 | 35.7 ms | 5.2x | 27.7 ms | 4.5x | +| Serial | 171.57 ms, 16.11 KB | 1.0x | 115.39 ms, 13.42 KB | 1.0x | +| Serial, `PrefetchWrite` | 104.92 ms, 18.43 KB | 1.6x | 60.02 ms, 16.25 KB | 1.9x | +| Parallel, 1 batch in flight | 165.22 ms, 48.09 KB | 1.0x | 107.81 ms, 45.48 KB | 1.1x | +| Parallel, 2 | 89.69 ms, 83.40 KB | 1.9x | 57.94 ms, 81.92 KB | 2.0x | +| Parallel, 4 | 46.29 ms, 79.39 KB | 3.7x | 30.62 ms, 77.87 KB | 3.8x | +| Parallel, 8 | 30.56 ms, 74.61 KB | 5.6x | 21.04 ms, 70.78 KB | 5.5x | +| Parallel, 16 | 27.93 ms, 72.66 KB | 6.1x | 18.25 ms, 66.78 KB | 6.3x | -It scales because a write is bound by building rows, not by deflate: `PrefetchWrite` already takes deflate off the calling thread and stops at 1.6x to 1.8x, so compressing chunks in parallel would add nothing on top of it, while formatting the rows in parallel does. +It scales because a write is bound by building rows, not by deflate: `PrefetchWrite` already takes deflate off the calling thread and stops at 1.6x to 1.9x, so compressing chunks in parallel would add nothing on top of it, while formatting the rows in parallel does. Past dop 8 the gain is small, because the machine has 8 physical cores. None of the rows triggers a garbage collection, and every write allocates under 85 KB for 1,000,000 rows. -Each batch is compressed with no reference to the one before it, which costs 0.18% of file size for XLSX (15,481,372 B to 15,508,760 B) and 0.04% for XLSB (16,863,131 B to 16,869,306 B). The process's peak working set rose by 12 MB over the serial write with 4 batches in flight and by 23 MB with 8, about 3 MB per batch, and does not depend on the number of rows. +Each batch is compressed with no reference to the one before it, which costs 0.18% of file size for XLSX (15,481,372 B to 15,508,760 B) and 0.04% for XLSB (16,863,131 B to 16,869,306 B). The process's peak working set rose by 12 MB over the serial write with 4 batches in flight and by 23 MB with 8, about 3 MB per batch, and does not depend on the number of rows. The file sizes and working sets come from a console harness, because BenchmarkDotNet does not report them. ### Matched-work reads (`*_Materialized`) diff --git a/tests/ExcelReader.Benchmarks/Read/LargeSheetParallelBenchmark.cs b/tests/ExcelReader.Benchmarks/Read/LargeSheetParallelBenchmark.cs new file mode 100644 index 00000000..899fe36d --- /dev/null +++ b/tests/ExcelReader.Benchmarks/Read/LargeSheetParallelBenchmark.cs @@ -0,0 +1,153 @@ +using BenchmarkDotNet.Attributes; +using ExcelReader.Core.Parser; +using ExcelReader.Core.Reader; +using ExcelReader.Core.Reader.Csv; +using ExcelReader.Core.Reader.Xlsb; +using ExcelReader.Core.Reader.Xlsx; +using ExcelReader.Core.Writer; +using ExcelReader.Core.Writer.Xlsb; +using ExcelReader.Core.Writer.Xlsx; + +namespace ExcelReader.Benchmarks +{ + [MemoryDiagnoser] + public class LargeSheetParallelBenchmark + { + private const int Rows = 1_000_000; + private const int CsvRows = 4_000_000; + + private sealed class Sum : IRowAccumulator + { + public long Total; + + public void Add(Record model) + { + Total += model.Id + (long)model.Value + model.Name!.Length; + } + + public void Merge(Sum following) + { + Total += following.Total; + } + } + + private sealed class CsvSum : IRowAccumulator + { + public long Total; + + public void Add(WideRowRef model) + { + Total += model.Units + (long)model.TotalRevenue + model.Region.Length; + } + + public void Merge(CsvSum following) + { + Total += following.Total; + } + } + + private readonly ExcelParser _parser = ExcelParser.FromAttributes(); + private readonly ExcelParser _wideParser = ExcelParser.FromAttributes(); + private readonly ExcelParser _wideRefParser = ExcelParser.FromAttributes(); + private byte[] _xlsx = []; + private byte[] _xlsb = []; + private byte[] _csv = []; + + [Params(1, 2, 4, 8, 16)] + public int Dop { get; set; } + + [GlobalSetup] + public async Task SetupAsync() + { + _csv = CsvGenerator.BuildConversionHeavy(CsvRows); + List records = WorkbookGenerator.Records(Rows); + ExcelRecordLayout layout = ExcelRecordLayout.FromAttributes(); + await using (var ms = new MemoryStream(32 * 1024 * 1024)) + { + await using (var writer = XlsxWorkbookWriter.Create(ms, leaveOpen: true)) + { + await using (var sheet = writer.AddSheet("S1")) + { + await sheet.WriteRecordsParallelAsync(records, layout); + } + } + _xlsx = ms.ToArray(); + } + await using (var ms = new MemoryStream(32 * 1024 * 1024)) + { + await using (var writer = XlsbWorkbookWriter.Create(ms, leaveOpen: true)) + { + await using (var sheet = writer.AddSheet("S1")) + { + await sheet.WriteRecordsParallelAsync(records, layout); + } + } + _xlsb = ms.ToArray(); + } + } + + [Benchmark] + public async Task Xlsx_Aggregate() + { + using XlsxWorkbook workbook = Excel.FromXlsx(_xlsx); + Sum sum = await SheetParallel.AggregateAsync(workbook.FirstSheet, _parser, new SheetParallelOptions { DegreeOfParallelism = Dop, HeaderRow = 1 }); + return sum.Total; + } + + [Benchmark] + public async Task Xlsb_Aggregate() + { + using XlsbWorkbook workbook = Excel.FromXlsb(_xlsb); + Sum sum = await SheetParallel.AggregateAsync(workbook.FirstSheet, _parser, new SheetParallelOptions { DegreeOfParallelism = Dop, HeaderRow = 1 }); + return sum.Total; + } + + [Benchmark] + public async Task Xlsx_Parse() + { + long total = 0; + using XlsxWorkbook workbook = Excel.FromXlsx(_xlsx); + await foreach (Record row in SheetParallel.ParseAsync(workbook.FirstSheet, _parser, new SheetParallelOptions { DegreeOfParallelism = Dop })) + { + total += row.Id; + } + return total; + } + + [Benchmark] + public async Task Xlsb_Parse() + { + long total = 0; + using XlsbWorkbook workbook = Excel.FromXlsb(_xlsb); + await foreach (Record row in SheetParallel.ParseAsync(workbook.FirstSheet, _parser, new SheetParallelOptions { DegreeOfParallelism = Dop })) + { + total += row.Id; + } + return total; + } + + [Benchmark] + public async Task Csv_Aggregate() + { + using CsvReader csv = Excel.FromCsv(_csv); + CsvSum sum = await SheetParallel.AggregateAsync(csv.FirstSheet, _wideRefParser, new SheetParallelOptions { DegreeOfParallelism = Dop, HeaderRow = 1 }); + if (sum.Total == 0) + { + throw new InvalidOperationException("WideRowRef bound no columns; the aggregate benchmark is measuring nothing."); + } + return sum.Total; + } + + [Benchmark] + public async Task Csv_Parse() + { + long total = 0; + using CsvReader csv = Excel.FromCsv(_csv); + await foreach (WideRow row in SheetParallel.ParseAsync(csv.FirstSheet, _wideParser, new SheetParallelOptions { DegreeOfParallelism = Dop })) + { + total += row.Units; + } + return total; + } + } +} diff --git a/tests/ExcelReader.Benchmarks/Shared/CsvGenerator.cs b/tests/ExcelReader.Benchmarks/Shared/CsvGenerator.cs index ed4a201b..af601303 100644 --- a/tests/ExcelReader.Benchmarks/Shared/CsvGenerator.cs +++ b/tests/ExcelReader.Benchmarks/Shared/CsvGenerator.cs @@ -55,9 +55,25 @@ public static byte[] BuildTyped(int rows) internal static string WriteConversionHeavyFile(int rows) { string path = Path.Combine(Path.GetTempPath(), $"exr-bench-wide-{Guid.NewGuid():N}.csv"); + using var writer = new StreamWriter(path, append: false); + WriteConversionHeavy(writer, rows); + return path; + } + + internal static byte[] BuildConversionHeavy(int rows) + { + using var ms = new MemoryStream(rows * 48); + using (var writer = new StreamWriter(ms, new UTF8Encoding(false), 1 << 16, leaveOpen: true)) + { + WriteConversionHeavy(writer, rows); + } + return ms.ToArray(); + } + + private static void WriteConversionHeavy(TextWriter writer, int rows) + { string[] regions = ["Europe", "Asia", "North America", "Sub-Saharan Africa"]; string[] countries = ["Portugal", "Japan", "Canada", "Kenya", "Brazil", "Norway"]; - using var writer = new StreamWriter(path, append: false); writer.WriteLine("Region,Country,OrderDate,UnitPrice,TotalRevenue,Units"); var start = new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Unspecified); for (int i = 0; i < rows; i++) @@ -77,7 +93,6 @@ internal static string WriteConversionHeavyFile(int rows) writer.Write(','); writer.WriteLine(((i % 500) + 1).ToString(CultureInfo.InvariantCulture)); } - return path; } internal static string WriteNarrowIntFile(int rows) diff --git a/tests/ExcelReader.Benchmarks/Write/ParallelRecordWriteBenchmark.cs b/tests/ExcelReader.Benchmarks/Write/ParallelRecordWriteBenchmark.cs new file mode 100644 index 00000000..84399709 --- /dev/null +++ b/tests/ExcelReader.Benchmarks/Write/ParallelRecordWriteBenchmark.cs @@ -0,0 +1,177 @@ +using System.Diagnostics.CodeAnalysis; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using ExcelReader.Core.Writer; +using ExcelReader.Core.Writer.Xlsb; +using ExcelReader.Core.Writer.Xlsx; + +namespace ExcelReader.Benchmarks +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + [CategoriesColumn] + [SuppressMessage("Design", "CA1001", Justification = "BenchmarkDotNet owns the instance; GlobalCleanup releases the stream.")] + public class ParallelRecordWriteBenchmark + { + private const int Rows = 1_000_000; + + private static readonly ExcelRecordLayout Layout = ExcelRecordLayout.FromAttributes(); + + private readonly MemoryStream _output = new(32 * 1024 * 1024); + private List _records = []; + + [GlobalSetup] + public void Setup() + { + _records = WorkbookGenerator.Records(Rows); + } + + [GlobalCleanup] + public void Cleanup() + { + _output.Dispose(); + } + + private async Task WriteXlsxAsync(int degree, bool prefetch = false) + { + _output.SetLength(0); + await using (var writer = XlsxWorkbookWriter.Create(_output, leaveOpen: true, options: new XlsxWriterOptions { PrefetchWrite = prefetch })) + { + await using (var sheet = writer.AddSheet("S1")) + { + await WriteAsync(sheet, degree); + } + } + return _output.Length; + } + + private async Task WriteXlsbAsync(int degree, bool prefetch = false) + { + _output.SetLength(0); + await using (var writer = XlsbWorkbookWriter.Create(_output, leaveOpen: true, options: new XlsbWriterOptions { PrefetchWrite = prefetch })) + { + await using (var sheet = writer.AddSheet("S1")) + { + await WriteAsync(sheet, degree); + } + } + return _output.Length; + } + + private ValueTask WriteAsync(XlsxSheetWriter sheet, int degree) + { + if (degree == 0) + { + return sheet.WriteRecordsAsync(_records, Layout); + } + return sheet.WriteRecordsParallelAsync(_records, Layout, new ParallelWriteOptions { DegreeOfParallelism = degree }); + } + + private ValueTask WriteAsync(XlsbSheetWriter sheet, int degree) + { + if (degree == 0) + { + return sheet.WriteRecordsAsync(_records, Layout); + } + return sheet.WriteRecordsParallelAsync(_records, Layout, new ParallelWriteOptions { DegreeOfParallelism = degree }); + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Xlsx")] + public Task Xlsx_Serial() + { + return WriteXlsxAsync(0); + } + + [Benchmark] + [BenchmarkCategory("Xlsx")] + public Task Xlsx_Serial_Prefetch() + { + return WriteXlsxAsync(0, prefetch: true); + } + + [Benchmark] + [BenchmarkCategory("Xlsx")] + public Task Xlsx_Parallel_1() + { + return WriteXlsxAsync(1); + } + + [Benchmark] + [BenchmarkCategory("Xlsx")] + public Task Xlsx_Parallel_2() + { + return WriteXlsxAsync(2); + } + + [Benchmark] + [BenchmarkCategory("Xlsx")] + public Task Xlsx_Parallel_4() + { + return WriteXlsxAsync(4); + } + + [Benchmark] + [BenchmarkCategory("Xlsx")] + public Task Xlsx_Parallel_8() + { + return WriteXlsxAsync(8); + } + + [Benchmark] + [BenchmarkCategory("Xlsx")] + public Task Xlsx_Parallel_16() + { + return WriteXlsxAsync(16); + } + + [Benchmark(Baseline = true)] + [BenchmarkCategory("Xlsb")] + public Task Xlsb_Serial() + { + return WriteXlsbAsync(0); + } + + [Benchmark] + [BenchmarkCategory("Xlsb")] + public Task Xlsb_Serial_Prefetch() + { + return WriteXlsbAsync(0, prefetch: true); + } + + [Benchmark] + [BenchmarkCategory("Xlsb")] + public Task Xlsb_Parallel_1() + { + return WriteXlsbAsync(1); + } + + [Benchmark] + [BenchmarkCategory("Xlsb")] + public Task Xlsb_Parallel_2() + { + return WriteXlsbAsync(2); + } + + [Benchmark] + [BenchmarkCategory("Xlsb")] + public Task Xlsb_Parallel_4() + { + return WriteXlsbAsync(4); + } + + [Benchmark] + [BenchmarkCategory("Xlsb")] + public Task Xlsb_Parallel_8() + { + return WriteXlsbAsync(8); + } + + [Benchmark] + [BenchmarkCategory("Xlsb")] + public Task Xlsb_Parallel_16() + { + return WriteXlsbAsync(16); + } + } +} diff --git a/tests/ExcelReader.Benchmarks/benchmark-groups.json b/tests/ExcelReader.Benchmarks/benchmark-groups.json index be6bdae8..fc91cad6 100644 --- a/tests/ExcelReader.Benchmarks/benchmark-groups.json +++ b/tests/ExcelReader.Benchmarks/benchmark-groups.json @@ -19,13 +19,13 @@ }, { "name": "write", - "filter": "ExcelReader.Benchmarks.WriteBenchmark.* ExcelReader.Benchmarks.RecordWriteBenchmark.* ExcelReader.Benchmarks.XlsWriteBenchmark.* ExcelReader.Benchmarks.CsvWriteBenchmark.* ExcelReader.Benchmarks.WritePathBenchmark.*", + "filter": "ExcelReader.Benchmarks.WriteBenchmark.* ExcelReader.Benchmarks.RecordWriteBenchmark.* ExcelReader.Benchmarks.XlsWriteBenchmark.* ExcelReader.Benchmarks.CsvWriteBenchmark.* ExcelReader.Benchmarks.WritePathBenchmark.* ExcelReader.Benchmarks.ParallelRecordWriteBenchmark.*", "pagesArgs": "--warmupCount 3 --iterationCount 10", "prArgs": "--warmupCount 1 --iterationCount 5" }, { "name": "realdata", - "filter": "ExcelReader.Benchmarks.RealDataReadBenchmark.* ExcelReader.Benchmarks.RealDataTypedParseBenchmark.* ExcelReader.Benchmarks.SheetParallelBenchmark.*", + "filter": "ExcelReader.Benchmarks.RealDataReadBenchmark.* ExcelReader.Benchmarks.RealDataTypedParseBenchmark.* ExcelReader.Benchmarks.SheetParallelBenchmark.* ExcelReader.Benchmarks.LargeSheetParallelBenchmark.*", "pagesArgs": "--warmupCount 3 --iterationCount 10", "prArgs": "--warmupCount 1 --iterationCount 5" }, From 449ca144a98b98ef119d17d0c4bccde2e0ab7cbc Mon Sep 17 00:00:00 2001 From: GabrielMarquezMatte Date: Fri, 16 Oct 2026 02:23:41 -0300 Subject: [PATCH 6/7] build(rust): track the workspace Cargo.lock crates.io has yanked every zip release except 9.0.3. calamine 0.36.1 needs zip ^8.6 and rust_xlsxwriter 0.99.1 needs zip ^8.3, so a fresh resolution fails ("version 8.6.0 is yanked") and every Rust CI job with it. Cargo keeps a yanked version that a lockfile already holds, so committing the lock restores CI until both crates move to zip 9. They are dev-dependencies of the comparison benchmarks only; nothing ships with zip. --- rust/.gitignore | 2 +- rust/Cargo.lock | 1455 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1456 insertions(+), 1 deletion(-) create mode 100644 rust/Cargo.lock diff --git a/rust/.gitignore b/rust/.gitignore index 78b02f0e..4c069698 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,5 +1,5 @@ /target/ -/Cargo.lock +# The workspace lockfile is tracked: every zip 8.x is yanked, and calamine and rust_xlsxwriter only resolve through a lock that already holds it # Workspace members shouldn't have their own Cargo.lock or target/ /excelreader/Cargo.lock /excelreader/target/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 00000000..926a59f3 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,1455 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "arrow" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234ce58e76c04e8162c2e4bc3fe53fa7c2418ef5fba7bb2838b8531b7d4fbf14" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2303ebedb1071f4f72d71171e383859a9a285c94d99e4892685a96fca9890720" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-array" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51dff6e4b9f158864a0aeb6a131ad53858ecbae7a2fc8989307b9b3c6b2f122e" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown", + "libc", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7235b863533e9db3ab86905b4521251de11226275b436ff3cb2d0010558d1c5" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cf840f0c34aeeb7e8b05aba72d66878e6c001827f423331f0b260209e7b7e" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-cmp" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a0e9adb28d95739fabcdae637261a2c14e618caf58ba1b88857120f9ee2687" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-schema", +] + +[[package]] +name = "arrow-data" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa06d5e22786c4966ffc4d2f171d0cb65d95f6bdea108a14e8c335e9faa437f" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ord" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7ac2c6a9cdc782fa9548d3763461eaec8c93347e297fddbaa44807e70a3698" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cmp", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758b87e9648d80001e0916c083334ed93074c677a81a96144d5c84bd2d8eab2a" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85a9a58cd03560526b0066487574aacb9008e413504fa0ee23d61141e96c338" +dependencies = [ + "bitflags", +] + +[[package]] +name = "arrow-select" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3482b7d03bf4a3bf0b195f08a7f2b071595ccd54897e35b975ac63d3104977" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-cmp", + "arrow-data", + "arrow-schema", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "60.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f300b4400b1e7cd58f7cbbd5dd2a0072cc59e73bfaee2a152b3ac660e46d9dd1" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + +[[package]] +name = "atoi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a8bbe9949e43a1edaa043038c68703b04774156afdfb62ba2cef5bf93d67be" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atoi_simd" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cdb3708a128e559a30fb830e8a77a5022ee6902806925c216658652b452a44" +dependencies = [ + "debug_unsafe", + "rustversion", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calamine" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa68281b1a76b54a62156474adb06bb380a67e07dd60656e3217152b42183f3" +dependencies = [ + "atoi_simd", + "byteorder", + "chrono", + "codepage", + "encoding_rs", + "fast-float2", + "log", + "quick-xml", + "serde", + "zip", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "debug_unsafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "excelreader" +version = "0.0.0" +dependencies = [ + "arrow", + "calamine", + "cc", + "chrono", + "criterion", + "excelreader-derive", + "rust_xlsxwriter", + "trybuild", +] + +[[package]] +name = "excelreader-derive" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "fast-float2" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rust_xlsxwriter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f2d2cda7223168a14fb86bc7411dbc3379bd7d49749ecc53b91ed98585b6b0" +dependencies = [ + "zip", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "trybuild" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] From 282e8b93142e0d3c2b8b9d93c092950080c29a17 Mon Sep 17 00:00:00 2001 From: GabrielMarquezMatte Date: Fri, 16 Oct 2026 02:50:43 -0300 Subject: [PATCH 7/7] docs: update benchmark results for performance improvements across C++, Python, and Rust implementations --- cpp/README.md | 58 +++++++++++++++---------------- docs/performance/benchmarks.md | 18 +++++----- python/README.md | 62 +++++++++++++++++----------------- rust/excelreader/README.md | 29 +++++++--------- 4 files changed, 82 insertions(+), 85 deletions(-) diff --git a/cpp/README.md b/cpp/README.md index bbcf5bcc..ff58c24f 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -413,15 +413,15 @@ on Windows 10 (22H2), 16 logical CPUs @ 3.39 GHz, MSVC 19.51 (Release), Google B | Benchmark | RealExcel.xlsb (100 rows) | 65K_Records_Data.xlsb (65,535 rows) | |---|---:|---:| -| `open` (from memory) | 24.0 µs | 1.17 ms | -| `parse` (4 or 6 bound columns) | 47.8 µs | 24.7 ms | -| `parse_batches`, batches of 1,000 / 10,000 rows | - | 23.8 ms / 23.8 ms | -| `infer_schema` (sample 100 / 1,000 rows) | 75.6 µs | 579.1 µs | +| `open` (from memory) | 23.4 µs | 1.17 ms | +| `parse` (4 or 6 bound columns) | 44.8 µs | 23.3 ms | +| `parse_batches`, batches of 1,000 / 10,000 rows | - | 22.4 ms / 22.9 ms | +| `infer_schema` (sample 100 / 1,000 rows) | 68.8 µs | 572.7 µs | `open` here is the byte-span overload, which copies the caller's buffer before returning (the caller may free it afterwards), so it scales with file size: 1.17 ms is mostly copying the 3.7 MB fixture. Opening by path reads only the package directory and stays nearly flat — see the Rust suite below. -`parse` scales linearly with rows × columns, at roughly 380 ns/row here. Draining the same +`parse` scales linearly with rows × columns, at roughly 360 ns/row here. Draining the same sheet through the batched typed reader costs the same as one whole-sheet `parse` while holding only one batch in memory at a time. `infer_schema` scales with its sample size, not the file's total row count. @@ -436,12 +436,12 @@ sides decode every cell into an owned value (`std::string` for text, not the zer | Library | Mean | |---|---:| -| ExcelReader (`parse`) | 62.9 ms | -| DuckDB (`read_xlsx`, summing every column in SQL) | 410.8 ms | -| xlsxio (`xlsxioread_sheet_next_cell_*`) | 512.9 ms | -| xlnt (`worksheet::rows()` + `cell::to_string()`/`value()`) | 2,394.1 ms | +| ExcelReader (`parse`) | 61.3 ms | +| DuckDB (`read_xlsx`, summing every column in SQL) | 413.8 ms | +| xlsxio (`xlsxioread_sheet_next_cell_*`) | 497.4 ms | +| xlnt (`worksheet::rows()` + `cell::to_string()`/`value()`) | 2,335.0 ms | -ExcelReader is ~6.5x faster than DuckDB, ~8.2x faster than xlsxio and ~38x faster than xlnt on this +ExcelReader is ~6.7x faster than DuckDB, ~8.1x faster than xlsxio and ~38x faster than xlnt on this workload. xlsxio is a lean, purpose-built C streaming reader — the same abstraction level as ExcelReader's own native core — so it was the strongest of the two competitors tested, though still well behind. @@ -481,11 +481,11 @@ Measured on the same machine as above, GCC 16.2.0 (MSYS2 UCRT64, `-O3 -mavx2` fo | Workload | ExcelReader | zsv `compat` | zsv `fast` | |---|---:|---:|---:| -| Typed, 14 columns into `FullRow` | 27.2 ms | 23.4 ms | 20.4 ms | -| Cells, byte count | 11.8 ms | 6.5 ms | 3.6 ms | +| Typed, 14 columns into `FullRow` | 27.4 ms | 22.6 ms | 20.0 ms | +| Cells, byte count | 11.6 ms | 5.8 ms | 3.5 ms | -**zsv is faster on both.** Typed, it is ~1.2x (`compat`) and ~1.3x (`fast`) faster than ExcelReader. -Counting cells, it is ~1.8x and ~3.3x faster. +**zsv is faster on both.** Typed, it is ~1.2x (`compat`) and ~1.4x (`fast`) faster than ExcelReader. +Counting cells, it is ~2.0x and ~3.3x faster. The cells gap is the C ABI, not the CSV parser. `xl_rows_next_view` copies every cell's value into a native row the cursor owns, one call per row, and that alone costs ~10.6 ms here (calling it without @@ -522,11 +522,11 @@ is handed buffers that are already columnar, while `BM_WriteSheet` starts from a | Benchmark | Time | Rows/s | |---|---:|---:| -| `BM_WriteColumns` (pre-transposed) | 45.8 ms | 1.43 M/s | -| `BM_WriteSheet` (from `std::vector`) | 52.2 ms | 1.27 M/s | +| `BM_WriteColumns` (pre-transposed) | 33.2 ms | 1.98 M/s | +| `BM_WriteSheet` (from `std::vector`) | 40.3 ms | 1.63 M/s | -The transpose costs ~14% here. It is not free, but it is not the dominant cost of producing the -file — see the comparison below, where the same two cases over 14 columns land within ~12%. +The transpose costs ~21% here. It is not free, but it is not the dominant cost of producing the +file — see the comparison below, where the same two cases over 14 columns land within ~14–16%. `excelreader_cpp_write_compare_benchmarks` (under `-DEXCELREADER_BUILD_BENCHMARKS_COMPARE=ON`) puts that against xlnt, xlsxio and [DuckDB](https://github.com/duckdb/duckdb)'s `excel` extension, all @@ -543,12 +543,12 @@ competitor. | Library | Wall | CPU | |---|---:|---:| -| ExcelReader (`xl::write_columns`, pre-transposed) | 85.4–86.5 ms | 85.1–87.1 ms | -| ExcelReader (`xl::write_sheet`) | 95.5–96.5 ms | 96.0–98.2 ms | -| DuckDB (`COPY ... TO ... WITH (FORMAT xlsx)`) | 828.9–833.2 ms | 828.1 ms | -| libxlsxwriter (`worksheet_write_string`/`_number`) | 1,193.9 ms | 1,203.1 ms | -| xlsxio (`xlsxiowrite_add_cell_*`) | 2,433.2 ms | 953.1 ms | -| xlnt (`worksheet::cell().value()` + `save()`) | 5,436.3 ms | 5,437.5 ms | +| ExcelReader (`xl::write_columns`, pre-transposed) | 64.2–66.9 ms | 63.9–68.2 ms | +| ExcelReader (`xl::write_sheet`) | 74.8–76.0 ms | 74.7–76.4 ms | +| DuckDB (`COPY ... TO ... WITH (FORMAT xlsx)`) | 831.4–831.6 ms | 828.1 ms | +| libxlsxwriter (`worksheet_write_string`/`_number`) | 1,206.7 ms | 1,187.5 ms | +| xlsxio (`xlsxiowrite_add_cell_*`) | 2,405.1 ms | 1,125.0 ms | +| xlnt (`worksheet::cell().value()` + `save()`) | 5,525.3 ms | 5,515.6 ms | Same machine as above; Google Benchmark's own iteration counts, no `--benchmark_repetitions` (each iteration writes a whole 65,535-row file, so the slower cases run once or a handful of times). xlnt @@ -558,8 +558,8 @@ DuckDB rows appear in both, and the small ranges above are those two independent sampling within one run. `write_sheet` — the matched-work number, since it starts from the same `std::vector` every -competitor is handed — is ~8.6–8.7x faster than DuckDB, ~12.4x faster than libxlsxwriter, ~25x -faster than xlsxio, and ~57x faster than xlnt on wall time. +competitor is handed — is ~11x faster than DuckDB, ~16x faster than libxlsxwriter, ~32x +faster than xlsxio, and ~74x faster than xlnt on wall time. ### Known issue: xlsxio + libxlsxwriter cannot share a binary @@ -596,10 +596,10 @@ Caveats, none of them optional when quoting these: - **xlsxio's CPU time is not stable across runs.** It read 843.8–906.3 ms before the fix (some runs missing the workbook.xml entry), 1,171.9 ms and 687.5 ms in the two runs after it, - 1,015.6 ms and 843.8 ms in the next two and 953.1 ms in the latest, while its wall time stayed at ~2.4 s throughout. Its wall time is dominated by I/O wait — + 1,015.6 ms and 843.8 ms in the next two, 953.1 ms in the one after and 1,125.0 ms in the latest, while its wall time stayed at ~2.4 s throughout. Its wall time is dominated by I/O wait — CPU is well under half of wall, against every other case here being CPU-bound (wall ≈ CPU) — so quote the wall-time ratio, which is what a caller waits. Against this run's CPU time the gap from - `write_sheet` to xlsxio is ~9.9x, not ~25x. + `write_sheet` to xlsxio is ~15x, not ~32x. - **ExcelReader does slightly more work here**, not less: it attaches a number format to the two date columns so Excel shows a date, while every competitor case writes those as bare serial numbers. That difference favours the competitors. @@ -611,6 +611,6 @@ Caveats, none of them optional when quoting these: a full analytical query engine doing far more than any Excel-writing library here; this measures one narrow slice of it, not "DuckDB" as a whole. -The two ExcelReader cases land within ~12–22% of each other. The transpose's share grew as producing +The two ExcelReader cases land within ~14–16% of each other. The transpose's share grew as producing the file got cheaper, but it is still a minority of the cost. Reach for `write_columns` when your data is already columnar, but `write_sheet` is not the slow path. diff --git a/docs/performance/benchmarks.md b/docs/performance/benchmarks.md index 43d74e0c..cf2dfeac 100644 --- a/docs/performance/benchmarks.md +++ b/docs/performance/benchmarks.md @@ -440,17 +440,17 @@ zero-copy advantage the other can't take: | Comparison | Format | ExcelReader | Competitor | Ratio | |---|---|---:|---:|---:| -| vs. [calamine](https://github.com/tafia/calamine) (Rust) | XLSX | 79.1 ms | 279.7 ms | ~3.5x faster | -| vs. calamine (Rust) | XLSB | 46.9 ms | 87.5 ms | ~1.9x faster | -| vs. [DuckDB](https://github.com/duckdb/duckdb) `read_xlsx` (C++) | XLSX | 62.9 ms | 410.8 ms | ~6.5x faster | -| vs. [xlsxio](https://github.com/brechtsanders/xlsxio) (C) | XLSX | 62.9 ms | 512.9 ms | ~8.2x faster | -| vs. [xlnt](https://github.com/tfussell/xlnt) (C++) | XLSX | 62.9 ms | 2,394.1 ms | ~38x faster | -| vs. [zsv](https://github.com/liquidaty/zsv) `fast` engine (C) | CSV | 27.2 ms | 20.4 ms | ~1.3x **slower** | +| vs. [calamine](https://github.com/tafia/calamine) (Rust) | XLSX | 72.3 ms | 271.2 ms | ~3.8x faster | +| vs. calamine (Rust) | XLSB | 46.3 ms | 82.9 ms | ~1.8x faster | +| vs. [DuckDB](https://github.com/duckdb/duckdb) `read_xlsx` (C++) | XLSX | 61.3 ms | 413.8 ms | ~6.7x faster | +| vs. [xlsxio](https://github.com/brechtsanders/xlsxio) (C) | XLSX | 61.3 ms | 497.4 ms | ~8.1x faster | +| vs. [xlnt](https://github.com/tfussell/xlnt) (C++) | XLSX | 61.3 ms | 2,335.0 ms | ~38x faster | +| vs. [zsv](https://github.com/liquidaty/zsv) `fast` engine (C) | CSV | 27.4 ms | 20.0 ms | ~1.4x **slower** | Writing the same 14 columns × 65,535 rows from row-shaped data, the C++ suite measures -`write_sheet` at 95.5–96.5 ms against DuckDB's 829–833 ms (~8.6–8.7x), libxlsxwriter's 1,194 ms (~12x), -xlsxio's 2,433 ms (~25x) and xlnt's 5,436 ms (~57x). The Rust suite measures 51.9 ms against -rust_xlsxwriter's 328.3 ms (~6.3x) over 7 columns. Caveats for both are in the binding READMEs. +`write_sheet` at 74.8–76.0 ms against DuckDB's 831 ms (~11x), libxlsxwriter's 1,207 ms (~16x), +xlsxio's 2,405 ms (~32x) and xlnt's 5,525 ms (~74x). The Rust suite measures 40.7 ms against +rust_xlsxwriter's 322.0 ms (~7.9x) over 7 columns. Caveats for both are in the binding READMEs. DuckDB, xlsxio and xlnt do not read `.xlsb`, so those comparisons are XLSX-only. calamine is a fast, well-optimized reader in its own right — the gap there is real but not the order of magnitude seen diff --git a/python/README.md b/python/README.md index 4463ffa5..eb3d9a33 100644 --- a/python/README.md +++ b/python/README.md @@ -511,23 +511,23 @@ encrypt_package(plain, secret, "hunter2") `tests/ExcelReader.Benchmarks/Data/65K_Records_Data.xlsb` (65,535 data rows, 14 columns), the same fixture the .NET, C++ and Rust suites use. Measured on Windows 10 (22H2), AMD Ryzen 7 5700X (16 logical CPUs @ 3.39 GHz), CPython 3.14.7, 10 runs each (medians shown; `min` is in the scripts' own -output), against a library published from commit 65945b4. Competitors: polars 1.44.1 (with +output), against a library published from commit 449ca144. Competitors: polars 1.44.1 (with fastexcel 0.21.0), pandas 3.0.5 (with openpyxl 3.1.5), xlsxwriter 3.2.9. ### Reading | API | Median | What it produces | |---|---:|---| -| `to_arrow_stream()`, read by pyarrow | 31.9 ms | Arrow record batches, streamed | -| `to_arrow()` | 31.8 ms | the same parse, handed to pyarrow zero-copy | -| `parse()` | 33.5 ms | typed columnar buffers, converted natively | -| `parse_batches()` | 34.2 ms | the same typed buffers, in batches | -| `to_polars()` | 34.6 ms | typed columnar DataFrame, schema inferred | -| `polars.read_excel()` | 125.6 ms | typed columnar DataFrame, types inferred | -| `rows()` | 1,034.6 ms | one `Cell` object per cell, streamed per row | +| `to_arrow_stream()`, read by pyarrow | 31.2 ms | Arrow record batches, streamed | +| `to_arrow()` | 30.1 ms | the same parse, handed to pyarrow zero-copy | +| `parse()` | 32.5 ms | typed columnar buffers, converted natively | +| `parse_batches()` | 33.2 ms | the same typed buffers, in batches | +| `to_polars()` | 33.6 ms | typed columnar DataFrame, schema inferred | +| `polars.read_excel()` | 120.6 ms | typed columnar DataFrame, types inferred | +| `rows()` | 975.7 ms | one `Cell` object per cell, streamed per row | The batched readers cost the same as their whole-sheet counterparts and lower the peak memory: -`to_arrow_stream(batch_size=10000)` peaks at 11.4 MiB against 18.8 MiB for one whole-sheet +`to_arrow_stream(batch_size=10000)` peaks at 11.4 MiB against 19.5 MiB for one whole-sheet record batch on this file. Only the `to_polars()` / `polars.read_excel()` pair is a like-for-like comparison (~3.6x), and even @@ -540,13 +540,13 @@ CSV, from `65K_Records_Data.csv` (8.2 MB), same 14 typed columns: | API | Median | |---|---:| -| `parse(parallelism=1)` | 23.6 ms | -| `parse(parallelism=0)` | 12.0 ms | -| `to_polars(parallelism=1)` | 25.0 ms | -| `to_polars(parallelism=0)` | 10.8 ms | -| `polars.read_csv(try_parse_dates=True)` | 7.3 ms | -| `pandas.read_csv`, pyarrow engine | 115.8 ms | -| `pandas.read_csv`, C engine | 138.7 ms | +| `parse(parallelism=1)` | 23.3 ms | +| `parse(parallelism=0)` | 11.3 ms | +| `to_polars(parallelism=1)` | 24.2 ms | +| `to_polars(parallelism=0)` | 9.7 ms | +| `polars.read_csv(try_parse_dates=True)` | 6.9 ms | +| `pandas.read_csv`, pyarrow engine | 116.0 ms | +| `pandas.read_csv`, C engine | 132.3 ms | polars' CSV reader is ahead (multithreaded, and it infers types from the text without our schema step); ExcelReader is ~10x faster than pandas on either engine. These include opening the file, so @@ -557,28 +557,28 @@ threads](#reading-a-large-sheet-on-several-threads). | API | Median | Output | |---|---:|---| -| `write_workbook()` → csv | 35.1 ms | 8.2 MB | -| `write_workbook()` → xls | 44.4 ms | 17.7 MB | -| `write_workbook()` → xlsb | 71.5 ms | 5.1 MB | -| `write_workbook()` → xlsx | 90.6 ms | 5.1 MB | -| `write_workbook(polars_df)` → xlsx | 487.4 ms | 5.1 MB | -| `write_workbook(pandas_df)` → xlsx | 493.8 ms | 5.1 MB | -| `polars.DataFrame.write_excel()` | 4,670.4 ms | 5.6 MB | -| `pandas.DataFrame.to_excel()` | 7,192.9 ms | 5.5 MB | +| `write_workbook()` → csv | 33.3 ms | 8.2 MB | +| `write_workbook()` → xlsb | 51.7 ms | 4.5 MB | +| `write_workbook()` → xls | 59.2 ms | 13.4 MB | +| `write_workbook()` → xlsx | 68.5 ms | 5.1 MB | +| `write_workbook(polars_df)` → xlsx | 456.9 ms | 5.1 MB | +| `write_workbook(pandas_df)` → xlsx | 465.5 ms | 5.1 MB | +| `polars.DataFrame.write_excel()` | 4,663.7 ms | 5.6 MB | +| `pandas.DataFrame.to_excel()` | 7,173.6 ms | 5.5 MB | The two DataFrame comparisons are matched work — same DataFrame in, xlsx out both times: -`write_workbook(polars_df)` is ~9.6x faster than polars' own `write_excel()`, and -`write_workbook(pandas_df)` ~14.6x faster than `to_excel()`. Both of ours pay a conversion the raw path does not: the DataFrame goes +`write_workbook(polars_df)` is ~10.2x faster than polars' own `write_excel()`, and +`write_workbook(pandas_df)` ~15.4x faster than `to_excel()`. Both of ours pay a conversion the raw path does not: the DataFrame goes through Arrow and then a Python list before reaching the native columns, which is most of the gap -between the ~490 ms rows and the 90 ms one. Handing `write_workbook()` buffers that are already +between the ~460 ms rows and the 68.5 ms one. Handing `write_workbook()` buffers that are already columnar — what `parse()` returns — skips all of it. -`write_workbook(xlsx)` at 90.6 ms lands within ~5 ms of the C++ binding's -`xl::write_columns` on the same 14 columns (85.4 ms), which is the expected result: both are thin wrappers +`write_workbook(xlsx)` at 68.5 ms lands within ~4 ms of the C++ binding's +`xl::write_columns` on the same 14 columns (64.2 ms), which is the expected result: both are thin wrappers over the same `xl_write_typed` call, and neither adds work per cell. -`xls` being both the largest file and faster than xlsb and xlsx is not a paradox — BIFF8 writes -fixed-width records with no compression, so it trades 3.5x the bytes for less work per cell. +`xls` being the largest file and still faster than xlsx is not a paradox — BIFF8 writes +fixed-width records with no compression, so it trades ~2.6x the bytes for less work per cell. xlsb is faster still, at 51.7 ms. ## Notes diff --git a/rust/excelreader/README.md b/rust/excelreader/README.md index 3be52cfe..ff6f18fe 100644 --- a/rust/excelreader/README.md +++ b/rust/excelreader/README.md @@ -466,12 +466,12 @@ takes 20 samples instead — each of its iterations writes a whole 65,535-row fi | Benchmark | RealExcel.xlsb (100 rows) | 65K_Records_Data.xlsb (65,535 rows) | |---|---:|---:| -| `open` | 85.8 µs | 98.9 µs | -| `parse_sheet` (2 or 6 bound columns) | 58.7 µs | 24.6 ms | -| typed chunks of 1,000 / 10,000 rows | - | 23.4 ms / 23.5 ms | -| `infer_schema` (sample 100 / 1,000 rows) | 86.8 µs | 577.4 µs | +| `open` | 85.8 µs | 99.4 µs | +| `parse_sheet` (2 or 6 bound columns) | 54.7 µs | 24.0 ms | +| typed chunks of 1,000 / 10,000 rows | - | 22.8 ms / 22.9 ms | +| `infer_schema` (sample 100 / 1,000 rows) | 83.9 µs | 571.0 µs | -`open` stays nearly flat across the 655x row-count jump (+15%) - XLSB's header carries its +`open` stays nearly flat across the 655x row-count jump (+16%) - XLSB's header carries its dimensions/index, so opening costs metadata, not row data. `parse_sheet` scales linearly with rows × columns. Reading the same sheet in typed chunks costs no more than one whole-sheet `parse_sheet`, while holding only one chunk at a time. `infer_schema` scales with its sample size, @@ -484,13 +484,10 @@ zero-copy advantage the other can't take: | Format | ExcelReader (`Sheet::parse::`) | calamine (`worksheet_range` + `Data` match) | |---|---:|---:| -| XLSX | 79.1 ms | 279.7 ms | -| XLSB | 46.9 ms | 87.5 ms | +| XLSX | 72.3 ms | 271.2 ms | +| XLSB | 46.3 ms | 82.9 ms | -The ExcelReader XLSX run was noisy (Criterion's interval is 76.0–83.0 ms; calamine's is 278.6–281.0 ms), so -read its ratio as 3.4–3.7x. - -ExcelReader is ~3.5x faster than calamine for XLSX and ~1.9x faster for XLSB on this workload - +ExcelReader is ~3.8x faster than calamine for XLSX and ~1.8x faster for XLSB on this workload - calamine is a fast, well-optimized reader in its own right, so the gap is real but not the order of magnitude seen against slower libraries. @@ -500,15 +497,15 @@ rows to `.xlsx`, all three starting from the same in-memory `Vec`: | Benchmark | Median | |---|---:| -| `columns` (`write_columns`, pre-transposed) | 45.7 ms | -| `sheet` (`write_sheet`, from `Vec`) | 51.9 ms | -| `rust_xlsxwriter` (cell-at-a-time) | 328.3 ms | +| `columns` (`write_columns`, pre-transposed) | 33.8 ms | +| `sheet` (`write_sheet`, from `Vec`) | 40.7 ms | +| `rust_xlsxwriter` (cell-at-a-time) | 322.0 ms | `sheet` is the matched-work number — it starts from the same shape `rust_xlsxwriter` is handed and -pays the row-to-column transpose itself — and is ~6.3x faster. `columns` is a ceiling no +pays the row-to-column transpose itself — and is ~7.9x faster. `columns` is a ceiling no cell-at-a-time API can reach, since it is handed buffers that are already columnar; read it only against `sheet`, as the cost of having row-shaped data in the first place. That cost turns out to be -about 14%: not free, but a minority of the cost of producing the file. +about 21%: not free, but a minority of the cost of producing the file. Two caveats, both running against the headline number rather than for it. ExcelReader does slightly *more* work here: it attaches a number format to the date column so Excel shows a date, and writes a