Coverage Report - org.apache.commons.lang3.time.DurationFormatUtils
 
Classes in this File Line Coverage Branch Coverage Complexity
DurationFormatUtils
94%
198/209
91%
122/133
5.15
DurationFormatUtils$Token
96%
30/31
93%
15/16
5.15
 
 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.lang3.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.lang3.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  
  * @since 2.1
 42  
  * @version $Id: DurationFormatUtils.java 1436770 2013-01-22 07:09:45Z ggregory $
 43  
  */
 44  
 public class DurationFormatUtils {
 45  
 
 46  
     /**
 47  
      * <p>DurationFormatUtils instances should NOT be constructed in standard programming.</p>
 48  
      *
 49  
      * <p>This constructor is public to permit tools that require a JavaBean instance
 50  
      * to operate.</p>
 51  
      */
 52  
     public DurationFormatUtils() {
 53  1
         super();
 54  1
     }
 55  
 
 56  
     /**
 57  
      * <p>Pattern used with <code>FastDateFormat</code> and <code>SimpleDateFormat</code>
 58  
      * for the ISO8601 period format used in durations.</p>
 59  
      * 
 60  
      * @see org.apache.commons.lang3.time.FastDateFormat
 61  
      * @see java.text.SimpleDateFormat
 62  
      */
 63  
     public static final String ISO_EXTENDED_FORMAT_PATTERN = "'P'yyyy'Y'M'M'd'DT'H'H'm'M's.S'S'";
 64  
 
 65  
     //-----------------------------------------------------------------------
 66  
     /**
 67  
      * <p>Formats the time gap as a string.</p>
 68  
      * 
 69  
      * <p>The format used is ISO8601-like:
 70  
      * <i>H</i>:<i>m</i>:<i>s</i>.<i>S</i>.</p>
 71  
      * 
 72  
      * @param durationMillis  the duration to format
 73  
      * @return the formatted duration, not null
 74  
      */
 75  
     public static String formatDurationHMS(final long durationMillis) {
 76  11
         return formatDuration(durationMillis, "H:mm:ss.SSS");
 77  
     }
 78  
 
 79  
     /**
 80  
      * <p>Formats the time gap as a string.</p>
 81  
      * 
 82  
      * <p>The format used is the ISO8601 period format.</p>
 83  
      * 
 84  
      * <p>This method formats durations using the days and lower fields of the
 85  
      * ISO format pattern, such as P7D6TH5M4.321S.</p>
 86  
      * 
 87  
      * @param durationMillis  the duration to format
 88  
      * @return the formatted duration, not null
 89  
      */
 90  
     public static String formatDurationISO(final long durationMillis) {
 91  5
         return formatDuration(durationMillis, ISO_EXTENDED_FORMAT_PATTERN, false);
 92  
     }
 93  
 
 94  
     /**
 95  
      * <p>Formats the time gap as a string, using the specified format, and padding with zeros and 
 96  
      * using the default timezone.</p>
 97  
      * 
 98  
      * <p>This method formats durations using the days and lower fields of the
 99  
      * 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(final long durationMillis, final String format) {
 106  99
         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, final String format, final boolean padWithZeros) {
 123  
 
 124  104
         final Token[] tokens = lexx(format);
 125  
 
 126  104
         int days         = 0;
 127  104
         int hours        = 0;
 128  104
         int minutes      = 0;
 129  104
         int seconds      = 0;
 130  104
         int milliseconds = 0;
 131  
         
 132  104
         if (Token.containsTokenWithValue(tokens, d) ) {
 133  77
             days = (int) (durationMillis / DateUtils.MILLIS_PER_DAY);
 134  77
             durationMillis = durationMillis - (days * DateUtils.MILLIS_PER_DAY);
 135  
         }
 136  104
         if (Token.containsTokenWithValue(tokens, H) ) {
 137  87
             hours = (int) (durationMillis / DateUtils.MILLIS_PER_HOUR);
 138  87
             durationMillis = durationMillis - (hours * DateUtils.MILLIS_PER_HOUR);
 139  
         }
 140  104
         if (Token.containsTokenWithValue(tokens, m) ) {
 141  88
             minutes = (int) (durationMillis / DateUtils.MILLIS_PER_MINUTE);
 142  88
             durationMillis = durationMillis - (minutes * DateUtils.MILLIS_PER_MINUTE);
 143  
         }
 144  104
         if (Token.containsTokenWithValue(tokens, s) ) {
 145  88
             seconds = (int) (durationMillis / DateUtils.MILLIS_PER_SECOND);
 146  88
             durationMillis = durationMillis - (seconds * DateUtils.MILLIS_PER_SECOND);
 147  
         }
 148  104
         if (Token.containsTokenWithValue(tokens, S) ) {
 149  19
             milliseconds = (int) durationMillis;
 150  
         }
 151  
 
 152  104
         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  
         final long durationMillis,
 168  
         final boolean suppressLeadingZeroElements,
 169  
         final 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  69
         String duration = formatDuration(durationMillis, "d' days 'H' hours 'm' minutes 's' seconds'");
 175  69
         if (suppressLeadingZeroElements) {
 176  
             // this is a temporary marker on the front. Like ^ in regexp.
 177  12
             duration = " " + duration;
 178  12
             String tmp = StringUtils.replaceOnce(duration, " 0 days", "");
 179  12
             if (tmp.length() != duration.length()) {
 180  10
                 duration = tmp;
 181  10
                 tmp = StringUtils.replaceOnce(duration, " 0 hours", "");
 182  10
                 if (tmp.length() != duration.length()) {
 183  8
                     duration = tmp;
 184  8
                     tmp = StringUtils.replaceOnce(duration, " 0 minutes", "");
 185  8
                     duration = tmp;
 186  8
                     if (tmp.length() != duration.length()) {
 187  0
                         duration = StringUtils.replaceOnce(tmp, " 0 seconds", "");
 188  
                     }
 189  
                 }
 190  
             }
 191  12
             if (duration.length() != 0) {
 192  
                 // strip the space off again
 193  12
                 duration = duration.substring(1);
 194  
             }
 195  
         }
 196  69
         if (suppressTrailingZeroElements) {
 197  12
             String tmp = StringUtils.replaceOnce(duration, " 0 seconds", "");
 198  12
             if (tmp.length() != duration.length()) {
 199  6
                 duration = tmp;
 200  6
                 tmp = StringUtils.replaceOnce(duration, " 0 minutes", "");
 201  6
                 if (tmp.length() != duration.length()) {
 202  2
                     duration = tmp;
 203  2
                     tmp = StringUtils.replaceOnce(duration, " 0 hours", "");
 204  2
                     if (tmp.length() != duration.length()) {
 205  2
                         duration = StringUtils.replaceOnce(tmp, " 0 days", "");
 206  
                     }
 207  
                 }
 208  
             }
 209  
         }
 210  
         // handle plurals
 211  69
         duration = " " + duration;
 212  69
         duration = StringUtils.replaceOnce(duration, " 1 seconds", " 1 second");
 213  69
         duration = StringUtils.replaceOnce(duration, " 1 minutes", " 1 minute");
 214  69
         duration = StringUtils.replaceOnce(duration, " 1 hours", " 1 hour");
 215  69
         duration = StringUtils.replaceOnce(duration, " 1 days", " 1 day");
 216  69
         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(final long startMillis, final long endMillis) {
 230  0
         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(final long startMillis, final long endMillis, final String format) {
 243  95230
         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(final long startMillis, final long endMillis, final String format, final boolean padWithZeros, 
 270  
             final 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  95232
         final Token[] tokens = lexx(format);
 279  
 
 280  
         // timezones get funky around 0, so normalizing everything to GMT 
 281  
         // stops the hours being off
 282  95232
         final Calendar start = Calendar.getInstance(timezone);
 283  95232
         start.setTime(new Date(startMillis));
 284  95232
         final Calendar end = Calendar.getInstance(timezone);
 285  95232
         end.setTime(new Date(endMillis));
 286  
 
 287  
         // initial estimates
 288  95232
         int milliseconds = end.get(Calendar.MILLISECOND) - start.get(Calendar.MILLISECOND);
 289  95232
         int seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND);
 290  95232
         int minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE);
 291  95232
         int hours = end.get(Calendar.HOUR_OF_DAY) - start.get(Calendar.HOUR_OF_DAY);
 292  95232
         int days = end.get(Calendar.DAY_OF_MONTH) - start.get(Calendar.DAY_OF_MONTH);
 293  95232
         int months = end.get(Calendar.MONTH) - start.get(Calendar.MONTH);
 294  95232
         int years = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
 295  
 
 296  
         // each initial estimate is adjusted in case it is under 0
 297  95232
         while (milliseconds < 0) {
 298  0
             milliseconds += 1000;
 299  0
             seconds -= 1;
 300  
         }
 301  95232
         while (seconds < 0) {
 302  0
             seconds += 60;
 303  0
             minutes -= 1;
 304  
         }
 305  95232
         while (minutes < 0) {
 306  0
             minutes += 60;
 307  0
             hours -= 1;
 308  
         }
 309  95233
         while (hours < 0) {
 310  1
             hours += 24;
 311  1
             days -= 1;
 312  
         }
 313  
        
 314  95232
         if (Token.containsTokenWithValue(tokens, M)) {
 315  1502
             while (days < 0) {
 316  10
                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
 317  10
                 months -= 1;
 318  10
                 start.add(Calendar.MONTH, 1);
 319  
             }
 320  
 
 321  1618
             while (months < 0) {
 322  126
                 months += 12;
 323  126
                 years -= 1;
 324  
             }
 325  
 
 326  1492
             if (!Token.containsTokenWithValue(tokens, y) && years != 0) {
 327  2912
                 while (years != 0) {
 328  1456
                     months += 12 * years;
 329  1456
                     years = 0;
 330  
                 }
 331  
             }
 332  
         } else {
 333  
             // there are no M's in the format string
 334  
 
 335  93740
             if( !Token.containsTokenWithValue(tokens, y) ) {
 336  93737
                 int target = end.get(Calendar.YEAR);
 337  93737
                 if (months < 0) {
 338  
                     // target is end-year -1
 339  379
                     target -= 1;
 340  
                 }
 341  
                 
 342  104933
                 while ( (start.get(Calendar.YEAR) != target)) {
 343  11196
                     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  11196
                     if (start instanceof GregorianCalendar &&
 347  
                             start.get(Calendar.MONTH) == Calendar.FEBRUARY &&
 348  
                             start.get(Calendar.DAY_OF_MONTH) == 29) {
 349  2247
                         days += 1;
 350  
                     }
 351  
                     
 352  11196
                     start.add(Calendar.YEAR, 1);
 353  
                     
 354  11196
                     days += start.get(Calendar.DAY_OF_YEAR);
 355  
                 }
 356  
                 
 357  93737
                 years = 0;
 358  
             }
 359  
             
 360  134219
             while( start.get(Calendar.MONTH) != end.get(Calendar.MONTH) ) {
 361  40479
                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
 362  40479
                 start.add(Calendar.MONTH, 1);
 363  
             }
 364  
             
 365  93740
             months = 0;            
 366  
 
 367  93740
             while (days < 0) {
 368  0
                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
 369  0
                 months -= 1;
 370  0
                 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  95232
         if (!Token.containsTokenWithValue(tokens, d)) {
 380  87896
             hours += 24 * days;
 381  87896
             days = 0;
 382  
         }
 383  95232
         if (!Token.containsTokenWithValue(tokens, H)) {
 384  8827
             minutes += 60 * hours;
 385  8827
             hours = 0;
 386  
         }
 387  95232
         if (!Token.containsTokenWithValue(tokens, m)) {
 388  8826
             seconds += 60 * minutes;
 389  8826
             minutes = 0;
 390  
         }
 391  95232
         if (!Token.containsTokenWithValue(tokens, s)) {
 392  8826
             milliseconds += 1000 * seconds;
 393  8826
             seconds = 0;
 394  
         }
 395  
 
 396  95232
         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(final Token[] tokens, final int years, final int months, final int days, final int hours, final int minutes, final int seconds,
 415  
             int milliseconds, final boolean padWithZeros) {
 416  95336
         final StringBuilder buffer = new StringBuilder();
 417  95336
         boolean lastOutputSeconds = false;
 418  95336
         final int sz = tokens.length;
 419  536971
         for (int i = 0; i < sz; i++) {
 420  441635
             final Token token = tokens[i];
 421  441635
             final Object value = token.getValue();
 422  441635
             final int count = token.getCount();
 423  441635
             if (value instanceof StringBuilder) {
 424  173190
                 buffer.append(value.toString());
 425  
             } else {
 426  268445
                 if (value == y) {
 427  26
                     buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(years), count, '0') : Integer
 428  
                             .toString(years));
 429  26
                     lastOutputSeconds = false;
 430  268419
                 } else if (value == M) {
 431  1501
                     buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(months), count, '0') : Integer
 432  
                             .toString(months));
 433  1501
                     lastOutputSeconds = false;
 434  266918
                 } else if (value == d) {
 435  7413
                     buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(days), count, '0') : Integer
 436  
                             .toString(days));
 437  7413
                     lastOutputSeconds = false;
 438  259505
                 } else if (value == H) {
 439  86492
                     buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(hours), count, '0') : Integer
 440  
                             .toString(hours));
 441  86492
                     lastOutputSeconds = false;
 442  173013
                 } else if (value == m) {
 443  86494
                     buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(minutes), count, '0') : Integer
 444  
                             .toString(minutes));
 445  86494
                     lastOutputSeconds = false;
 446  86519
                 } else if (value == s) {
 447  86494
                     buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(seconds), count, '0') : Integer
 448  
                             .toString(seconds));
 449  86494
                     lastOutputSeconds = true;
 450  25
                 } else if (value == S) {
 451  25
                     if (lastOutputSeconds) {
 452  19
                         milliseconds += 1000;
 453  19
                         final String str = padWithZeros
 454  
                                 ? StringUtils.leftPad(Integer.toString(milliseconds), count, '0')
 455  
                                 : Integer.toString(milliseconds);
 456  19
                         buffer.append(str.substring(1));
 457  19
                     } else {
 458  6
                         buffer.append(padWithZeros
 459  
                                 ? StringUtils.leftPad(Integer.toString(milliseconds), count, '0')
 460  
                                 : Integer.toString(milliseconds));
 461  
                     }
 462  25
                     lastOutputSeconds = false;
 463  
                 }
 464  
             }
 465  
         }
 466  95336
         return buffer.toString();
 467  
     }
 468  
 
 469  1
     static final Object y = "y";
 470  1
     static final Object M = "M";
 471  1
     static final Object d = "d";
 472  1
     static final Object H = "H";
 473  1
     static final Object m = "m";
 474  1
     static final Object s = "s";
 475  1
     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(final String format) {
 484  95339
         final char[] array = format.toCharArray();
 485  95339
         final ArrayList<Token> list = new ArrayList<Token>(array.length);
 486  
 
 487  95339
         boolean inLiteral = false;
 488  
         // Although the buffer is stored in a Token, the Tokens are only
 489  
         // used internally, so cannot be accessed by other threads
 490  95339
         StringBuilder buffer = null;
 491  95339
         Token previous = null;
 492  95339
         final int sz = array.length;
 493  539661
         for(int i=0; i<sz; i++) {
 494  444322
             final char ch = array[i];
 495  444322
             if(inLiteral && ch != '\'') {
 496  2176
                 buffer.append(ch); // buffer can't be null if inLiteral is true
 497  2176
                 continue;
 498  
             }
 499  442146
             Object value = null;
 500  442146
             switch(ch) {
 501  
                 // TODO: Need to handle escaping of '
 502  
                 case '\'' : 
 503  676
                   if(inLiteral) {
 504  338
                       buffer = null;
 505  338
                       inLiteral = false;
 506  
                   } else {
 507  338
                       buffer = new StringBuilder();
 508  338
                       list.add(new Token(buffer));
 509  338
                       inLiteral = true;
 510  
                   }
 511  338
                   break;
 512  66
                 case 'y'  : value = y; break;
 513  1521
                 case 'M'  : value = M; break;
 514  7440
                 case 'd'  : value = d; break;
 515  86496
                 case 'H'  : value = H; break;
 516  86512
                 case 'm'  : value = m; break;
 517  86512
                 case 's'  : value = s; break;
 518  60
                 case 'S'  : value = S; break;
 519  
                 default   : 
 520  172863
                   if(buffer == null) {
 521  172863
                       buffer = new StringBuilder();
 522  172863
                       list.add(new Token(buffer));
 523  
                   }
 524  172863
                   buffer.append(ch);
 525  
             }
 526  
 
 527  442146
             if(value != null) {
 528  268607
                 if(previous != null && previous.getValue() == value) {
 529  144
                     previous.increment();
 530  
                 } else {
 531  268463
                     final Token token = new Token(value);
 532  268463
                     list.add(token); 
 533  268463
                     previous = token;
 534  
                 }
 535  268607
                 buffer = null; 
 536  
             }
 537  
         }
 538  95339
         return list.toArray( new Token[list.size()] );
 539  
     }
 540  
 
 541  
     //-----------------------------------------------------------------------
 542  
     /**
 543  
      * Element that is parsed from the format pattern.
 544  
      */
 545  
     static class Token {
 546  
 
 547  
         /**
 548  
          * Helper method to determine if a set of tokens contain a value
 549  
          *
 550  
          * @param tokens set to look in
 551  
          * @param value to look for
 552  
          * @return boolean <code>true</code> if contained
 553  
          */
 554  
         static boolean containsTokenWithValue(final Token[] tokens, final Object value) {
 555  571912
             final int sz = tokens.length;
 556  2432635
             for (int i = 0; i < sz; i++) {
 557  2129143
                 if (tokens[i].getValue() == value) {
 558  268420
                     return true;
 559  
                 }
 560  
             }
 561  303492
             return false;
 562  
         }
 563  
 
 564  
         private final Object value;
 565  
         private int count;
 566  
 
 567  
         /**
 568  
          * Wraps a token around a value. A value would be something like a 'Y'.
 569  
          *
 570  
          * @param value to wrap
 571  
          */
 572  441665
         Token(final Object value) {
 573  441665
             this.value = value;
 574  441665
             this.count = 1;
 575  441665
         }
 576  
 
 577  
         /**
 578  
          * Wraps a token around a repeated number of a value, for example it would 
 579  
          * store 'yyyy' as a value for y and a count of 4.
 580  
          *
 581  
          * @param value to wrap
 582  
          * @param count to wrap
 583  
          */
 584  32
         Token(final Object value, final int count) {
 585  32
             this.value = value;
 586  32
             this.count = count;
 587  32
         }
 588  
 
 589  
         /**
 590  
          * Adds another one of the value
 591  
          */
 592  
         void increment() { 
 593  144
             count++;
 594  144
         }
 595  
 
 596  
         /**
 597  
          * Gets the current number of values represented
 598  
          *
 599  
          * @return int number of values represented
 600  
          */
 601  
         int getCount() {
 602  441635
             return count;
 603  
         }
 604  
 
 605  
         /**
 606  
          * Gets the particular value this token represents.
 607  
          * 
 608  
          * @return Object value
 609  
          */
 610  
         Object getValue() {
 611  2744046
             return value;
 612  
         }
 613  
 
 614  
         /**
 615  
          * Supports equality of this Token to another Token.
 616  
          *
 617  
          * @param obj2 Object to consider equality of
 618  
          * @return boolean <code>true</code> if equal
 619  
          */
 620  
         @Override
 621  
         public boolean equals(final Object obj2) {
 622  33
             if (obj2 instanceof Token) {
 623  32
                 final Token tok2 = (Token) obj2;
 624  32
                 if (this.value.getClass() != tok2.value.getClass()) {
 625  1
                     return false;
 626  
                 }
 627  31
                 if (this.count != tok2.count) {
 628  1
                     return false;
 629  
                 }
 630  30
                 if (this.value instanceof StringBuilder) {
 631  11
                     return this.value.toString().equals(tok2.value.toString());
 632  19
                 } else if (this.value instanceof Number) {
 633  1
                     return this.value.equals(tok2.value);
 634  
                 } else {
 635  18
                     return this.value == tok2.value;
 636  
                 }
 637  
             }
 638  1
             return false;
 639  
         }
 640  
 
 641  
         /**
 642  
          * Returns a hash code for the token equal to the 
 643  
          * hash code for the token's value. Thus 'TT' and 'TTTT' 
 644  
          * will have the same hash code. 
 645  
          *
 646  
          * @return The hash code for the token
 647  
          */
 648  
         @Override
 649  
         public int hashCode() {
 650  0
             return this.value.hashCode();
 651  
         }
 652  
 
 653  
         /**
 654  
          * Represents this token as a String.
 655  
          *
 656  
          * @return String representation of the token
 657  
          */
 658  
         @Override
 659  
         public String toString() {
 660  58
             return StringUtils.repeat(this.value.toString(), this.count);
 661  
         }
 662  
     }
 663  
 
 664  
 }