Skip to content

Commit cc2be8a

Browse files
committed
Add parsing methods for durations with default time units
1 parent 250f255 commit cc2be8a

File tree

1 file changed

+229
-12
lines changed

1 file changed

+229
-12
lines changed

SimpleAPI/src/main/java/com/bencodez/simpleapi/time/ParsedDuration.java

Lines changed: 229 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.time.Instant;
55
import java.util.Locale;
66
import java.util.Objects;
7+
import java.util.concurrent.TimeUnit;
78
import java.util.regex.Matcher;
89
import java.util.regex.Pattern;
910

@@ -20,10 +21,8 @@
2021
* <li>1d</li>
2122
* <li>2w</li>
2223
* <li>1mo (treated as a fixed 30 days)</li>
23-
* <li>Combined tokens: 1h30m, 2d12h, 1w2d3h4m5s6ms (spaces allowed between
24-
* segments)</li>
25-
* <li>ISO-8601 (Duration.parse): PT30M, PT12H, P1D, etc (months/years not
26-
* supported by Duration)</li>
24+
* <li>Combined tokens: 1h30m, 2d12h, 1w2d3h4m5s6ms (spaces allowed between segments)</li>
25+
* <li>ISO-8601 (Duration.parse): PT30M, PT12H, P1D, etc (months/years not supported by Duration)</li>
2726
* <li>Plain number: "30" -> uses a configurable default unit</li>
2827
* </ul>
2928
*/
@@ -75,6 +74,10 @@ public boolean isEmpty() {
7574
* <p>
7675
* Example: {@code parse("30", Unit.MINUTES)} => 30 minutes
7776
* </p>
77+
*
78+
* @param raw raw input string
79+
* @param defaultUnit default unit for plain numbers and unknown suffixes
80+
* @return parsed duration (never null)
7881
*/
7982
public static ParsedDuration parse(String raw, Unit defaultUnit) {
8083
if (raw == null) {
@@ -93,8 +96,7 @@ public static ParsedDuration parse(String raw, Unit defaultUnit) {
9396
String lower = s.toLowerCase(Locale.ROOT);
9497

9598
// ISO-8601 Duration support (PT30M, PT12H, P1D, etc)
96-
// Duration.parse does NOT support months/years anyway; it will throw for
97-
// P1M/P1Y.
99+
// Duration.parse does NOT support months/years anyway; it will throw for P1M/P1Y.
98100
if (lower.startsWith("p")) {
99101
try {
100102
Duration d = Duration.parse(s.toUpperCase(Locale.ROOT));
@@ -131,22 +133,118 @@ public static ParsedDuration parse(String raw, Unit defaultUnit) {
131133
return applySuffix(value, suffix, defaultUnit);
132134
}
133135

136+
/**
137+
* Parses the input using {@code defaultUnit} if the string is just a number.
138+
*
139+
* <p>
140+
* Example: {@code parse("30", TimeUnit.MINUTES)} => 30 minutes
141+
* </p>
142+
*
143+
* <p>
144+
* Note: {@link ParsedDuration} stores fixed milliseconds. If you use
145+
* {@link TimeUnit#MICROSECONDS} or {@link TimeUnit#NANOSECONDS} as the default unit,
146+
* the parsed value will be truncated to milliseconds (via {@link TimeUnit#toMillis(long)}).
147+
* </p>
148+
*
149+
* @param raw raw input string
150+
* @param defaultUnit default unit for plain numbers and unknown suffixes
151+
* @return parsed duration (never null)
152+
*/
153+
public static ParsedDuration parse(String raw, TimeUnit defaultUnit) {
154+
if (raw == null) {
155+
return empty();
156+
}
157+
String s = raw.trim();
158+
if (s.isEmpty()) {
159+
return empty();
160+
}
161+
162+
// number-only -> default time unit
163+
if (PLAIN_NUMBER.matcher(s).matches()) {
164+
return applyDefaultTimeUnit(s, defaultUnit);
165+
}
166+
167+
String lower = s.toLowerCase(Locale.ROOT);
168+
169+
// ISO-8601 Duration support (PT30M, PT12H, P1D, etc)
170+
if (lower.startsWith("p")) {
171+
try {
172+
Duration d = Duration.parse(s.toUpperCase(Locale.ROOT));
173+
return ofMillis(d.toMillis());
174+
} catch (Exception ignored) {
175+
// fall through
176+
}
177+
}
178+
179+
// Combined tokens parsing
180+
ParsedDuration combined = parseCombinedTokens(lower, defaultUnit);
181+
if (combined != null) {
182+
return combined;
183+
}
184+
185+
Matcher m = VALUE_SUFFIX.matcher(lower);
186+
if (!m.matches()) {
187+
String digits = extractLeadingDigits(lower);
188+
if (!digits.isEmpty()) {
189+
return applyDefaultTimeUnit(digits, defaultUnit);
190+
}
191+
return empty();
192+
}
193+
194+
long value = safeParseLong(m.group(1));
195+
String suffix = m.group(2);
196+
197+
if (value <= 0L) {
198+
return empty();
199+
}
200+
201+
return applySuffix(value, suffix, defaultUnit);
202+
}
203+
134204
/**
135205
* Parses the input using MINUTES as the default unit for number-only strings.
206+
*
207+
* @param raw raw input string
208+
* @return parsed duration (never null)
136209
*/
137210
public static ParsedDuration parse(String raw) {
138211
return parse(raw, Unit.MINUTES);
139212
}
140213

141214
/**
142-
* If {@code raw} is just a number, returns the same value with
143-
* {@code defaultUnit} applied, otherwise returns {@link #parse(String, Unit)}
144-
* result.
215+
* Parses the input using {@link TimeUnit#MINUTES} as the default unit for number-only strings.
216+
*
217+
* @param raw raw input string
218+
* @return parsed duration (never null)
219+
*/
220+
public static ParsedDuration parseTimeUnit(String raw) {
221+
return parse(raw, TimeUnit.MINUTES);
222+
}
223+
224+
/**
225+
* If {@code raw} is just a number, returns the same value with {@code defaultUnit} applied,
226+
* otherwise returns {@link #parse(String, Unit)} result.
227+
*
228+
* @param raw raw input string
229+
* @param defaultUnit default unit for plain numbers and unknown suffixes
230+
* @return parsed duration (never null)
145231
*/
146232
public static ParsedDuration withDefaultUnit(String raw, Unit defaultUnit) {
147233
return parse(raw, defaultUnit);
148234
}
149235

236+
/**
237+
* If {@code raw} is just a number, returns the same value with {@code defaultUnit} applied,
238+
* otherwise returns {@link #parse(String, TimeUnit)} result.
239+
*
240+
* @param raw raw input string
241+
* @param defaultUnit default unit for plain numbers and unknown suffixes
242+
* @return parsed duration (never null)
243+
*/
244+
public static ParsedDuration withDefaultUnit(String raw, TimeUnit defaultUnit) {
245+
return parse(raw, defaultUnit);
246+
}
247+
150248
private static ParsedDuration applyDefaultUnit(String digits, Unit unit) {
151249
Objects.requireNonNull(unit, "defaultUnit");
152250
long value = safeParseLong(digits);
@@ -172,9 +270,19 @@ private static ParsedDuration applyDefaultUnit(String digits, Unit unit) {
172270
}
173271
}
174272

273+
private static ParsedDuration applyDefaultTimeUnit(String digits, TimeUnit unit) {
274+
Objects.requireNonNull(unit, "defaultUnit");
275+
long value = safeParseLong(digits);
276+
if (value <= 0L) {
277+
return empty();
278+
}
279+
280+
// TimeUnit.toMillis includes overflow saturation to Long.MAX_VALUE
281+
return ofMillis(unit.toMillis(value));
282+
}
283+
175284
/**
176-
* Parses strings with multiple "value+suffix" segments like "1h30m" or "2d
177-
* 12h".
285+
* Parses strings with multiple "value+suffix" segments like "1h30m" or "2d 12h".
178286
*
179287
* <p>
180288
* Returns null if the input does not look like a valid combined-token duration.
@@ -222,6 +330,53 @@ private static ParsedDuration parseCombinedTokens(String lower, Unit defaultUnit
222330
return out.isEmpty() ? empty() : out;
223331
}
224332

333+
/**
334+
* Parses strings with multiple "value+suffix" segments like "1h30m" or "2d 12h",
335+
* using {@link TimeUnit} as the default unit for number-only tokens (and unknown suffixes).
336+
*
337+
* <p>
338+
* Returns null if the input does not look like a valid combined-token duration.
339+
* </p>
340+
*/
341+
private static ParsedDuration parseCombinedTokens(String lower, TimeUnit defaultUnit) {
342+
Matcher seg = SEGMENT.matcher(lower);
343+
int count = 0;
344+
int pos = 0;
345+
long totalMillis = 0L;
346+
347+
while (seg.find()) {
348+
if (!onlyWhitespaceBetween(lower, pos, seg.start())) {
349+
return null;
350+
}
351+
pos = seg.end();
352+
count++;
353+
354+
long value = safeParseLong(seg.group(1));
355+
String suffix = seg.group(2);
356+
if (value <= 0L) {
357+
return null;
358+
}
359+
360+
ParsedDuration piece = applySuffix(value, suffix, defaultUnit);
361+
if (piece == null) {
362+
return null;
363+
}
364+
365+
totalMillis = safeAddMillis(totalMillis, piece.millis);
366+
}
367+
368+
if (!onlyWhitespaceBetween(lower, pos, lower.length())) {
369+
return null;
370+
}
371+
372+
if (count < 2) {
373+
return null;
374+
}
375+
376+
ParsedDuration out = ofMillis(totalMillis);
377+
return out.isEmpty() ? empty() : out;
378+
}
379+
225380
private static boolean onlyWhitespaceBetween(String s, int from, int to) {
226381
for (int i = from; i < to; i++) {
227382
if (!Character.isWhitespace(s.charAt(i))) {
@@ -297,9 +452,69 @@ private static ParsedDuration applySuffix(long value, String suffixRaw, Unit def
297452
return applyDefaultUnit(Long.toString(value), defaultUnit);
298453
}
299454
}
300-
455+
456+
private static ParsedDuration applySuffix(long value, String suffixRaw, TimeUnit defaultUnit) {
457+
String suffix = suffixRaw.toLowerCase(Locale.ROOT);
458+
459+
switch (suffix) {
460+
case "ms":
461+
case "msec":
462+
case "msecs":
463+
case "millisecond":
464+
case "milliseconds":
465+
return ofMillis(value);
466+
467+
case "s":
468+
case "sec":
469+
case "secs":
470+
case "second":
471+
case "seconds":
472+
return ofMillis(safeMul(value, 1000L));
473+
474+
case "m":
475+
case "min":
476+
case "mins":
477+
case "minute":
478+
case "minutes":
479+
return ofMillis(safeMul(value, 60_000L));
480+
481+
case "h":
482+
case "hr":
483+
case "hrs":
484+
case "hour":
485+
case "hours":
486+
return ofMillis(safeMul(value, 3_600_000L));
487+
488+
case "d":
489+
case "day":
490+
case "days":
491+
return ofMillis(safeMul(value, 86_400_000L));
492+
493+
case "w":
494+
case "wk":
495+
case "wks":
496+
case "week":
497+
case "weeks":
498+
return ofMillis(safeMul(value, 604_800_000L));
499+
500+
case "mo":
501+
case "mon":
502+
case "mons":
503+
case "month":
504+
case "months":
505+
return ofMillis(safeMul(value, 30L * 86_400_000L));
506+
507+
default:
508+
// unknown suffix -> fallback to default timeunit
509+
return applyDefaultTimeUnit(Long.toString(value), defaultUnit);
510+
}
511+
}
512+
301513
/**
302514
* Adds this duration to the provided instant using fixed millis.
515+
*
516+
* @param base base instant
517+
* @return base + duration, or base if null/empty
303518
*/
304519
public Instant addTo(Instant base) {
305520
if (base == null || isEmpty()) {
@@ -312,6 +527,8 @@ public Instant addTo(Instant base) {
312527
* Delay in milliseconds from now until now + this duration.
313528
*
314529
* Guaranteed to return at least 1ms when not empty.
530+
*
531+
* @return delay in milliseconds (>= 1)
315532
*/
316533
public long delayMillisFromNow() {
317534
if (isEmpty()) {

0 commit comments

Comments
 (0)