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