Browse Source

Using a tokenizer instead of string split

Eli Arbel 8 years ago
parent
commit
898e7e4747

+ 1 - 0
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -26,6 +26,7 @@
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="..\Avalonia.Visuals\Utilities\StringTokenizer.cs" Link="Utils\StringTokenizer.cs" />
     <Compile Include="..\Shared\SharedAssemblyInfo.cs">
       <Link>Properties\SharedAssemblyInfo.cs</Link>
     </Compile>

+ 8 - 1
src/Avalonia.Controls/GridLength.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -210,7 +211,13 @@ namespace Avalonia.Controls
         /// <returns>The <see cref="GridLength"/>.</returns>
         public static IEnumerable<GridLength> ParseLengths(string s, CultureInfo culture)
         {
-            return s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Parse(x, culture));
+            using (var tokenizer = new StringTokenizer(s, culture))
+            {
+                while (tokenizer.NextString(out var item))
+                {
+                    yield return Parse(item, culture);
+                }
+            }
         }
     }
 }

+ 9 - 15
src/Avalonia.Visuals/Matrix.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Globalization;
 using System.Linq;
@@ -305,23 +306,16 @@ namespace Avalonia
         /// <returns>The <see cref="Matrix"/>.</returns>
         public static Matrix Parse(string s, CultureInfo culture)
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToArray();
-
-            if (parts.Length == 6)
+            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Matrix"))
             {
                 return new Matrix(
-                    double.Parse(parts[0], culture), 
-                    double.Parse(parts[1], culture), 
-                    double.Parse(parts[2], culture), 
-                    double.Parse(parts[3], culture), 
-                    double.Parse(parts[4], culture), 
-                    double.Parse(parts[5], culture));
-            }
-            else
-            {
-                throw new FormatException("Invalid Matrix.");
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired()
+                );
             }
         }
     }

+ 6 - 10
src/Avalonia.Visuals/Point.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Globalization;
 using System.Linq;
@@ -173,17 +174,12 @@ namespace Avalonia
         /// <returns>The <see cref="Thickness"/>.</returns>
         public static Point Parse(string s, CultureInfo culture)
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToList();
-
-            if (parts.Count == 2)
-            {
-                return new Point(double.Parse(parts[0], culture), double.Parse(parts[1], culture));
-            }
-            else
+            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Point"))
             {
-                throw new FormatException("Invalid Point.");
+                return new Point(
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired()
+                );
             }
         }
 

+ 7 - 13
src/Avalonia.Visuals/Rect.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Globalization;
 using System.Linq;
@@ -490,21 +491,14 @@ namespace Avalonia
         /// <returns>The parsed <see cref="Rect"/>.</returns>
         public static Rect Parse(string s, CultureInfo culture)
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToList();
-
-            if (parts.Count == 4)
+            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Rect"))
             {
                 return new Rect(
-                    double.Parse(parts[0], culture),
-                    double.Parse(parts[1], culture),
-                    double.Parse(parts[2], culture),
-                    double.Parse(parts[3], culture));
-            }
-            else
-            {
-                throw new FormatException("Invalid Rect.");
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired()
+                );
             }
         }
     }

+ 11 - 15
src/Avalonia.Visuals/RelativePoint.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Globalization;
 using System.Linq;
@@ -157,37 +158,32 @@ namespace Avalonia
         /// <returns>The parsed <see cref="RelativePoint"/>.</returns>
         public static RelativePoint Parse(string s, CultureInfo culture)
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToList();
-
-            if (parts.Count == 2)
+            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativePoint"))
             {
+                var x = tokenizer.NextStringRequired();
+                var y = tokenizer.NextStringRequired();
+
                 var unit = RelativeUnit.Absolute;
                 var scale = 1.0;
 
-                if (parts[0].EndsWith("%"))
+                if (x.EndsWith("%"))
                 {
-                    if (!parts[1].EndsWith("%"))
+                    if (!y.EndsWith("%"))
                     {
                         throw new FormatException("If one coordinate is relative, both must be.");
                     }
 
-                    parts[0] = parts[0].TrimEnd('%');
-                    parts[1] = parts[1].TrimEnd('%');
+                    x = x.TrimEnd('%');
+                    y = y.TrimEnd('%');
                     unit = RelativeUnit.Relative;
                     scale = 0.01;
                 }
 
                 return new RelativePoint(
-                    double.Parse(parts[0], culture) * scale,
-                    double.Parse(parts[1], culture) * scale,
+                    double.Parse(x, culture) * scale,
+                    double.Parse(y, culture) * scale,
                     unit);
             }
-            else
-            {
-                throw new FormatException("Invalid Point.");
-            }
         }
     }
 }

+ 29 - 26
src/Avalonia.Visuals/RelativeRect.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Globalization;
 using System.Linq;
@@ -12,6 +13,8 @@ namespace Avalonia
     /// </summary>
     public struct RelativeRect : IEquatable<RelativeRect>
     {
+        private static readonly char[] PercentChar = { '%' };
+
         /// <summary>
         /// A rectangle that represents 100% of an area.
         /// </summary>
@@ -159,7 +162,7 @@ namespace Avalonia
                     Rect.Width * size.Width,
                     Rect.Height * size.Height);
         }
-
+        
         /// <summary>
         /// Parses a <see cref="RelativeRect"/> string.
         /// </summary>
@@ -168,43 +171,43 @@ namespace Avalonia
         /// <returns>The parsed <see cref="RelativeRect"/>.</returns>
         public static RelativeRect Parse(string s, CultureInfo culture)
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToList();
-
-            if (parts.Count == 4)
+            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativeRect"))
             {
+                var x = tokenizer.NextStringRequired();
+                var y = tokenizer.NextStringRequired();
+                var width = tokenizer.NextStringRequired();
+                var height = tokenizer.NextStringRequired();
+
                 var unit = RelativeUnit.Absolute;
                 var scale = 1.0;
 
-                if (parts[0].EndsWith("%"))
+                var xRelative = x.EndsWith("%", StringComparison.Ordinal);
+                var yRelative = y.EndsWith("%", StringComparison.Ordinal);
+                var widthRelative = width.EndsWith("%", StringComparison.Ordinal);
+                var heightRelative = height.EndsWith("%", StringComparison.Ordinal);
+
+                if (xRelative && yRelative && widthRelative && heightRelative)
                 {
-                    if (!parts[1].EndsWith("%") 
-                        || !parts[2].EndsWith("%")
-                        || !parts[3].EndsWith("%"))
-                    {
-                        throw new FormatException("If one coordinate is relative, all other must be too.");
-                    }
-
-                    parts[0] = parts[0].TrimEnd('%');
-                    parts[1] = parts[1].TrimEnd('%');
-                    parts[2] = parts[2].TrimEnd('%');
-                    parts[3] = parts[3].TrimEnd('%');
+                    x = x.TrimEnd(PercentChar);
+                    y = y.TrimEnd(PercentChar);
+                    width = width.TrimEnd(PercentChar);
+                    height = height.TrimEnd(PercentChar);
+
                     unit = RelativeUnit.Relative;
                     scale = 0.01;
                 }
+                else if (xRelative || yRelative || widthRelative || heightRelative)
+                {
+                    throw new FormatException("If one coordinate is relative, all must be.");
+                }
 
                 return new RelativeRect(
-                    double.Parse(parts[0], culture) * scale,
-                    double.Parse(parts[1], culture) * scale,
-                    double.Parse(parts[2], culture) * scale,
-                    double.Parse(parts[3], culture) * scale,
+                    double.Parse(x, culture) * scale,
+                    double.Parse(y, culture) * scale,
+                    double.Parse(width, culture) * scale,
+                    double.Parse(height, culture) * scale,
                     unit);
             }
-            else
-            {
-                throw new FormatException("Invalid RelativeRect.");
-            }
         }
     }
 }

+ 5 - 10
src/Avalonia.Visuals/Size.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Globalization;
 using System.Linq;
@@ -153,17 +154,11 @@ namespace Avalonia
         /// <returns>The <see cref="Size"/>.</returns>
         public static Size Parse(string s, CultureInfo culture)
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToList();
-
-            if (parts.Count == 2)
-            {
-                return new Size(double.Parse(parts[0], culture), double.Parse(parts[1], culture));
-            }
-            else
+            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Size"))
             {
-                throw new FormatException("Invalid Size.");
+                return new Size(
+                    tokenizer.NextDoubleRequired(),
+                    tokenizer.NextDoubleRequired());
             }
         }
 

+ 15 - 20
src/Avalonia.Visuals/Thickness.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
 using System;
 using System.Globalization;
 using System.Linq;
@@ -163,28 +164,22 @@ namespace Avalonia
         /// <returns>The <see cref="Thickness"/>.</returns>
         public static Thickness Parse(string s, CultureInfo culture)
         {
-            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
-                .Select(x => x.Trim())
-                .ToList();
-
-            switch (parts.Count)
+            using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Thickness"))
             {
-                case 1:
-                    var uniform = double.Parse(parts[0], culture);
-                    return new Thickness(uniform);
-                case 2:
-                    var horizontal = double.Parse(parts[0], culture);
-                    var vertical = double.Parse(parts[1], culture);
-                    return new Thickness(horizontal, vertical);
-                case 4:
-                    var left = double.Parse(parts[0], culture);
-                    var top = double.Parse(parts[1], culture);
-                    var right = double.Parse(parts[2], culture);
-                    var bottom = double.Parse(parts[3], culture);
-                    return new Thickness(left, top, right, bottom);
+                var a = tokenizer.NextDoubleRequired();
+
+                if (tokenizer.NextDouble(out var b))
+                {
+                    if (tokenizer.NextDouble(out var c))
+                    {
+                        return new Thickness(a, b, c, tokenizer.NextDoubleRequired());
+                    }
+
+                    return new Thickness(a, b);
+                }
+                
+                return new Thickness(a);
             }
-
-            throw new FormatException("Invalid Thickness.");
         }
 
         /// <summary>

+ 205 - 0
src/Avalonia.Visuals/Utilities/StringTokenizer.cs

@@ -0,0 +1,205 @@
+using System;
+using System.Globalization;
+using static System.Char;
+
+namespace Avalonia.Utilities
+{
+    internal struct StringTokenizer : IDisposable
+    {
+        private const char DefaultSeparatorChar = ',';
+
+        private readonly string _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 StringTokenizer(string s, IFormatProvider formatProvider, string exceptionMessage = null)
+            : this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage)
+        {
+            _formatProvider = formatProvider;
+        }
+
+        public StringTokenizer(string s, char separator = DefaultSeparatorChar, string exceptionMessage = null)
+        {
+            _s = s ?? throw new ArgumentNullException(nameof(s));
+            _length = s?.Length ?? 0;
+            _separator = separator;
+            _exceptionMessage = exceptionMessage;
+            _formatProvider = CultureInfo.InvariantCulture;
+            _index = 0;
+            _tokenIndex = -1;
+            _tokenLength = 0;
+
+            while (_index < _length && IsWhiteSpace(_s, _index))
+            {
+                _index++;
+            }
+        }
+
+        public string CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength);
+
+        public void Dispose()
+        {
+            if (_index != _length)
+            {
+                throw GetFormatException();
+            }
+        }
+
+        public bool NextInt32(out Int32 result, char? separator = null)
+        {
+            var success = NextString(out var stringResult, separator);
+            result = success ? int.Parse(stringResult, _formatProvider) : 0;
+            return success;
+        }
+
+        public int NextInt32Required(char? separator = null)
+        {
+            if (!NextInt32(out var result, separator))
+            {
+                throw GetFormatException();
+            }
+
+            return result;
+        }
+
+        public bool NextDouble(out double result, char? separator = null)
+        {
+            var success = NextString(out var stringResult, separator);
+            result = success ? double.Parse(stringResult, _formatProvider) : 0;
+            return success;
+        }
+
+        public double NextDoubleRequired(char? separator = null)
+        {
+            if (!NextDouble(out var result, separator))
+            {
+                throw GetFormatException();
+            }
+
+            return result;
+        }
+
+        public bool NextString(out string result, char? separator = null)
+        {
+            var success = NextToken(separator ?? _separator);
+            result = CurrentToken;
+            return success;
+        }
+
+        public string NextStringRequired(char? separator = null)
+        {
+            if (!NextString(out var result, separator))
+            {
+                throw GetFormatException();
+            }
+
+            return result;
+        }
+
+        private bool NextToken(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;
+        }
+    }
+}

+ 8 - 0
tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
 using System.Globalization;
 using Xunit;
 
@@ -25,5 +26,12 @@ namespace Avalonia.Visuals.UnitTests
 
             Assert.Equal(new RelativeRect(0.1, 0.2, 0.4, 0.7, RelativeUnit.Relative), result, Compare);
         }
+
+        [Fact]
+        public void Parse_Should_Throw_Mixed_Values()
+        {
+            Assert.Throws<FormatException>(() =>
+                RelativeRect.Parse("10%, 20%, 40, 70%", CultureInfo.InvariantCulture));
+        }
     }
 }