Skip to content

Commit 568a52a

Browse files
committed
Refactor ParsedDuration to store months as fixed milliseconds
1 parent 6712306 commit 568a52a

File tree

2 files changed

+68
-151
lines changed

2 files changed

+68
-151
lines changed

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

Lines changed: 53 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,29 @@
77
import java.util.regex.Pattern;
88

99
/**
10-
* Parsed duration that preserves calendar-month units separately from fixed millis.
10+
* Parsed duration stored as fixed milliseconds.
1111
*
1212
* <p>
1313
* Supported formats (case-insensitive):
1414
* <ul>
15-
* <li>5000ms</li>
16-
* <li>60s</li>
17-
* <li>30m</li>
18-
* <li>12h</li>
19-
* <li>1d</li>
20-
* <li>2w</li>
21-
* <li>1mo (calendar months)</li>
22-
* <li>Combined tokens: 1h30m, 2d12h, 1w2d3h4m5s6ms, 1mo2d (spaces allowed between segments)</li>
23-
* <li>ISO-8601 (Duration.parse): PT30M, PT12H, P1D, etc (months/years not supported)</li>
24-
* <li>Plain number: "30" -> uses a configurable default unit</li>
25-
* </ul>
26-
*
27-
* <p>
28-
* Notes:
29-
* <ul>
30-
* <li>Calendar months are stored in {@link #months} and are NOT converted to millis.</li>
31-
* <li>For ISO strings, {@code P1M} is NOT accepted (ambiguous vs minutes).</li>
15+
* <li>5000ms</li>
16+
* <li>60s</li>
17+
* <li>30m</li>
18+
* <li>12h</li>
19+
* <li>1d</li>
20+
* <li>2w</li>
21+
* <li>1mo (treated as a fixed 30 days)</li>
22+
* <li>Combined tokens: 1h30m, 2d12h, 1w2d3h4m5s6ms (spaces allowed between
23+
* segments)</li>
24+
* <li>ISO-8601 (Duration.parse): PT30M, PT12H, P1D, etc (months/years not
25+
* supported by Duration)</li>
26+
* <li>Plain number: "30" -> uses a configurable default unit</li>
3227
* </ul>
3328
*/
3429
public final class ParsedDuration {
3530

3631
public enum Unit {
37-
MS("ms"),
38-
SECONDS("s"),
39-
MINUTES("m"),
40-
HOURS("h"),
41-
DAYS("d"),
42-
WEEKS("w"),
43-
MONTHS("mo");
32+
MS("ms"), SECONDS("s"), MINUTES("m"), HOURS("h"), DAYS("d"), WEEKS("w");
4433

4534
private final String suffix;
4635

@@ -58,45 +47,33 @@ public String getSuffix() {
5847
private static final Pattern SEGMENT = Pattern.compile("([0-9]+)\\s*([a-zA-Z]+)");
5948

6049
private final long millis;
61-
private final int months;
6250

63-
private ParsedDuration(long millis, int months) {
51+
private ParsedDuration(long millis) {
6452
this.millis = Math.max(0L, millis);
65-
this.months = Math.max(0, months);
6653
}
6754

6855
public static ParsedDuration empty() {
69-
return new ParsedDuration(0L, 0);
56+
return new ParsedDuration(0L);
7057
}
7158

7259
public static ParsedDuration ofMillis(long millis) {
73-
return new ParsedDuration(millis, 0);
74-
}
75-
76-
public static ParsedDuration ofMonths(int months) {
77-
return new ParsedDuration(0L, months);
78-
}
79-
80-
public static ParsedDuration of(long millis, int months) {
81-
return new ParsedDuration(millis, months);
60+
return new ParsedDuration(millis);
8261
}
8362

8463
public long getMillis() {
8564
return millis;
8665
}
8766

88-
public int getMonths() {
89-
return months;
90-
}
91-
9267
public boolean isEmpty() {
93-
return millis <= 0L && months <= 0;
68+
return millis <= 0L;
9469
}
9570

9671
/**
9772
* Parses the input using {@code defaultUnit} if the string is just a number.
9873
*
99-
* <p>Example: {@code parse("30", Unit.MINUTES)} => 30 minutes</p>
74+
* <p>
75+
* Example: {@code parse("30", Unit.MINUTES)} => 30 minutes
76+
* </p>
10077
*/
10178
public static ParsedDuration parse(String raw, Unit defaultUnit) {
10279
if (raw == null) {
@@ -115,19 +92,14 @@ public static ParsedDuration parse(String raw, Unit defaultUnit) {
11592
String lower = s.toLowerCase(Locale.ROOT);
11693

11794
// ISO-8601 Duration support (PT30M, PT12H, P1D, etc)
118-
// We explicitly reject strings that look like "P...M" without "T" (month ambiguous)
95+
// Duration.parse does NOT support months/years anyway; it will throw for
96+
// P1M/P1Y.
11997
if (lower.startsWith("p")) {
120-
// reject potential month/year ISO (P1M, P2Y, etc)
121-
// allow PnD / PTnH / PTnM / PTnS etc, but not months/years.
122-
if (looksLikeIsoWithMonthsOrYears(lower)) {
123-
// Let it fall through to suffix parsing if it's like "1mo" (not ISO), otherwise empty.
124-
} else {
125-
try {
126-
Duration d = Duration.parse(s.toUpperCase(Locale.ROOT));
127-
return ofMillis(d.toMillis());
128-
} catch (Exception ignored) {
129-
// fall through
130-
}
98+
try {
99+
Duration d = Duration.parse(s.toUpperCase(Locale.ROOT));
100+
return ofMillis(d.toMillis());
101+
} catch (Exception ignored) {
102+
// fall through
131103
}
132104
}
133105

@@ -166,12 +138,9 @@ public static ParsedDuration parse(String raw) {
166138
}
167139

168140
/**
169-
* If {@code raw} is just a number, returns the same value with {@code defaultUnit} applied,
170-
* otherwise returns {@link #parse(String, Unit)} result.
171-
*
172-
* <p>This is the "set default unit value if it's just a number" helper.</p>
173-
*
174-
* <p>Example: {@code ParsedDuration.withDefaultUnit("30", Unit.SECONDS)} => 30s</p>
141+
* If {@code raw} is just a number, returns the same value with
142+
* {@code defaultUnit} applied, otherwise returns {@link #parse(String, Unit)}
143+
* result.
175144
*/
176145
public static ParsedDuration withDefaultUnit(String raw, Unit defaultUnit) {
177146
return parse(raw, defaultUnit);
@@ -188,34 +157,33 @@ private static ParsedDuration applyDefaultUnit(String digits, Unit unit) {
188157
case MS:
189158
return ofMillis(value);
190159
case SECONDS:
191-
return ofMillis(value * 1000L);
160+
return ofMillis(safeMul(value, 1000L));
192161
case MINUTES:
193-
return ofMillis(value * 60_000L);
162+
return ofMillis(safeMul(value, 60_000L));
194163
case HOURS:
195-
return ofMillis(value * 3_600_000L);
164+
return ofMillis(safeMul(value, 3_600_000L));
196165
case DAYS:
197-
return ofMillis(value * 86_400_000L);
166+
return ofMillis(safeMul(value, 86_400_000L));
198167
case WEEKS:
199-
return ofMillis(value * 604_800_000L);
200-
case MONTHS:
201-
return ofMonths((int) Math.min(Integer.MAX_VALUE, value));
168+
return ofMillis(safeMul(value, 604_800_000L));
202169
default:
203170
return empty();
204171
}
205172
}
206173

207174
/**
208-
* Parses strings with multiple "value+suffix" segments like "1h30m" or "2d 12h".
175+
* Parses strings with multiple "value+suffix" segments like "1h30m" or "2d
176+
* 12h".
209177
*
210-
* <p>Returns null if the input does not look like a valid combined-token duration.</p>
178+
* <p>
179+
* Returns null if the input does not look like a valid combined-token duration.
180+
* </p>
211181
*/
212182
private static ParsedDuration parseCombinedTokens(String lower, Unit defaultUnit) {
213-
// Combined tokens must have at least two segments.
214183
Matcher seg = SEGMENT.matcher(lower);
215184
int count = 0;
216185
int pos = 0;
217186
long totalMillis = 0L;
218-
int totalMonths = 0;
219187

220188
while (seg.find()) {
221189
// Ensure we only skip whitespace between segments.
@@ -237,7 +205,6 @@ private static ParsedDuration parseCombinedTokens(String lower, Unit defaultUnit
237205
}
238206

239207
totalMillis = safeAddMillis(totalMillis, piece.millis);
240-
totalMonths = safeAddMonths(totalMonths, piece.months);
241208
}
242209

243210
// trailing non-whitespace means it's not a valid combined token string
@@ -250,7 +217,7 @@ private static ParsedDuration parseCombinedTokens(String lower, Unit defaultUnit
250217
return null;
251218
}
252219

253-
ParsedDuration out = of(totalMillis, totalMonths);
220+
ParsedDuration out = ofMillis(totalMillis);
254221
return out.isEmpty() ? empty() : out;
255222
}
256223

@@ -271,14 +238,6 @@ private static long safeAddMillis(long a, long b) {
271238
return r;
272239
}
273240

274-
private static int safeAddMonths(int a, int b) {
275-
long r = (long) a + (long) b;
276-
if (r > Integer.MAX_VALUE) {
277-
return Integer.MAX_VALUE;
278-
}
279-
return (int) r;
280-
}
281-
282241
private static ParsedDuration applySuffix(long value, String suffixRaw, Unit defaultUnit) {
283242
String suffix = suffixRaw.toLowerCase(Locale.ROOT);
284243

@@ -289,40 +248,49 @@ private static ParsedDuration applySuffix(long value, String suffixRaw, Unit def
289248
case "millisecond":
290249
case "milliseconds":
291250
return ofMillis(value);
251+
292252
case "s":
293253
case "sec":
294254
case "secs":
295255
case "second":
296256
case "seconds":
297257
return ofMillis(safeMul(value, 1000L));
258+
298259
case "m":
299260
case "min":
300261
case "mins":
301262
case "minute":
302263
case "minutes":
303264
return ofMillis(safeMul(value, 60_000L));
265+
304266
case "h":
305267
case "hr":
306268
case "hrs":
307269
case "hour":
308270
case "hours":
309271
return ofMillis(safeMul(value, 3_600_000L));
272+
310273
case "d":
311274
case "day":
312275
case "days":
313276
return ofMillis(safeMul(value, 86_400_000L));
277+
314278
case "w":
315279
case "wk":
316280
case "wks":
317281
case "week":
318282
case "weeks":
319283
return ofMillis(safeMul(value, 604_800_000L));
284+
285+
// "mo" is no longer calendar-based; it becomes a fixed millis approximation.
320286
case "mo":
321287
case "mon":
322288
case "mons":
323289
case "month":
324290
case "months":
325-
return ofMonths((int) Math.min(Integer.MAX_VALUE, value));
291+
// Fixed 30-day month (no calendar semantics)
292+
return ofMillis(safeMul(value, 30L * 86_400_000L));
293+
326294
default:
327295
// unknown suffix -> fallback to default unit
328296
return applyDefaultUnit(Long.toString(value), defaultUnit);
@@ -339,17 +307,6 @@ private static long safeMul(long a, long b) {
339307
return a * b;
340308
}
341309

342-
private static boolean looksLikeIsoWithMonthsOrYears(String lower) {
343-
if (!lower.startsWith("p")) {
344-
return false;
345-
}
346-
boolean hasT = lower.indexOf('t') >= 0;
347-
if (hasT) {
348-
return false;
349-
}
350-
return lower.indexOf('m') >= 0 || lower.indexOf('y') >= 0;
351-
}
352-
353310
private static String extractLeadingDigits(String s) {
354311
int i = 0;
355312
while (i < s.length() && Character.isDigit(s.charAt(i))) {
@@ -371,6 +328,6 @@ public String toString() {
371328
if (isEmpty()) {
372329
return "ParsedDuration{empty}";
373330
}
374-
return "ParsedDuration{millis=" + millis + ", months=" + months + "}";
331+
return "ParsedDuration{millis=" + millis + "}";
375332
}
376333
}

0 commit comments

Comments
 (0)