|
25 | 25 |
|
26 | 26 | import java.time.Duration; |
27 | 27 | import java.util.Collections; |
28 | | -import java.util.regex.Matcher; |
29 | | -import java.util.regex.Pattern; |
30 | 28 | import java.util.stream.Collectors; |
31 | 29 | import java.util.stream.IntStream; |
32 | 30 | import java.util.stream.Stream; |
33 | 31 | import org.apiguardian.api.API; |
34 | 32 | import org.checkerframework.checker.nullness.qual.NonNull; |
| 33 | +import org.checkerframework.checker.nullness.qual.Nullable; |
35 | 34 | import org.incendo.cloud.caption.CaptionVariable; |
36 | 35 | import org.incendo.cloud.caption.StandardCaptionKeys; |
37 | 36 | import org.incendo.cloud.component.CommandComponent; |
@@ -73,48 +72,65 @@ public final class DurationParser<C> implements ArgumentParser<C, Duration>, Blo |
73 | 72 | return CommandComponent.<C, Duration>builder().parser(durationParser()); |
74 | 73 | } |
75 | 74 |
|
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 | | - |
81 | 75 | @Override |
82 | 76 | public @NonNull ArgumentParseResult<Duration> parse( |
83 | 77 | final @NonNull CommandContext<C> commandContext, |
84 | 78 | final @NonNull CommandInput commandInput |
85 | 79 | ) { |
86 | | - final String input = commandInput.readString(); |
87 | | - |
88 | | - final Matcher matcher = DURATION_PATTERN.matcher(input); |
| 80 | + final String input = commandInput.peekString(); |
89 | 81 |
|
90 | 82 | Duration duration = Duration.ofNanos(0); |
91 | 83 |
|
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)); |
| 84 | + // substring range enclosing digits and unit (single char) |
| 85 | + int rangeStart = 0; |
| 86 | + int cursor = 0; |
| 87 | + |
| 88 | + while (cursor < input.length()) { |
| 89 | + // advance cursor until time unit or we reach end of input (in which case it's invalid anyway) |
| 90 | + while (cursor < input.length() && Character.isDigit(input.charAt(cursor))) { |
| 91 | + cursor += 1; |
| 92 | + } |
| 93 | + |
| 94 | + // reached end of input with no time unit |
| 95 | + if (cursor == input.length()) { |
| 96 | + return ArgumentParseResult.failure(new DurationParseException(input, commandContext)); |
| 97 | + } |
| 98 | + |
| 99 | + final long timeValue; |
| 100 | + try { |
| 101 | + timeValue = Long.parseUnsignedLong(input.substring(rangeStart, cursor)); |
| 102 | + } catch (final NumberFormatException ex) { |
| 103 | + return ArgumentParseResult.failure(new DurationParseException(ex, input, commandContext)); |
| 104 | + } |
| 105 | + |
| 106 | + final char timeUnit = input.charAt(cursor); |
96 | 107 | switch (timeUnit) { |
97 | | - case "d": |
| 108 | + case 'd': |
98 | 109 | duration = duration.plusDays(timeValue); |
99 | 110 | break; |
100 | | - case "h": |
| 111 | + case 'h': |
101 | 112 | duration = duration.plusHours(timeValue); |
102 | 113 | break; |
103 | | - case "m": |
| 114 | + case 'm': |
104 | 115 | duration = duration.plusMinutes(timeValue); |
105 | 116 | break; |
106 | | - case "s": |
| 117 | + case 's': |
107 | 118 | duration = duration.plusSeconds(timeValue); |
108 | 119 | break; |
109 | 120 | default: |
110 | 121 | return ArgumentParseResult.failure(new DurationParseException(input, commandContext)); |
111 | 122 | } |
| 123 | + |
| 124 | + // skip unit, reset rangeStart to start of next segment |
| 125 | + cursor += 1; |
| 126 | + rangeStart = cursor; |
112 | 127 | } |
113 | 128 |
|
114 | 129 | if (duration.isZero()) { |
115 | 130 | return ArgumentParseResult.failure(new DurationParseException(input, commandContext)); |
116 | 131 | } |
117 | 132 |
|
| 133 | + commandInput.readString(); // pop read input on success |
118 | 134 | return ArgumentParseResult.success(duration); |
119 | 135 | } |
120 | 136 |
|
@@ -172,6 +188,28 @@ public DurationParseException( |
172 | 188 | this.input = input; |
173 | 189 | } |
174 | 190 |
|
| 191 | + /** |
| 192 | + * Construct a new {@link DurationParseException} with a causing exception. |
| 193 | + * |
| 194 | + * @param cause cause of exception |
| 195 | + * @param input input string |
| 196 | + * @param context command context |
| 197 | + */ |
| 198 | + public DurationParseException( |
| 199 | + final @Nullable Throwable cause, |
| 200 | + final @NonNull String input, |
| 201 | + final @NonNull CommandContext<?> context |
| 202 | + ) { |
| 203 | + super( |
| 204 | + cause, |
| 205 | + DurationParser.class, |
| 206 | + context, |
| 207 | + StandardCaptionKeys.ARGUMENT_PARSE_FAILURE_DURATION, |
| 208 | + CaptionVariable.of("input", input) |
| 209 | + ); |
| 210 | + this.input = input; |
| 211 | + } |
| 212 | + |
175 | 213 | /** |
176 | 214 | * Returns the supplied input string. |
177 | 215 | * |
|
0 commit comments