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