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 */ 017 package org.apache.commons.lang3.time; 018 019 import java.util.ArrayList; 020 import java.util.Calendar; 021 import java.util.Date; 022 import java.util.GregorianCalendar; 023 import java.util.TimeZone; 024 025 import 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 1144993 2011-07-11 00:51:16Z ggregory $ 043 */ 044 public 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(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(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(long durationMillis, 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, String format, boolean padWithZeros) { 123 124 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 long durationMillis, 168 boolean suppressLeadingZeroElements, 169 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(long startMillis, 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(long startMillis, long endMillis, 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(long startMillis, long endMillis, String format, boolean padWithZeros, 270 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 Token[] tokens = lexx(format); 279 280 // timezones get funky around 0, so normalizing everything to GMT 281 // stops the hours being off 282 Calendar start = Calendar.getInstance(timezone); 283 start.setTime(new Date(startMillis)); 284 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(Token[] tokens, int years, int months, int days, int hours, int minutes, int seconds, 415 int milliseconds, boolean padWithZeros) { 416 StringBuffer buffer = new StringBuffer(); 417 boolean lastOutputSeconds = false; 418 int sz = tokens.length; 419 for (int i = 0; i < sz; i++) { 420 Token token = tokens[i]; 421 Object value = token.getValue(); 422 int count = token.getCount(); 423 if (value instanceof StringBuffer) { 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 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(String format) { 484 char[] array = format.toCharArray(); 485 ArrayList<Token> list = new ArrayList<Token>(array.length); 486 487 boolean inLiteral = false; 488 StringBuffer buffer = null; 489 Token previous = null; 490 int sz = array.length; 491 for(int i=0; i<sz; i++) { 492 char ch = array[i]; 493 if(inLiteral && ch != '\'') { 494 buffer.append(ch); // buffer can't be null if inLiteral is true 495 continue; 496 } 497 Object value = null; 498 switch(ch) { 499 // TODO: Need to handle escaping of ' 500 case '\'' : 501 if(inLiteral) { 502 buffer = null; 503 inLiteral = false; 504 } else { 505 buffer = new StringBuffer(); 506 list.add(new Token(buffer)); 507 inLiteral = true; 508 } 509 break; 510 case 'y' : value = y; break; 511 case 'M' : value = M; break; 512 case 'd' : value = d; break; 513 case 'H' : value = H; break; 514 case 'm' : value = m; break; 515 case 's' : value = s; break; 516 case 'S' : value = S; break; 517 default : 518 if(buffer == null) { 519 buffer = new StringBuffer(); 520 list.add(new Token(buffer)); 521 } 522 buffer.append(ch); 523 } 524 525 if(value != null) { 526 if(previous != null && previous.getValue() == value) { 527 previous.increment(); 528 } else { 529 Token token = new Token(value); 530 list.add(token); 531 previous = token; 532 } 533 buffer = null; 534 } 535 } 536 return list.toArray( new Token[list.size()] ); 537 } 538 539 //----------------------------------------------------------------------- 540 /** 541 * Element that is parsed from the format pattern. 542 */ 543 static class Token { 544 545 /** 546 * Helper method to determine if a set of tokens contain a value 547 * 548 * @param tokens set to look in 549 * @param value to look for 550 * @return boolean <code>true</code> if contained 551 */ 552 static boolean containsTokenWithValue(Token[] tokens, Object value) { 553 int sz = tokens.length; 554 for (int i = 0; i < sz; i++) { 555 if (tokens[i].getValue() == value) { 556 return true; 557 } 558 } 559 return false; 560 } 561 562 private final Object value; 563 private int count; 564 565 /** 566 * Wraps a token around a value. A value would be something like a 'Y'. 567 * 568 * @param value to wrap 569 */ 570 Token(Object value) { 571 this.value = value; 572 this.count = 1; 573 } 574 575 /** 576 * Wraps a token around a repeated number of a value, for example it would 577 * store 'yyyy' as a value for y and a count of 4. 578 * 579 * @param value to wrap 580 * @param count to wrap 581 */ 582 Token(Object value, int count) { 583 this.value = value; 584 this.count = count; 585 } 586 587 /** 588 * Adds another one of the value 589 */ 590 void increment() { 591 count++; 592 } 593 594 /** 595 * Gets the current number of values represented 596 * 597 * @return int number of values represented 598 */ 599 int getCount() { 600 return count; 601 } 602 603 /** 604 * Gets the particular value this token represents. 605 * 606 * @return Object value 607 */ 608 Object getValue() { 609 return value; 610 } 611 612 /** 613 * Supports equality of this Token to another Token. 614 * 615 * @param obj2 Object to consider equality of 616 * @return boolean <code>true</code> if equal 617 */ 618 @Override 619 public boolean equals(Object obj2) { 620 if (obj2 instanceof Token) { 621 Token tok2 = (Token) obj2; 622 if (this.value.getClass() != tok2.value.getClass()) { 623 return false; 624 } 625 if (this.count != tok2.count) { 626 return false; 627 } 628 if (this.value instanceof StringBuffer) { 629 return this.value.toString().equals(tok2.value.toString()); 630 } else if (this.value instanceof Number) { 631 return this.value.equals(tok2.value); 632 } else { 633 return this.value == tok2.value; 634 } 635 } 636 return false; 637 } 638 639 /** 640 * Returns a hash code for the token equal to the 641 * hash code for the token's value. Thus 'TT' and 'TTTT' 642 * will have the same hash code. 643 * 644 * @return The hash code for the token 645 */ 646 @Override 647 public int hashCode() { 648 return this.value.hashCode(); 649 } 650 651 /** 652 * Represents this token as a String. 653 * 654 * @return String representation of the token 655 */ 656 @Override 657 public String toString() { 658 return StringUtils.repeat(this.value.toString(), this.count); 659 } 660 } 661 662 }