001 package biweekly.util; 002 003 import java.util.regex.Matcher; 004 import java.util.regex.Pattern; 005 006 /* 007 Copyright (c) 2013, Michael Angstadt 008 All rights reserved. 009 010 Redistribution and use in source and binary forms, with or without 011 modification, are permitted provided that the following conditions are met: 012 013 1. Redistributions of source code must retain the above copyright notice, this 014 list of conditions and the following disclaimer. 015 2. Redistributions in binary form must reproduce the above copyright notice, 016 this list of conditions and the following disclaimer in the documentation 017 and/or other materials provided with the distribution. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 026 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030 031 /** 032 * <p> 033 * Represents a period of time (for example, "2 hours and 30 minutes"). 034 * </p> 035 * <p> 036 * This class is immutable. Use the {@link Builder Duration.Builder} class to 037 * construct a new instance, or the {@link #parse} method to parse a duration 038 * string. 039 * </p> 040 * 041 * <p> 042 * <b>Examples:</b> 043 * 044 * <pre> 045 * Duration duration = new Duration.Builder().hours(2).minutes(30).build(); 046 * Duration duration = Duration.parse("PT2H30M"); 047 * </pre> 048 * 049 * </p> 050 * @author Michael Angstadt 051 */ 052 public class Duration { 053 private final Integer weeks, days, hours, minutes, seconds; 054 private final boolean prior; 055 056 private Duration(Builder b) { 057 weeks = b.weeks; 058 days = b.days; 059 hours = b.hours; 060 minutes = b.minutes; 061 seconds = b.seconds; 062 prior = b.prior; 063 } 064 065 /** 066 * Parses a duration string. 067 * @param value the duration string (e.g. "P30DT10H") 068 * @return the parsed duration 069 * @throws IllegalArgumentException if the duration string is invalid 070 */ 071 public static Duration parse(String value) { 072 if (!value.matches("-?P.*")) { 073 throw new IllegalArgumentException("Invalid duration string: " + value); 074 } 075 076 //@formatter:off 077 return new Duration.Builder() 078 .prior(value.startsWith("-")) 079 .weeks(parseComponent(value, 'W')) 080 .days(parseComponent(value, 'D')) 081 .hours(parseComponent(value, 'H')) 082 .minutes(parseComponent(value, 'M')) 083 .seconds(parseComponent(value, 'S')) 084 .build(); 085 //@formatter:on 086 } 087 088 private static Integer parseComponent(String value, char ch) { 089 Pattern p = Pattern.compile("(\\d+)" + ch); 090 Matcher m = p.matcher(value); 091 return m.find() ? Integer.valueOf(m.group(1)) : null; 092 } 093 094 /** 095 * Gets whether the duration is negative. 096 * @return true if it's negative, false if not 097 */ 098 public boolean isPrior() { 099 return prior; 100 } 101 102 /** 103 * Gets the number of weeks. 104 * @return the number of weeks or null if not set 105 */ 106 public Integer getWeeks() { 107 return weeks; 108 } 109 110 /** 111 * Gets the number of days. 112 * @return the number of days or null if not set 113 */ 114 public Integer getDays() { 115 return days; 116 } 117 118 /** 119 * Gets the number of hours. 120 * @return the number of hours or null if not set 121 */ 122 public Integer getHours() { 123 return hours; 124 } 125 126 /** 127 * Gets the number of minutes. 128 * @return the number of minutes or null if not set 129 */ 130 public Integer getMinutes() { 131 return minutes; 132 } 133 134 /** 135 * Gets the number of seconds. 136 * @return the number of seconds or null if not set 137 */ 138 public Integer getSeconds() { 139 return seconds; 140 } 141 142 /** 143 * Determines if any time components are present. 144 * @return true if the duration has at least one time component, false if 145 * not 146 */ 147 public boolean hasTime() { 148 return hours != null || minutes != null || seconds != null; 149 } 150 151 @Override 152 public int hashCode() { 153 final int prime = 31; 154 int result = 1; 155 result = prime * result + ((days == null) ? 0 : days.hashCode()); 156 result = prime * result + ((hours == null) ? 0 : hours.hashCode()); 157 result = prime * result + ((minutes == null) ? 0 : minutes.hashCode()); 158 result = prime * result + (prior ? 1231 : 1237); 159 result = prime * result + ((seconds == null) ? 0 : seconds.hashCode()); 160 result = prime * result + ((weeks == null) ? 0 : weeks.hashCode()); 161 return result; 162 } 163 164 @Override 165 public boolean equals(Object obj) { 166 if (this == obj) 167 return true; 168 if (obj == null) 169 return false; 170 if (getClass() != obj.getClass()) 171 return false; 172 Duration other = (Duration) obj; 173 if (days == null) { 174 if (other.days != null) 175 return false; 176 } else if (!days.equals(other.days)) 177 return false; 178 if (hours == null) { 179 if (other.hours != null) 180 return false; 181 } else if (!hours.equals(other.hours)) 182 return false; 183 if (minutes == null) { 184 if (other.minutes != null) 185 return false; 186 } else if (!minutes.equals(other.minutes)) 187 return false; 188 if (prior != other.prior) 189 return false; 190 if (seconds == null) { 191 if (other.seconds != null) 192 return false; 193 } else if (!seconds.equals(other.seconds)) 194 return false; 195 if (weeks == null) { 196 if (other.weeks != null) 197 return false; 198 } else if (!weeks.equals(other.weeks)) 199 return false; 200 return true; 201 } 202 203 /** 204 * Converts the duration to its string representation. 205 * @return the string representation (e.g. "P4DT1H" for "4 days and 1 hour") 206 */ 207 @Override 208 public String toString() { 209 StringBuilder sb = new StringBuilder(); 210 211 if (prior) { 212 sb.append('-'); 213 } 214 sb.append('P'); 215 216 if (weeks != null) { 217 sb.append(weeks).append('W'); 218 } 219 220 if (days != null) { 221 sb.append(days).append('D'); 222 } 223 224 if (hasTime()) { 225 sb.append('T'); 226 227 if (hours != null) { 228 sb.append(hours).append('H'); 229 } 230 231 if (minutes != null) { 232 sb.append(minutes).append('M'); 233 } 234 235 if (seconds != null) { 236 sb.append(seconds).append('S'); 237 } 238 } 239 240 return sb.toString(); 241 } 242 243 /** 244 * Builds {@link Duration} objects. 245 */ 246 public static class Builder { 247 private Integer weeks, days, hours, minutes, seconds; 248 private boolean prior = false; 249 250 /** 251 * Creates a new {@link Duration} builder. 252 */ 253 public Builder() { 254 //empty 255 } 256 257 /** 258 * Creates a new {@link Duration} builder. 259 * @param source the object to copy from 260 */ 261 public Builder(Duration source) { 262 weeks = source.weeks; 263 days = source.days; 264 hours = source.hours; 265 minutes = source.minutes; 266 seconds = source.seconds; 267 prior = source.prior; 268 } 269 270 /** 271 * Sets the number of weeks. 272 * @param weeks the number of weeks 273 * @return this 274 */ 275 public Builder weeks(Integer weeks) { 276 this.weeks = weeks; 277 return this; 278 } 279 280 /** 281 * Sets the number of days 282 * @param days the number of days 283 * @return this 284 */ 285 public Builder days(Integer days) { 286 this.days = days; 287 return this; 288 } 289 290 /** 291 * Sets the number of hours 292 * @param hours the number of hours 293 * @return this 294 */ 295 public Builder hours(Integer hours) { 296 this.hours = hours; 297 return this; 298 } 299 300 /** 301 * Sets the number of minutes 302 * @param minutes the number of minutes 303 * @return this 304 */ 305 public Builder minutes(Integer minutes) { 306 this.minutes = minutes; 307 return this; 308 } 309 310 /** 311 * Sets the number of seconds. 312 * @param seconds the number of seconds 313 * @return this 314 */ 315 public Builder seconds(Integer seconds) { 316 this.seconds = seconds; 317 return this; 318 } 319 320 /** 321 * Sets whether the duration should be negative. 322 * @param prior true to be negative, false not to be 323 * @return this 324 */ 325 public Builder prior(boolean prior) { 326 this.prior = prior; 327 return this; 328 } 329 330 /** 331 * Builds the final {@link Duration} object. 332 * @return the object 333 */ 334 public Duration build() { 335 return new Duration(this); 336 } 337 } 338 }