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(&quot;PT2H30M&quot;);
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    }