001 package biweekly.util; 002 003 import java.util.Calendar; 004 import java.util.Date; 005 import java.util.regex.Matcher; 006 import java.util.regex.Pattern; 007 008 /* 009 Copyright (c) 2013, Michael Angstadt 010 All rights reserved. 011 012 Redistribution and use in source and binary forms, with or without 013 modification, are permitted provided that the following conditions are met: 014 015 1. Redistributions of source code must retain the above copyright notice, this 016 list of conditions and the following disclaimer. 017 2. Redistributions in binary form must reproduce the above copyright notice, 018 this list of conditions and the following disclaimer in the documentation 019 and/or other materials provided with the distribution. 020 021 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 022 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 023 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 024 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 025 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 026 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 027 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 028 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 029 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 030 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 031 */ 032 033 /** 034 * <p> 035 * Represents a period of time (for example, "2 hours and 30 minutes"). 036 * </p> 037 * <p> 038 * This class is immutable. Use the {@link #builder} method to construct a new 039 * instance, or the {@link #parse} method to parse a duration string. 040 * </p> 041 * 042 * <p> 043 * <b>Examples:</b> 044 * 045 * <pre class="brush:java"> 046 * Duration duration = Duration.builder().hours(2).minutes(30).build(); 047 * Duration duration = Duration.parse("PT2H30M"); 048 * 049 * //add a duration value to a Date 050 * Date start = ... 051 * Date end = duration.add(start); 052 * </pre> 053 * 054 * </p> 055 * @author Michael Angstadt 056 */ 057 public final class Duration { 058 private final Integer weeks, days, hours, minutes, seconds; 059 private final boolean prior; 060 061 private Duration(Builder b) { 062 weeks = b.weeks; 063 days = b.days; 064 hours = b.hours; 065 minutes = b.minutes; 066 seconds = b.seconds; 067 prior = b.prior; 068 } 069 070 /** 071 * Parses a duration string. 072 * @param value the duration string (e.g. "P30DT10H") 073 * @return the parsed duration 074 * @throws IllegalArgumentException if the duration string is invalid 075 */ 076 public static Duration parse(String value) { 077 if (!value.matches("-?P.*")) { 078 throw new IllegalArgumentException("Invalid duration string: " + value); 079 } 080 081 //@formatter:off 082 return builder() 083 .prior(value.startsWith("-")) 084 .weeks(parseComponent(value, 'W')) 085 .days(parseComponent(value, 'D')) 086 .hours(parseComponent(value, 'H')) 087 .minutes(parseComponent(value, 'M')) 088 .seconds(parseComponent(value, 'S')) 089 .build(); 090 //@formatter:on 091 } 092 093 /** 094 * Builds a duration based on the difference between two dates. 095 * @param start the start date 096 * @param end the end date 097 * @return the duration 098 */ 099 public static Duration diff(Date start, Date end) { 100 return fromMillis(end.getTime() - start.getTime()); 101 } 102 103 /** 104 * Builds a duration from a number of milliseconds. 105 * @param milliseconds the number of milliseconds 106 * @return the duration 107 */ 108 public static Duration fromMillis(long milliseconds) { 109 Duration.Builder builder = builder(); 110 111 if (milliseconds < 0) { 112 builder.prior(true); 113 milliseconds *= -1; 114 } 115 116 int seconds = (int) (milliseconds / 1000); 117 118 Integer weeks = seconds / (60 * 60 * 24 * 7); 119 if (weeks > 0) { 120 builder.weeks(weeks); 121 } 122 seconds %= 60 * 60 * 24 * 7; 123 124 Integer days = seconds / (60 * 60 * 24); 125 if (days > 0) { 126 builder.days(days); 127 } 128 seconds %= 60 * 60 * 24; 129 130 Integer hours = seconds / (60 * 60); 131 if (hours > 0) { 132 builder.hours(hours); 133 } 134 seconds %= 60 * 60; 135 136 Integer minutes = seconds / (60); 137 if (minutes > 0) { 138 builder.minutes(minutes); 139 } 140 seconds %= 60; 141 142 if (seconds > 0) { 143 builder.seconds(seconds); 144 } 145 146 return builder.build(); 147 } 148 149 /** 150 * Creates a builder object for constructing new instances of this class. 151 * @return the builder object 152 */ 153 public static Builder builder() { 154 return new Builder(); 155 } 156 157 private static Integer parseComponent(String value, char ch) { 158 Pattern p = Pattern.compile("(\\d+)" + ch); 159 Matcher m = p.matcher(value); 160 return m.find() ? Integer.valueOf(m.group(1)) : null; 161 } 162 163 /** 164 * Gets whether the duration is negative. 165 * @return true if it's negative, false if not 166 */ 167 public boolean isPrior() { 168 return prior; 169 } 170 171 /** 172 * Gets the number of weeks. 173 * @return the number of weeks or null if not set 174 */ 175 public Integer getWeeks() { 176 return weeks; 177 } 178 179 /** 180 * Gets the number of days. 181 * @return the number of days or null if not set 182 */ 183 public Integer getDays() { 184 return days; 185 } 186 187 /** 188 * Gets the number of hours. 189 * @return the number of hours or null if not set 190 */ 191 public Integer getHours() { 192 return hours; 193 } 194 195 /** 196 * Gets the number of minutes. 197 * @return the number of minutes or null if not set 198 */ 199 public Integer getMinutes() { 200 return minutes; 201 } 202 203 /** 204 * Gets the number of seconds. 205 * @return the number of seconds or null if not set 206 */ 207 public Integer getSeconds() { 208 return seconds; 209 } 210 211 /** 212 * Adds this duration value to a {@link Date} object. 213 * @param date the date to add to 214 * @return the new date value 215 */ 216 public Date add(Date date) { 217 Calendar c = Calendar.getInstance(); 218 c.setTime(date); 219 220 if (weeks != null) { 221 int weeks = this.weeks * (prior ? -1 : 1); 222 c.add(Calendar.DATE, weeks * 7); 223 } 224 if (days != null) { 225 int days = this.days * (prior ? -1 : 1); 226 c.add(Calendar.DATE, days); 227 } 228 if (hours != null) { 229 int hours = this.hours * (prior ? -1 : 1); 230 c.add(Calendar.HOUR_OF_DAY, hours); 231 } 232 if (minutes != null) { 233 int minutes = this.minutes * (prior ? -1 : 1); 234 c.add(Calendar.MINUTE, minutes); 235 } 236 if (seconds != null) { 237 int seconds = this.seconds * (prior ? -1 : 1); 238 c.add(Calendar.SECOND, seconds); 239 } 240 241 return c.getTime(); 242 } 243 244 /** 245 * Converts the duration value to milliseconds. 246 * @return the duration value in milliseconds (will be negative if 247 * {@link #isPrior} is true) 248 */ 249 public long toMillis() { 250 long totalSeconds = 0; 251 252 if (weeks != null) { 253 totalSeconds += 60 * 60 * 24 * 7 * weeks; 254 } 255 if (days != null) { 256 totalSeconds += 60 * 60 * 24 * days; 257 } 258 if (hours != null) { 259 totalSeconds += 60 * 60 * hours; 260 } 261 if (minutes != null) { 262 totalSeconds += 60 * minutes; 263 } 264 if (seconds != null) { 265 totalSeconds += seconds; 266 } 267 if (prior) { 268 totalSeconds *= -1; 269 } 270 271 return totalSeconds * 1000; 272 } 273 274 /** 275 * Determines if any time components are present. 276 * @return true if the duration has at least one time component, false if 277 * not 278 */ 279 public boolean hasTime() { 280 return hours != null || minutes != null || seconds != null; 281 } 282 283 @Override 284 public int hashCode() { 285 final int prime = 31; 286 int result = 1; 287 result = prime * result + ((days == null) ? 0 : days.hashCode()); 288 result = prime * result + ((hours == null) ? 0 : hours.hashCode()); 289 result = prime * result + ((minutes == null) ? 0 : minutes.hashCode()); 290 result = prime * result + (prior ? 1231 : 1237); 291 result = prime * result + ((seconds == null) ? 0 : seconds.hashCode()); 292 result = prime * result + ((weeks == null) ? 0 : weeks.hashCode()); 293 return result; 294 } 295 296 @Override 297 public boolean equals(Object obj) { 298 if (this == obj) 299 return true; 300 if (obj == null) 301 return false; 302 if (getClass() != obj.getClass()) 303 return false; 304 Duration other = (Duration) obj; 305 if (days == null) { 306 if (other.days != null) 307 return false; 308 } else if (!days.equals(other.days)) 309 return false; 310 if (hours == null) { 311 if (other.hours != null) 312 return false; 313 } else if (!hours.equals(other.hours)) 314 return false; 315 if (minutes == null) { 316 if (other.minutes != null) 317 return false; 318 } else if (!minutes.equals(other.minutes)) 319 return false; 320 if (prior != other.prior) 321 return false; 322 if (seconds == null) { 323 if (other.seconds != null) 324 return false; 325 } else if (!seconds.equals(other.seconds)) 326 return false; 327 if (weeks == null) { 328 if (other.weeks != null) 329 return false; 330 } else if (!weeks.equals(other.weeks)) 331 return false; 332 return true; 333 } 334 335 /** 336 * Converts the duration to its string representation. 337 * @return the string representation (e.g. "P4DT1H" for "4 days and 1 hour") 338 */ 339 @Override 340 public String toString() { 341 StringBuilder sb = new StringBuilder(); 342 343 if (prior) { 344 sb.append('-'); 345 } 346 sb.append('P'); 347 348 if (weeks != null) { 349 sb.append(weeks).append('W'); 350 } 351 352 if (days != null) { 353 sb.append(days).append('D'); 354 } 355 356 if (hasTime()) { 357 sb.append('T'); 358 359 if (hours != null) { 360 sb.append(hours).append('H'); 361 } 362 363 if (minutes != null) { 364 sb.append(minutes).append('M'); 365 } 366 367 if (seconds != null) { 368 sb.append(seconds).append('S'); 369 } 370 } 371 372 return sb.toString(); 373 } 374 375 /** 376 * Builds {@link Duration} objects. 377 */ 378 public static class Builder { 379 private Integer weeks, days, hours, minutes, seconds; 380 private boolean prior = false; 381 382 /** 383 * Creates a new {@link Duration} builder. 384 */ 385 public Builder() { 386 //empty 387 } 388 389 /** 390 * Creates a new {@link Duration} builder. 391 * @param source the object to copy from 392 */ 393 public Builder(Duration source) { 394 weeks = source.weeks; 395 days = source.days; 396 hours = source.hours; 397 minutes = source.minutes; 398 seconds = source.seconds; 399 prior = source.prior; 400 } 401 402 /** 403 * Sets the number of weeks. 404 * @param weeks the number of weeks 405 * @return this 406 */ 407 public Builder weeks(Integer weeks) { 408 this.weeks = weeks; 409 return this; 410 } 411 412 /** 413 * Sets the number of days 414 * @param days the number of days 415 * @return this 416 */ 417 public Builder days(Integer days) { 418 this.days = days; 419 return this; 420 } 421 422 /** 423 * Sets the number of hours 424 * @param hours the number of hours 425 * @return this 426 */ 427 public Builder hours(Integer hours) { 428 this.hours = hours; 429 return this; 430 } 431 432 /** 433 * Sets the number of minutes 434 * @param minutes the number of minutes 435 * @return this 436 */ 437 public Builder minutes(Integer minutes) { 438 this.minutes = minutes; 439 return this; 440 } 441 442 /** 443 * Sets the number of seconds. 444 * @param seconds the number of seconds 445 * @return this 446 */ 447 public Builder seconds(Integer seconds) { 448 this.seconds = seconds; 449 return this; 450 } 451 452 /** 453 * Sets whether the duration should be negative. 454 * @param prior true to be negative, false not to be 455 * @return this 456 */ 457 public Builder prior(boolean prior) { 458 this.prior = prior; 459 return this; 460 } 461 462 /** 463 * Builds the final {@link Duration} object. 464 * @return the object 465 */ 466 public Duration build() { 467 return new Duration(this); 468 } 469 } 470 }