From 35591b05e23e3b21443397a1925e406a0b91f6fe Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Thu, 22 Dec 2022 19:57:37 +0000 Subject: [PATCH] Add ReadOnlySpan overload for TimeSpanParser Also tidies up the code here to reduce complexity --- CHANGELOG.md | 2 + X10D.Tests/src/Time/CharSpanTests.cs | 31 ++++ X10D/src/Time/CharSpanExtensions.cs | 69 +++++++++ X10D/src/Time/StringExtensions.cs | 4 +- X10D/src/Time/TimeSpanParser.cs | 220 +++++++++++++++++++-------- 5 files changed, 259 insertions(+), 67 deletions(-) create mode 100644 X10D.Tests/src/Time/CharSpanTests.cs create mode 100644 X10D/src/Time/CharSpanExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b18482..6b0cdeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - X10D: Added `Quaternion.ToVector3()` - X10D: Added `ReadOnlySpan.CountSubstring(char)` - X10D: Added `ReadOnlySpan.CountSubstring(ReadOnlySpan[, StringComparison])` +- X10D: Added `ReadOnlySpan.ToTimeSpan()` - X10D: Added `ReadOnlySpan.Split(T)` - X10D: Added `ReadOnlySpan.Split(ReadOnlySpan)` - X10D: Added `RoundUpToPowerOf2()` for built-in integer types @@ -47,6 +48,7 @@ - X10D: Added `Span.Split(Span)` - X10D: Added `string.CountSubstring(char)` - X10D: Added `string.CountSubstring(string[, StringComparison])` +- X10D: Added `TimeSpan.TryParse(ReadOnlySpan, out TimeSpan)` - X10D: Added `Quaternion.Multiply(Vector3)` - this functions as an equivalent to Unity's `Quaternion * Vector3` operator - X10D: Added `Vector2.Deconstruct()` - X10D: Added `Vector2.IsOnLine(LineF)`, `Vector2.IsOnLine(PointF, PointF)`, and `Vector2.IsOnLine(Vector2, Vector2)` diff --git a/X10D.Tests/src/Time/CharSpanTests.cs b/X10D.Tests/src/Time/CharSpanTests.cs new file mode 100644 index 0000000..6a3f4cf --- /dev/null +++ b/X10D.Tests/src/Time/CharSpanTests.cs @@ -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 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()); + } +} diff --git a/X10D/src/Time/CharSpanExtensions.cs b/X10D/src/Time/CharSpanExtensions.cs new file mode 100644 index 0000000..a238503 --- /dev/null +++ b/X10D/src/Time/CharSpanExtensions.cs @@ -0,0 +1,69 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +namespace X10D.Time; + +/// +/// Time-related extension methods for of . +/// +public static class CharSpanExtensions +{ + /// + /// Parses this span of characters as a shorthand time span (e.g. 3w 2d 1h) and converts it to an instance of + /// . + /// + /// + /// The input span of characters. Floating point is not supported, but integers with the following units are supported: + /// + /// + /// + /// Suffix + /// Meaning + /// + /// + /// + /// ms + /// Milliseconds + /// + /// + /// s + /// Seconds + /// + /// + /// m + /// Minutes + /// + /// + /// h + /// Hours + /// + /// + /// d + /// Days + /// + /// + /// w + /// Weeks + /// + /// + /// mo + /// Months + /// + /// + /// y + /// Years + /// + /// + /// + /// A new instance of . + [Pure] +#if NETSTANDARD2_1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#endif + public static TimeSpan ToTimeSpan(this ReadOnlySpan input) + { + return TimeSpanParser.TryParse(input, out TimeSpan result) ? result : default; + } +} diff --git a/X10D/src/Time/StringExtensions.cs b/X10D/src/Time/StringExtensions.cs index ea3eb77..0772ff5 100644 --- a/X10D/src/Time/StringExtensions.cs +++ b/X10D/src/Time/StringExtensions.cs @@ -4,7 +4,7 @@ using System.Runtime.CompilerServices; namespace X10D.Time; /// -/// Extension methods for . +/// Time-related extension methods for . /// 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 . /// /// - /// 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: /// /// /// diff --git a/X10D/src/Time/TimeSpanParser.cs b/X10D/src/Time/TimeSpanParser.cs index a6078ee..b320566 100644 --- a/X10D/src/Time/TimeSpanParser.cs +++ b/X10D/src/Time/TimeSpanParser.cs @@ -7,6 +7,78 @@ namespace X10D.Time; /// public static class TimeSpanParser { + /// + /// Attempts to parses a shorthand time span (e.g. 3w 2d 1h) as a span of characters, converting it to an instance of + /// which represents that duration of time. + /// + /// + /// The input span of characters. Floating point is not supported, but range the following units are supported: + /// + /// + /// + /// Suffix + /// Meaning + /// + /// + /// + /// ms + /// Milliseconds + /// + /// + /// s + /// Seconds + /// + /// + /// m + /// Minutes + /// + /// + /// h + /// Hours + /// + /// + /// d + /// Days + /// + /// + /// w + /// Weeks + /// + /// + /// mo + /// Months + /// + /// + /// y + /// Years + /// + /// + /// + /// When this method returns, contains the parsed result. + /// if the parse was successful, otherwise. + public static bool TryParse(ReadOnlySpan 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; + } + /// /// Attempts to parses a shorthand time span string (e.g. 3w 2d 1h), converting it to an instance of /// 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) { result = TimeSpan.Zero; + return !string.IsNullOrWhiteSpace(value) && TryParse(value.AsSpan(), out result); + } - if (string.IsNullOrWhiteSpace(value)) + private static bool HandleCharacter( + ReadOnlySpan 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; - - for (var index = 0; index < value.Length; index++) + if (HandleSuffix(ref index, ref result, ref unitValue, current, next)) { - char current = value[index]; - 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; } - 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; } }