Add ReadOnlySpan<char> overload for TimeSpanParser

Also tidies up the code here to reduce complexity
This commit is contained in:
Oliver Booth 2022-12-22 19:57:37 +00:00
parent 8ff3447b85
commit 35591b05e2
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
5 changed files with 259 additions and 67 deletions

View File

@ -35,6 +35,7 @@
- X10D: Added `Quaternion.ToVector3()` - X10D: Added `Quaternion.ToVector3()`
- X10D: Added `ReadOnlySpan<char>.CountSubstring(char)` - X10D: Added `ReadOnlySpan<char>.CountSubstring(char)`
- X10D: Added `ReadOnlySpan<char>.CountSubstring(ReadOnlySpan<char>[, StringComparison])` - X10D: Added `ReadOnlySpan<char>.CountSubstring(ReadOnlySpan<char>[, StringComparison])`
- X10D: Added `ReadOnlySpan<char>.ToTimeSpan()`
- X10D: Added `ReadOnlySpan<T>.Split(T)` - X10D: Added `ReadOnlySpan<T>.Split(T)`
- X10D: Added `ReadOnlySpan<T>.Split(ReadOnlySpan<T>)` - X10D: Added `ReadOnlySpan<T>.Split(ReadOnlySpan<T>)`
- X10D: Added `RoundUpToPowerOf2()` for built-in integer types - X10D: Added `RoundUpToPowerOf2()` for built-in integer types
@ -47,6 +48,7 @@
- X10D: Added `Span<T>.Split(Span<T>)` - X10D: Added `Span<T>.Split(Span<T>)`
- X10D: Added `string.CountSubstring(char)` - X10D: Added `string.CountSubstring(char)`
- X10D: Added `string.CountSubstring(string[, StringComparison])` - X10D: Added `string.CountSubstring(string[, StringComparison])`
- X10D: Added `TimeSpan.TryParse(ReadOnlySpan<char>, out TimeSpan)`
- X10D: Added `Quaternion.Multiply(Vector3)` - this functions as an equivalent to Unity's `Quaternion * Vector3` operator - X10D: Added `Quaternion.Multiply(Vector3)` - this functions as an equivalent to Unity's `Quaternion * Vector3` operator
- X10D: Added `Vector2.Deconstruct()` - X10D: Added `Vector2.Deconstruct()`
- X10D: Added `Vector2.IsOnLine(LineF)`, `Vector2.IsOnLine(PointF, PointF)`, and `Vector2.IsOnLine(Vector2, Vector2)` - X10D: Added `Vector2.IsOnLine(LineF)`, `Vector2.IsOnLine(PointF, PointF)`, and `Vector2.IsOnLine(Vector2, Vector2)`

View File

@ -0,0 +1,31 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using X10D.Time;
namespace X10D.Tests.Time;
[TestClass]
public class CharSpanTests
{
[TestMethod]
public void ToTimeSpan_ShouldReturnCorrectTimeSpan_GivenSpanOfCharacters()
{
ReadOnlySpan<char> value = "1y 1mo 1w 1d 1h 1m 1s 1ms".AsSpan();
TimeSpan expected = TimeSpan.FromMilliseconds(1);
expected += TimeSpan.FromSeconds(1);
expected += TimeSpan.FromMinutes(1);
expected += TimeSpan.FromHours(1);
expected += TimeSpan.FromDays(1);
expected += TimeSpan.FromDays(7);
expected += TimeSpan.FromDays(30);
expected += TimeSpan.FromDays(365);
Assert.AreEqual(expected, value.ToTimeSpan());
}
[TestMethod]
public void ToTimeSpan_ShouldReturnZero_GivenInvalidSpanOfCharacters()
{
Assert.AreEqual(TimeSpan.Zero, "Hello World".AsSpan().ToTimeSpan());
}
}

View File

@ -0,0 +1,69 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
namespace X10D.Time;
/// <summary>
/// Time-related extension methods for <see cref="ReadOnlySpan{T}" /> of <see cref="char" />.
/// </summary>
public static class CharSpanExtensions
{
/// <summary>
/// Parses this span of characters as a shorthand time span (e.g. 3w 2d 1h) and converts it to an instance of
/// <see cref="TimeSpan" />.
/// </summary>
/// <param name="input">
/// The input span of characters. Floating point is not supported, but integers with the following units are supported:
///
/// <list type="table">
/// <listheader>
/// <term>Suffix</term>
/// <description>Meaning</description>
/// </listheader>
///
/// <item>
/// <term>ms</term>
/// <description>Milliseconds</description>
/// </item>
/// <item>
/// <term>s</term>
/// <description>Seconds</description>
/// </item>
/// <item>
/// <term>m</term>
/// <description>Minutes</description>
/// </item>
/// <item>
/// <term>h</term>
/// <description>Hours</description>
/// </item>
/// <item>
/// <term>d</term>
/// <description>Days</description>
/// </item>
/// <item>
/// <term>w</term>
/// <description>Weeks</description>
/// </item>
/// <item>
/// <term>mo</term>
/// <description>Months</description>
/// </item>
/// <item>
/// <term>y</term>
/// <description>Years</description>
/// </item>
/// </list>
/// </param>
/// <returns>A new instance of <see cref="TimeSpan" />.</returns>
[Pure]
#if NETSTANDARD2_1
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#endif
public static TimeSpan ToTimeSpan(this ReadOnlySpan<char> input)
{
return TimeSpanParser.TryParse(input, out TimeSpan result) ? result : default;
}
}

View File

@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
namespace X10D.Time; namespace X10D.Time;
/// <summary> /// <summary>
/// Extension methods for <see cref="string" />. /// Time-related extension methods for <see cref="string" />.
/// </summary> /// </summary>
public static class StringExtensions public static class StringExtensions
{ {
@ -12,7 +12,7 @@ public static class StringExtensions
/// Parses a shorthand time span string (e.g. 3w 2d 1h) and converts it to an instance of <see cref="TimeSpan" />. /// Parses a shorthand time span string (e.g. 3w 2d 1h) and converts it to an instance of <see cref="TimeSpan" />.
/// </summary> /// </summary>
/// <param name="input"> /// <param name="input">
/// The input string. Floating point is not supported, but range the following units are supported: /// The input string. Floating point is not supported, but integers with the following units are supported:
/// ///
/// <list type="table"> /// <list type="table">
/// <listheader> /// <listheader>

View File

@ -7,6 +7,78 @@ namespace X10D.Time;
/// </summary> /// </summary>
public static class TimeSpanParser public static class TimeSpanParser
{ {
/// <summary>
/// Attempts to parses a shorthand time span (e.g. 3w 2d 1h) as a span of characters, converting it to an instance of
/// <see cref="TimeSpan" /> which represents that duration of time.
/// </summary>
/// <param name="value">
/// The input span of characters. Floating point is not supported, but range the following units are supported:
///
/// <list type="table">
/// <listheader>
/// <term>Suffix</term>
/// <description>Meaning</description>
/// </listheader>
///
/// <item>
/// <term>ms</term>
/// <description>Milliseconds</description>
/// </item>
/// <item>
/// <term>s</term>
/// <description>Seconds</description>
/// </item>
/// <item>
/// <term>m</term>
/// <description>Minutes</description>
/// </item>
/// <item>
/// <term>h</term>
/// <description>Hours</description>
/// </item>
/// <item>
/// <term>d</term>
/// <description>Days</description>
/// </item>
/// <item>
/// <term>w</term>
/// <description>Weeks</description>
/// </item>
/// <item>
/// <term>mo</term>
/// <description>Months</description>
/// </item>
/// <item>
/// <term>y</term>
/// <description>Years</description>
/// </item>
/// </list>
/// </param>
/// <param name="result">When this method returns, contains the parsed result.</param>
/// <returns><see langword="true" /> if the parse was successful, <see langword="false" /> otherwise.</returns>
public static bool TryParse(ReadOnlySpan<char> value, out TimeSpan result)
{
result = TimeSpan.Zero;
if (value.Length == 0 || value.IsWhiteSpace())
{
return false;
}
var unitValue = 0;
for (var index = 0; index < value.Length; index++)
{
char current = value[index];
if (!HandleCharacter(value, ref result, current, ref unitValue, ref index))
{
return false;
}
}
return true;
}
/// <summary> /// <summary>
/// Attempts to parses a shorthand time span string (e.g. 3w 2d 1h), converting it to an instance of /// Attempts to parses a shorthand time span string (e.g. 3w 2d 1h), converting it to an instance of
/// <see cref="TimeSpan" /> which represents that duration of time. /// <see cref="TimeSpan" /> which represents that duration of time.
@ -59,77 +131,95 @@ public static class TimeSpanParser
public static bool TryParse([NotNullWhen(true)] string? value, out TimeSpan result) public static bool TryParse([NotNullWhen(true)] string? value, out TimeSpan result)
{ {
result = TimeSpan.Zero; result = TimeSpan.Zero;
return !string.IsNullOrWhiteSpace(value) && TryParse(value.AsSpan(), out result);
}
if (string.IsNullOrWhiteSpace(value)) private static bool HandleCharacter(
ReadOnlySpan<char> value,
ref TimeSpan result,
char current,
ref int unitValue,
ref int index
)
{
char next = index < value.Length - 1 ? value[index + 1] : '\0';
if (HandleSpecial(ref unitValue, current))
{ {
return false; return true;
} }
var unitValue = 0; if (HandleSuffix(ref index, ref result, ref unitValue, current, next))
for (var index = 0; index < value.Length; index++)
{ {
char current = value[index]; return true;
switch (current)
{
case var digitChar when char.IsDigit(digitChar):
var digit = (int)char.GetNumericValue(digitChar);
unitValue = unitValue * 10 + digit;
break;
case 'y':
result += TimeSpan.FromDays(unitValue * 365);
unitValue = 0;
break;
case 'm':
if (index < value.Length - 1 && value[index + 1] == 'o')
{
index++;
result += TimeSpan.FromDays(unitValue * 30);
}
else if (index < value.Length - 1 && value[index + 1] == 's')
{
index++;
result += TimeSpan.FromMilliseconds(unitValue);
}
else
{
result += TimeSpan.FromMinutes(unitValue);
}
unitValue = 0;
break;
case 'w':
result += TimeSpan.FromDays(unitValue * 7);
unitValue = 0;
break;
case 'd':
result += TimeSpan.FromDays(unitValue);
unitValue = 0;
break;
case 'h':
result += TimeSpan.FromHours(unitValue);
unitValue = 0;
break;
case 's':
result += TimeSpan.FromSeconds(unitValue);
unitValue = 0;
break;
case var space when char.IsWhiteSpace(space):
break;
default:
result = TimeSpan.Zero;
return false;
}
} }
return true; result = TimeSpan.Zero;
return false;
}
private static bool HandleSuffix(ref int index, ref TimeSpan result, ref int unitValue, char current, char next)
{
switch (current)
{
case 'm' when next == 'o':
index++;
result += TimeSpan.FromDays(unitValue * 30);
unitValue = 0;
return true;
case 'm' when next == 's':
index++;
result += TimeSpan.FromMilliseconds(unitValue);
unitValue = 0;
return true;
case 'm':
result += TimeSpan.FromMinutes(unitValue);
unitValue = 0;
return true;
case 'y':
result += TimeSpan.FromDays(unitValue * 365);
unitValue = 0;
return true;
case 'w':
result += TimeSpan.FromDays(unitValue * 7);
unitValue = 0;
return true;
case 'd':
result += TimeSpan.FromDays(unitValue);
unitValue = 0;
return true;
case 'h':
result += TimeSpan.FromHours(unitValue);
unitValue = 0;
return true;
case 's':
result += TimeSpan.FromSeconds(unitValue);
unitValue = 0;
return true;
}
return false;
}
private static bool HandleSpecial(ref int unitValue, char current)
{
switch (current)
{
case var _ when char.IsDigit(current):
var digit = (int)char.GetNumericValue(current);
unitValue = unitValue * 10 + digit;
return true;
case var _ when char.IsWhiteSpace(current):
return true;
}
return false;
} }
} }