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 }