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    
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 Software Foundation
042     * @author Apache Ant - DateUtils
043     * @author <a href="mailto:sbailliez@apache.org">Stephane Bailliez</a>
044     * @author <a href="mailto:stefan.bodewig@epost.de">Stefan Bodewig</a>
045     * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a>
046     * @since 2.1
047     * @version $Id: DurationFormatUtils.java 905684 2010-02-02 16:03:07Z niallp $
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, ISO_EXTENDED_FORMAT_PATTERN, false);
097        }
098    
099        /**
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 array of Token[]
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); // buffer can't be null if inLiteral is true
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    }