Skip to content

Commit 7178934

Browse files
author
GabrielMarquezMatte
committed
Refactor project files and reader components for improved performance and code clarity
1 parent 3ee1468 commit 7178934

11 files changed

Lines changed: 62 additions & 49 deletions

File tree

Directory.Build.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<Project>
22
<PropertyGroup>
3+
<LangVersion>latest</LangVersion>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<Deterministic>true</Deterministic>
37
<AnalysisLevel>latest</AnalysisLevel>
48
<AnalysisMode>All</AnalysisMode>
59
<!-- <TreatWarningsAsErrors>true</TreatWarningsAsErrors> -->

src/ExcelReader.Core/ExcelReader.Core.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
5-
<LangVersion>latest</LangVersion>
6-
<ImplicitUsings>enable</ImplicitUsings>
7-
<Nullable>enable</Nullable>
85
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
96

107
<!-- Ship XML docs (members without a doc comment do not warn). -->
@@ -35,7 +32,6 @@
3532
<DebugType>portable</DebugType>
3633
<PublishRepositoryUrl>true</PublishRepositoryUrl>
3734
<EmbedUntrackedSources>true</EmbedUntrackedSources>
38-
<Deterministic>true</Deterministic>
3935
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
4036
</PropertyGroup>
4137

src/ExcelReader.Core/Parser/Internal/ColumnParserFactory.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ internal static class ColumnParserFactory
5353
[
5454
typeof(int), typeof(long), typeof(double), typeof(float), typeof(decimal),
5555
typeof(short), typeof(byte), typeof(uint), typeof(ulong), typeof(ushort),
56+
// Guid is only reached here on net9+, where it implements IUtf8SpanParsable. On net8 the
57+
// dedicated Guid build paths (guarded by #if NET8_0 below) intercept it before this set.
5658
typeof(Guid),
5759
];
5860

src/ExcelReader.Core/Reader/Excel.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ private static IExcelRowReader OpenSeekable(Stream stream, bool leaveOpen, Excel
181181
}
182182
if (format is ExcelFileFormat.Unknown)
183183
{
184-
throw UnknownFormat(stream, leaveOpen);
184+
UnknownFormat(stream, leaveOpen);
185185
}
186186
return format switch
187187
{
@@ -207,7 +207,7 @@ private static async ValueTask<IExcelRowReader> OpenSeekableAsync(Stream stream,
207207
if (format is ExcelFileFormat.Unknown)
208208
{
209209
await DisposeOnFailureAsync(stream, leaveOpen).ConfigureAwait(false);
210-
throw new InvalidDataException("Unrecognized file format; expected an XLSX/XLSB (ZIP) or XLS (OLE2) workbook.");
210+
UnknownFormatException();
211211
}
212212
return format switch
213213
{
@@ -235,10 +235,17 @@ private static ValueTask DisposeOnFailureAsync(Stream stream, bool leaveOpen)
235235
return leaveOpen ? ValueTask.CompletedTask : stream.DisposeAsync();
236236
}
237237

238-
private static InvalidDataException UnknownFormat(Stream stream, bool leaveOpen)
238+
[DoesNotReturn]
239+
private static void UnknownFormat(Stream stream, bool leaveOpen)
239240
{
240241
DisposeOnFailure(stream, leaveOpen);
241-
return new InvalidDataException("Unrecognized file format; expected an XLSX/XLSB (ZIP) or XLS (OLE2) workbook.");
242+
UnknownFormatException();
243+
}
244+
245+
[DoesNotReturn]
246+
private static void UnknownFormatException()
247+
{
248+
throw new InvalidDataException("Unrecognized file format; expected an XLSX/XLSB (ZIP) or XLS (OLE2) workbook.");
242249
}
243250

244251
// Detection peeks the 8-byte signature then rewinds. For ZIP streams, opens a temporary

src/ExcelReader.Core/Reader/XlsReader.cs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -317,27 +317,7 @@ private static void DecodeSharedStrings(ReadOnlySpan<byte> sst, ReadOnlySpan<int
317317
pos += 4;
318318
}
319319
bool compressed = (flags & 1) == 0;
320-
int produced = 0;
321-
bool truncated = false;
322-
for (int c = 0; c < chars; c++)
323-
{
324-
// Drop boundaries already behind us (splits outside the character array), then
325-
// consume the grbit for a boundary that lands exactly on this character.
326-
while (boundaryIdx < boundaries.Length && boundaries[boundaryIdx] < pos) { boundaryIdx++; }
327-
if (boundaryIdx < boundaries.Length && boundaries[boundaryIdx] == pos)
328-
{
329-
if (pos >= sst.Length) { truncated = true; break; }
330-
compressed = (sst[pos] & 1) == 0;
331-
pos++;
332-
boundaryIdx++;
333-
}
334-
int step = compressed ? 1 : 2;
335-
if (pos + step > sst.Length) { truncated = true; break; }
336-
scratch[produced++] = compressed
337-
? DecodeCp1252(sst[pos])
338-
: (char)(sst[pos] | (sst[pos + 1] << 8));
339-
pos += step;
340-
}
320+
int produced = DecodeChars(sst, boundaries, chars, scratch, ref pos, ref boundaryIdx, ref compressed, out bool truncated);
341321
int maxBytes = System.Text.Encoding.UTF8.GetMaxByteCount(produced);
342322
EnsureSharedCapacity(options, ref flat, flatLen + maxBytes);
343323
flatLen += System.Text.Encoding.UTF8.GetBytes(scratch.AsSpan(0, produced), flat.AsSpan(flatLen));
@@ -360,6 +340,35 @@ private static void DecodeSharedStrings(ReadOnlySpan<byte> sst, ReadOnlySpan<int
360340
}
361341
}
362342

343+
// Decodes one shared string's code units into scratch, advancing the position and boundary
344+
// index and flipping the compression mode at each grbit boundary. Returns the unit count
345+
// produced, and reports truncated when the source ends mid-string.
346+
private static int DecodeChars(ReadOnlySpan<byte> sst, ReadOnlySpan<int> boundaries, int chars, char[] scratch, ref int pos, ref int boundaryIdx, ref bool compressed, out bool truncated)
347+
{
348+
truncated = false;
349+
int produced = 0;
350+
for (int c = 0; c < chars; c++)
351+
{
352+
// Drop boundaries already behind us (splits outside the character array), then
353+
// consume the grbit for a boundary that lands exactly on this character.
354+
while (boundaryIdx < boundaries.Length && boundaries[boundaryIdx] < pos) { boundaryIdx++; }
355+
if (boundaryIdx < boundaries.Length && boundaries[boundaryIdx] == pos)
356+
{
357+
if (pos >= sst.Length) { truncated = true; break; }
358+
compressed = (sst[pos] & 1) == 0;
359+
pos++;
360+
boundaryIdx++;
361+
}
362+
int step = compressed ? 1 : 2;
363+
if (pos + step > sst.Length) { truncated = true; break; }
364+
scratch[produced++] = compressed
365+
? DecodeCp1252(sst[pos])
366+
: (char)(sst[pos] | (sst[pos + 1] << 8));
367+
pos += step;
368+
}
369+
return produced;
370+
}
371+
363372
private static void EnsureSharedCapacity(ExcelReaderOptions options, ref byte[] buffer, int needed)
364373
{
365374
if (needed <= buffer.Length)

src/ExcelReader.Core/Reader/XlsxReader.Enumerator.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,12 @@ public ValueTask<bool> MoveNextAsync()
136136
case HeadKind.Row:
137137
return ReadRowAsync();
138138
default:
139-
ValueTask<bool> skipResult = SkipMarkupOrContinueAsync();
140-
if (skipResult.IsCompletedSuccessfully)
139+
ValueTask<bool>? skipResult = SkipMarkupOrContinue();
140+
if (skipResult is null)
141141
{
142-
return new ValueTask<bool>(skipResult.Result);
142+
break; // markup skipped — continue scanning for the next element
143143
}
144-
break;
144+
return skipResult.Value;
145145
}
146146
}
147147
}
@@ -178,7 +178,7 @@ private ValueTask<bool> ReadRowAsync()
178178
Justification = "The .Result access is guarded by IsCompletedSuccessfully immediately above it — never blocks.")]
179179
[SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly",
180180
Justification = "The ValueTask is either returned through AwaitThenRestartAsync or consumed once after confirming synchronous completion.")]
181-
private ValueTask<bool> SkipMarkupOrContinueAsync()
181+
private ValueTask<bool>? SkipMarkupOrContinue()
182182
{
183183
ValueTask<bool> skipTask = SkipMarkupAsync();
184184
if (!skipTask.IsCompletedSuccessfully)
@@ -187,9 +187,9 @@ private ValueTask<bool> SkipMarkupOrContinueAsync()
187187
}
188188
if (skipTask.Result)
189189
{
190-
return new ValueTask<bool>(true);
190+
return null; // markup skipped — caller continues the scan loop
191191
}
192-
return new ValueTask<bool>(false);
192+
return new ValueTask<bool>(false); // end of sheetData/worksheet
193193
}
194194

195195
// Safe for every pending step above except the row-buffered check below: none of them

src/ExcelReader.Core/Writer/CsvWriter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,7 @@ public async ValueTask FlushAsync(CancellationToken ct = default)
119119
ct.ThrowIfCancellationRequested();
120120
if (_buffer.Length > 0)
121121
{
122-
await _stream.WriteAsync(_buffer.Memory, ct).ConfigureAwait(false);
123-
_buffer.Reset();
122+
await FlushBufferAsync(ct).ConfigureAwait(false);
124123
}
125124
await _stream.FlushAsync(ct).ConfigureAwait(false);
126125
}

src/ExcelReader.Core/Writer/Internal/CellFormatter.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class CellFormatter
1010
// The 5 XML entity chars, '_' (to detect literal "_xHHHH_" escape sequences that must be
1111
// themselves escaped), and every C0 control char that's illegal in XML 1.0 text content
1212
// (0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F — tab/LF/CR are legal and excluded).
13-
private static readonly SearchValues<char> specialChars = SearchValues.Create(
13+
private static readonly SearchValues<char> SpecialChars = SearchValues.Create(
1414
"&<>\"'_" +
1515
"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u000B\u000C" +
1616
"\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F");
@@ -206,7 +206,7 @@ private static void ThrowIfNonFinite(double value)
206206
internal static void WriteEscaped(BiffBuffer xml, ReadOnlySpan<char> value)
207207
{
208208
int start = 0;
209-
int next = value.IndexOfAny(specialChars);
209+
int next = value.IndexOfAny(SpecialChars);
210210
while (next >= 0)
211211
{
212212
int i = start + next;
@@ -238,7 +238,7 @@ internal static void WriteEscaped(BiffBuffer xml, ReadOnlySpan<char> value)
238238
{
239239
// A plain '_' remains in the pending run, but the next scan must move past
240240
// it; otherwise this loop would rediscover the same underscore forever.
241-
int following = value[(i + 1)..].IndexOfAny(specialChars);
241+
int following = value[(i + 1)..].IndexOfAny(SpecialChars);
242242
if (following < 0)
243243
{
244244
break;
@@ -258,7 +258,7 @@ internal static void WriteEscaped(BiffBuffer xml, ReadOnlySpan<char> value)
258258
WriteHexEscape(xml, c);
259259
start = i + 1;
260260
}
261-
next = value[start..].IndexOfAny(specialChars);
261+
next = value[start..].IndexOfAny(SpecialChars);
262262
}
263263
if (start < value.Length)
264264
{

src/ExcelReader.Core/Writer/XlsbSheetWriter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,6 @@ internal void WriteRecord(int id, ReadOnlySpan<byte> payload = default)
158158
MaybeFlush();
159159
}
160160

161-
// Fixed-length records (known-size cells, row headers) write header + fields straight into
162-
// _records, skipping the Payload-buffer round trip that WriteRecord/Payload.Reset() needs for
163-
// variable-length records.
164161
private void MaybeFlush()
165162
{
166163
if (_records.Length >= SpillThreshold)
@@ -239,6 +236,9 @@ private void BeginRow()
239236
_rowActive = true;
240237
}
241238

239+
// Fixed-length records (known-size cells, row headers) write header + fields straight into
240+
// _records, skipping the Payload-buffer round trip that WriteRecord/Payload.Reset() needs for
241+
// variable-length records.
242242
private void WriteRowHeader(int rowNumber)
243243
{
244244
const int Length = (6 * 4) + 1; // 6 x u32 + 1 byte

tests/ExcelReader.Benchmarks/ExcelReader.Benchmarks.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net10.0</TargetFramework>
6-
<ImplicitUsings>enable</ImplicitUsings>
7-
<Nullable>enable</Nullable>
86
<!-- Benchmark host app: ConfigureAwait is irrelevant (no captured sync context). -->
97
<NoWarn>$(NoWarn);CA2007;MA0004</NoWarn>
108
</PropertyGroup>

0 commit comments

Comments
 (0)