1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.lang3.time;
18
19 import java.text.SimpleDateFormat;
20 import java.util.ArrayList;
21 import java.util.Calendar;
22 import java.util.Date;
23 import java.util.GregorianCalendar;
24 import java.util.Objects;
25 import java.util.TimeZone;
26 import java.util.stream.Stream;
27
28 import org.apache.commons.lang3.StringUtils;
29 import org.apache.commons.lang3.Validate;
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 public class DurationFormatUtils {
55
56
57
58
59
60
61
62 public DurationFormatUtils() {
63 }
64
65
66
67
68
69
70
71
72 public static final String ISO_EXTENDED_FORMAT_PATTERN = "'P'yyyy'Y'M'M'd'DT'H'H'm'M's.SSS'S'";
73
74
75
76
77
78
79
80
81
82
83 public static String formatDurationHMS(final long durationMillis) {
84 return formatDuration(durationMillis, "HH:mm:ss.SSS");
85 }
86
87
88
89
90
91
92
93
94
95
96
97
98
99 public static String formatDurationISO(final long durationMillis) {
100 return formatDuration(durationMillis, ISO_EXTENDED_FORMAT_PATTERN, false);
101 }
102
103
104
105
106
107
108
109
110
111
112
113
114 public static String formatDuration(final long durationMillis, final String format) {
115 return formatDuration(durationMillis, format, true);
116 }
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 public static String formatDuration(final long durationMillis, final String format, final boolean padWithZeros) {
132 Validate.inclusiveBetween(0, Long.MAX_VALUE, durationMillis, "durationMillis must not be negative");
133
134 final Token[] tokens = lexx(format);
135
136 long days = 0;
137 long hours = 0;
138 long minutes = 0;
139 long seconds = 0;
140 long milliseconds = durationMillis;
141
142 if (Token.containsTokenWithValue(tokens, d)) {
143 days = milliseconds / DateUtils.MILLIS_PER_DAY;
144 milliseconds = milliseconds - (days * DateUtils.MILLIS_PER_DAY);
145 }
146 if (Token.containsTokenWithValue(tokens, H)) {
147 hours = milliseconds / DateUtils.MILLIS_PER_HOUR;
148 milliseconds = milliseconds - (hours * DateUtils.MILLIS_PER_HOUR);
149 }
150 if (Token.containsTokenWithValue(tokens, m)) {
151 minutes = milliseconds / DateUtils.MILLIS_PER_MINUTE;
152 milliseconds = milliseconds - (minutes * DateUtils.MILLIS_PER_MINUTE);
153 }
154 if (Token.containsTokenWithValue(tokens, s)) {
155 seconds = milliseconds / DateUtils.MILLIS_PER_SECOND;
156 milliseconds = milliseconds - (seconds * DateUtils.MILLIS_PER_SECOND);
157 }
158
159 return format(tokens, 0, 0, days, hours, minutes, seconds, milliseconds, padWithZeros);
160 }
161
162
163
164
165
166
167
168
169
170
171
172
173
174 public static String formatDurationWords(
175 final long durationMillis,
176 final boolean suppressLeadingZeroElements,
177 final boolean suppressTrailingZeroElements) {
178
179
180
181
182 String duration = formatDuration(durationMillis, "d' days 'H' hours 'm' minutes 's' seconds'");
183 if (suppressLeadingZeroElements) {
184
185 duration = " " + duration;
186 String tmp = StringUtils.replaceOnce(duration, " 0 days", StringUtils.EMPTY);
187 if (tmp.length() != duration.length()) {
188 duration = tmp;
189 tmp = StringUtils.replaceOnce(duration, " 0 hours", StringUtils.EMPTY);
190 if (tmp.length() != duration.length()) {
191 duration = tmp;
192 tmp = StringUtils.replaceOnce(duration, " 0 minutes", StringUtils.EMPTY);
193 duration = tmp;
194 }
195 }
196 if (!duration.isEmpty()) {
197
198 duration = duration.substring(1);
199 }
200 }
201 if (suppressTrailingZeroElements) {
202 String tmp = StringUtils.replaceOnce(duration, " 0 seconds", StringUtils.EMPTY);
203 if (tmp.length() != duration.length()) {
204 duration = tmp;
205 tmp = StringUtils.replaceOnce(duration, " 0 minutes", StringUtils.EMPTY);
206 if (tmp.length() != duration.length()) {
207 duration = tmp;
208 tmp = StringUtils.replaceOnce(duration, " 0 hours", StringUtils.EMPTY);
209 if (tmp.length() != duration.length()) {
210 duration = StringUtils.replaceOnce(tmp, " 0 days", StringUtils.EMPTY);
211 }
212 }
213 }
214 }
215
216 duration = " " + duration;
217 duration = StringUtils.replaceOnce(duration, " 1 seconds", " 1 second");
218 duration = StringUtils.replaceOnce(duration, " 1 minutes", " 1 minute");
219 duration = StringUtils.replaceOnce(duration, " 1 hours", " 1 hour");
220 duration = StringUtils.replaceOnce(duration, " 1 days", " 1 day");
221 return duration.trim();
222 }
223
224
225
226
227
228
229
230
231
232
233
234 public static String formatPeriodISO(final long startMillis, final long endMillis) {
235 return formatPeriod(startMillis, endMillis, ISO_EXTENDED_FORMAT_PATTERN, false, TimeZone.getDefault());
236 }
237
238
239
240
241
242
243
244
245
246
247
248 public static String formatPeriod(final long startMillis, final long endMillis, final String format) {
249 return formatPeriod(startMillis, endMillis, format, true, TimeZone.getDefault());
250 }
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276 public static String formatPeriod(final long startMillis, final long endMillis, final String format, final boolean padWithZeros,
277 final TimeZone timezone) {
278 Validate.isTrue(startMillis <= endMillis, "startMillis must not be greater than endMillis");
279
280
281
282
283
284
285
286
287 final Token[] tokens = lexx(format);
288
289
290
291 final Calendar start = Calendar.getInstance(timezone);
292 start.setTime(new Date(startMillis));
293 final Calendar end = Calendar.getInstance(timezone);
294 end.setTime(new Date(endMillis));
295
296
297 int milliseconds = end.get(Calendar.MILLISECOND) - start.get(Calendar.MILLISECOND);
298 int seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND);
299 int minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE);
300 int hours = end.get(Calendar.HOUR_OF_DAY) - start.get(Calendar.HOUR_OF_DAY);
301 int days = end.get(Calendar.DAY_OF_MONTH) - start.get(Calendar.DAY_OF_MONTH);
302 int months = end.get(Calendar.MONTH) - start.get(Calendar.MONTH);
303 int years = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
304
305
306 while (milliseconds < 0) {
307 milliseconds += 1000;
308 seconds -= 1;
309 }
310 while (seconds < 0) {
311 seconds += 60;
312 minutes -= 1;
313 }
314 while (minutes < 0) {
315 minutes += 60;
316 hours -= 1;
317 }
318 while (hours < 0) {
319 hours += 24;
320 days -= 1;
321 }
322
323 if (Token.containsTokenWithValue(tokens, M)) {
324 while (days < 0) {
325 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
326 months -= 1;
327 start.add(Calendar.MONTH, 1);
328 }
329
330 while (months < 0) {
331 months += 12;
332 years -= 1;
333 }
334
335 if (!Token.containsTokenWithValue(tokens, y) && years != 0) {
336 while (years != 0) {
337 months += 12 * years;
338 years = 0;
339 }
340 }
341 } else {
342
343
344 if (!Token.containsTokenWithValue(tokens, y)) {
345 int target = end.get(Calendar.YEAR);
346 if (months < 0) {
347
348 target -= 1;
349 }
350
351 while (start.get(Calendar.YEAR) != target) {
352 days += start.getActualMaximum(Calendar.DAY_OF_YEAR) - start.get(Calendar.DAY_OF_YEAR);
353
354
355 if (start instanceof GregorianCalendar &&
356 start.get(Calendar.MONTH) == Calendar.FEBRUARY &&
357 start.get(Calendar.DAY_OF_MONTH) == 29) {
358 days += 1;
359 }
360
361 start.add(Calendar.YEAR, 1);
362
363 days += start.get(Calendar.DAY_OF_YEAR);
364 }
365
366 years = 0;
367 }
368
369 while (start.get(Calendar.MONTH) != end.get(Calendar.MONTH)) {
370 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
371 start.add(Calendar.MONTH, 1);
372 }
373
374 months = 0;
375
376 while (days < 0) {
377 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
378 months -= 1;
379 start.add(Calendar.MONTH, 1);
380 }
381
382 }
383
384
385
386
387
388 if (!Token.containsTokenWithValue(tokens, d)) {
389 hours += 24 * days;
390 days = 0;
391 }
392 if (!Token.containsTokenWithValue(tokens, H)) {
393 minutes += 60 * hours;
394 hours = 0;
395 }
396 if (!Token.containsTokenWithValue(tokens, m)) {
397 seconds += 60 * minutes;
398 minutes = 0;
399 }
400 if (!Token.containsTokenWithValue(tokens, s)) {
401 milliseconds += 1000 * seconds;
402 seconds = 0;
403 }
404
405 return format(tokens, years, months, days, hours, minutes, seconds, milliseconds, padWithZeros);
406 }
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422 static String format(final Token[] tokens, final long years, final long months, final long days, final long hours, final long minutes, final long seconds,
423 final long milliseconds, final boolean padWithZeros) {
424 final StringBuilder buffer = new StringBuilder();
425 boolean lastOutputSeconds = false;
426 for (final Token token : tokens) {
427 final Object value = token.getValue();
428 final int count = token.getCount();
429 if (value instanceof StringBuilder) {
430 buffer.append(value.toString());
431 } else if (value.equals(y)) {
432 buffer.append(paddedValue(years, padWithZeros, count));
433 lastOutputSeconds = false;
434 } else if (value.equals(M)) {
435 buffer.append(paddedValue(months, padWithZeros, count));
436 lastOutputSeconds = false;
437 } else if (value.equals(d)) {
438 buffer.append(paddedValue(days, padWithZeros, count));
439 lastOutputSeconds = false;
440 } else if (value.equals(H)) {
441 buffer.append(paddedValue(hours, padWithZeros, count));
442 lastOutputSeconds = false;
443 } else if (value.equals(m)) {
444 buffer.append(paddedValue(minutes, padWithZeros, count));
445 lastOutputSeconds = false;
446 } else if (value.equals(s)) {
447 buffer.append(paddedValue(seconds, padWithZeros, count));
448 lastOutputSeconds = true;
449 } else if (value.equals(S)) {
450 if (lastOutputSeconds) {
451
452 final int width = padWithZeros ? Math.max(3, count) : 3;
453 buffer.append(paddedValue(milliseconds, true, width));
454 } else {
455 buffer.append(paddedValue(milliseconds, padWithZeros, count));
456 }
457 lastOutputSeconds = false;
458 }
459 }
460 return buffer.toString();
461 }
462
463
464
465
466
467
468
469
470
471
472 private static String paddedValue(final long value, final boolean padWithZeros, final int count) {
473 final String longString = Long.toString(value);
474 return padWithZeros ? StringUtils.leftPad(longString, count, '0') : longString;
475 }
476
477 static final String y = "y";
478 static final String M = "M";
479 static final String d = "d";
480 static final String H = "H";
481 static final String m = "m";
482 static final String s = "s";
483 static final String S = "S";
484
485
486
487
488
489
490
491 static Token[] lexx(final String format) {
492 final ArrayList<Token> list = new ArrayList<>(format.length());
493
494 boolean inLiteral = false;
495
496
497 StringBuilder buffer = null;
498 Token previous = null;
499 for (int i = 0; i < format.length(); i++) {
500 final char ch = format.charAt(i);
501 if (inLiteral && ch != '\'') {
502 buffer.append(ch);
503 continue;
504 }
505 String value = null;
506 switch (ch) {
507
508 case '\'':
509 if (inLiteral) {
510 buffer = null;
511 inLiteral = false;
512 } else {
513 buffer = new StringBuilder();
514 list.add(new Token(buffer));
515 inLiteral = true;
516 }
517 break;
518 case 'y':
519 value = y;
520 break;
521 case 'M':
522 value = M;
523 break;
524 case 'd':
525 value = d;
526 break;
527 case 'H':
528 value = H;
529 break;
530 case 'm':
531 value = m;
532 break;
533 case 's':
534 value = s;
535 break;
536 case 'S':
537 value = S;
538 break;
539 default:
540 if (buffer == null) {
541 buffer = new StringBuilder();
542 list.add(new Token(buffer));
543 }
544 buffer.append(ch);
545 }
546
547 if (value != null) {
548 if (previous != null && previous.getValue().equals(value)) {
549 previous.increment();
550 } else {
551 final Token token = new Token(value);
552 list.add(token);
553 previous = token;
554 }
555 buffer = null;
556 }
557 }
558 if (inLiteral) {
559 throw new IllegalArgumentException("Unmatched quote in format: " + format);
560 }
561 return list.toArray(Token.EMPTY_ARRAY);
562 }
563
564
565
566
567 static class Token {
568
569
570 private static final Token[] EMPTY_ARRAY = {};
571
572
573
574
575
576
577
578
579 static boolean containsTokenWithValue(final Token[] tokens, final Object value) {
580 return Stream.of(tokens).anyMatch(token -> token.getValue() == value);
581 }
582
583 private final Object value;
584 private int count;
585
586
587
588
589
590
591 Token(final Object value) {
592 this(value, 1);
593 }
594
595
596
597
598
599
600
601
602 Token(final Object value, final int count) {
603 this.value = Objects.requireNonNull(value, "value");
604 this.count = count;
605 }
606
607
608
609
610 void increment() {
611 count++;
612 }
613
614
615
616
617
618
619 int getCount() {
620 return count;
621 }
622
623
624
625
626
627
628 Object getValue() {
629 return value;
630 }
631
632
633
634
635
636
637
638 @Override
639 public boolean equals(final Object obj2) {
640 if (obj2 instanceof Token) {
641 final Token tok2 = (Token) obj2;
642 if (this.value.getClass() != tok2.value.getClass()) {
643 return false;
644 }
645 if (this.count != tok2.count) {
646 return false;
647 }
648 if (this.value instanceof StringBuilder) {
649 return this.value.toString().equals(tok2.value.toString());
650 }
651 if (this.value instanceof Number) {
652 return this.value.equals(tok2.value);
653 }
654 return this.value == tok2.value;
655 }
656 return false;
657 }
658
659
660
661
662
663
664
665
666 @Override
667 public int hashCode() {
668 return this.value.hashCode();
669 }
670
671
672
673
674
675
676 @Override
677 public String toString() {
678 return StringUtils.repeat(this.value.toString(), this.count);
679 }
680 }
681
682 }