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