Browse Source

Add SpanStringTokenizer and avoid many string allocations in FontCollectionBase (#17745)

* Create ref struct variant of StringTokenizer and reduce string allocations in FontCollectionBase

* Replace constant enum mappings with EnumHelper shim calls

---------

Co-authored-by: Max Katz <maxkatz6@outlook.com>
pull/17439/head
Washi 2 years ago
committed by GitHub
parent
commit
e991d63527
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/Avalonia.Base/Animation/KeySpline.cs
  2. 2
      src/Avalonia.Base/Animation/Spring.cs
  3. 2
      src/Avalonia.Base/CornerRadius.cs
  4. 2
      src/Avalonia.Base/Matrix.cs
  5. 116
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  6. 2
      src/Avalonia.Base/Media/TextDecorationCollection.cs
  7. 2
      src/Avalonia.Base/PixelPoint.cs
  8. 2
      src/Avalonia.Base/PixelRect.cs
  9. 2
      src/Avalonia.Base/PixelSize.cs
  10. 2
      src/Avalonia.Base/Point.cs
  11. 2
      src/Avalonia.Base/Rect.cs
  12. 2
      src/Avalonia.Base/RelativePoint.cs
  13. 2
      src/Avalonia.Base/RelativeRect.cs
  14. 2
      src/Avalonia.Base/Size.cs
  15. 2
      src/Avalonia.Base/Thickness.cs
  16. 15
      src/Avalonia.Base/Utilities/EnumHelper.cs
  17. 252
      src/Avalonia.Base/Utilities/SpanStringTokenizer.cs
  18. 3
      src/Avalonia.Base/Utilities/StringTokenizer.cs
  19. 2
      src/Avalonia.Base/Vector.cs
  20. 2
      src/Avalonia.Base/Vector3D.cs
  21. 3
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  22. 8
      src/Avalonia.Controls/GridLength.cs
  23. 2
      src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs
  24. 100
      tests/Avalonia.Base.UnitTests/Utilities/SpanStringTokenizerTests.cs

2
src/Avalonia.Base/Animation/KeySpline.cs

@ -83,7 +83,7 @@ namespace Avalonia.Animation
{
culture ??= CultureInfo.InvariantCulture;
using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".");
using var tokenizer = new SpanStringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".");
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}

2
src/Avalonia.Base/Animation/Spring.cs

@ -61,7 +61,7 @@ internal class Spring
culture = CultureInfo.InvariantCulture;
}
using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\".");
using var tokenizer = new SpanStringTokenizer(value, culture, exceptionMessage: $"Invalid Spring string: \"{value}\".");
return new Spring(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}

2
src/Avalonia.Base/CornerRadius.cs

@ -94,7 +94,7 @@ namespace Avalonia
{
const string exceptionMessage = "Invalid CornerRadius.";
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
{
if (tokenizer.TryReadDouble(out var a))
{

2
src/Avalonia.Base/Matrix.cs

@ -504,7 +504,7 @@ namespace Avalonia
double v8 = 0;
double v9 = 0;
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix."))
{
var v1 = tokenizer.ReadDouble();
var v2 = tokenizer.ReadDouble();

116
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@ -4,6 +4,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -181,7 +182,7 @@ namespace Avalonia.Media.Fonts
glyphTypeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
@ -212,7 +213,7 @@ namespace Avalonia.Media.Fonts
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
@ -271,109 +272,54 @@ namespace Avalonia.Media.Fonts
var weight = typeface.Weight;
var stretch = typeface.Stretch;
if(TryGetStyle(ref normalizedFamilyName, out var foundStyle))
{
style = foundStyle;
}
if(TryGetWeight(ref normalizedFamilyName, out var foundWeight))
{
weight = foundWeight;
}
if(TryGetStretch(ref normalizedFamilyName, out var foundStretch))
{
stretch = foundStretch;
}
//Preserve old font source
return new Typeface(typeface.FontFamily, style, weight, stretch);
}
internal static bool TryGetWeight(ref string familyName, out FontWeight weight)
{
weight = FontWeight.Normal;
StringBuilder? normalizedFamilyNameBuilder = null;
var totalCharsRemoved = 0;
var tokenizer = new StringTokenizer(familyName, ' ');
var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' ');
// Skip initial family name.
tokenizer.ReadSpan();
while (tokenizer.TryReadString(out var weightString))
while (tokenizer.TryReadSpan(out var token))
{
if (new StringTokenizer(weightString).TryReadInt32(out _))
// Don't try to match numbers.
if (new SpanStringTokenizer(token).TryReadInt32(out _))
{
continue;
}
if (!Enum.TryParse(weightString, true, out weight))
{
continue;
}
familyName = familyName.Replace(" " + weightString, "").TrimEnd();
return true;
}
return false;
}
internal static bool TryGetStyle(ref string familyName, out FontStyle style)
{
style = FontStyle.Normal;
var tokenizer = new StringTokenizer(familyName, ' ');
tokenizer.ReadSpan();
while (tokenizer.TryReadString(out var styleString))
{
//Do not try to parse an integer
if (new StringTokenizer(styleString).TryReadInt32(out _))
// Try match with font style, weight or stretch and update accordingly.
var match = false;
if (EnumHelper.TryParse<FontStyle>(token, true, out var newStyle))
{
continue;
style = newStyle;
match = true;
}
if (!Enum.TryParse(styleString, true, out style))
else if (EnumHelper.TryParse<FontWeight>(token, true, out var newWeight))
{
continue;
weight = newWeight;
match = true;
}
familyName = familyName.Replace(" " + styleString, "").TrimEnd();
return true;
}
return false;
}
internal static bool TryGetStretch(ref string familyName, out FontStretch stretch)
{
stretch = FontStretch.Normal;
var tokenizer = new StringTokenizer(familyName, ' ');
tokenizer.ReadSpan();
while (tokenizer.TryReadString(out var stretchString))
{
if (new StringTokenizer(stretchString).TryReadInt32(out _))
else if (EnumHelper.TryParse<FontStretch>(token, true, out var newStretch))
{
continue;
stretch = newStretch;
match = true;
}
if (!Enum.TryParse(stretchString, true, out stretch))
if (match)
{
continue;
// Carve out matched word from the normalized name.
normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName);
normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length);
totalCharsRemoved += token.Length;
}
familyName = familyName.Replace(" " + stretchString, "").TrimEnd();
return true;
}
return false;
// Get rid of any trailing spaces.
normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd();
//Preserve old font source
return new Typeface(typeface.FontFamily, style, weight, stretch);
}
}
}

2
src/Avalonia.Base/Media/TextDecorationCollection.cs

@ -29,7 +29,7 @@ namespace Avalonia.Media
{
var locations = new List<TextDecorationLocation>();
using (var tokenizer = new StringTokenizer(s, ',', "Invalid text decoration."))
using (var tokenizer = new SpanStringTokenizer(s, ',', "Invalid text decoration."))
{
while (tokenizer.TryReadSpan(out var name))
{

2
src/Avalonia.Base/PixelPoint.cs

@ -117,7 +117,7 @@ namespace Avalonia
/// <returns>The <see cref="PixelPoint"/>.</returns>
public static PixelPoint Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelPoint."))
{
return new PixelPoint(
tokenizer.ReadInt32(),

2
src/Avalonia.Base/PixelRect.cs

@ -433,7 +433,7 @@ namespace Avalonia
/// <returns>The parsed <see cref="PixelRect"/>.</returns>
public static PixelRect Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PixelRect."))
{
return new PixelRect(
tokenizer.ReadInt32(),

2
src/Avalonia.Base/PixelSize.cs

@ -92,7 +92,7 @@ namespace Avalonia
{
return false;
}
using (var tokenizer = new StringTokenizer(source, exceptionMessage: "Invalid PixelSize."))
using (var tokenizer = new SpanStringTokenizer(source, exceptionMessage: "Invalid PixelSize."))
{
if (tokenizer.TryReadInt32(out var w) && tokenizer.TryReadInt32(out var h))
{

2
src/Avalonia.Base/Point.cs

@ -184,7 +184,7 @@ namespace Avalonia
/// <returns>The <see cref="Point"/>.</returns>
public static Point Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point."))
{
return new Point(
tokenizer.ReadDouble(),

2
src/Avalonia.Base/Rect.cs

@ -592,7 +592,7 @@ namespace Avalonia
/// <returns>The parsed <see cref="Rect"/>.</returns>
public static Rect Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Rect."))
{
return new Rect(
tokenizer.ReadDouble(),

2
src/Avalonia.Base/RelativePoint.cs

@ -169,7 +169,7 @@ namespace Avalonia
/// <returns>The parsed <see cref="RelativePoint"/>.</returns>
public static RelativePoint Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint."))
{
var x = tokenizer.ReadString();
var y = tokenizer.ReadString();

2
src/Avalonia.Base/RelativeRect.cs

@ -176,7 +176,7 @@ namespace Avalonia
/// <returns>The parsed <see cref="RelativeRect"/>.</returns>
public static RelativeRect Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect."))
using (var tokenizer = new SpanStringTokenizer(s, exceptionMessage: "Invalid RelativeRect."))
{
var x = tokenizer.ReadSpan();
var y = tokenizer.ReadSpan();

2
src/Avalonia.Base/Size.cs

@ -161,7 +161,7 @@ namespace Avalonia
/// <returns>The <see cref="Size"/>.</returns>
public static Size Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Size."))
{
return new Size(
tokenizer.ReadDouble(),

2
src/Avalonia.Base/Thickness.cs

@ -197,7 +197,7 @@ namespace Avalonia
{
const string exceptionMessage = "Invalid Thickness.";
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage))
{
if (tokenizer.TryReadDouble(out var a))
{

15
src/Avalonia.Base/Utilities/EnumHelper.cs

@ -9,11 +9,26 @@ namespace Avalonia.Utilities
{
return Enum.Parse<T>(key, ignoreCase);
}
public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key, ignoreCase, out result);
}
#else
public static T Parse<T>(string key, bool ignoreCase) where T : struct
{
return (T)Enum.Parse(typeof(T), key, ignoreCase);
}
public static bool TryParse<T>(string key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key, ignoreCase, out result);
}
public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct
{
return Enum.TryParse(key.ToString(), ignoreCase, out result);
}
#endif
}
}

252
src/Avalonia.Base/Utilities/SpanStringTokenizer.cs

@ -0,0 +1,252 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using static System.Char;
namespace Avalonia.Utilities
{
internal ref struct SpanStringTokenizer
{
private const char DefaultSeparatorChar = ',';
private readonly ReadOnlySpan<char> _s;
private readonly int _length;
private readonly char _separator;
private readonly string? _exceptionMessage;
private readonly IFormatProvider _formatProvider;
private int _index;
private int _tokenIndex;
private int _tokenLength;
public SpanStringTokenizer(string s, IFormatProvider formatProvider, string? exceptionMessage = null)
: this(s.AsSpan(), GetSeparatorFromFormatProvider(formatProvider), exceptionMessage)
{
_formatProvider = formatProvider;
}
public SpanStringTokenizer(string s, char separator = DefaultSeparatorChar, string? exceptionMessage = null)
: this(s.AsSpan(), separator, exceptionMessage)
{
}
public SpanStringTokenizer(ReadOnlySpan<char> s, IFormatProvider formatProvider, string? exceptionMessage = null)
: this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage)
{
_formatProvider = formatProvider;
}
public SpanStringTokenizer(ReadOnlySpan<char> s, char separator = DefaultSeparatorChar, string? exceptionMessage = null)
{
_s = s;
_length = s.Length;
_separator = separator;
_exceptionMessage = exceptionMessage;
_formatProvider = CultureInfo.InvariantCulture;
_index = 0;
_tokenIndex = -1;
_tokenLength = 0;
while (_index < _length && IsWhiteSpace(_s[_index]))
{
_index++;
}
}
public int CurrentTokenIndex => _tokenIndex;
public string? CurrentToken => _tokenIndex < 0 ? null : _s.Slice(_tokenIndex, _tokenLength).ToString();
public ReadOnlySpan<char> CurrentTokenSpan => _tokenIndex < 0 ? ReadOnlySpan<char>.Empty : _s.Slice(_tokenIndex, _tokenLength);
public void Dispose()
{
if (_index != _length)
{
throw GetFormatException();
}
}
public bool TryReadInt32(out Int32 result, char? separator = null)
{
if (TryReadSpan(out var stringResult, separator) &&
SpanHelpers.TryParseInt(stringResult, NumberStyles.Integer, _formatProvider, out result))
{
return true;
}
else
{
result = default;
return false;
}
}
public int ReadInt32(char? separator = null)
{
if (!TryReadInt32(out var result, separator))
{
throw GetFormatException();
}
return result;
}
public bool TryReadDouble(out double result, char? separator = null)
{
if (TryReadSpan(out var stringResult, separator) &&
SpanHelpers.TryParseDouble(stringResult, NumberStyles.Float, _formatProvider, out result))
{
return true;
}
else
{
result = default;
return false;
}
}
public double ReadDouble(char? separator = null)
{
if (!TryReadDouble(out var result, separator))
{
throw GetFormatException();
}
return result;
}
public bool TryReadString([NotNull] out string result, char? separator = null)
{
var success = TryReadToken(separator ?? _separator);
result = CurrentTokenSpan.ToString();
return success;
}
public string ReadString(char? separator = null)
{
if (!TryReadString(out var result, separator))
{
throw GetFormatException();
}
return result;
}
public bool TryReadSpan(out ReadOnlySpan<char> result, char? separator = null)
{
var success = TryReadToken(separator ?? _separator);
result = CurrentTokenSpan;
return success;
}
public ReadOnlySpan<char> ReadSpan(char? separator = null)
{
if (!TryReadSpan(out var result, separator))
{
throw GetFormatException();
}
return result;
}
private bool TryReadToken(char separator)
{
_tokenIndex = -1;
if (_index >= _length)
{
return false;
}
var c = _s[_index];
var index = _index;
var length = 0;
while (_index < _length)
{
c = _s[_index];
if (IsWhiteSpace(c) || c == separator)
{
break;
}
_index++;
length++;
}
SkipToNextToken(separator);
_tokenIndex = index;
_tokenLength = length;
if (_tokenLength < 1)
{
throw GetFormatException();
}
return true;
}
private void SkipToNextToken(char separator)
{
if (_index < _length)
{
var c = _s[_index];
if (c != separator && !IsWhiteSpace(c))
{
throw GetFormatException();
}
var length = 0;
while (_index < _length)
{
c = _s[_index];
if (c == separator)
{
length++;
_index++;
if (length > 1)
{
throw GetFormatException();
}
}
else
{
if (!IsWhiteSpace(c))
{
break;
}
_index++;
}
}
if (length > 0 && _index >= _length)
{
throw GetFormatException();
}
}
}
private FormatException GetFormatException() =>
_exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException();
private static char GetSeparatorFromFormatProvider(IFormatProvider provider)
{
var c = DefaultSeparatorChar;
var formatInfo = NumberFormatInfo.GetInstance(provider);
if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0])
{
c = ';';
}
return c;
}
}
}

3
src/Avalonia.Base/Utilities/StringTokenizer.cs

@ -5,6 +5,9 @@ using static System.Char;
namespace Avalonia.Utilities
{
// TODO12: Remove this struct in 12.0 (breaking change)
[Obsolete("This type has been superseded by SpanStringTokenizer.")]
#if !BUILDTASK
public
#endif

2
src/Avalonia.Base/Vector.cs

@ -101,7 +101,7 @@ namespace Avalonia
/// <returns>The <see cref="Vector"/>.</returns>
public static Vector Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
{
return new Vector(
tokenizer.ReadDouble(),

2
src/Avalonia.Base/Vector3D.cs

@ -15,7 +15,7 @@ public readonly record struct Vector3D(double X, double Y, double Z)
/// <returns>The <see cref="Vector3D"/>.</returns>
public static Vector3D Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
{
return new Vector3D(
tokenizer.ReadDouble(),

3
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -20,6 +20,9 @@
<Compile Include="../Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs">
<Link>Shared/AvaloniaResourcesIndex.cs</Link>
</Compile>
<Compile Include="../Avalonia.Base/Utilities/SpanStringTokenizer.cs">
<Link>Shared/SpanStringTokenizer.cs</Link>
</Compile>
<Compile Include="../Avalonia.Base/Platform/Internal/Constants.cs">
<Link>Shared/Constants.cs</Link>
</Compile>

8
src/Avalonia.Controls/GridLength.cs

@ -217,13 +217,17 @@ namespace Avalonia.Controls
/// <returns>The <see cref="GridLength"/>.</returns>
public static IEnumerable<GridLength> ParseLengths(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture))
var result = new List<GridLength>();
using (var tokenizer = new SpanStringTokenizer(s, CultureInfo.InvariantCulture))
{
while (tokenizer.TryReadString(out var item))
{
yield return Parse(item);
result.Add(Parse(item));
}
}
return result;
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs

@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
{
var points = new List<Point>();
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList."))
using (var tokenizer = new SpanStringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList."))
{
while (tokenizer.TryReadDouble(out double x))
{

100
tests/Avalonia.Base.UnitTests/Utilities/SpanStringTokenizerTests.cs

@ -0,0 +1,100 @@
using System;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Base.UnitTests.Utilities
{
public class SpanStringTokenizerTests
{
// Explicit delegate because C# generics do not allow ref structs.
private delegate void TokenizerAction(SpanStringTokenizer tokenizer);
private static TException AssertThrows<TException>(SpanStringTokenizer tokenizer, TokenizerAction action)
where TException : Exception
{
try
{
action(tokenizer);
}
catch (Exception ex)
{
if (ex.GetType() == typeof(TException))
return (TException)ex;
throw Xunit.Sdk.ThrowsException.ForIncorrectExceptionType(typeof(TException), ex);
}
throw Xunit.Sdk.ThrowsException.ForNoException(typeof(TException));
}
[Fact]
public void ReadInt32_Reads_Values()
{
var target = new SpanStringTokenizer("123,456");
Assert.Equal(123, target.ReadInt32());
Assert.Equal(456, target.ReadInt32());
AssertThrows<FormatException>(target, t => t.ReadInt32());
}
[Fact]
public void ReadDouble_Reads_Values()
{
var target = new SpanStringTokenizer("12.3,45.6");
Assert.Equal(12.3, target.ReadDouble());
Assert.Equal(45.6, target.ReadDouble());
AssertThrows<FormatException>(target, t => t.ReadDouble());
}
[Fact]
public void TryReadInt32_Reads_Values()
{
var target = new SpanStringTokenizer("123,456");
Assert.True(target.TryReadInt32(out var value));
Assert.Equal(123, value);
Assert.True(target.TryReadInt32(out value));
Assert.Equal(456, value);
Assert.False(target.TryReadInt32(out value));
}
[Fact]
public void TryReadInt32_Doesnt_Throw()
{
var target = new SpanStringTokenizer("abc");
Assert.False(target.TryReadInt32(out var value));
}
[Fact]
public void TryReadDouble_Reads_Values()
{
var target = new SpanStringTokenizer("12.3,45.6");
Assert.True(target.TryReadDouble(out var value));
Assert.Equal(12.3, value);
Assert.True(target.TryReadDouble(out value));
Assert.Equal(45.6, value);
Assert.False(target.TryReadDouble(out value));
}
[Fact]
public void TryReadDouble_Doesnt_Throw()
{
var target = new SpanStringTokenizer("abc");
Assert.False(target.TryReadDouble(out var value));
}
[Fact]
public void ReadSpan_And_ReadString_Reads_Same()
{
var target1 = new SpanStringTokenizer("abc,def");
var target2 = new SpanStringTokenizer("abc,def");
Assert.Equal(target1.ReadString(), target2.ReadSpan().ToString());
Assert.True(target1.ReadSpan().SequenceEqual(target2.ReadString()));
}
}
}
Loading…
Cancel
Save