Skip to content

Commit

Permalink
Added negative TimeSpan handling (discord-net#1666)
Browse files Browse the repository at this point in the history
- Added unit tests for the TimeSpanTypeReader
- Fixes discord-net#1657
  • Loading branch information
kjhf authored Nov 24, 2021
1 parent e0dbe7c commit 6abdfcb
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 19 deletions.
58 changes: 39 additions & 19 deletions src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// <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'", // 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
};

/// <inheritdoc />
public override Task<TypeReaderResult> 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"));
}
}
}
}
70 changes: 70 additions & 0 deletions test/Discord.Net.Tests.Unit/TimeSpanTypeReaderTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}

0 comments on commit 6abdfcb

Please sign in to comment.