View Javadoc

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