Browse Source
* 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
committed by
GitHub
24 changed files with 427 additions and 104 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue