Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8379db3
feat: enhance support for various Arrow types and improve handling of…
Oct 5, 2026
74050b3
feat(core): add ByteSource, a positional view over memory, files and …
Oct 6, 2026
a3a3752
feat(core): add ByteSourceStream, a buffered range stream over a Byte…
Oct 6, 2026
702e21a
refactor(core): rename ZipMemoryIndex to ZipIndex and index names int…
Oct 6, 2026
40b7a04
feat(core): let ZipIndex read its directory and entries through any B…
Oct 6, 2026
5f79408
fix(core): reject stored ZIP entries whose sizes disagree
Oct 6, 2026
9492ec3
refactor(core): read XLSX from streams and files through ZipIndex
Oct 6, 2026
676d146
refactor(core): read XLSB from streams and files through ZipIndex
Oct 6, 2026
38b7a10
refactor(core): detect ZIP workbooks with ZipIndex and drop ZipArchiv…
Oct 6, 2026
fc38fdf
refactor(core): load XLS sectors through ByteSource instead of seekin…
Oct 6, 2026
696134e
fix(core): accept bytes after the ZIP end-of-central-directory record
Oct 6, 2026
f03340e
test(core): cover ZIP64, async and failure paths of the positional ZI…
Oct 6, 2026
fd30186
fix(core): restore stream position on failed detection and wrap only …
Oct 6, 2026
d408b0f
test(core): make the inverting MemoryStream subclass not recurse
Oct 6, 2026
08933ce
fix(core): count decompressed bytes atomically
GabrielMarquezMatte Oct 6, 2026
0e99b30
refactor(core): publish cached shared strings with compare-and-swap
GabrielMarquezMatte Oct 6, 2026
e686e73
feat(core): add OnceGate and ReaderLifetime for shared reader state
GabrielMarquezMatte Oct 6, 2026
1e49a2b
feat(core): load XLSX shared strings once and keep the reader alive f…
GabrielMarquezMatte Oct 6, 2026
fecefc4
feat(core): keep an XLSB reader alive for its enumerators
GabrielMarquezMatte Oct 6, 2026
c8dbaa6
feat(core): keep an XLS reader alive for its enumerators
GabrielMarquezMatte Oct 6, 2026
5f745f1
fix(core): refuse use after dispose in the sources and reject negativ…
GabrielMarquezMatte Oct 6, 2026
cee9ea5
fix(core): dispose the row enumerator when the ExcelDataReader constr…
GabrielMarquezMatte Oct 6, 2026
6256c76
test(core): read sheets of one reader in parallel across formats and …
GabrielMarquezMatte Oct 6, 2026
d157b8f
fix(core): stop a disposed row enumerator from reopening its sheet
GabrielMarquezMatte Oct 6, 2026
ea28790
test(core): cancel an XLSX shared-string load and load again
GabrielMarquezMatte Oct 6, 2026
853f168
feat(core): read CSV positionally and keep the reader alive for its e…
GabrielMarquezMatte Oct 6, 2026
ea85fef
fix(core): offer no raw chunk source for a transcoded CSV file
GabrielMarquezMatte Oct 6, 2026
c773e7d
perf(core): read CSV sources straight into the enumerator buffer
GabrielMarquezMatte Oct 6, 2026
1723f07
Merge branch 'master' into develop
GabrielMarquezMatte Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/reading.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ using XlsxWorkbookWriter workbook = XlsxWorkbookWriter.Create(File.Create("repor
workbook.WriteRecordBatch(batch); // adds the sheet, writes the header + every row, ends the workbook
```

Supports the same seven Arrow types `ToArrowRecordBatch` produces (string, int64, double, boolean, date32, time64, timestamp); any other type throws `NotSupportedException`. Pass `writeHeader: false` to skip the header row, or a `sheetName` (XLSX/XLSB/XLS only — CSV has no sheet name) to rename it from the `"Sheet1"` default.
Supports string, large string, string view, every integer width (signed and unsigned), half float, float, double, decimal128, decimal256, boolean, date32, date64, time32, time64, timestamp and null columns; any other type (binary, nested, dictionary) throws `NotSupportedException`. Pass `writeHeader: false` to skip the header row, or a `sheetName` (XLSX/XLSB/XLS only — CSV has no sheet name) to rename it from the `"Sheet1"` default.

## Open by auto-detecting the format

Expand Down
98 changes: 85 additions & 13 deletions src/ExcelReader.Arrow/ArrowWriteExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using Apache.Arrow;
using Apache.Arrow.Types;
using ExcelReader.Core.Writer;
Expand All @@ -24,29 +25,30 @@ public static class ArrowWriteExtensions
/// </summary>
/// <param name="workbook">A freshly created, not-yet-started workbook writer.</param>
/// <param name="batch">
/// The batch to write. Only the seven Arrow types <see cref="ArrowConversionExtensions.ToArrowRecordBatch"/>
/// produces (string, int64, double, boolean, date32, time64, timestamp) are supported.
/// The batch to write. Supported Arrow types: string, large string, string view, every integer
/// width (signed and unsigned), half float, float, double, decimal128, decimal256, boolean,
/// date32, date64, time32, time64, timestamp and null.
/// </param>
/// <param name="sheetName">The sheet's name.</param>
/// <param name="writeHeader">Whether to write a header row of column names first.</param>
/// <exception cref="ArgumentNullException"><paramref name="workbook"/> or <paramref name="batch"/> is <see langword="null"/>.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the seven supported types.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the supported types.</exception>
public static void WriteRecordBatch(this XlsxWorkbookWriter workbook, RecordBatch batch, string sheetName = "Sheet1", bool writeHeader = true)
{
WriteRecordBatchCore<XlsxSheetWriter, XlsxRowWriter>(workbook, batch, sheetName, writeHeader);
}

/// <summary>Same as <see cref="WriteRecordBatch(XlsxWorkbookWriter, RecordBatch, string, bool)"/>, for an XLSB workbook.</summary>
/// <exception cref="ArgumentNullException"><paramref name="workbook"/> or <paramref name="batch"/> is <see langword="null"/>.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the seven supported types.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the supported types.</exception>
public static void WriteRecordBatch(this XlsbWorkbookWriter workbook, RecordBatch batch, string sheetName = "Sheet1", bool writeHeader = true)
{
WriteRecordBatchCore<XlsbSheetWriter, XlsbRowWriter>(workbook, batch, sheetName, writeHeader);
}

/// <summary>Same as <see cref="WriteRecordBatch(XlsxWorkbookWriter, RecordBatch, string, bool)"/>, for a legacy XLS workbook.</summary>
/// <exception cref="ArgumentNullException"><paramref name="workbook"/> or <paramref name="batch"/> is <see langword="null"/>.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the seven supported types.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the supported types.</exception>
public static void WriteRecordBatch(this XlsWorkbookWriter workbook, RecordBatch batch, string sheetName = "Sheet1", bool writeHeader = true)
{
WriteRecordBatchCore<XlsSheetWriter, XlsRowWriter>(workbook, batch, sheetName, writeHeader);
Expand All @@ -57,7 +59,7 @@ public static void WriteRecordBatch(this XlsWorkbookWriter workbook, RecordBatch
/// in a single call.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="workbook"/> or <paramref name="batch"/> is <see langword="null"/>.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the seven supported types.</exception>
/// <exception cref="NotSupportedException">A column's Arrow type is not one of the supported types.</exception>
public static void WriteRecordBatch(this CsvWorkbookWriter workbook, RecordBatch batch, bool writeHeader = true)
{
WriteRecordBatchCore<CsvSheetWriter, CsvRowWriter>(workbook, batch, sheetName: "Sheet1", writeHeader);
Expand Down Expand Up @@ -166,8 +168,10 @@ private static void ApplyTemporalStyles<TSheet, TRow>(IWorkbookWriter<TSheet> wo
switch (fields[i].DataType.TypeId)
{
case ArrowTypeId.Date32:
case ArrowTypeId.Date64:
sheet.SetColumnStyle(i, BuiltinDateStyleId);
break;
case ArrowTypeId.Time32:
case ArrowTypeId.Time64:
timeStyle = timeStyle < 0 ? workbook.AddStyle(new CellStyle { NumberFormat = "hh:mm:ss" }) : timeStyle;
sheet.SetColumnStyle(i, timeStyle);
Expand All @@ -187,13 +191,55 @@ private static void WriteCell(IRowWriter row, IArrowArray array, ArrowTypeId typ
switch (typeId)
{
case ArrowTypeId.String:
var strings = (StringArray)array;
if (strings.IsNull(index))
{
row.Write((string?)null);
return;
}
row.WriteUtf8(strings.GetBytes(index));
WriteText(row, ((StringArray)array).GetBytes(index, out bool isNull), isNull);
return;
case ArrowTypeId.LargeString:
WriteText(row, ((LargeStringArray)array).GetBytes(index, out bool isLargeNull), isLargeNull);
return;
case ArrowTypeId.StringView:
WriteText(row, ((StringViewArray)array).GetBytes(index, out bool isViewNull), isViewNull);
return;
case ArrowTypeId.Null:
row.Write((string?)null);
return;
case ArrowTypeId.Int8:
row.Write(((Int8Array)array).GetValue(index));
return;
case ArrowTypeId.Int16:
row.Write(((Int16Array)array).GetValue(index));
return;
case ArrowTypeId.Int32:
row.Write(((Int32Array)array).GetValue(index));
return;
case ArrowTypeId.UInt8:
row.Write(((UInt8Array)array).GetValue(index));
return;
case ArrowTypeId.UInt16:
row.Write(((UInt16Array)array).GetValue(index));
return;
case ArrowTypeId.UInt32:
row.Write(((UInt32Array)array).GetValue(index));
return;
case ArrowTypeId.UInt64:
row.Write(((UInt64Array)array).GetValue(index));
return;
case ArrowTypeId.HalfFloat:
row.Write(ToShortestDouble(((HalfFloatArray)array).GetValue(index)));
return;
case ArrowTypeId.Float:
row.Write(ToShortestDouble(((FloatArray)array).GetValue(index)));
return;
case ArrowTypeId.Decimal128:
row.Write(((Decimal128Array)array).GetValue(index));
return;
case ArrowTypeId.Decimal256:
row.Write(((Decimal256Array)array).GetValue(index));
return;
case ArrowTypeId.Date64:
row.Write(((Date64Array)array).GetDateOnly(index));
return;
case ArrowTypeId.Time32:
row.Write(((Time32Array)array).GetTime(index));
return;
case ArrowTypeId.Int64:
row.Write(((Int64Array)array).GetValue(index));
Expand All @@ -217,5 +263,31 @@ private static void WriteCell(IRowWriter row, IArrowArray array, ArrowTypeId typ
throw new NotSupportedException($"Arrow type {typeId} is not supported for writing.");
}
}

/// <summary>
/// Widens through the value's shortest decimal text, so <c>0.1f</c> lands as <c>0.1</c> in every
/// format instead of the binary writers storing <c>0.10000000149011612</c>.
/// </summary>
private static double? ToShortestDouble<T>(T? value)
where T : struct, IUtf8SpanFormattable
{
if (value is not T narrow)
{
return null;
}
Span<byte> text = stackalloc byte[32];
narrow.TryFormat(text, out int written, default, CultureInfo.InvariantCulture);
return double.Parse(text[..written], NumberStyles.Float, CultureInfo.InvariantCulture);
}

private static void WriteText(IRowWriter row, ReadOnlySpan<byte> utf8, bool isNull)
{
if (isNull)
{
row.Write((string?)null);
return;
}
row.WriteUtf8(utf8);
}
}
}
10 changes: 8 additions & 2 deletions src/ExcelReader.Core/Reader/Cell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,14 @@ public string GetString()
}
if (_sharedCache is not null && !Value.IsEmpty && (uint)_sharedIndex < (uint)_sharedCache.Length)
{
ref string? cached = ref _sharedCache[_sharedIndex];
return cached ??= Encoding.UTF8.GetString(Value);
ref string? slot = ref _sharedCache[_sharedIndex];
string? cached = Volatile.Read(ref slot);
if (cached is not null)
{
return cached;
}
string created = Encoding.UTF8.GetString(Value);
return Interlocked.CompareExchange(ref slot, created, null) ?? created;
}
if (_contentCache is not null && !Value.IsEmpty)
{
Expand Down
14 changes: 13 additions & 1 deletion src/ExcelReader.Core/Reader/Csv/CsvReader.Enumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,28 @@ private struct FieldState
}

internal Enumerator(Stream stream, CsvReaderOptions options, CancellationToken ct = default)
: base(stream, options.MaxCellBytes, nameof(CsvReaderOptions.MaxCellBytes), 64 * 1024, ownsSource: false, ct)
: this(stream, options, ownsSource: false, lease: null, ct)
{
}

internal Enumerator(Stream stream, CsvReaderOptions options, bool ownsSource, ReaderLifetime? lease, CancellationToken ct)
: base(stream, options.MaxCellBytes, nameof(CsvReaderOptions.MaxCellBytes), 64 * 1024, ownsSource, ct)
{
_delimiter = options.Delimiter;
_quote = options.Quote;
_stripBom = options.DetectEncodingFromByteOrderMark;
_contentCache = options.InternStrings ? new Utf8StringCache() : null;
_scanner = new CsvStructuralScanner(_delimiter, _quote);
_scannerSupported = CsvStructuralScanner.Supports(_delimiter, _quote);
_lease = lease;
}

internal Enumerator(ReadOnlyMemory<byte> content, CsvReaderOptions options, CancellationToken ct = default)
: this(content, options, lease: null, ct)
{
}

internal Enumerator(ReadOnlyMemory<byte> content, CsvReaderOptions options, ReaderLifetime? lease, CancellationToken ct)
: base(content, options.MaxCellBytes, nameof(CsvReaderOptions.MaxCellBytes), ct)
{
_delimiter = options.Delimiter;
Expand All @@ -93,6 +104,7 @@ internal Enumerator(ReadOnlyMemory<byte> content, CsvReaderOptions options, Canc
_contentCache = options.InternStrings ? new Utf8StringCache() : null;
_scanner = new CsvStructuralScanner(_delimiter, _quote);
_scannerSupported = CsvStructuralScanner.Supports(_delimiter, _quote);
_lease = lease;
}

/// <inheritdoc/>
Expand Down
Loading
Loading