diff --git a/README.md b/README.md
index fd430da9..5e74194c 100644
--- a/README.md
+++ b/README.md
@@ -828,8 +828,33 @@ public CultureInfo? CultureInfo { get; init; }
///
public bool WriteHeader { get; init; } = true;
///
-/// Specifies whether to escape column values
-/// when writing.
+/// Disables checking if column count is the
+/// same for all rows.
+///
+///
+/// When true, the
+/// will define how columns that are not set
+/// are handled. For example, whether to skip
+/// or write an empty column if a column has
+/// not been set for a given row.
+///
+/// If any columns are skipped, then columns of
+/// a row may, therefore, be out of sync with
+/// column names if
+/// is true.
+///
+/// As such, any number of columns can be
+/// written as long as done sequentially.
+///
+public bool DisableColCountCheck { get; init; } = false;
+///
+/// Specifies how to handle columns that are
+/// not set.
+///
+public SepColNotSetOption ColNotSetOption { get; init; } = SepColNotSetOption.Throw;
+///
+/// Specifies whether to escape column names
+/// and values when writing.
///
///
/// When true, if a column contains a separator
@@ -838,6 +863,8 @@ public bool WriteHeader { get; init; } = true;
/// is prefixed and suffixed with quotes `"`
/// and any quote in the column is escaped by
/// adding an extra quote so it becomes `""`.
+/// Note that escape applies to column names
+/// too, but only the written name.
///
public bool Escape { get; init; } = false;
```
@@ -1854,6 +1881,12 @@ namespace nietras.SeparatedValues
public static nietras.SeparatedValues.SepWriterOptions Writer() { }
public static nietras.SeparatedValues.SepWriterOptions Writer(System.Func configure) { }
}
+ public enum SepColNotSetOption : byte
+ {
+ Throw = 0,
+ Empty = 1,
+ Skip = 2,
+ }
public delegate nietras.SeparatedValues.SepToString SepCreateToString(nietras.SeparatedValues.SepReaderHeader? maybeHeader, int colCount);
public static class SepDefaults
{
@@ -2142,7 +2175,9 @@ namespace nietras.SeparatedValues
{
public SepWriterOptions() { }
public SepWriterOptions(nietras.SeparatedValues.Sep sep) { }
+ public nietras.SeparatedValues.SepColNotSetOption ColNotSetOption { get; init; }
public System.Globalization.CultureInfo? CultureInfo { get; init; }
+ public bool DisableColCountCheck { get; init; }
public bool Escape { get; init; }
public nietras.SeparatedValues.Sep Sep { get; init; }
public bool WriteHeader { get; init; }
diff --git a/src/Sep.Test/SepWriterOptionsTest.cs b/src/Sep.Test/SepWriterOptionsTest.cs
index 6c5a0f39..ddd0f42e 100644
--- a/src/Sep.Test/SepWriterOptionsTest.cs
+++ b/src/Sep.Test/SepWriterOptionsTest.cs
@@ -13,6 +13,8 @@ public void SepWriterOptionsTest_Defaults()
Assert.AreEqual(Sep.Default, sut.Sep);
Assert.AreSame(SepDefaults.CultureInfo, sut.CultureInfo);
Assert.IsTrue(sut.WriteHeader);
+ Assert.IsFalse(sut.DisableColCountCheck);
+ Assert.AreEqual(SepColNotSetOption.Throw, sut.ColNotSetOption);
Assert.IsFalse(sut.Escape);
}
}
diff --git a/src/Sep.Test/SepWriterTest.cs b/src/Sep.Test/SepWriterTest.cs
index 1e7f63a0..6b9d963a 100644
--- a/src/Sep.Test/SepWriterTest.cs
+++ b/src/Sep.Test/SepWriterTest.cs
@@ -176,6 +176,32 @@ public void SepWriterTest_ColMissingInSecondRow()
Assert.AreEqual(expected, writer.ToString());
}
+ [TestMethod]
+ public void SepWriterTest_ColMissingInSecondRow_ColNotSetEmpty()
+ {
+ using var writer = Sep.Writer(
+ o => o with { ColNotSetOption = SepColNotSetOption.Empty }).ToText();
+ {
+ using var row1 = writer.NewRow();
+ row1["A"].Set("1");
+ row1["B"].Set("2");
+ }
+ {
+ var row2 = writer.NewRow();
+ row2["B"].Set("3");
+ var e = AssertThrowsException(row2,
+ r => { r.Dispose(); });
+ // TODO: Make detailed exception message
+ Assert.AreEqual("Not all expected columns 'A,B' have been set.", e.Message);
+ }
+ // Expected output should only be valid rows
+ var expected =
+@"A;B
+1;2
+";
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
[TestMethod]
public void SepWriterTest_ToString_ToStreamWriter_Throws()
{
@@ -350,6 +376,248 @@ public void SepWriterTest_WriteHeader_False_ColMissingInSecondRow()
Assert.AreEqual(expected, writer.ToString());
}
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetDefaultThrow_Header_LessColumns_Throws()
+ {
+ var options = new SepWriterOptions { DisableColCountCheck = true };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+ }
+ Assert.ThrowsException(() =>
+ {
+ using var row = writer.NewRow();
+ row["B"].Set("R2C2");
+ });
+ }
+
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetDefaultThrow_Header_MoreColumns_Ok()
+ {
+ var options = new SepWriterOptions { DisableColCountCheck = true };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R2C1");
+ row["B"].Set("R2C2");
+ row[2].Set("R2C3");
+ };
+ var expected =
+@"A;B
+R1C1;R1C2
+R2C1;R2C2;R2C3
+";
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetSkip_Header()
+ {
+ var options = new SepWriterOptions
+ {
+ DisableColCountCheck = true,
+ ColNotSetOption = SepColNotSetOption.Skip,
+ };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row["B"].Set("R2C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R3C1");
+ }
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R4C1");
+ row[2].Set("R4C3");
+ }
+ var expected =
+@"A;B
+R1C1;R1C2
+R2C2
+R3C1
+R4C1;R4C3
+";
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetEmpty_Header()
+ {
+ var options = new SepWriterOptions
+ {
+ DisableColCountCheck = true,
+ ColNotSetOption = SepColNotSetOption.Empty,
+ };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+ row["C"].Set("R1C3");
+ }
+ {
+ using var row = writer.NewRow();
+ row["B"].Set("R2C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R3C1");
+ row["C"].Set("R3C3");
+ }
+ {
+ using var row = writer.NewRow();
+ row["B"].Set("R4C2");
+ row[3].Set("R4C4");
+ }
+ var expected =
+@"A;B;C
+R1C1;R1C2;R1C3
+;R2C2;
+R3C1;;R3C3
+;R4C2;;R4C4
+";
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
+
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetDefaultThrow_NoHeader_LessColumns_Throws()
+ {
+ var options = new SepWriterOptions { WriteHeader = false, DisableColCountCheck = true };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+ }
+ Assert.ThrowsException(() =>
+ {
+ using var row = writer.NewRow();
+ row["B"].Set("R2C2");
+ });
+ }
+
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetDefaultThrow_NoHeader_MoreColumns_Ok()
+ {
+ var options = new SepWriterOptions { WriteHeader = false, DisableColCountCheck = true };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R2C1");
+ row["B"].Set("R2C2");
+ row[2].Set("R2C3");
+ };
+ var expected =
+@"R1C1;R1C2
+R2C1;R2C2;R2C3
+";
+ Assert.AreEqual(expected, writer.ToString());
+ }
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetSkip_NoHeader()
+ {
+ var options = new SepWriterOptions
+ {
+ WriteHeader = false,
+ DisableColCountCheck = true,
+ ColNotSetOption = SepColNotSetOption.Skip,
+ };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+
+ }
+ {
+ using var row = writer.NewRow();
+ row[0].Set("R2C1");
+ row[1].Set("R2C2");
+ row[2].Set("R2C3");
+ row[3].Set("R2C4");
+ }
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R3C1");
+ row[2].Set("R3C3");
+ row[1].Set("R3C2");
+ }
+ var expected =
+@"R1C1;R1C2
+R2C1;R2C2;R2C3;R2C4
+R3C1;R3C2;R3C3
+";
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
+ [TestMethod]
+ public void SepWriterTest_DisableColCountCheck_ColNotSetEmpty_NoHeader()
+ {
+ var options = new SepWriterOptions
+ {
+ WriteHeader = false,
+ DisableColCountCheck = true,
+ ColNotSetOption = SepColNotSetOption.Empty,
+ };
+ using var writer = options.ToText();
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R1C1");
+ row["B"].Set("R1C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row["B"].Set("R2C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row[0].Set("R3C1");
+ row[1].Set("R3C2");
+ row[2].Set("R3C3");
+ row[3].Set("R3C4");
+ }
+ {
+ using var row = writer.NewRow();
+ row["A"].Set("R4C1");
+ row[2].Set("R4C3");
+ row[1].Set("R4C2");
+ }
+ {
+ using var row = writer.NewRow();
+ row[2].Set("R5C3");
+ }
+ // Note how empty columns are written depending on previously written
+ // maximum column count
+ var expected =
+@"R1C1;R1C2
+;R2C2
+R3C1;R3C2;R3C3;R3C4
+R4C1;R4C2;R4C3;
+;;R5C3;
+";
+ Assert.AreEqual(expected, writer.ToString());
+ }
+
static SepWriter CreateWriter() =>
Sep.New(';').Writer().ToText();
diff --git a/src/Sep/Internals/SepThrow.cs b/src/Sep/Internals/SepThrow.cs
index c1950b1a..389bc124 100644
--- a/src/Sep/Internals/SepThrow.cs
+++ b/src/Sep/Internals/SepThrow.cs
@@ -82,7 +82,7 @@ internal static void InvalidOperationException_WriterDoesNotHaveActiveRow()
}
[DoesNotReturn]
- internal static void InvalidOperationException_NotAllColsSet(List cols, string[] colNamesHeader)
+ internal static void InvalidOperationException_NotAllExpectedColsSet(List cols, string[] colNamesHeader)
{
// TODO: Make detailed exception
if (colNamesHeader.Length == 0)
diff --git a/src/Sep/SepColNotSetOption.cs b/src/Sep/SepColNotSetOption.cs
new file mode 100644
index 00000000..f70a9e79
--- /dev/null
+++ b/src/Sep/SepColNotSetOption.cs
@@ -0,0 +1,20 @@
+namespace nietras.SeparatedValues;
+
+///
+/// Specifies how to handle columns that are not set when writing.
+///
+public enum SepColNotSetOption : byte
+{
+ ///
+ /// Throw exception if a column is not set.
+ ///
+ Throw,
+ ///
+ /// Write empty column if it is not set.
+ ///
+ Empty,
+ ///
+ /// Skip column if it is not set.
+ ///
+ Skip,
+}
diff --git a/src/Sep/SepWriter.Row.cs b/src/Sep/SepWriter.Row.cs
index c391f165..5c65d27e 100644
--- a/src/Sep/SepWriter.Row.cs
+++ b/src/Sep/SepWriter.Row.cs
@@ -89,7 +89,7 @@ public void Dispose()
internal ColImpl GetOrAddCol(int colIndex)
{
var cols = _cols;
- if (colIndex == cols.Count && !_writeHeader)
+ if (colIndex == cols.Count && (!_writeHeader || _disableColCountCheck))
{
var col = new ColImpl(this, colIndex, string.Empty, SepStringBuilderPool.Take());
_cols.Add(col);
diff --git a/src/Sep/SepWriter.cs b/src/Sep/SepWriter.cs
index 0d594801..f1324383 100644
--- a/src/Sep/SepWriter.cs
+++ b/src/Sep/SepWriter.cs
@@ -13,6 +13,8 @@ public sealed partial class SepWriter : IDisposable
readonly Sep _sep;
readonly CultureInfo? _cultureInfo;
internal readonly bool _writeHeader;
+ readonly bool _disableColCountCheck;
+ readonly SepColNotSetOption _colNotSetOption;
readonly bool _escape;
// _writer dispose handled by _disposeTextWriter
#pragma warning disable CA2213 // Disposable fields should be disposed
@@ -29,6 +31,7 @@ public sealed partial class SepWriter : IDisposable
internal readonly SepArrayPoolAccessIndexed _arrayPool = new();
internal bool _headerWrittenOrSkipped = false;
+ internal int _headerOrFirstRowColCount = -1;
bool _newRowActive = false;
int _cacheIndex = 0;
@@ -37,6 +40,8 @@ internal SepWriter(SepWriterOptions options, TextWriter writer, Action
public bool WriteHeader { get; init; } = true;
///
+ /// Disables checking if column count is the
+ /// same for all rows.
+ ///
+ ///
+ /// When true, the
+ /// will define how columns that are not set
+ /// are handled. For example, whether to skip
+ /// or write an empty column if a column has
+ /// not been set for a given row.
+ ///
+ /// If any columns are skipped, then columns of
+ /// a row may, therefore, be out of sync with
+ /// column names if
+ /// is true.
+ ///
+ /// As such, any number of columns can be
+ /// written as long as done sequentially.
+ ///
+ public bool DisableColCountCheck { get; init; } = false;
+ ///
+ /// Specifies how to handle columns that are
+ /// not set.
+ ///
+ public SepColNotSetOption ColNotSetOption { get; init; } = SepColNotSetOption.Throw;
+ ///
/// Specifies whether to escape column names
/// and values when writing.
///