001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.lang3.time; 018 019import java.util.ArrayList; 020import java.util.Calendar; 021import java.util.Date; 022import java.util.GregorianCalendar; 023import java.util.TimeZone; 024 025import org.apache.commons.lang3.StringUtils; 026 027/** 028 * <p>Duration formatting utilities and constants. The following table describes the tokens 029 * used in the pattern language for formatting. </p> 030 * <table border="1"> 031 * <tr><th>character</th><th>duration element</th></tr> 032 * <tr><td>y</td><td>years</td></tr> 033 * <tr><td>M</td><td>months</td></tr> 034 * <tr><td>d</td><td>days</td></tr> 035 * <tr><td>H</td><td>hours</td></tr> 036 * <tr><td>m</td><td>minutes</td></tr> 037 * <tr><td>s</td><td>seconds</td></tr> 038 * <tr><td>S</td><td>milliseconds</td></tr> 039 * </table> 040 * 041 * @since 2.1 042 * @version $Id: DurationFormatUtils.java 1478487 2013-05-02 19:04:35Z ggregory $ 043 */ 044public class DurationFormatUtils { 045 046 /** 047 * <p>DurationFormatUtils instances should NOT be constructed in standard programming.</p> 048 * 049 * <p>This constructor is public to permit tools that require a JavaBean instance 050 * to operate.</p> 051 */ 052 public DurationFormatUtils() { 053 super(); 054 } 055 056 /** 057 * <p>Pattern used with <code>FastDateFormat</code> and <code>SimpleDateFormat</code> 058 * for the ISO8601 period format used in durations.</p> 059 * 060 * @see org.apache.commons.lang3.time.FastDateFormat 061 * @see java.text.SimpleDateFormat 062 */ 063 public static final String ISO_EXTENDED_FORMAT_PATTERN = "'P'yyyy'Y'M'M'd'DT'H'H'm'M's.S'S'"; 064 065 //----------------------------------------------------------------------- 066 /** 067 * <p>Formats the time gap as a string.</p> 068 * 069 * <p>The format used is ISO8601-like: 070 * <i>H</i>:<i>m</i>:<i>s</i>.<i>S</i>.</p> 071 * 072 * @param durationMillis the duration to format 073 * @return the formatted duration, not null 074 */ 075 public static String formatDurationHMS(final long durationMillis) { 076 return formatDuration(durationMillis, "H:mm:ss.SSS"); 077 } 078 079 /** 080 * <p>Formats the time gap as a string.</p> 081 * 082 * <p>The format used is the ISO8601 period format.</p> 083 * 084 * <p>This method formats durations using the days and lower fields of the 085 * ISO format pattern, such as P7D6TH5M4.321S.</p> 086 * 087 * @param durationMillis the duration to format 088 * @return the formatted duration, not null 089 */ 090 public static String formatDurationISO(final long durationMillis) { 091 return formatDuration(durationMillis, ISO_EXTENDED_FORMAT_PATTERN, false); 092 } 093 094 /** 095 * <p>Formats the time gap as a string, using the specified format, and padding with zeros and 096 * using the default timezone.</p> 097 * 098 * <p>This method formats durations using the days and lower fields of the 099 * format pattern. Months and larger are not used.</p> 100 * 101 * @param durationMillis the duration to format 102 * @param format the way in which to format the duration, not null 103 * @return the formatted duration, not null 104 */ 105 public static String formatDuration(final long durationMillis, final String format) { 106 return formatDuration(durationMillis, format, true); 107 } 108 109 /** 110 * <p>Formats the time gap as a string, using the specified format. 111 * Padding the left hand side of numbers with zeroes is optional and 112 * the timezone may be specified.</p> 113 * 114 * <p>This method formats durations using the days and lower fields of the 115 * format pattern. Months and larger are not used.</p> 116 * 117 * @param durationMillis the duration to format 118 * @param format the way in which to format the duration, not null 119 * @param padWithZeros whether to pad the left hand side of numbers with 0's 120 * @return the formatted duration, not null 121 */ 122 public static String formatDuration(long durationMillis, final String format, final boolean padWithZeros) { 123 124 final Token[] tokens = lexx(format); 125 126 int days = 0; 127 int hours = 0; 128 int minutes = 0; 129 int seconds = 0; 130 int milliseconds = 0; 131 132 if (Token.containsTokenWithValue(tokens, d) ) { 133 days = (int) (durationMillis / DateUtils.MILLIS_PER_DAY); 134 durationMillis = durationMillis - (days * DateUtils.MILLIS_PER_DAY); 135 } 136 if (Token.containsTokenWithValue(tokens, H) ) { 137 hours = (int) (durationMillis / DateUtils.MILLIS_PER_HOUR); 138 durationMillis = durationMillis - (hours * DateUtils.MILLIS_PER_HOUR); 139 } 140 if (Token.containsTokenWithValue(tokens, m) ) { 141 minutes = (int) (durationMillis / DateUtils.MILLIS_PER_MINUTE); 142 durationMillis = durationMillis - (minutes * DateUtils.MILLIS_PER_MINUTE); 143 } 144 if (Token.containsTokenWithValue(tokens, s) ) { 145 seconds = (int) (durationMillis / DateUtils.MILLIS_PER_SECOND); 146 durationMillis = durationMillis - (seconds * DateUtils.MILLIS_PER_SECOND); 147 } 148 if (Token.containsTokenWithValue(tokens, S) ) { 149 milliseconds = (int) durationMillis; 150 } 151 152 return format(tokens, 0, 0, days, hours, minutes, seconds, milliseconds, padWithZeros); 153 } 154 155 /** 156 * <p>Formats an elapsed time into a plurialization correct string.</p> 157 * 158 * <p>This method formats durations using the days and lower fields of the 159 * format pattern. Months and larger are not used.</p> 160 * 161 * @param durationMillis the elapsed time to report in milliseconds 162 * @param suppressLeadingZeroElements suppresses leading 0 elements 163 * @param suppressTrailingZeroElements suppresses trailing 0 elements 164 * @return the formatted text in days/hours/minutes/seconds, not null 165 */ 166 public static String formatDurationWords( 167 final long durationMillis, 168 final boolean suppressLeadingZeroElements, 169 final boolean suppressTrailingZeroElements) { 170 171 // This method is generally replacable by the format method, but 172 // there are a series of tweaks and special cases that require 173 // trickery to replicate. 174 String duration = formatDuration(durationMillis, "d' days 'H' hours 'm' minutes 's' seconds'"); 175 if (suppressLeadingZeroElements) { 176 // this is a temporary marker on the front. Like ^ in regexp. 177 duration = " " + duration; 178 String tmp = StringUtils.replaceOnce(duration, " 0 days", ""); 179 if (tmp.length() != duration.length()) { 180 duration = tmp; 181 tmp = StringUtils.replaceOnce(duration, " 0 hours", ""); 182 if (tmp.length() != duration.length()) { 183 duration = tmp; 184 tmp = StringUtils.replaceOnce(duration, " 0 minutes", ""); 185 duration = tmp; 186 if (tmp.length() != duration.length()) { 187 duration = StringUtils.replaceOnce(tmp, " 0 seconds", ""); 188 } 189 } 190 } 191 if (duration.length() != 0) { 192 // strip the space off again 193 duration = duration.substring(1); 194 } 195 } 196 if (suppressTrailingZeroElements) { 197 String tmp = StringUtils.replaceOnce(duration, " 0 seconds", ""); 198 if (tmp.length() != duration.length()) { 199 duration = tmp; 200 tmp = StringUtils.replaceOnce(duration, " 0 minutes", ""); 201 if (tmp.length() != duration.length()) { 202 duration = tmp; 203 tmp = StringUtils.replaceOnce(duration, " 0 hours", ""); 204 if (tmp.length() != duration.length()) { 205 duration = StringUtils.replaceOnce(tmp, " 0 days", ""); 206 } 207 } 208 } 209 } 210 // handle plurals 211 duration = " " + duration; 212 duration = StringUtils.replaceOnce(duration, " 1 seconds", " 1 second"); 213 duration = StringUtils.replaceOnce(duration, " 1 minutes", " 1 minute"); 214 duration = StringUtils.replaceOnce(duration, " 1 hours", " 1 hour"); 215 duration = StringUtils.replaceOnce(duration, " 1 days", " 1 day"); 216 return duration.trim(); 217 } 218 219 //----------------------------------------------------------------------- 220 /** 221 * <p>Formats the time gap as a string.</p> 222 * 223 * <p>The format used is the ISO8601 period format.</p> 224 * 225 * @param startMillis the start of the duration to format 226 * @param endMillis the end of the duration to format 227 * @return the formatted duration, not null 228 */ 229 public static String formatPeriodISO(final long startMillis, final long endMillis) { 230 return formatPeriod(startMillis, endMillis, ISO_EXTENDED_FORMAT_PATTERN, false, TimeZone.getDefault()); 231 } 232 233 /** 234 * <p>Formats the time gap as a string, using the specified format. 235 * Padding the left hand side of numbers with zeroes is optional. 236 * 237 * @param startMillis the start of the duration 238 * @param endMillis the end of the duration 239 * @param format the way in which to format the duration, not null 240 * @return the formatted duration, not null 241 */ 242 public static String formatPeriod(final long startMillis, final long endMillis, final String format) { 243 return formatPeriod(startMillis, endMillis, format, true, TimeZone.getDefault()); 244 } 245 246 /** 247 * <p>Formats the time gap as a string, using the specified format. 248 * Padding the left hand side of numbers with zeroes is optional and 249 * the timezone may be specified. </p> 250 * 251 * <p>When calculating the difference between months/days, it chooses to 252 * calculate months first. So when working out the number of months and 253 * days between January 15th and March 10th, it choose 1 month and 254 * 23 days gained by choosing January->February = 1 month and then 255 * calculating days forwards, and not the 1 month and 26 days gained by 256 * choosing March -> February = 1 month and then calculating days 257 * backwards. </p> 258 * 259 * <p>For more control, the <a href="http://joda-time.sf.net/">Joda-Time</a> 260 * library is recommended.</p> 261 * 262 * @param startMillis the start of the duration 263 * @param endMillis the end of the duration 264 * @param format the way in which to format the duration, not null 265 * @param padWithZeros whether to pad the left hand side of numbers with 0's 266 * @param timezone the millis are defined in 267 * @return the formatted duration, not null 268 */ 269 public static String formatPeriod(final long startMillis, final long endMillis, final String format, final boolean padWithZeros, 270 final TimeZone timezone) { 271 272 // Used to optimise for differences under 28 days and 273 // called formatDuration(millis, format); however this did not work 274 // over leap years. 275 // TODO: Compare performance to see if anything was lost by 276 // losing this optimisation. 277 278 final Token[] tokens = lexx(format); 279 280 // timezones get funky around 0, so normalizing everything to GMT 281 // stops the hours being off 282 final Calendar start = Calendar.getInstance(timezone); 283 start.setTime(new Date(startMillis)); 284 final Calendar end = Calendar.getInstance(timezone); 285 end.setTime(new Date(endMillis)); 286 287 // initial estimates 288 int milliseconds = end.get(Calendar.MILLISECOND) - start.get(Calendar.MILLISECOND); 289 int seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND); 290 int minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE); 291 int hours = end.get(Calendar.HOUR_OF_DAY) - start.get(Calendar.HOUR_OF_DAY); 292 int days = end.get(Calendar.DAY_OF_MONTH) - start.get(Calendar.DAY_OF_MONTH); 293 int months = end.get(Calendar.MONTH) - start.get(Calendar.MONTH); 294 int years = end.get(Calendar.YEAR) - start.get(Calendar.YEAR); 295 296 // each initial estimate is adjusted in case it is under 0 297 while (milliseconds < 0) { 298 milliseconds += 1000; 299 seconds -= 1; 300 } 301 while (seconds < 0) { 302 seconds += 60; 303 minutes -= 1; 304 } 305 while (minutes < 0) { 306 minutes += 60; 307 hours -= 1; 308 } 309 while (hours < 0) { 310 hours += 24; 311 days -= 1; 312 } 313 314 if (Token.containsTokenWithValue(tokens, M)) { 315 while (days < 0) { 316 days += start.getActualMaximum(Calendar.DAY_OF_MONTH); 317 months -= 1; 318 start.add(Calendar.MONTH, 1); 319 } 320 321 while (months < 0) { 322 months += 12; 323 years -= 1; 324 } 325 326 if (!Token.containsTokenWithValue(tokens, y) && years != 0) { 327 while (years != 0) { 328 months += 12 * years; 329 years = 0; 330 } 331 } 332 } else { 333 // there are no M's in the format string 334 335 if( !Token.containsTokenWithValue(tokens, y) ) { 336 int target = end.get(Calendar.YEAR); 337 if (months < 0) { 338 // target is end-year -1 339 target -= 1; 340 } 341 342 while (start.get(Calendar.YEAR) != target) { 343 days += start.getActualMaximum(Calendar.DAY_OF_YEAR) - start.get(Calendar.DAY_OF_YEAR); 344 345 // Not sure I grok why this is needed, but the brutal tests show it is 346 if (start instanceof GregorianCalendar && 347 start.get(Calendar.MONTH) == Calendar.FEBRUARY && 348 start.get(Calendar.DAY_OF_MONTH) == 29) { 349 days += 1; 350 } 351 352 start.add(Calendar.YEAR, 1); 353 354 days += start.get(Calendar.DAY_OF_YEAR); 355 } 356 357 years = 0; 358 } 359 360 while( start.get(Calendar.MONTH) != end.get(Calendar.MONTH) ) { 361 days += start.getActualMaximum(Calendar.DAY_OF_MONTH); 362 start.add(Calendar.MONTH, 1); 363 } 364 365 months = 0; 366 367 while (days < 0) { 368 days += start.getActualMaximum(Calendar.DAY_OF_MONTH); 369 months -= 1; 370 start.add(Calendar.MONTH, 1); 371 } 372 373 } 374 375 // The rest of this code adds in values that 376 // aren't requested. This allows the user to ask for the 377 // number of months and get the real count and not just 0->11. 378 379 if (!Token.containsTokenWithValue(tokens, d)) { 380 hours += 24 * days; 381 days = 0; 382 } 383 if (!Token.containsTokenWithValue(tokens, H)) { 384 minutes += 60 * hours; 385 hours = 0; 386 } 387 if (!Token.containsTokenWithValue(tokens, m)) { 388 seconds += 60 * minutes; 389 minutes = 0; 390 } 391 if (!Token.containsTokenWithValue(tokens, s)) { 392 milliseconds += 1000 * seconds; 393 seconds = 0; 394 } 395 396 return format(tokens, years, months, days, hours, minutes, seconds, milliseconds, padWithZeros); 397 } 398 399 //----------------------------------------------------------------------- 400 /** 401 * <p>The internal method to do the formatting.</p> 402 * 403 * @param tokens the tokens 404 * @param years the number of years 405 * @param months the number of months 406 * @param days the number of days 407 * @param hours the number of hours 408 * @param minutes the number of minutes 409 * @param seconds the number of seconds 410 * @param milliseconds the number of millis 411 * @param padWithZeros whether to pad 412 * @return the formatted string 413 */ 414 static String format(final Token[] tokens, final int years, final int months, final int days, final int hours, final int minutes, final int seconds, 415 int milliseconds, final boolean padWithZeros) { 416 final StringBuilder buffer = new StringBuilder(); 417 boolean lastOutputSeconds = false; 418 final int sz = tokens.length; 419 for (int i = 0; i < sz; i++) { 420 final Token token = tokens[i]; 421 final Object value = token.getValue(); 422 final int count = token.getCount(); 423 if (value instanceof StringBuilder) { 424 buffer.append(value.toString()); 425 } else { 426 if (value == y) { 427 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(years), count, '0') : Integer 428 .toString(years)); 429 lastOutputSeconds = false; 430 } else if (value == M) { 431 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(months), count, '0') : Integer 432 .toString(months)); 433 lastOutputSeconds = false; 434 } else if (value == d) { 435 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(days), count, '0') : Integer 436 .toString(days)); 437 lastOutputSeconds = false; 438 } else if (value == H) { 439 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(hours), count, '0') : Integer 440 .toString(hours)); 441 lastOutputSeconds = false; 442 } else if (value == m) { 443 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(minutes), count, '0') : Integer 444 .toString(minutes)); 445 lastOutputSeconds = false; 446 } else if (value == s) { 447 buffer.append(padWithZeros ? StringUtils.leftPad(Integer.toString(seconds), count, '0') : Integer 448 .toString(seconds)); 449 lastOutputSeconds = true; 450 } else if (value == S) { 451 if (lastOutputSeconds) { 452 milliseconds += 1000; 453 final String str = padWithZeros 454 ? StringUtils.leftPad(Integer.toString(milliseconds), count, '0') 455 : Integer.toString(milliseconds); 456 buffer.append(str.substring(1)); 457 } else { 458 buffer.append(padWithZeros 459 ? StringUtils.leftPad(Integer.toString(milliseconds), count, '0') 460 : Integer.toString(milliseconds)); 461 } 462 lastOutputSeconds = false; 463 } 464 } 465 } 466 return buffer.toString(); 467 } 468 469 static final Object y = "y"; 470 static final Object M = "M"; 471 static final Object d = "d"; 472 static final Object H = "H"; 473 static final Object m = "m"; 474 static final Object s = "s"; 475 static final Object S = "S"; 476 477 /** 478 * Parses a classic date format string into Tokens 479 * 480 * @param format the format to parse, not null 481 * @return array of Token[] 482 */ 483 static Token[] lexx(final String format) { 484 final char[] array = format.toCharArray(); 485 final ArrayList<Token> list = new ArrayList<Token>(array.length); 486 487 boolean inLiteral = false; 488 // Although the buffer is stored in a Token, the Tokens are only 489 // used internally, so cannot be accessed by other threads 490 StringBuilder buffer = null; 491 Token previous = null; 492 final int sz = array.length; 493 for (int i = 0; i < sz; i++) { 494 final char ch = array[i]; 495 if (inLiteral && ch != '\'') { 496 buffer.append(ch); // buffer can't be null if inLiteral is true 497 continue; 498 } 499 Object value = null; 500 switch (ch) { 501 // TODO: Need to handle escaping of ' 502 case '\'': 503 if (inLiteral) { 504 buffer = null; 505 inLiteral = false; 506 } else { 507 buffer = new StringBuilder(); 508 list.add(new Token(buffer)); 509 inLiteral = true; 510 } 511 break; 512 case 'y': 513 value = y; 514 break; 515 case 'M': 516 value = M; 517 break; 518 case 'd': 519 value = d; 520 break; 521 case 'H': 522 value = H; 523 break; 524 case 'm': 525 value = m; 526 break; 527 case 's': 528 value = s; 529 break; 530 case 'S': 531 value = S; 532 break; 533 default: 534 if (buffer == null) { 535 buffer = new StringBuilder(); 536 list.add(new Token(buffer)); 537 } 538 buffer.append(ch); 539 } 540 541 if (value != null) { 542 if (previous != null && previous.getValue() == value) { 543 previous.increment(); 544 } else { 545 final Token token = new Token(value); 546 list.add(token); 547 previous = token; 548 } 549 buffer = null; 550 } 551 } 552 return list.toArray(new Token[list.size()]); 553 } 554 555 //----------------------------------------------------------------------- 556 /** 557 * Element that is parsed from the format pattern. 558 */ 559 static class Token { 560 561 /** 562 * Helper method to determine if a set of tokens contain a value 563 * 564 * @param tokens set to look in 565 * @param value to look for 566 * @return boolean <code>true</code> if contained 567 */ 568 static boolean containsTokenWithValue(final Token[] tokens, final Object value) { 569 final int sz = tokens.length; 570 for (int i = 0; i < sz; i++) { 571 if (tokens[i].getValue() == value) { 572 return true; 573 } 574 } 575 return false; 576 } 577 578 private final Object value; 579 private int count; 580 581 /** 582 * Wraps a token around a value. A value would be something like a 'Y'. 583 * 584 * @param value to wrap 585 */ 586 Token(final Object value) { 587 this.value = value; 588 this.count = 1; 589 } 590 591 /** 592 * Wraps a token around a repeated number of a value, for example it would 593 * store 'yyyy' as a value for y and a count of 4. 594 * 595 * @param value to wrap 596 * @param count to wrap 597 */ 598 Token(final Object value, final int count) { 599 this.value = value; 600 this.count = count; 601 } 602 603 /** 604 * Adds another one of the value 605 */ 606 void increment() { 607 count++; 608 } 609 610 /** 611 * Gets the current number of values represented 612 * 613 * @return int number of values represented 614 */ 615 int getCount() { 616 return count; 617 } 618 619 /** 620 * Gets the particular value this token represents. 621 * 622 * @return Object value 623 */ 624 Object getValue() { 625 return value; 626 } 627 628 /** 629 * Supports equality of this Token to another Token. 630 * 631 * @param obj2 Object to consider equality of 632 * @return boolean <code>true</code> if equal 633 */ 634 @Override 635 public boolean equals(final Object obj2) { 636 if (obj2 instanceof Token) { 637 final Token tok2 = (Token) obj2; 638 if (this.value.getClass() != tok2.value.getClass()) { 639 return false; 640 } 641 if (this.count != tok2.count) { 642 return false; 643 } 644 if (this.value instanceof StringBuilder) { 645 return this.value.toString().equals(tok2.value.toString()); 646 } else if (this.value instanceof Number) { 647 return this.value.equals(tok2.value); 648 } else { 649 return this.value == tok2.value; 650 } 651 } 652 return false; 653 } 654 655 /** 656 * Returns a hash code for the token equal to the 657 * hash code for the token's value. Thus 'TT' and 'TTTT' 658 * will have the same hash code. 659 * 660 * @return The hash code for the token 661 */ 662 @Override 663 public int hashCode() { 664 return this.value.hashCode(); 665 } 666 667 /** 668 * Represents this token as a String. 669 * 670 * @return String representation of the token 671 */ 672 @Override 673 public String toString() { 674 return StringUtils.repeat(this.value.toString(), this.count); 675 } 676 } 677 678}