|
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; |
|
46 | 45 | /** |
47 | 46 | * Parser for {@link Duration}. |
48 | 47 | * |
| 48 | + * <p>Matches durations in the format of: <code>2d15h7m12s</code>.</p> |
| 49 | + * |
49 | 50 | * @param <C> command sender type |
50 | 51 | */ |
51 | 52 | @API(status = API.Status.STABLE) |
@@ -73,42 +74,64 @@ public final class DurationParser<C> implements ArgumentParser<C, Duration>, Blo |
73 | 74 | return CommandComponent.<C, Duration>builder().parser(durationParser()); |
74 | 75 | } |
75 | 76 |
|
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 | 77 | @Override |
82 | 78 | public @NonNull ArgumentParseResult<Duration> parse( |
83 | 79 | final @NonNull CommandContext<C> commandContext, |
84 | 80 | final @NonNull CommandInput commandInput |
85 | 81 | ) { |
86 | 82 | final String input = commandInput.readString(); |
87 | 83 |
|
88 | | - final Matcher matcher = DURATION_PATTERN.matcher(input); |
89 | | - |
90 | 84 | Duration duration = Duration.ofNanos(0); |
91 | 85 |
|
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)); |
111 | 101 | } |
| 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; |
112 | 135 | } |
113 | 136 |
|
114 | 137 | if (duration.isZero()) { |
@@ -172,6 +195,28 @@ public DurationParseException( |
172 | 195 | this.input = input; |
173 | 196 | } |
174 | 197 |
|
| 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 | + |
175 | 220 | /** |
176 | 221 | * Returns the supplied input string. |
177 | 222 | * |
|
0 commit comments