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