Browse Source

Using a tokenizer instead of string split

pull/1125/head
Eli Arbel 9 years ago
parent
commit
898e7e4747
  1. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  2. 9
      src/Avalonia.Controls/GridLength.cs
  3. 24
      src/Avalonia.Visuals/Matrix.cs
  4. 16
      src/Avalonia.Visuals/Point.cs
  5. 20
      src/Avalonia.Visuals/Rect.cs
  6. 26
      src/Avalonia.Visuals/RelativePoint.cs
  7. 55
      src/Avalonia.Visuals/RelativeRect.cs
  8. 15
      src/Avalonia.Visuals/Size.cs
  9. 35
      src/Avalonia.Visuals/Thickness.cs
  10. 205
      src/Avalonia.Visuals/Utilities/StringTokenizer.cs
  11. 8
      tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs

1
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>

9
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);
}
}
}
}
}

24
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()
);
}
}
}

16
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()
);
}
}

20
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()
);
}
}
}

26
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.");
}
}
}
}

55
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.");
}
}
}
}

15
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());
}
}

35
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
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
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));
}
}
}

Loading…
Cancel
Save