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 }