• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.text.SimpleDateFormat;
20 import java.util.ArrayList;
21 import java.util.Calendar;
22 import java.util.Date;
23 import java.util.GregorianCalendar;
24 import java.util.TimeZone;
25 import java.util.stream.Stream;
26 
27 import org.apache.commons.lang3.StringUtils;
28 import org.apache.commons.lang3.Validate;
29 
30 /**
31  * Duration formatting utilities and constants. The following table describes the tokens
32  * used in the pattern language for formatting.
33  * <table border="1">
34  *  <caption>Pattern Tokens</caption>
35  *  <tr><th>character</th><th>duration element</th></tr>
36  *  <tr><td>y</td><td>years</td></tr>
37  *  <tr><td>M</td><td>months</td></tr>
38  *  <tr><td>d</td><td>days</td></tr>
39  *  <tr><td>H</td><td>hours</td></tr>
40  *  <tr><td>m</td><td>minutes</td></tr>
41  *  <tr><td>s</td><td>seconds</td></tr>
42  *  <tr><td>S</td><td>milliseconds</td></tr>
43  *  <tr><td>'text'</td><td>arbitrary text content</td></tr>
44  * </table>
45  *
46  * <b>Note: It's not currently possible to include a single-quote in a format.</b>
47  * <br>
48  * Token values are printed using decimal digits.
49  * A token character can be repeated to ensure that the field occupies a certain minimum
50  * size. Values will be left-padded with 0 unless padding is disabled in the method invocation.
51  * @since 2.1
52  */
53 public class DurationFormatUtils {
54 
55     /**
56      * DurationFormatUtils instances should NOT be constructed in standard programming.
57      *
58      * <p>This constructor is public to permit tools that require a JavaBean instance
59      * to operate.</p>
60      */
DurationFormatUtils()61     public DurationFormatUtils() {
62     }
63 
64     /**
65      * Pattern used with {@link FastDateFormat} and {@link SimpleDateFormat}
66      * for the ISO 8601 period format used in durations.
67      *
68      * @see org.apache.commons.lang3.time.FastDateFormat
69      * @see java.text.SimpleDateFormat
70      */
71     public static final String ISO_EXTENDED_FORMAT_PATTERN = "'P'yyyy'Y'M'M'd'DT'H'H'm'M's.SSS'S'";
72 
73     /**
74      * Formats the time gap as a string.
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 IllegalArgumentException if durationMillis is negative
81      */
formatDurationHMS(final long durationMillis)82     public static String formatDurationHMS(final long durationMillis) {
83         return formatDuration(durationMillis, "HH:mm:ss.SSS");
84     }
85 
86     /**
87      * Formats the time gap as a string.
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 IllegalArgumentException if durationMillis is negative
97      */
formatDurationISO(final long durationMillis)98     public static String formatDurationISO(final long durationMillis) {
99         return formatDuration(durationMillis, ISO_EXTENDED_FORMAT_PATTERN, false);
100     }
101 
102     /**
103      * Formats the time gap as a string, using the specified format, and padding with zeros.
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 IllegalArgumentException if durationMillis is negative
112      */
formatDuration(final long durationMillis, final String format)113     public static String formatDuration(final long durationMillis, final String format) {
114         return formatDuration(durationMillis, format, true);
115     }
116 
117     /**
118      * Formats the time gap as a string, using the specified format.
119      * Padding the left-hand side of numbers with zeroes is optional.
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 IllegalArgumentException if durationMillis is negative
129      */
formatDuration(final long durationMillis, final String format, final boolean padWithZeros)130     public static String formatDuration(final long durationMillis, final String format, final boolean padWithZeros) {
131         Validate.inclusiveBetween(0, Long.MAX_VALUE, durationMillis, "durationMillis must not be negative");
132 
133         final Token[] tokens = lexx(format);
134 
135         long days = 0;
136         long hours = 0;
137         long minutes = 0;
138         long seconds = 0;
139         long milliseconds = durationMillis;
140 
141         if (Token.containsTokenWithValue(tokens, d)) {
142             days = milliseconds / DateUtils.MILLIS_PER_DAY;
143             milliseconds = milliseconds - (days * DateUtils.MILLIS_PER_DAY);
144         }
145         if (Token.containsTokenWithValue(tokens, H)) {
146             hours = milliseconds / DateUtils.MILLIS_PER_HOUR;
147             milliseconds = milliseconds - (hours * DateUtils.MILLIS_PER_HOUR);
148         }
149         if (Token.containsTokenWithValue(tokens, m)) {
150             minutes = milliseconds / DateUtils.MILLIS_PER_MINUTE;
151             milliseconds = milliseconds - (minutes * DateUtils.MILLIS_PER_MINUTE);
152         }
153         if (Token.containsTokenWithValue(tokens, s)) {
154             seconds = milliseconds / DateUtils.MILLIS_PER_SECOND;
155             milliseconds = milliseconds - (seconds * DateUtils.MILLIS_PER_SECOND);
156         }
157 
158         return format(tokens, 0, 0, days, hours, minutes, seconds, milliseconds, padWithZeros);
159     }
160 
161     /**
162      * Formats an elapsed time into a pluralization correct string.
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 IllegalArgumentException if durationMillis is negative
172      */
formatDurationWords( final long durationMillis, final boolean suppressLeadingZeroElements, final boolean suppressTrailingZeroElements)173     public static String formatDurationWords(
174         final long durationMillis,
175         final boolean suppressLeadingZeroElements,
176         final boolean suppressTrailingZeroElements) {
177 
178         // This method is generally replaceable by the format method, but
179         // there are a series of tweaks and special cases that require
180         // trickery to replicate.
181         String duration = formatDuration(durationMillis, "d' days 'H' hours 'm' minutes 's' seconds'");
182         if (suppressLeadingZeroElements) {
183             // this is a temporary marker on the front. Like ^ in regexp.
184             duration = " " + duration;
185             String tmp = StringUtils.replaceOnce(duration, " 0 days", StringUtils.EMPTY);
186             if (tmp.length() != duration.length()) {
187                 duration = tmp;
188                 tmp = StringUtils.replaceOnce(duration, " 0 hours", StringUtils.EMPTY);
189                 if (tmp.length() != duration.length()) {
190                     duration = tmp;
191                     tmp = StringUtils.replaceOnce(duration, " 0 minutes", StringUtils.EMPTY);
192                     duration = tmp;
193                     if (tmp.length() != duration.length()) {
194                         duration = StringUtils.replaceOnce(tmp, " 0 seconds", StringUtils.EMPTY);
195                     }
196                 }
197             }
198             if (!duration.isEmpty()) {
199                 // strip the space off again
200                 duration = duration.substring(1);
201             }
202         }
203         if (suppressTrailingZeroElements) {
204             String tmp = StringUtils.replaceOnce(duration, " 0 seconds", StringUtils.EMPTY);
205             if (tmp.length() != duration.length()) {
206                 duration = tmp;
207                 tmp = StringUtils.replaceOnce(duration, " 0 minutes", StringUtils.EMPTY);
208                 if (tmp.length() != duration.length()) {
209                     duration = tmp;
210                     tmp = StringUtils.replaceOnce(duration, " 0 hours", StringUtils.EMPTY);
211                     if (tmp.length() != duration.length()) {
212                         duration = StringUtils.replaceOnce(tmp, " 0 days", StringUtils.EMPTY);
213                     }
214                 }
215             }
216         }
217         // handle plurals
218         duration = " " + duration;
219         duration = StringUtils.replaceOnce(duration, " 1 seconds", " 1 second");
220         duration = StringUtils.replaceOnce(duration, " 1 minutes", " 1 minute");
221         duration = StringUtils.replaceOnce(duration, " 1 hours", " 1 hour");
222         duration = StringUtils.replaceOnce(duration, " 1 days", " 1 day");
223         return duration.trim();
224     }
225 
226     /**
227      * Formats the time gap as a string.
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 IllegalArgumentException if startMillis is greater than endMillis
235      */
formatPeriodISO(final long startMillis, final long endMillis)236     public static String formatPeriodISO(final long startMillis, final long endMillis) {
237         return formatPeriod(startMillis, endMillis, ISO_EXTENDED_FORMAT_PATTERN, false, TimeZone.getDefault());
238     }
239 
240     /**
241      * 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 IllegalArgumentException if startMillis is greater than endMillis
249      */
formatPeriod(final long startMillis, final long endMillis, final String format)250     public static String formatPeriod(final long startMillis, final long endMillis, final String format) {
251         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 time zone may be specified.
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="https://www.joda.org/joda-time/">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 IllegalArgumentException if startMillis is greater than endMillis
277      */
formatPeriod(final long startMillis, final long endMillis, final String format, final boolean padWithZeros, final TimeZone timezone)278     public static String formatPeriod(final long startMillis, final long endMillis, final String format, final boolean padWithZeros,
279             final TimeZone timezone) {
280         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         final Token[] tokens = lexx(format);
290 
291         // time zones get funky around 0, so normalizing everything to GMT
292         // stops the hours being off
293         final Calendar start = Calendar.getInstance(timezone);
294         start.setTime(new Date(startMillis));
295         final Calendar end = Calendar.getInstance(timezone);
296         end.setTime(new Date(endMillis));
297 
298         // initial estimates
299         int milliseconds = end.get(Calendar.MILLISECOND) - start.get(Calendar.MILLISECOND);
300         int seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND);
301         int minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE);
302         int hours = end.get(Calendar.HOUR_OF_DAY) - start.get(Calendar.HOUR_OF_DAY);
303         int days = end.get(Calendar.DAY_OF_MONTH) - start.get(Calendar.DAY_OF_MONTH);
304         int months = end.get(Calendar.MONTH) - start.get(Calendar.MONTH);
305         int years = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
306 
307         // each initial estimate is adjusted in case it is under 0
308         while (milliseconds < 0) {
309             milliseconds += 1000;
310             seconds -= 1;
311         }
312         while (seconds < 0) {
313             seconds += 60;
314             minutes -= 1;
315         }
316         while (minutes < 0) {
317             minutes += 60;
318             hours -= 1;
319         }
320         while (hours < 0) {
321             hours += 24;
322             days -= 1;
323         }
324 
325         if (Token.containsTokenWithValue(tokens, M)) {
326             while (days < 0) {
327                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
328                 months -= 1;
329                 start.add(Calendar.MONTH, 1);
330             }
331 
332             while (months < 0) {
333                 months += 12;
334                 years -= 1;
335             }
336 
337             if (!Token.containsTokenWithValue(tokens, y) && years != 0) {
338                 while (years != 0) {
339                     months += 12 * years;
340                     years = 0;
341                 }
342             }
343         } else {
344             // there are no M's in the format string
345 
346             if (!Token.containsTokenWithValue(tokens, y)) {
347                 int target = end.get(Calendar.YEAR);
348                 if (months < 0) {
349                     // target is end-year -1
350                     target -= 1;
351                 }
352 
353                 while (start.get(Calendar.YEAR) != target) {
354                     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                     if (start instanceof GregorianCalendar &&
358                             start.get(Calendar.MONTH) == Calendar.FEBRUARY &&
359                             start.get(Calendar.DAY_OF_MONTH) == 29) {
360                         days += 1;
361                     }
362 
363                     start.add(Calendar.YEAR, 1);
364 
365                     days += start.get(Calendar.DAY_OF_YEAR);
366                 }
367 
368                 years = 0;
369             }
370 
371             while (start.get(Calendar.MONTH) != end.get(Calendar.MONTH)) {
372                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
373                 start.add(Calendar.MONTH, 1);
374             }
375 
376             months = 0;
377 
378             while (days < 0) {
379                 days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
380                 months -= 1;
381                 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         if (!Token.containsTokenWithValue(tokens, d)) {
391             hours += 24 * days;
392             days = 0;
393         }
394         if (!Token.containsTokenWithValue(tokens, H)) {
395             minutes += 60 * hours;
396             hours = 0;
397         }
398         if (!Token.containsTokenWithValue(tokens, m)) {
399             seconds += 60 * minutes;
400             minutes = 0;
401         }
402         if (!Token.containsTokenWithValue(tokens, s)) {
403             milliseconds += 1000 * seconds;
404             seconds = 0;
405         }
406 
407         return format(tokens, years, months, days, hours, minutes, seconds, milliseconds, padWithZeros);
408     }
409 
410     /**
411      * The internal method to do the formatting.
412      *
413      * @param tokens  the tokens
414      * @param years  the number of years
415      * @param months  the number of months
416      * @param days  the number of days
417      * @param hours  the number of hours
418      * @param minutes  the number of minutes
419      * @param seconds  the number of seconds
420      * @param milliseconds  the number of millis
421      * @param padWithZeros  whether to pad
422      * @return the formatted string
423      */
format(final Token[] tokens, final long years, final long months, final long days, final long hours, final long minutes, final long seconds, final long milliseconds, final boolean padWithZeros)424     static String format(final Token[] tokens, final long years, final long months, final long days, final long hours, final long minutes, final long seconds,
425             final long milliseconds, final boolean padWithZeros) {
426         final StringBuilder buffer = new StringBuilder();
427         boolean lastOutputSeconds = false;
428         for (final Token token : tokens) {
429             final Object value = token.getValue();
430             final int count = token.getCount();
431             if (value instanceof StringBuilder) {
432                 buffer.append(value.toString());
433             } else if (value.equals(y)) {
434                 buffer.append(paddedValue(years, padWithZeros, count));
435                 lastOutputSeconds = false;
436             } else if (value.equals(M)) {
437                 buffer.append(paddedValue(months, padWithZeros, count));
438                 lastOutputSeconds = false;
439             } else if (value.equals(d)) {
440                 buffer.append(paddedValue(days, padWithZeros, count));
441                 lastOutputSeconds = false;
442             } else if (value.equals(H)) {
443                 buffer.append(paddedValue(hours, padWithZeros, count));
444                 lastOutputSeconds = false;
445             } else if (value.equals(m)) {
446                 buffer.append(paddedValue(minutes, padWithZeros, count));
447                 lastOutputSeconds = false;
448             } else if (value.equals(s)) {
449                 buffer.append(paddedValue(seconds, padWithZeros, count));
450                 lastOutputSeconds = true;
451             } else if (value.equals(S)) {
452                 if (lastOutputSeconds) {
453                     // ensure at least 3 digits are displayed even if padding is not selected
454                     final int width = padWithZeros ? Math.max(3, count) : 3;
455                     buffer.append(paddedValue(milliseconds, true, width));
456                 } else {
457                     buffer.append(paddedValue(milliseconds, padWithZeros, count));
458                 }
459                 lastOutputSeconds = false;
460             }
461         }
462         return buffer.toString();
463     }
464 
465     /**
466      * Converts a {@code long} to a {@link String} with optional
467      * zero padding.
468      *
469      * @param value the value to convert
470      * @param padWithZeros whether to pad with zeroes
471      * @param count the size to pad to (ignored if {@code padWithZeros} is false)
472      * @return the string result
473      */
paddedValue(final long value, final boolean padWithZeros, final int count)474     private static String paddedValue(final long value, final boolean padWithZeros, final int count) {
475         final String longString = Long.toString(value);
476         return padWithZeros ? StringUtils.leftPad(longString, count, '0') : longString;
477     }
478 
479     static final String y = "y";
480     static final String M = "M";
481     static final String d = "d";
482     static final String H = "H";
483     static final String m = "m";
484     static final String s = "s";
485     static final String S = "S";
486 
487     /**
488      * Parses a classic date format string into Tokens
489      *
490      * @param format  the format to parse, not null
491      * @return array of Token[]
492      */
lexx(final String format)493     static Token[] lexx(final String format) {
494         final ArrayList<Token> list = new ArrayList<>(format.length());
495 
496         boolean inLiteral = false;
497         // Although the buffer is stored in a Token, the Tokens are only
498         // used internally, so cannot be accessed by other threads
499         StringBuilder buffer = null;
500         Token previous = null;
501         for (int i = 0; i < format.length(); i++) {
502             final char ch = format.charAt(i);
503             if (inLiteral && ch != '\'') {
504                 buffer.append(ch); // buffer can't be null if inLiteral is true
505                 continue;
506             }
507             String value = null;
508             switch (ch) {
509             // TODO: Need to handle escaping of '
510             case '\'':
511                 if (inLiteral) {
512                     buffer = null;
513                     inLiteral = false;
514                 } else {
515                     buffer = new StringBuilder();
516                     list.add(new Token(buffer));
517                     inLiteral = true;
518                 }
519                 break;
520             case 'y':
521                 value = y;
522                 break;
523             case 'M':
524                 value = M;
525                 break;
526             case 'd':
527                 value = d;
528                 break;
529             case 'H':
530                 value = H;
531                 break;
532             case 'm':
533                 value = m;
534                 break;
535             case 's':
536                 value = s;
537                 break;
538             case 'S':
539                 value = S;
540                 break;
541             default:
542                 if (buffer == null) {
543                     buffer = new StringBuilder();
544                     list.add(new Token(buffer));
545                 }
546                 buffer.append(ch);
547             }
548 
549             if (value != null) {
550                 if (previous != null && previous.getValue().equals(value)) {
551                     previous.increment();
552                 } else {
553                     final Token token = new Token(value);
554                     list.add(token);
555                     previous = token;
556                 }
557                 buffer = null;
558             }
559         }
560         if (inLiteral) { // i.e. we have not found the end of the literal
561             throw new IllegalArgumentException("Unmatched quote in format: " + format);
562         }
563         return list.toArray(Token.EMPTY_ARRAY);
564     }
565 
566     /**
567      * Element that is parsed from the format pattern.
568      */
569     static class Token {
570 
571         /** Empty array. */
572         private static final Token[] EMPTY_ARRAY = {};
573 
574         /**
575          * Helper method to determine if a set of tokens contain a value
576          *
577          * @param tokens set to look in
578          * @param value to look for
579          * @return boolean {@code true} if contained
580          */
containsTokenWithValue(final Token[] tokens, final Object value)581         static boolean containsTokenWithValue(final Token[] tokens, final Object value) {
582             return Stream.of(tokens).anyMatch(token -> token.getValue() == value);
583         }
584 
585         private final Object value;
586         private int count;
587 
588         /**
589          * Wraps a token around a value. A value would be something like a 'Y'.
590          *
591          * @param value to wrap
592          */
Token(final Object value)593         Token(final Object value) {
594             this.value = value;
595             this.count = 1;
596         }
597 
598         /**
599          * Wraps a token around a repeated number of a value, for example it would
600          * store 'yyyy' as a value for y and a count of 4.
601          *
602          * @param value to wrap
603          * @param count to wrap
604          */
Token(final Object value, final int count)605         Token(final Object value, final int count) {
606             this.value = value;
607             this.count = count;
608         }
609 
610         /**
611          * Adds another one of the value
612          */
increment()613         void increment() {
614             count++;
615         }
616 
617         /**
618          * Gets the current number of values represented
619          *
620          * @return int number of values represented
621          */
getCount()622         int getCount() {
623             return count;
624         }
625 
626         /**
627          * Gets the particular value this token represents.
628          *
629          * @return Object value
630          */
getValue()631         Object getValue() {
632             return value;
633         }
634 
635         /**
636          * Supports equality of this Token to another Token.
637          *
638          * @param obj2 Object to consider equality of
639          * @return boolean {@code true} if equal
640          */
641         @Override
equals(final Object obj2)642         public boolean equals(final Object obj2) {
643             if (obj2 instanceof Token) {
644                 final Token tok2 = (Token) obj2;
645                 if (this.value.getClass() != tok2.value.getClass()) {
646                     return false;
647                 }
648                 if (this.count != tok2.count) {
649                     return false;
650                 }
651                 if (this.value instanceof StringBuilder) {
652                     return this.value.toString().equals(tok2.value.toString());
653                 }
654                 if (this.value instanceof Number) {
655                     return this.value.equals(tok2.value);
656                 }
657                 return this.value == tok2.value;
658             }
659             return false;
660         }
661 
662         /**
663          * Returns a hash code for the token equal to the
664          * hash code for the token's value. Thus 'TT' and 'TTTT'
665          * will have the same hash code.
666          *
667          * @return The hash code for the token
668          */
669         @Override
hashCode()670         public int hashCode() {
671             return this.value.hashCode();
672         }
673 
674         /**
675          * Represents this token as a String.
676          *
677          * @return String representation of the token
678          */
679         @Override
toString()680         public String toString() {
681             return StringUtils.repeat(this.value.toString(), this.count);
682         }
683     }
684 
685 }
686