001package biweekly.util;
002
003import java.util.Calendar;
004import java.util.Date;
005import java.util.regex.Matcher;
006import java.util.regex.Pattern;
007
008/*
009 Copyright (c) 2013-2015, 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 */
057public 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 += 60L * 60 * 24 * 7 * weeks;
254                }
255                if (days != null) {
256                        totalSeconds += 60L * 60 * 24 * days;
257                }
258                if (hours != null) {
259                        totalSeconds += 60L * 60 * hours;
260                }
261                if (minutes != null) {
262                        totalSeconds += 60L * 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}