001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.lang.time;
018
019 import java.util.ArrayList;
020 import java.util.Calendar;
021 import java.util.Date;
022 import java.util.GregorianCalendar;
023 import java.util.TimeZone;
024
025 import org.apache.commons.lang.StringUtils;
026 import org.apache.commons.lang.text.StrBuilder;
027
028 /**
029 * <p>Duration formatting utilities and constants. The following table describes the tokens
030 * used in the pattern language for formatting. </p>
031 * <table border="1">
032 * <tr><th>character</th><th>duration element</th></tr>
033 * <tr><td>y</td><td>years</td></tr>
034 * <tr><td>M</td><td>months</td></tr>
035 * <tr><td>d</td><td>days</td></tr>
036 * <tr><td>H</td><td>hours</td></tr>
037 * <tr><td>m</td><td>minutes</td></tr>
038 * <tr><td>s</td><td>seconds</td></tr>
039 * <tr><td>S</td><td>milliseconds</td></tr>
040 * </table>
041 *
042 * @author Apache Software Foundation
043 * @author Apache Ant - DateUtils
044 * @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a>
045 * @author <a href="mailto:stefan.bodewig@epost.de">Stefan Bodewig</a>
046 * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
047 * @since 2.1
048 * @version $Id: DurationFormatUtils.java 1057072 2011-01-10 01:55:57Z niallp $
049 */
050 public class DurationFormatUtils {
051
052 /**
053 * <p>DurationFormatUtils instances should NOT be constructed in standard programming.</p>
054 *
055 * <p>This constructor is public to permit tools that require a JavaBean instance
056 * to operate.</p>
057 */
058 public DurationFormatUtils() {
059 super();
060 }
061
062 /**
063 * <p>Pattern used with <code>FastDateFormat</code> and <code>SimpleDateFormat</code>
064 * for the ISO8601 period format used in durations.</p>
065 *
066 * @see org.apache.commons.lang.time.FastDateFormat
067 * @see java.text.SimpleDateFormat
068 */
069 public static final String ISO_EXTENDED_FORMAT_PATTERN = "'P'yyyy'Y'M'M'd'DT'H'H'm'M's.S'S'";
070
071 //-----------------------------------------------------------------------
072 /**
073 * <p>Formats the time gap as a string.</p>
074 *
075 * <p>The format used is ISO8601-like:
076 * <i>H</i>:<i>m</i>:<i>s</i>.<i>S</i>.</p>
077 *
078 * @param durationMillis the duration to format
079 * @return the time as a String
080 */
081 public static String formatDurationHMS(long durationMillis) {
082 return formatDuration(durationMillis, "H:mm:ss.SSS");
083 }
084
085 /**
086 * <p>Formats the time gap as a string.</p>
087 *
088 * <p>The format used is the ISO8601 period format.</p>
089 *
090 * <p>This method formats durations using the days and lower fields of the
091 * ISO format pattern, such as P7D6TH5M4.321S.</p>
092 *
093 * @param durationMillis the duration to format
094 * @return the time as a String
095 */
096 public static String formatDurationISO(long durationMillis) {
097 return formatDuration(durationMillis, ISO_EXTENDED_FORMAT_PATTERN, false);
098 }
099
100 /**
101 * <p>Formats the time gap as a string, using the specified format, and padding with zeros and
102 * using the default timezone.</p>
103 *
104 * <p>This method formats durations using the days and lower fields of the
105 * format pattern. Months and larger are not used.</p>
106 *
107 * @param durationMillis the duration to format
108 * @param format the way in which to format the duration
109 * @return the time as a String
110 */
111 public static String formatDuration(long durationMillis, String format) {
112 return formatDuration(durationMillis, format, true);
113 }
114
115 /**
116 * <p>Formats the time gap as a string, using the specified format.
117 * Padding the left hand side of numbers with zeroes is optional and
118 * the timezone may be specified.</p>
119 *
120 * <p>This method formats durations using the days and lower fields of the
121 * format pattern. Months and larger are not used.</p>
122 *
123 * @param durationMillis the duration to format
124 * @param format the way in which to format the duration
125 * @param padWithZeros whether to pad the left hand side of numbers with 0's
126 * @return the time as a String
127 */
128 public static String formatDuration(long durationMillis, String format, boolean padWithZeros) {
129
130 Token[] tokens = lexx(format);
131
132 int days = 0;
133 int hours = 0;
134 int minutes = 0;
135 int seconds = 0;
136 int milliseconds = 0;
137
138 if (Token.containsTokenWithValue(tokens, d) ) {
139 days = (int) (durationMillis / DateUtils.MILLIS_PER_DAY);
140 durationMillis = durationMillis - (days * DateUtils.MILLIS_PER_DAY);
141 }
142 if (Token.containsTokenWithValue(tokens, H) ) {
143 hours = (int) (durationMillis / DateUtils.MILLIS_PER_HOUR);
144 durationMillis = durationMillis - (hours * DateUtils.MILLIS_PER_HOUR);
145 }
146 if (Token.containsTokenWithValue(tokens, m) ) {
147 minutes = (int) (durationMillis / DateUtils.MILLIS_PER_MINUTE);
148 durationMillis = durationMillis - (minutes * DateUtils.MILLIS_PER_MINUTE);
149 }
150 if (Token.containsTokenWithValue(tokens, s) ) {
151 seconds = (int) (durationMillis / DateUtils.MILLIS_PER_SECOND);
152 durationMillis = durationMillis - (seconds * DateUtils.MILLIS_PER_SECOND);
153 }
154 if (Token.containsTokenWithValue(tokens, S) ) {
155 milliseconds = (int) durationMillis;
156 }
157
158 return format(tokens, 0, 0, days, hours, minutes, seconds, milliseconds, padWithZeros);
159 }
160
161 /**
162 * <p>Formats an elapsed time into a plurialization correct string.</p>
163 *
164 * <p>This method formats durations using the days and lower fields of the
165 * format pattern. Months and larger are not used.</p>
166 *
167 * @param durationMillis the elapsed time to report in milliseconds
168 * @param suppressLeadingZeroElements suppresses leading 0 elements
169 * @param suppressTrailingZeroElements suppresses trailing 0 elements
170 * @return the formatted text in days/hours/minutes/seconds
171 */
172 public static String formatDurationWords(
173 long durationMillis,
174 boolean suppressLeadingZeroElements,
175 boolean suppressTrailingZeroElements) {
176
177 // This method is generally replacable by the format method, but
178 // there are a series of tweaks and special cases that require
179 // trickery to replicate.
180 String duration = formatDuration(durationMillis, "d' days 'H' hours 'm' minutes 's' seconds'");
181 if (suppressLeadingZeroElements) {
182 // this is a temporary marker on the front. Like ^ in regexp.
183 duration = " " + duration;
184 String tmp = StringUtils.replaceOnce(duration, " 0 days", "");
185 if (tmp.length() != duration.length()) {
186 duration = tmp;
187 tmp = StringUtils.replaceOnce(duration, " 0 hours", "");
188 if (tmp.length() != duration.length()) {
189 duration = tmp;
190 tmp = StringUtils.replaceOnce(duration, " 0 minutes", "");
191 duration = tmp;
192 if (tmp.length() != duration.length()) {
193 duration = StringUtils.replaceOnce(tmp, " 0 seconds", "");
194 }
195 }
196 }
197 if (duration.length() != 0) {
198 // strip the space off again
199 duration = duration.substring(1);
200 }
201 }
202 if (suppressTrailingZeroElements) {
203 String tmp = StringUtils.replaceOnce(duration, " 0 seconds", "");
204 if (tmp.length() != duration.length()) {
205 duration = tmp;
206 tmp = StringUtils.replaceOnce(duration, " 0 minutes", "");
207 if (tmp.length() != duration.length()) {
208 duration = tmp;
209 tmp = StringUtils.replaceOnce(duration, " 0 hours", "");
210 if (tmp.length() != duration.length()) {
211 duration = StringUtils.replaceOnce(tmp, " 0 days", "");
212 }
213 }
214 }
215 }
216 // handle plurals
217 duration = " " + duration;
218 duration = StringUtils.replaceOnce(duration, " 1 seconds", " 1 second");
219 duration = StringUtils.replaceOnce(duration, " 1 minutes", " 1 minute");
220 duration = StringUtils.replaceOnce(duration, " 1 hours", " 1 hour");
221 duration = StringUtils.replaceOnce(duration, " 1 days", " 1 day");
222 return duration.trim();
223 }
224
225 //-----------------------------------------------------------------------
226 /**
227 * <p>Formats the time gap as a string.</p>
228 *
229 * <p>The format used is the ISO8601 period format.</p>
230 *
231 * @param startMillis the start of the duration to format
232 * @param endMillis the end of the duration to format
233 * @return the time as a String
234 */
235 public static String formatPeriodISO(long startMillis, long endMillis) {
236 return formatPeriod(startMillis, endMillis, ISO_EXTENDED_FORMAT_PATTERN, false, TimeZone.getDefault());
237 }
238
239 /**
240 * <p>Formats the time gap as a string, using the specified format.
241 * Padding the left hand side of numbers with zeroes is optional.
242 *
243 * @param startMillis the start of the duration
244 * @param endMillis the end of the duration
245 * @param format the way in which to format the duration
246 * @return the time as a String
247 */
248 public static String formatPeriod(long startMillis, long endMillis, String format) {
249 return formatPeriod(startMillis, endMillis, format, true, TimeZone.getDefault());
250 }
251
252 /**
253 * <p>Formats the time gap as a string, using the specified format.
254 * Padding the left hand side of numbers with zeroes is optional and
255 * the timezone may be specified. </p>
256 *
257 * <p>When calculating the difference between months/days, it chooses to
258 * calculate months first. So when working out the number of months and
259 * days between January 15th and March 10th, it choose 1 month and
260 * 23 days gained by choosing January->February = 1 month and then
261 * calculating days forwards, and not the 1 month and 26 days gained by
262 * choosing March -> February = 1 month and then calculating days
263 * backwards. </p>
264 *
265 * <p>For more control, the <a href="http://joda-time.sf.net/">Joda-Time</a>
266 * library is recommended.</p>
267 *
268 * @param startMillis the start of the duration
269 * @param endMillis the end of the duration
270 * @param format the way in which to format the duration
271 * @param padWithZeros whether to pad the left hand side of numbers with 0's
272 * @param timezone the millis are defined in
273 * @return the time as a String
274 */
275 public static String formatPeriod(long startMillis, long endMillis, String format, boolean padWithZeros,
276 TimeZone timezone) {
277
278 // Used to optimise for differences under 28 days and
279 // called formatDuration(millis, format); however this did not work
280 // over leap years.
281 // TODO: Compare performance to see if anything was lost by
282 // losing this optimisation.
283
284 Token[] tokens = lexx(format);
285
286 // timezones get funky around 0, so normalizing everything to GMT
287 // stops the hours being off
288 Calendar start = Calendar.getInstance(timezone);
289 start.setTime(new Date(startMillis));
290 Calendar end = Calendar.getInstance(timezone);
291 end.setTime(new Date(endMillis));
292
293 // initial estimates
294 int milliseconds = end.get(Calendar.MILLISECOND) - start.get(Calendar.MILLISECOND);
295 int seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND);
296 int minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE);
297 int hours = end.get(Calendar.HOUR_OF_DAY) - start.get(Calendar.HOUR_OF_DAY);
298 int days = end.get(Calendar.DAY_OF_MONTH) - start.get(Calendar.DAY_OF_MONTH);
299 int months = end.get(Calendar.MONTH) - start.get(Calendar.MONTH);
300 int years = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
301
302 // each initial estimate is adjusted in case it is under 0
303 while (milliseconds < 0) {
304 milliseconds += 1000;
305 seconds -= 1;
306 }
307 while (seconds < 0) {
308 seconds += 60;
309 minutes -= 1;
310 }
311 while (minutes < 0) {
312 minutes += 60;
313 hours -= 1;
314 }
315 while (hours < 0) {
316 hours += 24;
317 days -= 1;
318 }
319
320 if (Token.containsTokenWithValue(tokens, M)) {
321 while (days < 0) {
322 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
323 months -= 1;
324 start.add(Calendar.MONTH, 1);
325 }
326
327 while (months < 0) {
328 months += 12;
329 years -= 1;
330 }
331
332 if (!Token.containsTokenWithValue(tokens, y) && years != 0) {
333 while (years != 0) {
334 months += 12 * years;
335 years = 0;
336 }
337 }
338 } else {
339 // there are no M's in the format string
340
341 if( !Token.containsTokenWithValue(tokens, y) ) {
342 int target = end.get(Calendar.YEAR);
343 if (months < 0) {
344 // target is end-year -1
345 target -= 1;
346 }
347
348 while ( (start.get(Calendar.YEAR) != target)) {
349 days += start.getActualMaximum(Calendar.DAY_OF_YEAR) - start.get(Calendar.DAY_OF_YEAR);
350
351 // Not sure I grok why this is needed, but the brutal tests show it is
352 if(start instanceof GregorianCalendar) {
353 if( (start.get(Calendar.MONTH) == Calendar.FEBRUARY) &&
354 (start.get(Calendar.DAY_OF_MONTH) == 29 ) )
355 {
356 days += 1;
357 }
358 }
359
360 start.add(Calendar.YEAR, 1);
361
362 days += start.get(Calendar.DAY_OF_YEAR);
363 }
364
365 years = 0;
366 }
367
368 while( start.get(Calendar.MONTH) != end.get(Calendar.MONTH) ) {
369 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
370 start.add(Calendar.MONTH, 1);
371 }
372
373 months = 0;
374
375 while (days < 0) {
376 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
377 months -= 1;
378 start.add(Calendar.MONTH, 1);
379 }
380
381 }
382
383 // The rest of this code adds in values that
384 // aren't requested. This allows the user to ask for the
385 // number of months and get the real count and not just 0->11.
386
387 if (!Token.containsTokenWithValue(tokens, d)) {
388 hours += 24 * days;
389 days = 0;
390 }
391 if (!Token.containsTokenWithValue(tokens, H)) {
392 minutes += 60 * hours;
393 hours = 0;
394 }
395 if (!Token.containsTokenWithValue(tokens, m)) {
396 seconds += 60 * minutes;
397 minutes = 0;
398 }
399 if (!Token.containsTokenWithValue(tokens, s)) {
400 milliseconds += 1000 * seconds;
401 seconds = 0;
402 }
403
404 return format(tokens, years, months, days, hours, minutes, seconds, milliseconds, padWithZeros);
405 }
406
407 //-----------------------------------------------------------------------
408 /**
409 * <p>The internal method to do the formatting.</p>
410 *
411 * @param tokens the tokens
412 * @param years the number of years
413 * @param months the number of months
414 * @param days the number of days
415 * @param hours the number of hours
416 * @param minutes the number of minutes
417 * @param seconds the number of seconds
418 * @param milliseconds the number of millis
419 * @param padWithZeros whether to pad
420 * @return the formatted string
421 */
422 static String format(Token[] tokens, int years, int months, int days, int hours, int minutes, int seconds,
423 int milliseconds, boolean padWithZeros) {
424 StrBuilder buffer = new StrBuilder();
425 boolean lastOutputSeconds = false;
426 int sz = tokens.length;
427 for (int i = 0; i < sz; i++) {
428 Token token = tokens[i];
429 Object value = token.getValue();
430 int count = token.getCount();
431 if (value instanceof StringBuffer) {
432 buffer.append(value.toString());
433 } else {
434 if (value == y) {
435 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(years), count, '0') : Integer
436 .toString(years));
437 lastOutputSeconds = false;
438 } else if (value == M) {
439 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(months), count, '0') : Integer
440 .toString(months));
441 lastOutputSeconds = false;
442 } else if (value == d) {
443 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(days), count, '0') : Integer
444 .toString(days));
445 lastOutputSeconds = false;
446 } else if (value == H) {
447 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(hours), count, '0') : Integer
448 .toString(hours));
449 lastOutputSeconds = false;
450 } else if (value == m) {
451 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(minutes), count, '0') : Integer
452 .toString(minutes));
453 lastOutputSeconds = false;
454 } else if (value == s) {
455 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(seconds), count, '0') : Integer
456 .toString(seconds));
457 lastOutputSeconds = true;
458 } else if (value == S) {
459 if (lastOutputSeconds) {
460 milliseconds += 1000;
461 String str = padWithZeros
462 ? StringUtils.leftPad(Integer.toString(milliseconds), count, '0')
463 : Integer.toString(milliseconds);
464 buffer.append(str.substring(1));
465 } else {
466 buffer.append(padWithZeros
467 ? StringUtils.leftPad(Integer.toString(milliseconds), count, '0')
468 : Integer.toString(milliseconds));
469 }
470 lastOutputSeconds = false;
471 }
472 }
473 }
474 return buffer.toString();
475 }
476
477 static final Object y = "y";
478 static final Object M = "M";
479 static final Object d = "d";
480 static final Object H = "H";
481 static final Object m = "m";
482 static final Object s = "s";
483 static final Object S = "S";
484
485 /**
486 * Parses a classic date format string into Tokens
487 *
488 * @param format to parse
489 * @return array of Token[]
490 */
491 static Token[] lexx(String format) {
492 char[] array = format.toCharArray();
493 ArrayList list = new ArrayList(array.length);
494
495 boolean inLiteral = false;
496 StringBuffer buffer = null;
497 Token previous = null;
498 int sz = array.length;
499 for(int i=0; i<sz; i++) {
500 char ch = array[i];
501 if(inLiteral && ch != '\'') {
502 buffer.append(ch); // buffer can't be null if inLiteral is true
503 continue;
504 }
505 Object value = null;
506 switch(ch) {
507 // TODO: Need to handle escaping of '
508 case '\'' :
509 if(inLiteral) {
510 buffer = null;
511 inLiteral = false;
512 } else {
513 buffer = new StringBuffer();
514 list.add(new Token(buffer));
515 inLiteral = true;
516 }
517 break;
518 case 'y' : value = y; break;
519 case 'M' : value = M; break;
520 case 'd' : value = d; break;
521 case 'H' : value = H; break;
522 case 'm' : value = m; break;
523 case 's' : value = s; break;
524 case 'S' : value = S; break;
525 default :
526 if(buffer == null) {
527 buffer = new StringBuffer();
528 list.add(new Token(buffer));
529 }
530 buffer.append(ch);
531 }
532
533 if(value != null) {
534 if(previous != null && previous.getValue() == value) {
535 previous.increment();
536 } else {
537 Token token = new Token(value);
538 list.add(token);
539 previous = token;
540 }
541 buffer = null;
542 }
543 }
544 return (Token[]) list.toArray( new Token[list.size()] );
545 }
546
547 /**
548 * Element that is parsed from the format pattern.
549 */
550 static class Token {
551
552 /**
553 * Helper method to determine if a set of tokens contain a value
554 *
555 * @param tokens set to look in
556 * @param value to look for
557 * @return boolean <code>true</code> if contained
558 */
559 static boolean containsTokenWithValue(Token[] tokens, Object value) {
560 int sz = tokens.length;
561 for (int i = 0; i < sz; i++) {
562 if (tokens[i].getValue() == value) {
563 return true;
564 }
565 }
566 return false;
567 }
568
569 private Object value;
570 private int count;
571
572 /**
573 * Wraps a token around a value. A value would be something like a 'Y'.
574 *
575 * @param value to wrap
576 */
577 Token(Object value) {
578 this.value = value;
579 this.count = 1;
580 }
581
582 /**
583 * Wraps a token around a repeated number of a value, for example it would
584 * store 'yyyy' as a value for y and a count of 4.
585 *
586 * @param value to wrap
587 * @param count to wrap
588 */
589 Token(Object value, int count) {
590 this.value = value;
591 this.count = count;
592 }
593
594 /**
595 * Adds another one of the value
596 */
597 void increment() {
598 count++;
599 }
600
601 /**
602 * Gets the current number of values represented
603 *
604 * @return int number of values represented
605 */
606 int getCount() {
607 return count;
608 }
609
610 /**
611 * Gets the particular value this token represents.
612 *
613 * @return Object value
614 */
615 Object getValue() {
616 return value;
617 }
618
619 /**
620 * Supports equality of this Token to another Token.
621 *
622 * @param obj2 Object to consider equality of
623 * @return boolean <code>true</code> if equal
624 */
625 public boolean equals(Object obj2) {
626 if (obj2 instanceof Token) {
627 Token tok2 = (Token) obj2;
628 if (this.value.getClass() != tok2.value.getClass()) {
629 return false;
630 }
631 if (this.count != tok2.count) {
632 return false;
633 }
634 if (this.value instanceof StringBuffer) {
635 return this.value.toString().equals(tok2.value.toString());
636 } else if (this.value instanceof Number) {
637 return this.value.equals(tok2.value);
638 } else {
639 return this.value == tok2.value;
640 }
641 }
642 return false;
643 }
644
645 /**
646 * Returns a hashcode for the token equal to the
647 * hashcode for the token's value. Thus 'TT' and 'TTTT'
648 * will have the same hashcode.
649 *
650 * @return The hashcode for the token
651 */
652 public int hashCode() {
653 return this.value.hashCode();
654 }
655
656 /**
657 * Represents this token as a String.
658 *
659 * @return String representation of the token
660 */
661 public String toString() {
662 return StringUtils.repeat(this.value.toString(), this.count);
663 }
664 }
665
666 }