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