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