Skip to content

Commit 80815cd

Browse files
committed
Csv: add FillMissingColumnsWithNull option (issue #979)
Rows with fewer columns than the header used to throw ColumnNotFoundException, which blocked processing files with trailing optional fields. Adds a CsvConfiguration option that pads missing columns with null instead, keeping the default behavior unchanged.
1 parent a78fe0c commit 80815cd

3 files changed

Lines changed: 40 additions & 1 deletion

File tree

‎src/MiniExcel.Csv/CsvConfiguration.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ public class CsvConfiguration : MiniExcelBaseConfiguration
1212
public string NewLine { get; set; } = "\r\n";
1313
public bool ReadLineBreaksWithinQuotes { get; set; } = true;
1414
public bool ReadEmptyStringAsNull { get; set; } = false;
15+
16+
/// <summary>
17+
/// When true, rows with fewer columns than the header are padded with null values
18+
/// instead of throwing a ColumnNotFoundException. Default is false.
19+
/// </summary>
20+
public bool FillMissingColumnsWithNull { get; set; } = false;
1521
public bool AlwaysQuote { get; set; } = false;
1622
public bool QuoteWhitespaces { get; set; } = true;
1723
public Func<string, string[]>? SplitFn { get; set; }

‎src/MiniExcel.Csv/CsvReader.cs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration, bool l
2020
_config = configuration as CsvConfiguration ?? CsvConfiguration.Default;
2121
}
2222

23+
/// <summary>
24+
/// Reads CSV rows as dynamic objects. When FillMissingColumnsWithNull is enabled,
25+
/// rows with fewer columns than the header are padded with null values instead of throwing.
26+
/// </summary>
2327
[CreateSyncVersion]
2428
public async IAsyncEnumerable<IDictionary<string, object?>> QueryAsync(bool hasHeaderRow, string? sheetName, string startCell, [EnumeratorCancellation] CancellationToken cancellationToken = default)
2529
{
@@ -67,7 +71,7 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration, bool l
6771
var read = Split(finalRow);
6872

6973
// invalid row check
70-
if (read.Length < headRows.Count)
74+
if (read.Length < headRows.Count && !_config.FillMissingColumnsWithNull)
7175
{
7276
var colIndex = read.Length;
7377
var headers = headRows.ToDictionary(x => x.Value, x => x.Key);

‎tests/MiniExcel.Csv.Tests/Issues/GithubIssuesTests.cs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,35 @@ public class GithubIssuesTests
88
private readonly OpenXmlExporter _openXmlExporter = MiniExcel.Exporters.GetOpenXmlExporter();
99
private readonly OpenXmlImporter _openXmlImporter = MiniExcel.Importers.GetOpenXmlImporter();
1010

11+
/// <summary>
12+
/// Rows with fewer columns than the header throw by default; with
13+
/// FillMissingColumnsWithNull enabled they are padded with null instead (issue #979).
14+
/// </summary>
15+
[Fact]
16+
public async Task Issue979()
17+
{
18+
const string text = "A,B,C\n1,2\n";
19+
using var path = AutoDeletingPath.Create(ExcelType.Csv);
20+
File.WriteAllText(path.ToString(), text);
21+
22+
// default behavior: a row with fewer columns than the header throws
23+
await Assert.ThrowsAsync<ColumnNotFoundException>(async () =>
24+
{
25+
await foreach (var _ in _csvImporter.QueryAsync(path.ToString(), hasHeaderRow: true)) { }
26+
});
27+
28+
// with FillMissingColumnsWithNull, missing columns are padded with null
29+
var config = new CsvConfiguration { FillMissingColumnsWithNull = true };
30+
var rows = new List<dynamic>();
31+
await foreach (var row in _csvImporter.QueryAsync(path.ToString(), hasHeaderRow: true, configuration: config))
32+
rows.Add(row);
33+
34+
var casted = (IDictionary<string, object?>)rows[0];
35+
Assert.Equal("1", casted["A"]?.ToString());
36+
Assert.Equal("2", casted["B"]?.ToString());
37+
Assert.Null(casted["C"]);
38+
}
39+
1140
// Support for Enum Mapping
1241
[Fact]
1342
public void Issue89()

0 commit comments

Comments
 (0)