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