From 6abdfcbf878329f8294884b60e90acf797bf1f9b Mon Sep 17 00:00:00 2001 From: Slate Date: Wed, 24 Nov 2021 12:55:07 +0000 Subject: [PATCH] Added negative TimeSpan handling (#1666) - Added unit tests for the TimeSpanTypeReader - Fixes https://github.com/discord-net/Discord.Net/issues/1657 --- .../Readers/TimeSpanTypeReader.cs | 58 ++++++++++----- .../TimeSpanTypeReaderTests.cs | 70 +++++++++++++++++++ 2 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs diff --git a/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs b/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs index b4a27cb5b..5448553b3 100644 --- a/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs @@ -6,30 +6,50 @@ namespace Discord.Commands { internal class TimeSpanTypeReader : TypeReader { - private static readonly string[] Formats = { - "%d'd'%h'h'%m'm'%s's'", //4d3h2m1s - "%d'd'%h'h'%m'm'", //4d3h2m - "%d'd'%h'h'%s's'", //4d3h 1s - "%d'd'%h'h'", //4d3h - "%d'd'%m'm'%s's'", //4d 2m1s - "%d'd'%m'm'", //4d 2m - "%d'd'%s's'", //4d 1s - "%d'd'", //4d - "%h'h'%m'm'%s's'", // 3h2m1s - "%h'h'%m'm'", // 3h2m - "%h'h'%s's'", // 3h 1s - "%h'h'", // 3h - "%m'm'%s's'", // 2m1s - "%m'm'", // 2m - "%s's'", // 1s + /// + /// TimeSpan try parse formats. + /// + private static readonly string[] Formats = + { + "%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s + "%d'd'%h'h'%m'm'", // 4d3h2m + "%d'd'%h'h'%s's'", // 4d3h 1s + "%d'd'%h'h'", // 4d3h + "%d'd'%m'm'%s's'", // 4d 2m1s + "%d'd'%m'm'", // 4d 2m + "%d'd'%s's'", // 4d 1s + "%d'd'", // 4d + "%h'h'%m'm'%s's'", // 3h2m1s + "%h'h'%m'm'", // 3h2m + "%h'h'%s's'", // 3h 1s + "%h'h'", // 3h + "%m'm'%s's'", // 2m1s + "%m'm'", // 2m + "%s's'", // 1s }; /// public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - return (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) - ? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)) - : Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); + if (string.IsNullOrEmpty(input)) + throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input)); + + var isNegative = input[0] == '-'; // Char for CultureInfo.InvariantCulture.NumberFormat.NegativeSign + if (isNegative) + { + input = input.Substring(1); + } + + if (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) + { + return isNegative + ? Task.FromResult(TypeReaderResult.FromSuccess(-timeSpan)) + : Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)); + } + else + { + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); + } } } } diff --git a/test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs b/test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs new file mode 100644 index 000000000..4cd9cae09 --- /dev/null +++ b/test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs @@ -0,0 +1,70 @@ +using Discord.Commands; +using System; +using Xunit; + +namespace Discord +{ + public class TimeSpanTypeReaderTests + { + [Theory] + [InlineData("4d3h2m1s", false)] // tests format "%d'd'%h'h'%m'm'%s's'" + [InlineData("4d3h2m", false)] // tests format "%d'd'%h'h'%m'm'" + [InlineData("4d3h1s", false)] // tests format "%d'd'%h'h'%s's'" + [InlineData("4d3h", false)] // tests format "%d'd'%h'h'" + [InlineData("4d2m1s", false)] // tests format "%d'd'%m'm'%s's'" + [InlineData("4d2m", false)] // tests format "%d'd'%m'm'" + [InlineData("4d1s", false)] // tests format "%d'd'%s's'" + [InlineData("4d", false)] // tests format "%d'd'" + [InlineData("3h2m1s", false)] // tests format "%h'h'%m'm'%s's'" + [InlineData("3h2m", false)] // tests format "%h'h'%m'm'" + [InlineData("3h1s", false)] // tests format "%h'h'%s's'" + [InlineData("3h", false)] // tests format "%h'h'" + [InlineData("2m1s", false)] // tests format "%m'm'%s's'" + [InlineData("2m", false)] // tests format "%m'm'" + [InlineData("1s", false)] // tests format "%s's'" + // Negatives + [InlineData("-4d3h2m1s", true)] // tests format "-%d'd'%h'h'%m'm'%s's'" + [InlineData("-4d3h2m", true)] // tests format "-%d'd'%h'h'%m'm'" + [InlineData("-4d3h1s", true)] // tests format "-%d'd'%h'h'%s's'" + [InlineData("-4d3h", true)] // tests format "-%d'd'%h'h'" + [InlineData("-4d2m1s", true)] // tests format "-%d'd'%m'm'%s's'" + [InlineData("-4d2m", true)] // tests format "-%d'd'%m'm'" + [InlineData("-4d1s", true)] // tests format "-%d'd'%s's'" + [InlineData("-4d", true)] // tests format "-%d'd'" + [InlineData("-3h2m1s", true)] // tests format "-%h'h'%m'm'%s's'" + [InlineData("-3h2m", true)] // tests format "-%h'h'%m'm'" + [InlineData("-3h1s", true)] // tests format "-%h'h'%s's'" + [InlineData("-3h", true)] // tests format "-%h'h'" + [InlineData("-2m1s", true)] // tests format "-%m'm'%s's'" + [InlineData("-2m", true)] // tests format "-%m'm'" + [InlineData("-1s", true)] // tests format "-%s's'" + public void TestTimeSpanParse(string input, bool isNegative) + { + var reader = new TimeSpanTypeReader(); + var result = reader.ReadAsync(null, input, null).Result; + Assert.True(result.IsSuccess); + + var actual = (TimeSpan)result.BestMatch; + Assert.True(actual != TimeSpan.Zero); + + if (isNegative) + { + Assert.True(actual < TimeSpan.Zero); + + Assert.True(actual.Seconds == 0 || actual.Seconds == -1); + Assert.True(actual.Minutes == 0 || actual.Minutes == -2); + Assert.True(actual.Hours == 0 || actual.Hours == -3); + Assert.True(actual.Days == 0 || actual.Days == -4); + } + else + { + Assert.True(actual > TimeSpan.Zero); + + Assert.True(actual.Seconds == 0 || actual.Seconds == 1); + Assert.True(actual.Minutes == 0 || actual.Minutes == 2); + Assert.True(actual.Hours == 0 || actual.Hours == 3); + Assert.True(actual.Days == 0 || actual.Days == 4); + } + } + } +}