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