Added negative TimeSpan handling (#1666)

- Added unit tests for the TimeSpanTypeReader
- Fixes https://github.com/discord-net/Discord.Net/issues/1657
This commit is contained in:
Slate
2021-11-24 12:55:07 +00:00
committed by GitHub
parent e0dbe7c695
commit 6abdfcbf87
2 changed files with 109 additions and 19 deletions

View File

@@ -6,7 +6,11 @@ namespace Discord.Commands
{ {
internal class TimeSpanTypeReader : TypeReader internal class TimeSpanTypeReader : TypeReader
{ {
private static readonly string[] Formats = { /// <summary>
/// TimeSpan try parse formats.
/// </summary>
private static readonly string[] Formats =
{
"%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s "%d'd'%h'h'%m'm'%s's'", // 4d3h2m1s
"%d'd'%h'h'%m'm'", // 4d3h2m "%d'd'%h'h'%m'm'", // 4d3h2m
"%d'd'%h'h'%s's'", // 4d3h 1s "%d'd'%h'h'%s's'", // 4d3h 1s
@@ -27,9 +31,25 @@ namespace Discord.Commands
/// <inheritdoc /> /// <inheritdoc />
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services) public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{ {
return (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan)) if (string.IsNullOrEmpty(input))
? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)) throw new ArgumentException(message: $"{nameof(input)} must not be null or empty.", paramName: nameof(input));
: Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan"));
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"));
}
} }
} }
} }

View File

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