77import 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 */
3429public 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