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