Skip to content

Commit

Permalink
Add SepWriter.DisableColCountCheck/ColNotSetOption (#213)
Browse files Browse the repository at this point in the history
Fix #72
Fix #164
  • Loading branch information
nietras authored Jan 11, 2025
1 parent 4ebd2c6 commit c4107e5
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 29 deletions.
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,8 +828,33 @@ public CultureInfo? CultureInfo { get; init; }
/// </summary>
public bool WriteHeader { get; init; } = true;
/// <summary>
/// Specifies whether to escape column values
/// when writing.
/// Disables checking if column count is the
/// same for all rows.
/// </summary>
/// <remarks>
/// When true, the <see cref="ColNotSetOption"/>
/// 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.
/// <para>
/// If any columns are skipped, then columns of
/// a row may, therefore, be out of sync with
/// column names if <see cref="WriteHeader"/>
/// is true.
/// </para>
/// As such, any number of columns can be
/// written as long as done sequentially.
/// </remarks>
public bool DisableColCountCheck { get; init; } = false;
/// <summary>
/// Specifies how to handle columns that are
/// not set.
/// </summary>
public SepColNotSetOption ColNotSetOption { get; init; } = SepColNotSetOption.Throw;
/// <summary>
/// Specifies whether to escape column names
/// and values when writing.
/// </summary>
/// <remarks>
/// When true, if a column contains a separator
Expand All @@ -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.
/// </remarks>
public bool Escape { get; init; } = false;
```
Expand Down Expand Up @@ -1854,6 +1881,12 @@ namespace nietras.SeparatedValues
public static nietras.SeparatedValues.SepWriterOptions Writer() { }
public static nietras.SeparatedValues.SepWriterOptions Writer(System.Func<nietras.SeparatedValues.SepWriterOptions, nietras.SeparatedValues.SepWriterOptions> 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
{
Expand Down Expand Up @@ -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; }
Expand Down
2 changes: 2 additions & 0 deletions src/Sep.Test/SepWriterOptionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
268 changes: 268 additions & 0 deletions src/Sep.Test/SepWriterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>(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()
{
Expand Down Expand Up @@ -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<InvalidOperationException>(() =>
{
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<InvalidOperationException>(() =>
{
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();

Expand Down
2 changes: 1 addition & 1 deletion src/Sep/Internals/SepThrow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal static void InvalidOperationException_WriterDoesNotHaveActiveRow()
}

[DoesNotReturn]
internal static void InvalidOperationException_NotAllColsSet(List<ColImpl> cols, string[] colNamesHeader)
internal static void InvalidOperationException_NotAllExpectedColsSet(List<ColImpl> cols, string[] colNamesHeader)
{
// TODO: Make detailed exception
if (colNamesHeader.Length == 0)
Expand Down
20 changes: 20 additions & 0 deletions src/Sep/SepColNotSetOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace nietras.SeparatedValues;

/// <summary>
/// Specifies how to handle columns that are not set when writing.
/// </summary>
public enum SepColNotSetOption : byte
{
/// <summary>
/// Throw exception if a column is not set.
/// </summary>
Throw,
/// <summary>
/// Write empty column if it is not set.
/// </summary>
Empty,
/// <summary>
/// Skip column if it is not set.
/// </summary>
Skip,
}
2 changes: 1 addition & 1 deletion src/Sep/SepWriter.Row.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit c4107e5

Please sign in to comment.