Skip to content

Commit 21bf4d8

Browse files
authored
Merge pull request #30 from Rutherther/fix/timespanparser
TimeSpanParser unintuitive edge cases
2 parents 900af40 + 25b740d commit 21bf4d8

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

Remora.Commands/Parsers/Builtin/TimeSpanParser.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class TimeSpanParser : AbstractTypeParser<TimeSpan>
4040
{
4141
private static readonly Regex _pattern = new
4242
(
43-
"(?<Years>\\d+(?=y))|(?<Months>\\d+(?=mo))|(?<Weeks>\\d+(?=w))|(?<Days>\\d+(?=d))|(?<Hours>\\d+(?=h))|(?<Minutes>\\d+(?=m))|(?<Seconds>\\d+(?=s))",
43+
"^-|(?<Years>\\d+y)|(?<Months>\\d+mo)|(?<Weeks>\\d+w)|(?<Days>\\d+d)|(?<Hours>\\d+h)|(?<Minutes>\\d+m)|(?<Seconds>\\d+s)",
4444
RegexOptions.Compiled
4545
);
4646

@@ -65,6 +65,11 @@ public override ValueTask<Result<TimeSpan>> TryParseAsync(string? value, Cancell
6565
return new ValueTask<Result<TimeSpan>>(new ParsingError<TimeSpan>(value));
6666
}
6767

68+
if (value.Length != matches.Sum(x => x.Length))
69+
{
70+
return new ValueTask<Result<TimeSpan>>(new ParsingError<TimeSpan>(value));
71+
}
72+
6873
var timeSpan = TimeSpan.Zero;
6974
foreach (var match in matches.Cast<Match>())
7075
{
@@ -74,8 +79,14 @@ public override ValueTask<Result<TimeSpan>> TryParseAsync(string? value, Cancell
7479
.Skip(1)
7580
.Select(g => (g.Name, g.Value));
7681

77-
foreach (var (key, groupValue) in groups)
82+
foreach (var (key, groupRawValue) in groups)
7883
{
84+
var groupValue = groupRawValue[..^1];
85+
if (key is "Months")
86+
{
87+
groupValue = groupValue[..^1];
88+
}
89+
7990
if (!double.TryParse(groupValue, out var parsedGroupValue))
8091
{
8192
return new ValueTask<Result<TimeSpan>>(new ParsingError<TimeSpan>(value));
@@ -123,6 +134,11 @@ public override ValueTask<Result<TimeSpan>> TryParseAsync(string? value, Cancell
123134
}
124135
}
125136

137+
if (value[0] == '-')
138+
{
139+
timeSpan = -timeSpan;
140+
}
141+
126142
return new ValueTask<Result<TimeSpan>>(timeSpan);
127143
}
128144
}

Tests/Remora.Commands.Tests/Parsers/TimeSpanParserTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
using System.Collections.Generic;
2525
using System.Threading.Tasks;
2626
using Remora.Commands.Parsers;
27+
using Remora.Commands.Results;
2728
using Xunit;
2829

2930
namespace Remora.Commands.Tests.Parsers;
@@ -86,9 +87,25 @@ public class TimeSpanParserTests
8687

8788
return then - now + TimeSpan.FromMinutes(1);
8889
})
90+
],
91+
[
92+
"-1m",
93+
new Func<TimeSpan>(() => -TimeSpan.FromMinutes(1))
8994
]
9095
];
9196

97+
/// <summary>
98+
/// Gets a set of various test cases that aren't parseable.
99+
/// </summary>
100+
public static IEnumerable<object?[]> InvalidCases =>
101+
[
102+
["x60sx"],
103+
["1m-60s"],
104+
["s"],
105+
[string.Empty],
106+
[null],
107+
];
108+
92109
/// <summary>
93110
/// Tests whether the parser can successfully parse various inputs.
94111
/// </summary>
@@ -106,4 +123,21 @@ public async Task CanParse(string value, Func<TimeSpan> expected)
106123
Assert.True(result.IsSuccess);
107124
Assert.Equal(expected(), result.Entity);
108125
}
126+
127+
/// <summary>
128+
/// Tests whether the parser outputs errors in case of malformed inputs.
129+
/// </summary>
130+
/// <param name="value">The value to parse.</param>
131+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
132+
[Theory]
133+
[MemberData(nameof(InvalidCases))]
134+
public async Task DoesntAcceptInvalidInput(string? value)
135+
{
136+
var parser = new TimeSpanParser();
137+
138+
var result = await parser.TryParseAsync(value);
139+
140+
Assert.False(result.IsSuccess);
141+
Assert.IsType<ParsingError<TimeSpan>>(result.Error);
142+
}
109143
}

0 commit comments

Comments
 (0)