From 18fcfcc16348401e74fd74abba11223e6f7b4fb0 Mon Sep 17 00:00:00 2001 From: Meloman19 Date: Thu, 5 Dec 2024 04:48:26 +0300 Subject: [PATCH] Expanding StringTokenizator with ReadOnlySpan. (#17645) * Tokenizer returns a ReadOnly Span instead of a string. * Returning the old property for no API break changes --------- Co-authored-by: Meloman19 <23280622+Meloman19@users.noreply.github.com> --- .../Media/Fonts/FontCollectionBase.cs | 8 ++--- .../Media/TextDecorationCollection.cs | 6 ++-- src/Avalonia.Base/RelativeRect.cs | 26 ++++++++-------- src/Avalonia.Base/Utilities/SpanHelpers.cs | 30 ++++++++++++++++++ .../Utilities/StringTokenizer.cs | 31 +++++++++++++++---- .../Utilities/StringTokenizerTests.cs | 10 ++++++ 6 files changed, 85 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index bbf10fdca8..9a34eaf643 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -132,7 +132,7 @@ namespace Avalonia.Media.Fonts glyphTypeface = typeface; return true; - } + } } return false; @@ -297,7 +297,7 @@ namespace Avalonia.Media.Fonts var tokenizer = new StringTokenizer(familyName, ' '); - tokenizer.ReadString(); + tokenizer.ReadSpan(); while (tokenizer.TryReadString(out var weightString)) { @@ -325,7 +325,7 @@ namespace Avalonia.Media.Fonts var tokenizer = new StringTokenizer(familyName, ' '); - tokenizer.ReadString(); + tokenizer.ReadSpan(); while (tokenizer.TryReadString(out var styleString)) { @@ -354,7 +354,7 @@ namespace Avalonia.Media.Fonts var tokenizer = new StringTokenizer(familyName, ' '); - tokenizer.ReadString(); + tokenizer.ReadSpan(); while (tokenizer.TryReadString(out var stretchString)) { diff --git a/src/Avalonia.Base/Media/TextDecorationCollection.cs b/src/Avalonia.Base/Media/TextDecorationCollection.cs index 2d7bd17b20..b703eaba57 100644 --- a/src/Avalonia.Base/Media/TextDecorationCollection.cs +++ b/src/Avalonia.Base/Media/TextDecorationCollection.cs @@ -31,7 +31,7 @@ namespace Avalonia.Media using (var tokenizer = new StringTokenizer(s, ',', "Invalid text decoration.")) { - while (tokenizer.TryReadString(out var name)) + while (tokenizer.TryReadSpan(out var name)) { var location = GetTextDecorationLocation(name); @@ -59,9 +59,9 @@ namespace Avalonia.Media /// /// The string. /// The . - private static TextDecorationLocation GetTextDecorationLocation(string s) + private static TextDecorationLocation GetTextDecorationLocation(ReadOnlySpan s) { - if (Enum.TryParse(s,true, out var location)) + if (SpanHelpers.TryParseEnum(s,true, out var location)) { return location; } diff --git a/src/Avalonia.Base/RelativeRect.cs b/src/Avalonia.Base/RelativeRect.cs index 9f2010ea72..7109ec41e9 100644 --- a/src/Avalonia.Base/RelativeRect.cs +++ b/src/Avalonia.Base/RelativeRect.cs @@ -152,7 +152,7 @@ namespace Avalonia Rect.Width * size.Width, Rect.Height * size.Height); } - + /// /// Converts a into pixels. /// @@ -178,18 +178,18 @@ namespace Avalonia { using (var tokenizer = new StringTokenizer(s, exceptionMessage: "Invalid RelativeRect.")) { - var x = tokenizer.ReadString(); - var y = tokenizer.ReadString(); - var width = tokenizer.ReadString(); - var height = tokenizer.ReadString(); + var x = tokenizer.ReadSpan(); + var y = tokenizer.ReadSpan(); + var width = tokenizer.ReadSpan(); + var height = tokenizer.ReadSpan(); var unit = RelativeUnit.Absolute; var scale = 1.0; - var xRelative = x.EndsWith("%", StringComparison.Ordinal); - var yRelative = y.EndsWith("%", StringComparison.Ordinal); - var widthRelative = width.EndsWith("%", StringComparison.Ordinal); - var heightRelative = height.EndsWith("%", StringComparison.Ordinal); + var xRelative = x.EndsWith(PercentChar, StringComparison.Ordinal); + var yRelative = y.EndsWith(PercentChar, StringComparison.Ordinal); + var widthRelative = width.EndsWith(PercentChar, StringComparison.Ordinal); + var heightRelative = height.EndsWith(PercentChar, StringComparison.Ordinal); if (xRelative && yRelative && widthRelative && heightRelative) { @@ -207,10 +207,10 @@ namespace Avalonia } return new RelativeRect( - double.Parse(x, CultureInfo.InvariantCulture) * scale, - double.Parse(y, CultureInfo.InvariantCulture) * scale, - double.Parse(width, CultureInfo.InvariantCulture) * scale, - double.Parse(height, CultureInfo.InvariantCulture) * scale, + SpanHelpers.ParseDouble(x, CultureInfo.InvariantCulture) * scale, + SpanHelpers.ParseDouble(y, CultureInfo.InvariantCulture) * scale, + SpanHelpers.ParseDouble(width, CultureInfo.InvariantCulture) * scale, + SpanHelpers.ParseDouble(height, CultureInfo.InvariantCulture) * scale, unit); } } diff --git a/src/Avalonia.Base/Utilities/SpanHelpers.cs b/src/Avalonia.Base/Utilities/SpanHelpers.cs index f80ac7c046..8ca3206a57 100644 --- a/src/Avalonia.Base/Utilities/SpanHelpers.cs +++ b/src/Avalonia.Base/Utilities/SpanHelpers.cs @@ -29,6 +29,16 @@ namespace Avalonia.Utilities #endif } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseInt(this ReadOnlySpan span, NumberStyles style, IFormatProvider provider, out int value) + { +#if NETSTANDARD2_0 + return int.TryParse(span.ToString(), style, provider, out value); +#else + return int.TryParse(span, style, provider, out value); +#endif + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseDouble(this ReadOnlySpan span, NumberStyles style, IFormatProvider provider, out double value) { @@ -39,6 +49,16 @@ namespace Avalonia.Utilities #endif } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double ParseDouble(this ReadOnlySpan span, IFormatProvider provider) + { +#if NETSTANDARD2_0 + return double.Parse(span.ToString(), provider); +#else + return double.Parse(span, provider: provider); +#endif + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseByte(this ReadOnlySpan span, NumberStyles style, IFormatProvider provider, out byte value) { @@ -46,6 +66,16 @@ namespace Avalonia.Utilities return byte.TryParse(span.ToString(), style, provider, out value); #else return byte.TryParse(span, style, provider, out value); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseEnum(this ReadOnlySpan span, bool ignoreCase, out TEnum value) where TEnum : struct + { +#if NETSTANDARD2_0 + return Enum.TryParse(span.ToString(), ignoreCase, out value); +#else + return Enum.TryParse(span, ignoreCase, out value); #endif } } diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs index aad742e02b..027817be2c 100644 --- a/src/Avalonia.Base/Utilities/StringTokenizer.cs +++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs @@ -46,6 +46,8 @@ namespace Avalonia.Utilities public string? CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength); + public ReadOnlySpan CurrentTokenSpan => _tokenIndex < 0 ? ReadOnlySpan.Empty : _s.AsSpan().Slice(_tokenIndex, _tokenLength); + public void Dispose() { if (_index != _length) @@ -56,8 +58,8 @@ namespace Avalonia.Utilities public bool TryReadInt32(out Int32 result, char? separator = null) { - if (TryReadString(out var stringResult, separator) && - int.TryParse(stringResult, NumberStyles.Integer, _formatProvider, out result)) + if (TryReadSpan(out var stringResult, separator) && + SpanHelpers.TryParseInt(stringResult, NumberStyles.Integer, _formatProvider, out result)) { return true; } @@ -80,8 +82,8 @@ namespace Avalonia.Utilities public bool TryReadDouble(out double result, char? separator = null) { - if (TryReadString(out var stringResult, separator) && - double.TryParse(stringResult, NumberStyles.Float, _formatProvider, out result)) + if (TryReadSpan(out var stringResult, separator) && + SpanHelpers.TryParseDouble(stringResult, NumberStyles.Float, _formatProvider, out result)) { return true; } @@ -102,10 +104,10 @@ namespace Avalonia.Utilities return result; } - public bool TryReadString([MaybeNullWhen(false)] out string result, char? separator = null) + public bool TryReadString([NotNull] out string result, char? separator = null) { var success = TryReadToken(separator ?? _separator); - result = CurrentToken; + result = CurrentTokenSpan.ToString(); return success; } @@ -119,6 +121,23 @@ namespace Avalonia.Utilities return result; } + public bool TryReadSpan(out ReadOnlySpan result, char? separator = null) + { + var success = TryReadToken(separator ?? _separator); + result = CurrentTokenSpan; + return success; + } + + public ReadOnlySpan ReadSpan(char? separator = null) + { + if (!TryReadSpan(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + private bool TryReadToken(char separator) { _tokenIndex = -1; diff --git a/tests/Avalonia.Base.UnitTests/Utilities/StringTokenizerTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/StringTokenizerTests.cs index 4f94b1ca80..42a4a406ef 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/StringTokenizerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/StringTokenizerTests.cs @@ -65,5 +65,15 @@ namespace Avalonia.Base.UnitTests.Utilities Assert.False(target.TryReadDouble(out var value)); } + + [Fact] + public void ReadSpan_And_ReadString_Reads_Same() + { + var target1 = new StringTokenizer("abc,def"); + var target2 = new StringTokenizer("abc,def"); + + Assert.Equal(target1.ReadString(), target2.ReadSpan().ToString()); + Assert.True(target1.ReadSpan().SequenceEqual(target2.ReadString())); + } } }