Skip to content

Commit 156d985

Browse files
authored
Merge branch 'master' into fix/expose-immutables-annotations
2 parents 5c5b832 + e8f9532 commit 156d985

2 files changed

Lines changed: 117 additions & 29 deletions

File tree

cloud-core/src/main/java/org/incendo/cloud/parser/standard/DurationParser.java

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@
2525

2626
import java.time.Duration;
2727
import java.util.Collections;
28-
import java.util.regex.Matcher;
29-
import java.util.regex.Pattern;
3028
import java.util.stream.Collectors;
3129
import java.util.stream.IntStream;
3230
import java.util.stream.Stream;
3331
import org.apiguardian.api.API;
3432
import org.checkerframework.checker.nullness.qual.NonNull;
33+
import org.checkerframework.checker.nullness.qual.Nullable;
3534
import org.incendo.cloud.caption.CaptionVariable;
3635
import org.incendo.cloud.caption.StandardCaptionKeys;
3736
import org.incendo.cloud.component.CommandComponent;
@@ -46,6 +45,8 @@
4645
/**
4746
* Parser for {@link Duration}.
4847
*
48+
* <p>Matches durations in the format of: <code>2d15h7m12s</code>.</p>
49+
*
4950
* @param <C> command sender type
5051
*/
5152
@API(status = API.Status.STABLE)
@@ -73,42 +74,64 @@ public final class DurationParser<C> implements ArgumentParser<C, Duration>, Blo
7374
return CommandComponent.<C, Duration>builder().parser(durationParser());
7475
}
7576

76-
/**
77-
* Matches durations in the format of: <code>2d15h7m12s</code>
78-
*/
79-
private static final Pattern DURATION_PATTERN = Pattern.compile("(([1-9][0-9]+|[1-9])[dhms])");
80-
8177
@Override
8278
public @NonNull ArgumentParseResult<Duration> parse(
8379
final @NonNull CommandContext<C> commandContext,
8480
final @NonNull CommandInput commandInput
8581
) {
8682
final String input = commandInput.readString();
8783

88-
final Matcher matcher = DURATION_PATTERN.matcher(input);
89-
9084
Duration duration = Duration.ofNanos(0);
9185

92-
while (matcher.find()) {
93-
String group = matcher.group();
94-
String timeUnit = String.valueOf(group.charAt(group.length() - 1));
95-
int timeValue = Integer.parseInt(group.substring(0, group.length() - 1));
96-
switch (timeUnit) {
97-
case "d":
98-
duration = duration.plusDays(timeValue);
99-
break;
100-
case "h":
101-
duration = duration.plusHours(timeValue);
102-
break;
103-
case "m":
104-
duration = duration.plusMinutes(timeValue);
105-
break;
106-
case "s":
107-
duration = duration.plusSeconds(timeValue);
108-
break;
109-
default:
110-
return ArgumentParseResult.failure(new DurationParseException(input, commandContext));
86+
// substring range enclosing digits and unit (single char)
87+
int rangeStart = 0;
88+
int cursor = 0;
89+
90+
while (cursor < input.length()) {
91+
// advance cursor until time unit or we reach end of input (in which case it's invalid anyway)
92+
while (cursor < input.length()
93+
&& input.charAt(cursor) >= '0'
94+
&& input.charAt(cursor) <= '9') {
95+
cursor += 1;
96+
}
97+
98+
// reached end of input with no time unit
99+
if (cursor == input.length()) {
100+
return ArgumentParseResult.failure(new DurationParseException(input, commandContext));
111101
}
102+
103+
final long timeValue;
104+
try {
105+
timeValue = Long.parseLong(input.substring(rangeStart, cursor));
106+
} catch (final NumberFormatException ex) {
107+
return ArgumentParseResult.failure(new DurationParseException(ex, input, commandContext));
108+
}
109+
110+
final char timeUnit = input.charAt(cursor);
111+
try {
112+
switch (timeUnit) {
113+
case 'd':
114+
duration = duration.plusDays(timeValue);
115+
break;
116+
case 'h':
117+
duration = duration.plusHours(timeValue);
118+
break;
119+
case 'm':
120+
duration = duration.plusMinutes(timeValue);
121+
break;
122+
case 's':
123+
duration = duration.plusSeconds(timeValue);
124+
break;
125+
default:
126+
return ArgumentParseResult.failure(new DurationParseException(input, commandContext));
127+
}
128+
} catch (final ArithmeticException ex) {
129+
return ArgumentParseResult.failure(new DurationParseException(ex, input, commandContext));
130+
}
131+
132+
// skip unit, reset rangeStart to start of next segment
133+
cursor += 1;
134+
rangeStart = cursor;
112135
}
113136

114137
if (duration.isZero()) {
@@ -172,6 +195,28 @@ public DurationParseException(
172195
this.input = input;
173196
}
174197

198+
/**
199+
* Construct a new {@link DurationParseException} with a causing exception.
200+
*
201+
* @param cause cause of exception
202+
* @param input input string
203+
* @param context command context
204+
*/
205+
public DurationParseException(
206+
final @Nullable Throwable cause,
207+
final @NonNull String input,
208+
final @NonNull CommandContext<?> context
209+
) {
210+
super(
211+
cause,
212+
DurationParser.class,
213+
context,
214+
StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_DURATION,
215+
CaptionVariable.of("input", input)
216+
);
217+
this.input = input;
218+
}
219+
175220
/**
176221
* Returns the supplied input string.
177222
*

cloud-core/src/test/java/org/incendo/cloud/parser/standard/DurationParserTest.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,58 @@ void single_multiple_units() {
7575
}
7676

7777
@Test
78-
void invalid_format_failing() {
78+
void invalid_format_no_time_value() {
7979
Assertions.assertThrows(
8080
CompletionException.class,
8181
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration d").join()
8282
);
83+
}
84+
85+
@Test
86+
void invalid_format_no_time_unit() {
87+
Assertions.assertThrows(
88+
CompletionException.class,
89+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1").join()
90+
);
91+
}
8392

93+
@Test
94+
void invalid_format_invalid_unit() {
8495
Assertions.assertThrows(
8596
CompletionException.class,
8697
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1x").join()
8798
);
8899
}
100+
101+
@Test
102+
void invalid_format_leading_garbage() {
103+
Assertions.assertThrows(
104+
CompletionException.class,
105+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration foo1d").join()
106+
);
107+
}
108+
109+
@Test
110+
void invalid_format_garbage() {
111+
Assertions.assertThrows(
112+
CompletionException.class,
113+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1dfoo2h").join()
114+
);
115+
}
116+
117+
@Test
118+
void invalid_format_trailing_garbage() {
119+
Assertions.assertThrows(
120+
CompletionException.class,
121+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 1dfoo").join()
122+
);
123+
}
124+
125+
@Test
126+
void invalid_format_unicode_digits() {
127+
Assertions.assertThrows(
128+
CompletionException.class,
129+
() -> manager.commandExecutor().executeCommand(new TestCommandSender(), "duration 12h").join()
130+
);
131+
}
89132
}

0 commit comments

Comments
 (0)