Skip to content

Commit

Permalink
Add SepReader.Cols.ParseToArray/ToStrings/ToStringsArray, Improve Deb…
Browse files Browse the repository at this point in the history
…uggability (#26)

* Verify `Parse<string>` and similar works on .NET 8 where `string :
ISpanParsable<string>`, but also introduce `ToStrings`/`ToStringsArray`
for convenience
* `SepReader` DebuggerDisplay will show source for reader via internal
`SepReader.Info`
* `SepReader.Row` adds `DebuggerTypeProxy` to show each col when
expanding in debug view
* Add `From(string name, Func<string, Stream> nameToStream)` and
`From(string name, Func<string, TextReader> nameToReader)` overloads to
support abstraction contexts
  • Loading branch information
nietras authored Sep 14, 2023
1 parent 46d2013 commit 2f1f02a
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 53 deletions.
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ public bool DisableColCountCheck { get; init; }

#### SepReader Debuggability
Debuggability is an important part of any library and while this is still a work
in progress for Sep, `SepReader` does have a unique feature when looking at it's
row in a debug context. Given the below example code:
in progress for Sep, `SepReader` does have a unique feature when looking at it
and it's row or cols in a debug context. Given the below example code:
```csharp
var text = """
Key;Value
Expand All @@ -298,18 +298,28 @@ var text = """
using var reader = Sep.Reader().FromText(text);
foreach (var row in reader)
{
// Hover over row when breaking here
// Hover over reader, row or col when breaking here
var col = row[1];
if (Debugger.IsAttached && row.RowIndex == 2) { Debugger.Break(); }
Debug.WriteLine(col.ToString());
}
```
and you are hovering over `row` when the break is triggered then this will show
something like:
and you are hovering over `reader` when the break is triggered then this will
show something like:
```
2:[5..9] = 'B;"Apple\r\nBanana\r\nOrange\r\nPear"
String Length=55
```
That is, it will show information of the source for the reader, in this case a
string of length 55.

##### SepReader.Row Debuggability
If you are hovering over `row` then this will show something like:
```
2:[5..9] = "B;\"Apple\r\nBanana\r\nOrange\r\nPear\""
```
This has the format shown below.
```
<ROWINDEX>:[<LINENUMBERRANGE>] = '<ROW>'
<ROWINDEX>:[<LINENUMBERRANGE>] = "<ROW>"
```
Note how this shows line number range `[FromIncl..ToExcl]`, as in C# [range
expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/ranges#systemrange),
Expand All @@ -320,6 +330,19 @@ that makes Sep a bit slower but which is a price considered worth paying.
> GitHub doesn't show line numbers in code blocks so consider copying the
> example text to notepad or similar to see the effect.
Additionally, if you expand the `row` in the debugger (e.g. via the small
triangle) you will see each column of the row similar to below.
```
00:'Key' = "B"
01:'Value' = "\"Apple\r\nBanana\r\nOrange\r\nPear\""
```

##### SepReader.Col Debuggability
If you hover over `col` you should see:
```
"\"Apple\r\nBanana\r\nOrange\r\nPear\""
```

#### Why SepReader Is Not IEnumerable and LINQ Compatible
As mentioned earlier Sep only allows enumeration and access to one row at a time
and `SepReader.Row` is just a simple *facade* or indirection to the underlying
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<AssemblyVersion>0.2.0</AssemblyVersion>
<FileVersion>0.2.4</FileVersion>
<FileVersion>0.2.5</FileVersion>
<InformationalVersion>$(FileVersion)</InformationalVersion>
<PackageVersion>$(InformationalVersion)</PackageVersion>

Expand Down
4 changes: 3 additions & 1 deletion src/Sep.Test/ReadMeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ public void ReadMeTest_SepReader_Debuggability()
using var reader = Sep.Reader().FromText(text);
foreach (var row in reader)
{
// Hover over row when breaking here
// Hover over reader, row or col when breaking here
var col = row[1];
if (Debugger.IsAttached && row.RowIndex == 2) { Debugger.Break(); }
Debug.WriteLine(col.ToString());
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/Sep.Test/SepReaderColTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ public void SepReaderColTest_TryParse_Out()
AssertTryParseOutFloats(o => o with { CultureInfo = null, DisableFastFloat = true });
}

#if NET8_0_OR_GREATER
// string unfortunately did not implement ISpanParsable until .NET 8

[TestMethod]
public void SepReaderColTest_Parse_String()
{
Run(col => Assert.AreEqual(ColText, col.Parse<string>()));
}

[TestMethod]
public void SepReaderColTest_TryParse_Out_String()
{
Run(col => Assert.AreEqual(ColText, col.TryParse<string>(out var v) ? v : null));
}
#endif

static void Run(SepReader.ColAction action, string colValue = ColText, Func<SepReaderOptions, SepReaderOptions>? configure = null)
{
Func<SepReaderOptions, SepReaderOptions> defaultConfigure = static c => c;
Expand Down
27 changes: 27 additions & 0 deletions src/Sep.Test/SepReaderColsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,38 @@ public void SepReaderColsTest_Length()
Run(cols => Assert.AreEqual(_colsCount, cols.Length));
}

[TestMethod]
public void SepReaderColsTest_ToStringsArray()
{
Run(cols => CollectionAssert.AreEqual(_colTexts, cols.ToStringsArray()));
}

[TestMethod]
public void SepReaderColsTest_ToStrings()
{
Run(cols => CollectionAssert.AreEqual(_colTexts, cols.ToStrings().ToArray()));
}

[TestMethod]
public void SepReaderColsTest_ParseToArray()
{
Run(cols => CollectionAssert.AreEqual(_colValues, cols.ParseToArray<int>()));
Run(cols => CollectionAssert.AreEqual(_colValuesFloat, cols.ParseToArray<float>()));
#if NET8_0_OR_GREATER
// string unfortunately did not implement ISpanParsable until .NET 8 see ToStringsArray
Run(cols => CollectionAssert.AreEqual(_colTexts, cols.ParseToArray<string>()));
#endif
}

[TestMethod]
public void SepReaderColsTest_Parse()
{
Run(cols => CollectionAssert.AreEqual(_colValues, cols.Parse<int>().ToArray()));
Run(cols => CollectionAssert.AreEqual(_colValuesFloat, cols.Parse<float>().ToArray()));
#if NET8_0_OR_GREATER
// string unfortunately did not implement ISpanParsable until .NET 8 see ToStrings
Run(cols => CollectionAssert.AreEqual(_colTexts, cols.Parse<string>().ToArray()));
#endif
}

[TestMethod]
Expand Down
20 changes: 18 additions & 2 deletions src/Sep.Test/SepReaderRowTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,25 @@ public void SepReaderRowTest_Row_Indexer_Multiple_ColNames_OutOfRange()
}

[TestMethod]
public void SepReaderRowTest_Row_DebuggerDisplay()
public void SepReaderRowTest_Row_DebuggerDisplayPrefix()
{
Assert.AreEqual(" 1:[2..3] = ' ;;1.23456789;abcdefgh\t, ._'", _enumerator.Current.DebuggerDisplay);
Assert.AreEqual(" 1:[2..3] = ", _enumerator.Current.DebuggerDisplayPrefix);
}

[TestMethod]
public void SepReaderRowTest_Row_DebugView()
{
var rowDebugView = new SepReader.Row.DebugView(_enumerator.Current);
var cols = rowDebugView.Cols;
Assert.AreEqual(_cols, cols.Length);
for (var colIndex = 0; colIndex < cols.Length; colIndex++)
{
var col = cols[colIndex];
Assert.AreEqual(colIndex, col.ColIndex);
Assert.AreEqual(_colNames[colIndex], col.ColName);
Assert.AreEqual(_colValues[colIndex], col.ColValue);
Assert.AreEqual($"{colIndex:D2}:'{_colNames[colIndex]}'", col.ColIndexName);
}
}

static void AssertCols(ReadOnlySpan<string> expected, in SepReader.Cols cols)
Expand Down
54 changes: 54 additions & 0 deletions src/Sep.Test/SepReaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,60 @@ public void SepReaderTest_TextReaderLengthLongerThan32Bit()
Assert.AreEqual(true, reader.MoveNext());
}

[TestMethod]
public void SepReaderTest_DebuggerDisplay_FromText()
{
using var reader = Sep.Reader().FromText("A;B");
Assert.AreEqual("String Length=3", reader.DebuggerDisplay);
}

[TestMethod]
public void SepReaderTest_DebuggerDisplay_FromFile()
{
var filePath = Path.GetTempFileName();
File.WriteAllText(filePath, "A;B");
using (var reader = Sep.Reader().FromFile(filePath))
{
Assert.AreEqual($"File='{filePath}'", reader.DebuggerDisplay);
}
File.Delete(filePath);
}

[TestMethod]
public void SepReaderTest_DebuggerDisplay_FromBytes()
{
using var reader = Sep.Reader().From(Encoding.UTF8.GetBytes("A;B"));
Assert.AreEqual($"Bytes Length=3", reader.DebuggerDisplay);
}

[TestMethod]
public void SepReaderTest_DebuggerDisplay_FromNameStream()
{
var name = "TEST";
using var reader = Sep.Reader().From(name, n => (Stream)new MemoryStream(Encoding.UTF8.GetBytes("A;B")));
Assert.AreEqual($"Stream Name='{name}'", reader.DebuggerDisplay);
}
[TestMethod]
public void SepReaderTest_DebuggerDisplay_FromStream()
{
using var reader = Sep.Reader().From((Stream)new MemoryStream(Encoding.UTF8.GetBytes("A;B")));
Assert.AreEqual($"Stream='{typeof(MemoryStream)}'", reader.DebuggerDisplay);
}

[TestMethod]
public void SepReaderTest_DebuggerDisplay_FromNameTextReader()
{
var name = "TEST";
using var reader = Sep.Reader().From(name, n => (TextReader)new StringReader("A;B"));
Assert.AreEqual($"TextReader Name='{name}'", reader.DebuggerDisplay);
}
[TestMethod]
public void SepReaderTest_DebuggerDisplay_FromTextReader()
{
using var reader = Sep.Reader().From((TextReader)new StringReader("A;B"));
Assert.AreEqual($"TextReader='{typeof(StringReader)}'", reader.DebuggerDisplay);
}

public class FakeLongMemoryStream : MemoryStream
{
readonly long _fakeLength;
Expand Down
91 changes: 56 additions & 35 deletions src/Sep/SepReader.Col.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace nietras.SeparatedValues;

public partial class SepReader
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerDisplay("{DebuggerDisplay}")]
public readonly ref struct Col
{
readonly SepReader _reader;
Expand Down Expand Up @@ -95,59 +95,80 @@ public string ToStringRaw(int index)

T Parse<T>(int index) where T : ISpanParsable<T>
{
var span = GetColSpan(index);
var decimalSeparator = _fastFloatDecimalSeparatorOrZero;
if (decimalSeparator != '\0')
// To ensure SepToString and potential string pooling used for generic
// case, check if type is T and use normal ToString
if (typeof(T) == typeof(string))
{
if (typeof(T) == typeof(float))
{
var v = csFastFloat.FastFloatParser.ParseFloat(span,
decimal_separator: decimalSeparator);
return Unsafe.As<float, T>(ref v);
}
else if (typeof(T) == typeof(double))
var s = ToString(index);
return Unsafe.As<string, T>(ref s);
}
else
{
var span = GetColSpan(index);
var decimalSeparator = _fastFloatDecimalSeparatorOrZero;
if (decimalSeparator != '\0')
{
var v = csFastFloat.FastDoubleParser.ParseDouble(span,
decimal_separator: decimalSeparator);
return Unsafe.As<double, T>(ref v);
if (typeof(T) == typeof(float))
{
var v = csFastFloat.FastFloatParser.ParseFloat(span,
decimal_separator: decimalSeparator);
return Unsafe.As<float, T>(ref v);
}
else if (typeof(T) == typeof(double))
{
var v = csFastFloat.FastDoubleParser.ParseDouble(span,
decimal_separator: decimalSeparator);
return Unsafe.As<double, T>(ref v);
}
}
return T.Parse(span, _cultureInfo);
}
return T.Parse(span, _cultureInfo);
}

T? TryParse<T>(int index) where T : struct, ISpanParsable<T> =>
TryParse<T>(index, out var value) ? value : null;

bool TryParse<T>(int index, out T value) where T : ISpanParsable<T>
{
var span = GetColSpan(index);
var decimalSeparator = _fastFloatDecimalSeparatorOrZero;
if (decimalSeparator != '\0')
// To ensure SepToString and potential string pooling used for generic
// case, check if type is T and use normal ToString
if (typeof(T) == typeof(string))
{
var s = ToString(index);
value = Unsafe.As<string, T>(ref s);
return true;
}
else
{
if (typeof(T) == typeof(float))
var span = GetColSpan(index);
var decimalSeparator = _fastFloatDecimalSeparatorOrZero;
if (decimalSeparator != '\0')
{
if (csFastFloat.FastFloatParser.TryParseFloat(span, out var v,
decimal_separator: decimalSeparator))
if (typeof(T) == typeof(float))
{
value = Unsafe.As<float, T>(ref v);
return true;
if (csFastFloat.FastFloatParser.TryParseFloat(span, out var v,
decimal_separator: decimalSeparator))
{
value = Unsafe.As<float, T>(ref v);
return true;
}
value = default!;
return false;
}
value = default!;
return false;
}
else if (typeof(T) == typeof(double))
{
if (csFastFloat.FastDoubleParser.TryParseDouble(span, out var v,
decimal_separator: decimalSeparator))
else if (typeof(T) == typeof(double))
{
value = Unsafe.As<double, T>(ref v);
return true;
if (csFastFloat.FastDoubleParser.TryParseDouble(span, out var v,
decimal_separator: decimalSeparator))
{
value = Unsafe.As<double, T>(ref v);
return true;
}
value = default!;
return false;
}
value = default!;
return false;
}
return T.TryParse(span, _cultureInfo, out value!);
}
return T.TryParse(span, _cultureInfo, out value!);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
Loading

0 comments on commit 2f1f02a

Please sign in to comment.