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