|
1 /** |
|
2 * Copyright (c) 2012, Ben Fortuna |
|
3 * All rights reserved. |
|
4 * |
|
5 * Redistribution and use in source and binary forms, with or without |
|
6 * modification, are permitted provided that the following conditions |
|
7 * are met: |
|
8 * |
|
9 * o Redistributions of source code must retain the above copyright |
|
10 * notice, this list of conditions and the following disclaimer. |
|
11 * |
|
12 * o Redistributions in binary form must reproduce the above copyright |
|
13 * notice, this list of conditions and the following disclaimer in the |
|
14 * documentation and/or other materials provided with the distribution. |
|
15 * |
|
16 * o Neither the name of Ben Fortuna nor the names of any other contributors |
|
17 * may be used to endorse or promote products derived from this software |
|
18 * without specific prior written permission. |
|
19 * |
|
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
31 */ |
|
32 package net.fortuna.ical4j.model; |
|
33 |
|
34 import java.io.IOException; |
|
35 import java.io.Serializable; |
|
36 import java.util.Calendar; |
|
37 import java.util.Date; |
|
38 import java.util.StringTokenizer; |
|
39 import net.fortuna.ical4j.util.Dates; |
|
40 |
|
41 import org.apache.commons.lang.builder.HashCodeBuilder; |
|
42 |
|
43 /** |
|
44 * $Id$ |
|
45 * |
|
46 * Created on 20/06/2005 |
|
47 * |
|
48 * Represents a duration of time in iCalendar. Note that according to RFC2445 durations represented in weeks are |
|
49 * mutually exclusive of other duration fields. |
|
50 * |
|
51 * <pre> |
|
52 * 4.3.6 Duration |
|
53 * |
|
54 * Value Name: DURATION |
|
55 * |
|
56 * Purpose: This value type is used to identify properties that contain |
|
57 * a duration of time. |
|
58 * |
|
59 * Formal Definition: The value type is defined by the following |
|
60 * notation: |
|
61 * |
|
62 * dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) |
|
63 * |
|
64 * dur-date = dur-day [dur-time] |
|
65 * dur-time = "T" (dur-hour / dur-minute / dur-second) |
|
66 * dur-week = 1*DIGIT "W" |
|
67 * dur-hour = 1*DIGIT "H" [dur-minute] |
|
68 * dur-minute = 1*DIGIT "M" [dur-second] |
|
69 * dur-second = 1*DIGIT "S" |
|
70 * dur-day = 1*DIGIT "D" |
|
71 * </pre> |
|
72 * |
|
73 * @author Ben Fortuna |
|
74 */ |
|
75 public class Dur implements Comparable, Serializable { |
|
76 |
|
77 private static final long serialVersionUID = 5013232281547134583L; |
|
78 |
|
79 private static final int DAYS_PER_WEEK = 7; |
|
80 |
|
81 private static final int SECONDS_PER_MINUTE = 60; |
|
82 |
|
83 private static final int MINUTES_PER_HOUR = 60; |
|
84 |
|
85 private static final int HOURS_PER_DAY = 24; |
|
86 |
|
87 private static final int DAYS_PER_YEAR = 365; |
|
88 |
|
89 private boolean negative; |
|
90 |
|
91 private int weeks; |
|
92 |
|
93 private int days; |
|
94 |
|
95 private int hours; |
|
96 |
|
97 private int minutes; |
|
98 |
|
99 private int seconds; |
|
100 |
|
101 /** |
|
102 * Constructs a new duration instance from a string representation. |
|
103 * @param value a string representation of a duration |
|
104 */ |
|
105 public Dur(final String value) { |
|
106 negative = false; |
|
107 weeks = 0; |
|
108 days = 0; |
|
109 hours = 0; |
|
110 minutes = 0; |
|
111 seconds = 0; |
|
112 |
|
113 String token = null; |
|
114 String prevToken = null; |
|
115 |
|
116 final StringTokenizer t = new StringTokenizer(value, "+-PWDTHMS", true); |
|
117 while (t.hasMoreTokens()) { |
|
118 prevToken = token; |
|
119 token = t.nextToken(); |
|
120 |
|
121 if ("+".equals(token)) { |
|
122 negative = false; |
|
123 } |
|
124 else if ("-".equals(token)) { |
|
125 negative = true; |
|
126 } |
|
127 else if ("P".equals(token)) { |
|
128 // does nothing.. |
|
129 } |
|
130 else if ("W".equals(token)) { |
|
131 weeks = Integer.parseInt(prevToken); |
|
132 } |
|
133 else if ("D".equals(token)) { |
|
134 days = Integer.parseInt(prevToken); |
|
135 } |
|
136 else if ("T".equals(token)) { |
|
137 // does nothing.. |
|
138 } |
|
139 else if ("H".equals(token)) { |
|
140 hours = Integer.parseInt(prevToken); |
|
141 } |
|
142 else if ("M".equals(token)) { |
|
143 minutes = Integer.parseInt(prevToken); |
|
144 } |
|
145 else if ("S".equals(token)) { |
|
146 seconds = Integer.parseInt(prevToken); |
|
147 } |
|
148 } |
|
149 } |
|
150 |
|
151 /** |
|
152 * Constructs a new duration from the specified weeks. |
|
153 * @param weeks a duration in weeks. |
|
154 */ |
|
155 public Dur(final int weeks) { |
|
156 this.weeks = Math.abs(weeks); |
|
157 this.days = 0; |
|
158 this.hours = 0; |
|
159 this.minutes = 0; |
|
160 this.seconds = 0; |
|
161 this.negative = weeks < 0; |
|
162 } |
|
163 |
|
164 /** |
|
165 * Constructs a new duration from the specified arguments. |
|
166 * @param days duration in days |
|
167 * @param hours duration in hours |
|
168 * @param minutes duration in minutes |
|
169 * @param seconds duration in seconds |
|
170 */ |
|
171 public Dur(final int days, final int hours, final int minutes, |
|
172 final int seconds) { |
|
173 |
|
174 if (!(days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0) |
|
175 && !(days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0)) { |
|
176 |
|
177 throw new IllegalArgumentException("Invalid duration representation"); |
|
178 } |
|
179 |
|
180 this.weeks = 0; |
|
181 this.days = Math.abs(days); |
|
182 this.hours = Math.abs(hours); |
|
183 this.minutes = Math.abs(minutes); |
|
184 this.seconds = Math.abs(seconds); |
|
185 |
|
186 this.negative = days < 0 || hours < 0 || minutes < 0 || seconds < 0; |
|
187 } |
|
188 |
|
189 /** |
|
190 * Constructs a new duration representing the time between the two specified dates. The end date may precede the |
|
191 * start date in order to represent a negative duration. |
|
192 * @param date1 the first date of the duration |
|
193 * @param date2 the second date of the duration |
|
194 */ |
|
195 public Dur(final Date date1, final Date date2) { |
|
196 |
|
197 Date start = null; |
|
198 Date end = null; |
|
199 |
|
200 // Negative range? (start occurs after end) |
|
201 negative = date1.compareTo(date2) > 0; |
|
202 if (negative) { |
|
203 // Swap the dates (which eliminates the need to bother with |
|
204 // negative after this!) |
|
205 start = date2; |
|
206 end = date1; |
|
207 } |
|
208 else { |
|
209 start = date1; |
|
210 end = date2; |
|
211 } |
|
212 |
|
213 final Calendar startCal; |
|
214 if (start instanceof net.fortuna.ical4j.model.Date) { |
|
215 startCal = Dates.getCalendarInstance((net.fortuna.ical4j.model.Date)start); |
|
216 } else { |
|
217 startCal = Calendar.getInstance(); |
|
218 } |
|
219 startCal.setTime(start); |
|
220 final Calendar endCal = Calendar.getInstance(startCal.getTimeZone()); |
|
221 endCal.setTime(end); |
|
222 |
|
223 // Init our duration interval (which is in units that evolve as we |
|
224 // compute, below) |
|
225 int dur = 0; |
|
226 |
|
227 // Count days to get to the right year (loop in the very rare chance |
|
228 // that a leap year causes us to come up short) |
|
229 int nYears = endCal.get(Calendar.YEAR) - startCal.get(Calendar.YEAR); |
|
230 while (nYears > 0) { |
|
231 startCal.add(Calendar.DATE, DAYS_PER_YEAR * nYears); |
|
232 dur += DAYS_PER_YEAR * nYears; |
|
233 nYears = endCal.get(Calendar.YEAR) - startCal.get(Calendar.YEAR); |
|
234 } |
|
235 |
|
236 // Count days to get to the right day |
|
237 dur += endCal.get(Calendar.DAY_OF_YEAR) |
|
238 - startCal.get(Calendar.DAY_OF_YEAR); |
|
239 |
|
240 // Count hours to get to right hour |
|
241 dur *= HOURS_PER_DAY; // days -> hours |
|
242 dur += endCal.get(Calendar.HOUR_OF_DAY) |
|
243 - startCal.get(Calendar.HOUR_OF_DAY); |
|
244 |
|
245 // ... to the right minute |
|
246 dur *= MINUTES_PER_HOUR; // hours -> minutes |
|
247 dur += endCal.get(Calendar.MINUTE) - startCal.get(Calendar.MINUTE); |
|
248 |
|
249 // ... and second |
|
250 dur *= SECONDS_PER_MINUTE; // minutes -> seconds |
|
251 dur += endCal.get(Calendar.SECOND) - startCal.get(Calendar.SECOND); |
|
252 |
|
253 // Now unwind our units |
|
254 seconds = dur % SECONDS_PER_MINUTE; |
|
255 dur = dur / SECONDS_PER_MINUTE; // seconds -> minutes (drop remainder seconds) |
|
256 minutes = dur % MINUTES_PER_HOUR; |
|
257 dur /= MINUTES_PER_HOUR; // minutes -> hours (drop remainder minutes) |
|
258 hours = dur % HOURS_PER_DAY; |
|
259 dur /= HOURS_PER_DAY; // hours -> days (drop remainder hours) |
|
260 days = dur; |
|
261 weeks = 0; |
|
262 |
|
263 // Special case for week-only representation |
|
264 if (seconds == 0 && minutes == 0 && hours == 0 |
|
265 && (days % DAYS_PER_WEEK) == 0) { |
|
266 weeks = days / DAYS_PER_WEEK; |
|
267 days = 0; |
|
268 } |
|
269 } |
|
270 |
|
271 /** |
|
272 * Returns a date representing the end of this duration from the specified start date. |
|
273 * @param start the date to start the duration |
|
274 * @return the end of the duration as a date |
|
275 */ |
|
276 public final Date getTime(final Date start) { |
|
277 final Calendar cal; |
|
278 if (start instanceof net.fortuna.ical4j.model.Date) { |
|
279 cal = Dates.getCalendarInstance((net.fortuna.ical4j.model.Date)start); |
|
280 } else { |
|
281 cal = Calendar.getInstance(); |
|
282 } |
|
283 |
|
284 cal.setTime(start); |
|
285 if (isNegative()) { |
|
286 cal.add(Calendar.WEEK_OF_YEAR, -weeks); |
|
287 cal.add(Calendar.DAY_OF_WEEK, -days); |
|
288 cal.add(Calendar.HOUR_OF_DAY, -hours); |
|
289 cal.add(Calendar.MINUTE, -minutes); |
|
290 cal.add(Calendar.SECOND, -seconds); |
|
291 } |
|
292 else { |
|
293 cal.add(Calendar.WEEK_OF_YEAR, weeks); |
|
294 cal.add(Calendar.DAY_OF_WEEK, days); |
|
295 cal.add(Calendar.HOUR_OF_DAY, hours); |
|
296 cal.add(Calendar.MINUTE, minutes); |
|
297 cal.add(Calendar.SECOND, seconds); |
|
298 } |
|
299 return cal.getTime(); |
|
300 } |
|
301 |
|
302 /** |
|
303 * Provides a negation of this instance. |
|
304 * @return a Dur instance that represents a negation of this instance |
|
305 */ |
|
306 public final Dur negate() { |
|
307 final Dur negated = new Dur(days, hours, minutes, seconds); |
|
308 negated.weeks = weeks; |
|
309 negated.negative = !negative; |
|
310 return negated; |
|
311 } |
|
312 |
|
313 /** |
|
314 * Add two durations. Durations may only be added if they are both positive |
|
315 * or both negative durations. |
|
316 * @param duration the duration to add to this duration |
|
317 * @return a new instance representing the sum of the two durations. |
|
318 */ |
|
319 public final Dur add(final Dur duration) { |
|
320 if ((!isNegative() && duration.isNegative()) |
|
321 || (isNegative() && !duration.isNegative())) { |
|
322 |
|
323 throw new IllegalArgumentException( |
|
324 "Cannot add a negative and a positive duration"); |
|
325 } |
|
326 |
|
327 Dur sum = null; |
|
328 if (weeks > 0 && duration.weeks > 0) { |
|
329 sum = new Dur(weeks + duration.weeks); |
|
330 } |
|
331 else { |
|
332 int daySum = (weeks > 0) ? weeks * DAYS_PER_WEEK + days : days; |
|
333 int hourSum = hours; |
|
334 int minuteSum = minutes; |
|
335 int secondSum = seconds; |
|
336 |
|
337 if ((secondSum + duration.seconds) / SECONDS_PER_MINUTE > 0) { |
|
338 minuteSum += (secondSum + duration.seconds) / SECONDS_PER_MINUTE; |
|
339 secondSum = (secondSum + duration.seconds) % SECONDS_PER_MINUTE; |
|
340 } |
|
341 else { |
|
342 secondSum += duration.seconds; |
|
343 } |
|
344 |
|
345 if ((minuteSum + duration.minutes) / MINUTES_PER_HOUR > 0) { |
|
346 hourSum += (minuteSum + duration.minutes) / MINUTES_PER_HOUR; |
|
347 minuteSum = (minuteSum + duration.minutes) % MINUTES_PER_HOUR; |
|
348 } |
|
349 else { |
|
350 minuteSum += duration.minutes; |
|
351 } |
|
352 |
|
353 if ((hourSum + duration.hours) / HOURS_PER_DAY > 0) { |
|
354 daySum += (hourSum + duration.hours) / HOURS_PER_DAY; |
|
355 hourSum = (hourSum + duration.hours) % HOURS_PER_DAY; |
|
356 } |
|
357 else { |
|
358 hourSum += duration.hours; |
|
359 } |
|
360 |
|
361 daySum += (duration.weeks > 0) ? duration.weeks * DAYS_PER_WEEK |
|
362 + duration.days : duration.days; |
|
363 |
|
364 sum = new Dur(daySum, hourSum, minuteSum, secondSum); |
|
365 } |
|
366 sum.negative = negative; |
|
367 return sum; |
|
368 } |
|
369 |
|
370 /** |
|
371 * {@inheritDoc} |
|
372 */ |
|
373 public final String toString() { |
|
374 final StringBuffer b = new StringBuffer(); |
|
375 if (negative) { |
|
376 b.append('-'); |
|
377 } |
|
378 b.append('P'); |
|
379 if (weeks > 0) { |
|
380 b.append(weeks); |
|
381 b.append('W'); |
|
382 } |
|
383 else { |
|
384 if (days > 0) { |
|
385 b.append(days); |
|
386 b.append('D'); |
|
387 } |
|
388 if (hours > 0 || minutes > 0 || seconds > 0) { |
|
389 b.append('T'); |
|
390 if (hours > 0) { |
|
391 b.append(hours); |
|
392 b.append('H'); |
|
393 } |
|
394 if (minutes > 0) { |
|
395 b.append(minutes); |
|
396 b.append('M'); |
|
397 } |
|
398 if (seconds > 0) { |
|
399 b.append(seconds); |
|
400 b.append('S'); |
|
401 } |
|
402 } |
|
403 // handle case of zero length duration |
|
404 if ((hours + minutes + seconds + days + weeks) == 0) { |
|
405 b.append("T0S"); |
|
406 } |
|
407 } |
|
408 return b.toString(); |
|
409 } |
|
410 |
|
411 /** |
|
412 * {@inheritDoc} |
|
413 */ |
|
414 public final int compareTo(final Object arg0) { |
|
415 return compareTo((Dur) arg0); |
|
416 } |
|
417 |
|
418 /** |
|
419 * Compares this duration with another, acording to their length. |
|
420 * @param arg0 another duration instance |
|
421 * @return a postive value if this duration is longer, zero if the duration |
|
422 * lengths are equal, otherwise a negative value |
|
423 */ |
|
424 public final int compareTo(final Dur arg0) { |
|
425 int result; |
|
426 if (isNegative() != arg0.isNegative()) { |
|
427 // return Boolean.valueOf(isNegative()).compareTo(Boolean.valueOf(arg0.isNegative())); |
|
428 // for pre-java 1.5 compatibility.. |
|
429 if (isNegative()) { |
|
430 return Integer.MIN_VALUE; |
|
431 } |
|
432 else { |
|
433 return Integer.MAX_VALUE; |
|
434 } |
|
435 } |
|
436 else if (getWeeks() != arg0.getWeeks()) { |
|
437 result = getWeeks() - arg0.getWeeks(); |
|
438 } |
|
439 else if (getDays() != arg0.getDays()) { |
|
440 result = getDays() - arg0.getDays(); |
|
441 } |
|
442 else if (getHours() != arg0.getHours()) { |
|
443 result = getHours() - arg0.getHours(); |
|
444 } |
|
445 else if (getMinutes() != arg0.getMinutes()) { |
|
446 result = getMinutes() - arg0.getMinutes(); |
|
447 } |
|
448 else { |
|
449 result = getSeconds() - arg0.getSeconds(); |
|
450 } |
|
451 // invert sense of all tests if both durations are negative |
|
452 if (isNegative()) { |
|
453 return -result; |
|
454 } |
|
455 else { |
|
456 return result; |
|
457 } |
|
458 } |
|
459 |
|
460 /** |
|
461 * {@inheritDoc} |
|
462 */ |
|
463 public boolean equals(final Object obj) { |
|
464 if (obj instanceof Dur) { |
|
465 return ((Dur) obj).compareTo(this) == 0; |
|
466 } |
|
467 return super.equals(obj); |
|
468 } |
|
469 |
|
470 /** |
|
471 * {@inheritDoc} |
|
472 */ |
|
473 public int hashCode() { |
|
474 return new HashCodeBuilder().append(weeks).append(days).append( |
|
475 hours).append(minutes).append(seconds).append(negative).toHashCode(); |
|
476 } |
|
477 |
|
478 /** |
|
479 * @return Returns the days. |
|
480 */ |
|
481 public final int getDays() { |
|
482 return days; |
|
483 } |
|
484 |
|
485 /** |
|
486 * @return Returns the hours. |
|
487 */ |
|
488 public final int getHours() { |
|
489 return hours; |
|
490 } |
|
491 |
|
492 /** |
|
493 * @return Returns the minutes. |
|
494 */ |
|
495 public final int getMinutes() { |
|
496 return minutes; |
|
497 } |
|
498 |
|
499 /** |
|
500 * @return Returns the negative. |
|
501 */ |
|
502 public final boolean isNegative() { |
|
503 return negative; |
|
504 } |
|
505 |
|
506 /** |
|
507 * @return Returns the seconds. |
|
508 */ |
|
509 public final int getSeconds() { |
|
510 return seconds; |
|
511 } |
|
512 |
|
513 /** |
|
514 * @return Returns the weeks. |
|
515 */ |
|
516 public final int getWeeks() { |
|
517 return weeks; |
|
518 } |
|
519 |
|
520 /** |
|
521 * @param stream |
|
522 * @throws IOException |
|
523 * @throws ClassNotFoundException |
|
524 */ |
|
525 private void readObject(final java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { |
|
526 stream.defaultReadObject(); |
|
527 } |
|
528 } |