Coverage Report - org.apache.commons.lang3.time.DurationFormatUtils
 
Classes in this File Line Coverage Branch Coverage Complexity
DurationFormatUtils
95%
205/215
91%
113/123
4,714
DurationFormatUtils$Token
93%
28/30
93%
15/16
4,714
 
 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  
 import org.apache.commons.lang3.Validate;
 27  
 
 28  
 /**
 29  
  * <p>Duration formatting utilities and constants. The following table describes the tokens 
 30  
  * used in the pattern language for formatting. </p>
 31  
  * <table border="1" summary="Pattern Tokens">
 32  
  *  <tr><th>character</th><th>duration element</th></tr>
 33  
  *  <tr><td>y</td><td>years</td></tr>
 34  
  *  <tr><td>M</td><td>months</td></tr>
 35  
  *  <tr><td>d</td><td>days</td></tr>
 36  
  *  <tr><td>H</td><td>hours</td></tr>
 37  
  *  <tr><td>m</td><td>minutes</td></tr>
 38  
  *  <tr><td>s</td><td>seconds</td></tr>
 39  
  *  <tr><td>S</td><td>milliseconds</td></tr>
 40  
  *  <tr><td>'text'</td><td>arbitrary text content</td></tr>
 41  
  * </table>
 42  
  *
 43  
  * <b>Note: It's not currently possible to include a single-quote in a format.</b>
 44  
  * <br>
 45  
  * Token values are printed using decimal digits.
 46  
  * A token character can be repeated to ensure that the field occupies a certain minimum
 47  
  * size. Values will be left-padded with 0 unless padding is disabled in the method invocation.
 48  
  * @since 2.1
 49  
  */
 50  
 public class DurationFormatUtils {
 51  
 
 52  
     /**
 53  
      * <p>DurationFormatUtils instances should NOT be constructed in standard programming.</p>
 54  
      *
 55  
      * <p>This constructor is public to permit tools that require a JavaBean instance
 56  
      * to operate.</p>
 57  
      */
 58  
     public DurationFormatUtils() {
 59  1
         super();
 60  1
     }
 61  
 
 62  
     /**
 63  
      * <p>Pattern used with <code>FastDateFormat</code> and <code>SimpleDateFormat</code>
 64  
      * for the ISO 8601 period format used in durations.</p>
 65  
      * 
 66  
      * @see org.apache.commons.lang3.time.FastDateFormat
 67  
      * @see java.text.SimpleDateFormat
 68  
      */
 69  
     public static final String ISO_EXTENDED_FORMAT_PATTERN = "'P'yyyy'Y'M'M'd'DT'H'H'm'M's.SSS'S'";
 70  
 
 71  
     //-----------------------------------------------------------------------
 72  
     /**
 73  
      * <p>Formats the time gap as a string.</p>
 74  
      * 
 75  
      * <p>The format used is ISO 8601-like: {@code HH:mm:ss.SSS}.</p>
 76  
      *
 77  
      * @param durationMillis  the duration to format
 78  
      * @return the formatted duration, not null
 79  
      * @throws java.lang.IllegalArgumentException if durationMillis is negative
 80  
      */
 81  
     public static String formatDurationHMS(final long durationMillis) {
 82  12
         return formatDuration(durationMillis, "HH:mm:ss.SSS");
 83  
     }
 84  
 
 85  
     /**
 86  
      * <p>Formats the time gap as a string.</p>
 87  
      * 
 88  
      * <p>The format used is the ISO 8601 period format.</p>
 89  
      * 
 90  
      * <p>This method formats durations using the days and lower fields of the
 91  
      * ISO format pattern, such as P7D6TH5M4.321S.</p>
 92  
      * 
 93  
      * @param durationMillis  the duration to format
 94  
      * @return the formatted duration, not null
 95  
      * @throws java.lang.IllegalArgumentException if durationMillis is negative
 96  
      */
 97  
     public static String formatDurationISO(final long durationMillis) {
 98  6
         return formatDuration(durationMillis, ISO_EXTENDED_FORMAT_PATTERN, false);
 99  
     }
 100  
 
 101  
     /**
 102  
      * <p>Formats the time gap as a string, using the specified format, and padding with zeros.</p>
 103  
      * 
 104  
      * <p>This method formats durations using the days and lower fields of the
 105  
      * format pattern. Months and larger are not used.</p>
 106  
      * 
 107  
      * @param durationMillis  the duration to format
 108  
      * @param format  the way in which to format the duration, not null
 109  
      * @return the formatted duration, not null
 110  
      * @throws java.lang.IllegalArgumentException if durationMillis is negative
 111  
      */
 112  
     public static String formatDuration(final long durationMillis, final String format) {
 113  113
         return formatDuration(durationMillis, format, true);
 114  
     }
 115  
 
 116  
     /**
 117  
      * <p>Formats the time gap as a string, using the specified format.
 118  
      * Padding the left hand side of numbers with zeroes is optional.</p>
 119  
      * 
 120  
      * <p>This method formats durations using the days and lower fields of the
 121  
      * format pattern. Months and larger are not used.</p>
 122  
      * 
 123  
      * @param durationMillis  the duration to format
 124  
      * @param format  the way in which to format the duration, not null
 125  
      * @param padWithZeros  whether to pad the left hand side of numbers with 0's
 126  
      * @return the formatted duration, not null
 127  
      * @throws java.lang.IllegalArgumentException if durationMillis is negative
 128  
      */
 129  
     public static String formatDuration(final long durationMillis, final String format, final boolean padWithZeros) {
 130  120
         Validate.inclusiveBetween(0, Long.MAX_VALUE, durationMillis, "durationMillis must not be negative");        
 131  
 
 132  116
         final Token[] tokens = lexx(format);
 133  
 
 134  116
         long days         = 0;
 135  116
         long hours        = 0;
 136  116
         long minutes      = 0;
 137  116
         long seconds      = 0;
 138  116
         long milliseconds = durationMillis;
 139  
         
 140  116
         if (Token.containsTokenWithValue(tokens, d) ) {
 141  77
             days = milliseconds / DateUtils.MILLIS_PER_DAY;
 142  77
             milliseconds = milliseconds - (days * DateUtils.MILLIS_PER_DAY);
 143  
         }
 144  116
         if (Token.containsTokenWithValue(tokens, H) ) {
 145  87
             hours = milliseconds / DateUtils.MILLIS_PER_HOUR;
 146  87
             milliseconds = milliseconds - (hours * DateUtils.MILLIS_PER_HOUR);
 147  
         }
 148  116
         if (Token.containsTokenWithValue(tokens, m) ) {
 149  92
             minutes = milliseconds / DateUtils.MILLIS_PER_MINUTE;
 150  92
             milliseconds = milliseconds - (minutes * DateUtils.MILLIS_PER_MINUTE);
 151  
         }
 152  116
         if (Token.containsTokenWithValue(tokens, s) ) {
 153  92
             seconds = milliseconds / DateUtils.MILLIS_PER_SECOND;
 154  92
             milliseconds = milliseconds - (seconds * DateUtils.MILLIS_PER_SECOND);
 155  
         }
 156  
 
 157  116
         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, not null
 170  
      * @throws java.lang.IllegalArgumentException if durationMillis is negative
 171  
      */
 172  
     public static String formatDurationWords(
 173  
         final long durationMillis,
 174  
         final boolean suppressLeadingZeroElements,
 175  
         final boolean suppressTrailingZeroElements) {
 176  
 
 177  
         // This method is generally replacable by the format method, but 
 178  
         // there are a series of tweaks and special cases that require 
 179  
         // trickery to replicate.
 180  70
         String duration = formatDuration(durationMillis, "d' days 'H' hours 'm' minutes 's' seconds'");
 181  69
         if (suppressLeadingZeroElements) {
 182  
             // this is a temporary marker on the front. Like ^ in regexp.
 183  12
             duration = " " + duration;
 184  12
             String tmp = StringUtils.replaceOnce(duration, " 0 days", "");
 185  12
             if (tmp.length() != duration.length()) {
 186  10
                 duration = tmp;
 187  10
                 tmp = StringUtils.replaceOnce(duration, " 0 hours", "");
 188  10
                 if (tmp.length() != duration.length()) {
 189  8
                     duration = tmp;
 190  8
                     tmp = StringUtils.replaceOnce(duration, " 0 minutes", "");
 191  8
                     duration = tmp;
 192  8
                     if (tmp.length() != duration.length()) {
 193  0
                         duration = StringUtils.replaceOnce(tmp, " 0 seconds", "");
 194  
                     }
 195  
                 }
 196  
             }
 197  12
             if (duration.length() != 0) {
 198  
                 // strip the space off again
 199  12
                 duration = duration.substring(1);
 200  
             }
 201  
         }
 202  69
         if (suppressTrailingZeroElements) {
 203  12
             String tmp = StringUtils.replaceOnce(duration, " 0 seconds", "");
 204  12
             if (tmp.length() != duration.length()) {
 205  6
                 duration = tmp;
 206  6
                 tmp = StringUtils.replaceOnce(duration, " 0 minutes", "");
 207  6
                 if (tmp.length() != duration.length()) {
 208  2
                     duration = tmp;
 209  2
                     tmp = StringUtils.replaceOnce(duration, " 0 hours", "");
 210  2
                     if (tmp.length() != duration.length()) {
 211  2
                         duration = StringUtils.replaceOnce(tmp, " 0 days", "");
 212  
                     }
 213  
                 }
 214  
             }
 215  
         }
 216  
         // handle plurals
 217  69
         duration = " " + duration;
 218  69
         duration = StringUtils.replaceOnce(duration, " 1 seconds", " 1 second");
 219  69
         duration = StringUtils.replaceOnce(duration, " 1 minutes", " 1 minute");
 220  69
         duration = StringUtils.replaceOnce(duration, " 1 hours", " 1 hour");
 221  69
         duration = StringUtils.replaceOnce(duration, " 1 days", " 1 day");
 222  69
         return duration.trim();
 223  
     }
 224  
 
 225  
     //-----------------------------------------------------------------------
 226  
     /**
 227  
      * <p>Formats the time gap as a string.</p>
 228  
      * 
 229  
      * <p>The format used is the ISO 8601 period format.</p>
 230  
      * 
 231  
      * @param startMillis  the start of the duration to format
 232  
      * @param endMillis  the end of the duration to format
 233  
      * @return the formatted duration, not null
 234  
      * @throws java.lang.IllegalArgumentException if startMillis is greater than endMillis
 235  
      */
 236  
     public static String formatPeriodISO(final long startMillis, final long endMillis) {
 237  4
         return formatPeriod(startMillis, endMillis, ISO_EXTENDED_FORMAT_PATTERN, false, TimeZone.getDefault());
 238  
     }
 239  
 
 240  
     /**
 241  
      * <p>Formats the time gap as a string, using the specified format.
 242  
      * Padding the left hand side of numbers with zeroes is optional.
 243  
      * 
 244  
      * @param startMillis  the start of the duration
 245  
      * @param endMillis  the end of the duration
 246  
      * @param format  the way in which to format the duration, not null
 247  
      * @return the formatted duration, not null
 248  
      * @throws java.lang.IllegalArgumentException if startMillis is greater than endMillis
 249  
      */
 250  
     public static String formatPeriod(final long startMillis, final long endMillis, final String format) {
 251  95231
         return formatPeriod(startMillis, endMillis, format, true, TimeZone.getDefault());
 252  
     }
 253  
 
 254  
     /**
 255  
      * <p>Formats the time gap as a string, using the specified format.
 256  
      * Padding the left hand side of numbers with zeroes is optional and 
 257  
      * the timezone may be specified. </p>
 258  
      *
 259  
      * <p>When calculating the difference between months/days, it chooses to 
 260  
      * calculate months first. So when working out the number of months and 
 261  
      * days between January 15th and March 10th, it choose 1 month and 
 262  
      * 23 days gained by choosing January-&gt;February = 1 month and then 
 263  
      * calculating days forwards, and not the 1 month and 26 days gained by 
 264  
      * choosing March -&gt; February = 1 month and then calculating days 
 265  
      * backwards. </p>
 266  
      *
 267  
      * <p>For more control, the <a href="http://joda-time.sf.net/">Joda-Time</a>
 268  
      * library is recommended.</p>
 269  
      * 
 270  
      * @param startMillis  the start of the duration
 271  
      * @param endMillis  the end of the duration
 272  
      * @param format  the way in which to format the duration, not null
 273  
      * @param padWithZeros  whether to pad the left hand side of numbers with 0's
 274  
      * @param timezone  the millis are defined in
 275  
      * @return the formatted duration, not null
 276  
      * @throws java.lang.IllegalArgumentException if startMillis is greater than endMillis
 277  
      */
 278  
     public static String formatPeriod(final long startMillis, final long endMillis, final String format, final boolean padWithZeros, 
 279  
             final TimeZone timezone) {
 280  95237
         Validate.isTrue(startMillis <= endMillis, "startMillis must not be greater than endMillis");
 281  
         
 282  
 
 283  
         // Used to optimise for differences under 28 days and 
 284  
         // called formatDuration(millis, format); however this did not work 
 285  
         // over leap years. 
 286  
         // TODO: Compare performance to see if anything was lost by 
 287  
         // losing this optimisation. 
 288  
         
 289  95235
         final Token[] tokens = lexx(format);
 290  
 
 291  
         // timezones get funky around 0, so normalizing everything to GMT 
 292  
         // stops the hours being off
 293  95235
         final Calendar start = Calendar.getInstance(timezone);
 294  95235
         start.setTime(new Date(startMillis));
 295  95235
         final Calendar end = Calendar.getInstance(timezone);
 296  95235
         end.setTime(new Date(endMillis));
 297  
 
 298  
         // initial estimates
 299  95235
         int milliseconds = end.get(Calendar.MILLISECOND) - start.get(Calendar.MILLISECOND);
 300  95235
         int seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND);
 301  95235
         int minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE);
 302  95235
         int hours = end.get(Calendar.HOUR_OF_DAY) - start.get(Calendar.HOUR_OF_DAY);
 303  95235
         int days = end.get(Calendar.DAY_OF_MONTH) - start.get(Calendar.DAY_OF_MONTH);
 304  95235
         int months = end.get(Calendar.MONTH) - start.get(Calendar.MONTH);
 305  95235
         int years = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
 306  
 
 307  
         // each initial estimate is adjusted in case it is under 0
 308  95235
         while (milliseconds < 0) {
 309  0
             milliseconds += 1000;
 310  0
             seconds -= 1;
 311  
         }
 312  95235
         while (seconds < 0) {
 313  0
             seconds += 60;
 314  0
             minutes -= 1;
 315  
         }
 316  95235
         while (minutes < 0) {
 317  0
             minutes += 60;
 318  0
             hours -= 1;
 319  
         }
 320  95236
         while (hours < 0) {
 321  1
             hours += 24;
 322  1
             days -= 1;
 323  
         }
 324  
        
 325  95235
         if (Token.containsTokenWithValue(tokens, M)) {
 326  1505
             while (days < 0) {
 327  10
                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
 328  10
                 months -= 1;
 329  10
                 start.add(Calendar.MONTH, 1);
 330  
             }
 331  
 
 332  1621
             while (months < 0) {
 333  126
                 months += 12;
 334  126
                 years -= 1;
 335  
             }
 336  
 
 337  1495
             if (!Token.containsTokenWithValue(tokens, y) && years != 0) {
 338  2912
                 while (years != 0) {
 339  1456
                     months += 12 * years;
 340  1456
                     years = 0;
 341  
                 }
 342  
             }
 343  
         } else {
 344  
             // there are no M's in the format string
 345  
 
 346  93740
             if( !Token.containsTokenWithValue(tokens, y) ) {
 347  93737
                 int target = end.get(Calendar.YEAR);
 348  93737
                 if (months < 0) {
 349  
                     // target is end-year -1
 350  379
                     target -= 1;
 351  
                 }
 352  
                 
 353  104933
                 while (start.get(Calendar.YEAR) != target) {
 354  11196
                     days += start.getActualMaximum(Calendar.DAY_OF_YEAR) - start.get(Calendar.DAY_OF_YEAR);
 355  
                     
 356  
                     // Not sure I grok why this is needed, but the brutal tests show it is
 357  11196
                     if (start instanceof GregorianCalendar &&
 358  
                             start.get(Calendar.MONTH) == Calendar.FEBRUARY &&
 359  
                             start.get(Calendar.DAY_OF_MONTH) == 29) {
 360  2247
                         days += 1;
 361  
                     }
 362  
                     
 363  11196
                     start.add(Calendar.YEAR, 1);
 364  
                     
 365  11196
                     days += start.get(Calendar.DAY_OF_YEAR);
 366  
                 }
 367  
                 
 368  93737
                 years = 0;
 369  
             }
 370  
             
 371  134219
             while( start.get(Calendar.MONTH) != end.get(Calendar.MONTH) ) {
 372  40479
                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
 373  40479
                 start.add(Calendar.MONTH, 1);
 374  
             }
 375  
             
 376  93740
             months = 0;            
 377  
 
 378  93740
             while (days < 0) {
 379  0
                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
 380  0
                 months -= 1;
 381  0
                 start.add(Calendar.MONTH, 1);
 382  
             }
 383  
             
 384  
         }
 385  
 
 386  
         // The rest of this code adds in values that 
 387  
         // aren't requested. This allows the user to ask for the 
 388  
         // number of months and get the real count and not just 0->11.
 389  
 
 390  95235
         if (!Token.containsTokenWithValue(tokens, d)) {
 391  87896
             hours += 24 * days;
 392  87896
             days = 0;
 393  
         }
 394  95235
         if (!Token.containsTokenWithValue(tokens, H)) {
 395  8827
             minutes += 60 * hours;
 396  8827
             hours = 0;
 397  
         }
 398  95235
         if (!Token.containsTokenWithValue(tokens, m)) {
 399  8826
             seconds += 60 * minutes;
 400  8826
             minutes = 0;
 401  
         }
 402  95235
         if (!Token.containsTokenWithValue(tokens, s)) {
 403  8826
             milliseconds += 1000 * seconds;
 404  8826
             seconds = 0;
 405  
         }
 406  
 
 407  95235
         return format(tokens, years, months, days, hours, minutes, seconds, milliseconds, padWithZeros);
 408  
     }
 409  
 
 410  
     //-----------------------------------------------------------------------
 411  
     /**
 412  
      * <p>The internal method to do the formatting.</p>
 413  
      * 
 414  
      * @param tokens  the tokens
 415  
      * @param years  the number of years
 416  
      * @param months  the number of months
 417  
      * @param days  the number of days
 418  
      * @param hours  the number of hours
 419  
      * @param minutes  the number of minutes
 420  
      * @param seconds  the number of seconds
 421  
      * @param milliseconds  the number of millis
 422  
      * @param padWithZeros  whether to pad
 423  
      * @return the formatted string
 424  
      */
 425  
     static String format(final Token[] tokens, final long years, final long months, final long days, final long hours, final long minutes, final long seconds,
 426  
             final long milliseconds, final boolean padWithZeros) {
 427  95351
         final StringBuilder buffer = new StringBuilder();
 428  95351
         boolean lastOutputSeconds = false;
 429  537059
         for (final Token token : tokens) {
 430  441708
             final Object value = token.getValue();
 431  441708
             final int count = token.getCount();
 432  441708
             if (value instanceof StringBuilder) {
 433  173222
                 buffer.append(value.toString());
 434  
             } else {
 435  268486
                 if (value.equals(y)) {
 436  29
                     buffer.append(paddedValue(years, padWithZeros, count));
 437  29
                     lastOutputSeconds = false;
 438  268457
                 } else if (value.equals(M)) {
 439  1504
                     buffer.append(paddedValue(months, padWithZeros, count));
 440  1504
                     lastOutputSeconds = false;
 441  266953
                 } else if (value.equals(d)) {
 442  7416
                     buffer.append(paddedValue(days, padWithZeros, count));
 443  7416
                     lastOutputSeconds = false;
 444  259537
                 } else if (value.equals(H)) {
 445  86495
                     buffer.append(paddedValue(hours, padWithZeros, count));
 446  86495
                     lastOutputSeconds = false;
 447  173042
                 } else if (value.equals(m)) {
 448  86501
                     buffer.append(paddedValue(minutes, padWithZeros, count));
 449  86501
                     lastOutputSeconds = false;
 450  86541
                 } else if (value.equals(s)) {
 451  86501
                     buffer.append(paddedValue(seconds, padWithZeros, count));
 452  86501
                     lastOutputSeconds = true;
 453  40
                 } else if (value.equals(S)) {
 454  40
                     if (lastOutputSeconds) {
 455  
                         // ensure at least 3 digits are displayed even if padding is not selected
 456  26
                         final int width = padWithZeros ? Math.max(3, count) : 3;
 457  26
                         buffer.append(paddedValue(milliseconds, true, width));
 458  26
                     } else {
 459  14
                         buffer.append(paddedValue(milliseconds, padWithZeros, count));
 460  
                     }
 461  40
                     lastOutputSeconds = false;
 462  
                 }
 463  
             }
 464  
         }
 465  95351
         return buffer.toString();
 466  
     }
 467  
 
 468  
     /**
 469  
      * <p>Converts a {@code long} to a {@code String} with optional
 470  
      * zero padding.</p>
 471  
      *
 472  
      * @param value the value to convert
 473  
      * @param padWithZeros whether to pad with zeroes
 474  
      * @param count the size to pad to (ignored if {@code padWithZeros} is false)
 475  
      * @return the string result
 476  
      */
 477  
     private static String paddedValue(final long value, final boolean padWithZeros, final int count) {
 478  268486
         final String longString = Long.toString(value);
 479  268486
         return padWithZeros ? StringUtils.leftPad(longString, count, '0') : longString;
 480  
     }
 481  
 
 482  1
     static final Object y = "y";
 483  1
     static final Object M = "M";
 484  1
     static final Object d = "d";
 485  1
     static final Object H = "H";
 486  1
     static final Object m = "m";
 487  1
     static final Object s = "s";
 488  1
     static final Object S = "S";
 489  
     
 490  
     /**
 491  
      * Parses a classic date format string into Tokens
 492  
      *
 493  
      * @param format  the format to parse, not null
 494  
      * @return array of Token[]
 495  
      */
 496  
     static Token[] lexx(final String format) {
 497  95355
         final ArrayList<Token> list = new ArrayList<Token>(format.length());
 498  
 
 499  95355
         boolean inLiteral = false;
 500  
         // Although the buffer is stored in a Token, the Tokens are only
 501  
         // used internally, so cannot be accessed by other threads
 502  95355
         StringBuilder buffer = null;
 503  95355
         Token previous = null;
 504  539866
         for (int i = 0; i < format.length(); i++) {
 505  444511
             final char ch = format.charAt(i);
 506  444511
             if (inLiteral && ch != '\'') {
 507  2207
                 buffer.append(ch); // buffer can't be null if inLiteral is true
 508  2207
                 continue;
 509  
             }
 510  442304
             Object value = null;
 511  442304
             switch (ch) {
 512  
             // TODO: Need to handle escaping of '
 513  
             case '\'':
 514  721
                 if (inLiteral) {
 515  360
                     buffer = null;
 516  360
                     inLiteral = false;
 517  
                 } else {
 518  361
                     buffer = new StringBuilder();
 519  361
                     list.add(new Token(buffer));
 520  361
                     inLiteral = true;
 521  
                 }
 522  361
                 break;
 523  
             case 'y':
 524  78
                 value = y;
 525  78
                 break;
 526  
             case 'M':
 527  1524
                 value = M;
 528  1524
                 break;
 529  
             case 'd':
 530  7443
                 value = d;
 531  7443
                 break;
 532  
             case 'H':
 533  86511
                 value = H;
 534  86511
                 break;
 535  
             case 'm':
 536  86519
                 value = m;
 537  86519
                 break;
 538  
             case 's':
 539  86519
                 value = s;
 540  86519
                 break;
 541  
             case 'S':
 542  115
                 value = S;
 543  115
                 break;
 544  
             default:
 545  172874
                 if (buffer == null) {
 546  172874
                     buffer = new StringBuilder();
 547  172874
                     list.add(new Token(buffer));
 548  
                 }
 549  172874
                 buffer.append(ch);
 550  
             }
 551  
 
 552  442304
             if (value != null) {
 553  268709
                 if (previous != null && previous.getValue().equals(value)) {
 554  205
                     previous.increment();
 555  
                 } else {
 556  268504
                     final Token token = new Token(value);
 557  268504
                     list.add(token);
 558  268504
                     previous = token;
 559  
                 }
 560  268709
                 buffer = null;
 561  
             }
 562  
         }
 563  95355
         if (inLiteral) { // i.e. we have not found the end of the literal
 564  1
             throw new IllegalArgumentException("Unmatched quote in format: " + format);
 565  
         }
 566  95354
         return list.toArray(new Token[list.size()]);
 567  
     }
 568  
 
 569  
     //-----------------------------------------------------------------------
 570  
     /**
 571  
      * Element that is parsed from the format pattern.
 572  
      */
 573  
     static class Token {
 574  
 
 575  
         /**
 576  
          * Helper method to determine if a set of tokens contain a value
 577  
          *
 578  
          * @param tokens set to look in
 579  
          * @param value to look for
 580  
          * @return boolean <code>true</code> if contained
 581  
          */
 582  
         static boolean containsTokenWithValue(final Token[] tokens, final Object value) {
 583  2432087
             for (final Token token : tokens) {
 584  2128640
                 if (token.getValue() == value) {
 585  268427
                     return true;
 586  
                 }
 587  
             }
 588  303447
             return false;
 589  
         }
 590  
 
 591  
         private final Object value;
 592  
         private int count;
 593  
 
 594  
         /**
 595  
          * Wraps a token around a value. A value would be something like a 'Y'.
 596  
          *
 597  
          * @param value to wrap
 598  
          */
 599  441740
         Token(final Object value) {
 600  441740
             this.value = value;
 601  441740
             this.count = 1;
 602  441740
         }
 603  
 
 604  
         /**
 605  
          * Wraps a token around a repeated number of a value, for example it would 
 606  
          * store 'yyyy' as a value for y and a count of 4.
 607  
          *
 608  
          * @param value to wrap
 609  
          * @param count to wrap
 610  
          */
 611  32
         Token(final Object value, final int count) {
 612  32
             this.value = value;
 613  32
             this.count = count;
 614  32
         }
 615  
 
 616  
         /**
 617  
          * Adds another one of the value
 618  
          */
 619  
         void increment() { 
 620  205
             count++;
 621  205
         }
 622  
 
 623  
         /**
 624  
          * Gets the current number of values represented
 625  
          *
 626  
          * @return int number of values represented
 627  
          */
 628  
         int getCount() {
 629  441708
             return count;
 630  
         }
 631  
 
 632  
         /**
 633  
          * Gets the particular value this token represents.
 634  
          * 
 635  
          * @return Object value
 636  
          */
 637  
         Object getValue() {
 638  2743703
             return value;
 639  
         }
 640  
 
 641  
         /**
 642  
          * Supports equality of this Token to another Token.
 643  
          *
 644  
          * @param obj2 Object to consider equality of
 645  
          * @return boolean <code>true</code> if equal
 646  
          */
 647  
         @Override
 648  
         public boolean equals(final Object obj2) {
 649  33
             if (obj2 instanceof Token) {
 650  32
                 final Token tok2 = (Token) obj2;
 651  32
                 if (this.value.getClass() != tok2.value.getClass()) {
 652  1
                     return false;
 653  
                 }
 654  31
                 if (this.count != tok2.count) {
 655  1
                     return false;
 656  
                 }
 657  30
                 if (this.value instanceof StringBuilder) {
 658  11
                     return this.value.toString().equals(tok2.value.toString());
 659  19
                 } else if (this.value instanceof Number) {
 660  1
                     return this.value.equals(tok2.value);
 661  
                 } else {
 662  18
                     return this.value == tok2.value;
 663  
                 }
 664  
             }
 665  1
             return false;
 666  
         }
 667  
 
 668  
         /**
 669  
          * Returns a hash code for the token equal to the 
 670  
          * hash code for the token's value. Thus 'TT' and 'TTTT' 
 671  
          * will have the same hash code. 
 672  
          *
 673  
          * @return The hash code for the token
 674  
          */
 675  
         @Override
 676  
         public int hashCode() {
 677  0
             return this.value.hashCode();
 678  
         }
 679  
 
 680  
         /**
 681  
          * Represents this token as a String.
 682  
          *
 683  
          * @return String representation of the token
 684  
          */
 685  
         @Override
 686  
         public String toString() {
 687  0
             return StringUtils.repeat(this.value.toString(), this.count);
 688  
         }
 689  
     }
 690  
 
 691  
 }