17c7e97efb
Add a pure, no-I/O RFC 4180 CSV reader and writer to the Commons project (greenfield — no CSV code existed). CsvParser: string / TextReader -> rows of string[]; streaming IEnumerable overload + eager Parse(string) + thin ParseWithHeader. Handles quoted fields (embedded delimiter/CR/LF/CRLF, "" -> " escape), space preservation, CRLF/LF/CR terminators, no phantom trailing row. Strict malformed-input policy: FormatException with 1-based line/column on a quote inside an unquoted field, a char after a closing quote, or an unterminated quote. Faithful empty-line policy: a blank line is one empty field (callers filter). CsvWriter: rows -> string / TextWriter; quote-on-demand (delimiter, quote, CR, LF only; internal quotes doubled), configurable delimiter/newline (default CRLF), optional quote-all, no trailing terminator. Tests (xUnit + Shouldly, 109): full RFC edge corpus for the parser, the writer quote-on-demand rules, and the required Parse(Write(rows)) == rows round-trip property over a table of every tricky character (x CRLF/LF/ quote-all/semicolon). Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
127 lines
3.3 KiB
C#
127 lines
3.3 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Csv;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.Csv;
|
|
|
|
/// <summary>
|
|
/// Quote-on-demand and terminator behaviour for <see cref="CsvWriter"/>: a field is quoted only when
|
|
/// it must be (delimiter / quote / CR / LF), internal quotes double, CRLF terminates records by
|
|
/// default (configurable), and no terminator trails the final row.
|
|
/// </summary>
|
|
public sealed class CsvWriterTests
|
|
{
|
|
[Fact]
|
|
public void Writes_simple_rows_with_crlf_default()
|
|
{
|
|
var csv = CsvWriter.WriteToString(new[]
|
|
{
|
|
new[] { "a", "b", "c" },
|
|
new[] { "d", "e", "f" },
|
|
});
|
|
|
|
csv.ShouldBe("a,b,c\r\nd,e,f");
|
|
}
|
|
|
|
[Fact]
|
|
public void No_trailing_newline_after_last_row()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a" } }).ShouldBe("a");
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_rows_yield_empty_string()
|
|
{
|
|
CsvWriter.WriteToString(Array.Empty<string[]>()).ShouldBe("");
|
|
}
|
|
|
|
// ---- quote-on-demand ----
|
|
|
|
[Fact]
|
|
public void Plain_field_is_not_quoted()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "plain" } }).ShouldBe("plain");
|
|
}
|
|
|
|
[Fact]
|
|
public void Field_with_comma_is_quoted()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a,b" } }).ShouldBe("\"a,b\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Field_with_quote_is_quoted_and_doubled()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a\"b" } }).ShouldBe("\"a\"\"b\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Field_with_lf_is_quoted()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a\nb" } }).ShouldBe("\"a\nb\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Field_with_cr_is_quoted()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a\rb" } }).ShouldBe("\"a\rb\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Leading_and_trailing_spaces_are_not_quoted()
|
|
{
|
|
// RFC does not require quoting for spaces; the parser preserves them either way.
|
|
CsvWriter.WriteToString(new[] { new[] { " a " } }).ShouldBe(" a ");
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_fields_are_written_bare()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a", "", "c" } }).ShouldBe("a,,c");
|
|
}
|
|
|
|
[Fact]
|
|
public void Null_field_is_written_as_empty()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a", null, "c" } }).ShouldBe("a,,c");
|
|
}
|
|
|
|
// ---- options ----
|
|
|
|
[Fact]
|
|
public void QuoteAllFields_forces_every_field_quoted()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a", "b" } }, quoteAllFields: true)
|
|
.ShouldBe("\"a\",\"b\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Custom_newline_lf()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a" }, new[] { "b" } }, newline: "\n")
|
|
.ShouldBe("a\nb");
|
|
}
|
|
|
|
[Fact]
|
|
public void Custom_delimiter_semicolon_quotes_on_semicolon_not_comma()
|
|
{
|
|
CsvWriter.WriteToString(new[] { new[] { "a,b", "c;d" } }, delimiter: ';')
|
|
.ShouldBe("a,b;\"c;d\"");
|
|
}
|
|
|
|
[Fact]
|
|
public void Writes_to_a_textwriter()
|
|
{
|
|
var sw = new StringWriter();
|
|
CsvWriter.Write(sw, new[] { new[] { "x", "y" } });
|
|
sw.ToString().ShouldBe("x,y");
|
|
}
|
|
|
|
[Fact]
|
|
public void Delimiter_may_not_be_a_quote()
|
|
{
|
|
Should.Throw<ArgumentException>(() =>
|
|
CsvWriter.WriteToString(new[] { new[] { "a" } }, delimiter: '"'));
|
|
}
|
|
}
|