Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SepWriter.Header with Add methods for predefining header #124

Merged
merged 9 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1733,6 +1733,7 @@ namespace nietras.SeparatedValues
}
public sealed class SepWriter : System.IDisposable
{
public nietras.SeparatedValues.SepWriterHeader Header { get; }
public nietras.SeparatedValues.SepSpec Spec { get; }
public void Dispose() { }
public void Flush() { }
Expand Down Expand Up @@ -1822,6 +1823,16 @@ namespace nietras.SeparatedValues
public static nietras.SeparatedValues.SepWriterOptions Writer(this nietras.SeparatedValues.Sep sep, System.Func<nietras.SeparatedValues.SepWriterOptions, nietras.SeparatedValues.SepWriterOptions> configure) { }
public static nietras.SeparatedValues.SepWriterOptions Writer(this nietras.SeparatedValues.SepSpec spec, System.Func<nietras.SeparatedValues.SepWriterOptions, nietras.SeparatedValues.SepWriterOptions> configure) { }
}
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
[System.Diagnostics.DebuggerTypeProxy(typeof(nietras.SeparatedValues.SepWriterHeader.DebugView))]
public sealed class SepWriterHeader
{
public void Add(System.Collections.Generic.IReadOnlyList<string> colNames) { }
public void Add(System.ReadOnlySpan<string> colNames) { }
public void Add(string colName) { }
public void Add(string[] colNames) { }
public void Write() { }
}
public readonly struct SepWriterOptions : System.IEquatable<nietras.SeparatedValues.SepWriterOptions>
{
public SepWriterOptions() { }
Expand Down
177 changes: 177 additions & 0 deletions src/Sep.Test/SepWriterHeaderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace nietras.SeparatedValues.Test;

[TestClass]
public class SepWriterHeaderTest
{
// SepWriterHeader tests are all done through SepWriter

[TestMethod]
public void SepWriterHeaderTest_DebuggerDisplay_WriteHeader_true()
{
using var writer = CreateWriter();

Assert.AreEqual("Count = 0 State = 'Not yet written'", writer.Header.DebuggerDisplay);

var colNames = new string[] { "A", "B", "C" };
writer.Header.Add(colNames);

Assert.AreEqual("Count = 3 State = 'Not yet written'", writer.Header.DebuggerDisplay);

writer.Header.Write();

Assert.AreEqual("Count = 3 State = 'Written'", writer.Header.DebuggerDisplay);
}

[TestMethod]
public void SepWriterHeaderTest_DebuggerDisplay_WriteHeader_false()
{
using var writer = Sep.New(';').Writer(o => o with { WriteHeader = false }).ToText();

Assert.AreEqual("Count = 0 State = 'To be skipped'", writer.Header.DebuggerDisplay);

var colNames = new string[] { "A", "B", "C" };
writer.Header.Add(colNames);

Assert.AreEqual("Count = 3 State = 'To be skipped'", writer.Header.DebuggerDisplay);

writer.Header.Write();

Assert.AreEqual("Count = 3 State = 'Skipped'", writer.Header.DebuggerDisplay);
}

[TestMethod]
public void SepWriterHeaderTest_Add_Array_Rows_0()
{
using var writer = CreateWriter();
var colNames = new string[] { "A", "B", "C" };
writer.Header.Add(colNames);
var expected =
@"A;B;C
";
// Header written on Dispose
writer.Dispose();

Assert.AreEqual(expected, writer.ToString());
}

[TestMethod]
public void SepWriterHeaderTest_Add_ReadOnlyList_Rows_0()
{
using var writer = CreateWriter();
IReadOnlyList<string> colNames = ["A", "B", "C"];
writer.Header.Add(colNames);
var expected =
@"A;B;C
";
// Header written on Dispose
writer.Dispose();

Assert.AreEqual(expected, writer.ToString());
}

[TestMethod]
public void SepWriterHeaderTest_Add_ReadOnlySpan_Rows_0()
{
using var writer = CreateWriter();
ReadOnlySpan<string> colNames = ["A", "B", "C"];
writer.Header.Add(colNames);
var expected =
@"A;B;C
";
// Header written on Dispose
writer.Dispose();

Assert.AreEqual(expected, writer.ToString());
}

[TestMethod]
public void SepWriterHeaderTest_Add_String_Rows_0()
{
using var writer = CreateWriter();
writer.Header.Add("A");
var expected =
@"A
";
// Header written on Dispose
writer.Dispose();

Assert.AreEqual(expected, writer.ToString());
}

[TestMethod]
public void SepWriterHeaderTest_Add_ReadOnlyList_Rows_1()
{
using var writer = CreateWriter();
IReadOnlyList<string> colNames = ["A", "B", "C"];
writer.Header.Add(colNames);
using (var row = writer.NewRow())
{
row["C"].Set("3");
row["A"].Set("1");
row["B"].Set("2");
}
var expected = """
A;B;C
1;2;3

""";
Assert.AreEqual(expected, writer.ToString());
}

[TestMethod]
public void SepWriterHeaderTest_Add_Twice_Throws()
{
using var writer = CreateWriter();
var header = writer.Header;
header.Add("A");

var e = Assert.ThrowsException<ArgumentException>(() => header.Add("A"));
Assert.AreEqual("Column name 'A' already exists (Parameter 'colName')", e.Message);
}

[TestMethod]
public void SepWriterHeaderTest_Add_After_Written_Throws()
{
using var writer = CreateWriter();
var header = writer.Header;
header.Add("A");
header.Write();

var e = Assert.ThrowsException<InvalidOperationException>(() => header.Add("B"));
Assert.AreEqual("Cannot add column name 'B since header or first row already written.", e.Message);
}

[TestMethod]
public void SepWriterHeaderTest_Add_After_Skipped_Throws()
{
using var writer = Sep.New(';').Writer(o => o with { WriteHeader = false }).ToText();
var header = writer.Header;
header.Add("A");
using (var row = writer.NewRow()) { row["A"].Set("1"); }

var e = Assert.ThrowsException<InvalidOperationException>(() => header.Add("B"));
Assert.AreEqual("Cannot add column name 'B since header or first row already written.", e.Message);
}

[TestMethod]
public void SepWriterHeaderTest_DebugView()
{
using var writer = CreateWriter();
var debugView = new SepWriterHeader.DebugView(writer.Header);

CollectionAssert.AreEqual(Array.Empty<string>(), debugView.ColNames);

var colNames = new string[] { "A", "B", "C" };
writer.Header.Add(colNames);

CollectionAssert.AreEqual(colNames, debugView.ColNames);
}


static SepWriter CreateWriter() =>
Sep.New(';').Writer().ToText();
}
1 change: 1 addition & 0 deletions src/Sep.Test/SepWriterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public void SepWriterTest_TwoRowsThreeCols_ReadAfterWriteDoesNotClearStringBuild
Assert.AreEqual(expected, writer.ToString());
}


[TestMethod]
public void SepWriterTest_NewRowWhenAlreadyNewRow_Throws()
{
Expand Down
12 changes: 12 additions & 0 deletions src/Sep/Internals/SepThrow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,16 @@ internal static void NotSupportedException_BufferOrRowLengthExceedsMaximumSuppor
$"Buffer or row has reached maximum supported length of {maxLength}. " +
$"If no such row should exist ensure quotes \" are terminated.");
}

[DoesNotReturn]
internal static void ArgumentException_ColNameAlreadyExists(string colName)
{
throw new ArgumentException($"Column name '{colName}' already exists", nameof(colName));
}

[DoesNotReturn]
internal static void InvalidOperationException_CannotAddColNameHeaderAlreadyWritten(string colName)
{
throw new InvalidOperationException($"Cannot add column name '{colName} since header or first row already written.");
}
}
29 changes: 14 additions & 15 deletions src/Sep/SepWriter.Row.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,23 @@ internal ColImpl GetOrAddCol(string colName)

if (!_colNameToCol.TryGetValue(colName, out var col))
{
if (!_headerWrittenOrSkipped)
{
var colIndex = _colNameToCol.Count;
col = new ColImpl(this, colIndex, colName, SepStringBuilderPool.Take());
_colNameToCol.Add(colName, col);
_cols.Add(col);
_colNameCache.Add((colName, colIndex));
// Is it really necessary to increment cache colIndex for first row,
// we won't hit cache here any way.
++_cacheIndex;
}
else
{
throw new KeyNotFoundException(colName);
}
col = !_headerWrittenOrSkipped ? AddCol(colName) : throw new KeyNotFoundException(colName);
}
// Should we cache on else

return col;
}

internal ColImpl AddCol(string colName)
{
var colIndex = _colNameToCol.Count;
var col = new ColImpl(this, colIndex, colName, SepStringBuilderPool.Take());
_colNameToCol.Add(colName, col);
_cols.Add(col);
_colNameCache.Add((colName, colIndex));
// Is it really necessary to increment cache colIndex for first row,
// we won't hit cache here any way.
++_cacheIndex;
return col;
}
}
69 changes: 41 additions & 28 deletions src/Sep/SepWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public sealed partial class SepWriter : IDisposable
const int DefaultCapacity = 16;
readonly Sep _sep;
readonly CultureInfo? _cultureInfo;
readonly bool _writeHeader;
internal readonly bool _writeHeader;
// _writer dispose handled by _disposeTextWriter
#pragma warning disable CA2213 // Disposable fields should be disposed
readonly TextWriter _writer;
Expand All @@ -19,13 +19,13 @@ public sealed partial class SepWriter : IDisposable
internal readonly List<(string ColName, int ColIndex)> _colNameCache = new(DefaultCapacity);

// TODO: Add Stack<ColImpl> for remove/add cols when manipulating
readonly Dictionary<string, ColImpl> _colNameToCol = new(DefaultCapacity);
internal readonly Dictionary<string, ColImpl> _colNameToCol = new(DefaultCapacity);
// Once header is written cols cannot be added or removed
internal List<ColImpl> _cols = new(DefaultCapacity);
internal string[] _colNamesHeader = Array.Empty<string>();

internal readonly SepArrayPoolAccessIndexed _arrayPool = new();
bool _headerWrittenOrSkipped = false;
internal bool _headerWrittenOrSkipped = false;
bool _newRowActive = false;
int _cacheIndex = 0;

Expand All @@ -36,9 +36,11 @@ internal SepWriter(SepWriterOptions options, TextWriter writer, Action<TextWrite
_writeHeader = options.WriteHeader;
_writer = writer;
_disposeTextWriter = disposeTextWriter;
Header = new(this);
}

public SepSpec Spec => new(_sep, _cultureInfo);
public SepWriterHeader Header { get; }

public Row NewRow()
{
Expand All @@ -60,31 +62,7 @@ internal void EndRow(Row row)
// Header
if (!_headerWrittenOrSkipped)
{
if (_writeHeader)
{
A.Assert(_colNamesHeader.Length == 0);
if (cols.Count != _colNamesHeader.Length)
{
_colNamesHeader = new string[cols.Count];
}
var notFirstHeader = false;
for (var colIndex = 0; colIndex < cols.Count; ++colIndex)
{
var col = cols[colIndex];
A.Assert(colIndex == col.Index);

if (notFirstHeader)
{
_writer.Write(_sep.Separator);
}
var name = col.Name;
_writer.Write(name);
_colNamesHeader[colIndex] = name;
notFirstHeader = true;
}
_writer.WriteLine();
}
_headerWrittenOrSkipped = true;
WriteHeader();
}
else
{
Expand Down Expand Up @@ -138,8 +116,43 @@ public override string ToString()
return null;
}

internal void WriteHeader()
{
if (_writeHeader)
{
var cols = _cols;
A.Assert(_colNamesHeader.Length == 0);
if (cols.Count != _colNamesHeader.Length)
{
_colNamesHeader = new string[cols.Count];
}
var notFirstHeader = false;
for (var colIndex = 0; colIndex < cols.Count; ++colIndex)
{
var col = cols[colIndex];
A.Assert(colIndex == col.Index);

if (notFirstHeader)
{
_writer.Write(_sep.Separator);
}
var name = col.Name;
_writer.Write(name);
_colNamesHeader[colIndex] = name;
notFirstHeader = true;
}
_writer.WriteLine();
}
_headerWrittenOrSkipped = true;
}

void DisposeManaged()
{
if (!_headerWrittenOrSkipped && _cols.Count > 0)
{
WriteHeader();
}

_disposeTextWriter(_writer);
_arrayPool.Dispose();
foreach (var col in _colNameToCol.Values)
Expand Down
Loading
Loading