michael@0: /** michael@0: * Copyright (c) 2012, Ben Fortuna michael@0: * All rights reserved. michael@0: * michael@0: * Redistribution and use in source and binary forms, with or without michael@0: * modification, are permitted provided that the following conditions michael@0: * are met: michael@0: * michael@0: * o Redistributions of source code must retain the above copyright michael@0: * notice, this list of conditions and the following disclaimer. michael@0: * michael@0: * o Redistributions in binary form must reproduce the above copyright michael@0: * notice, this list of conditions and the following disclaimer in the michael@0: * documentation and/or other materials provided with the distribution. michael@0: * michael@0: * o Neither the name of Ben Fortuna nor the names of any other contributors michael@0: * may be used to endorse or promote products derived from this software michael@0: * without specific prior written permission. michael@0: * michael@0: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS michael@0: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT michael@0: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR michael@0: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR michael@0: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, michael@0: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, michael@0: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR michael@0: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF michael@0: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING michael@0: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS michael@0: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: */ michael@0: package net.fortuna.ical4j.model; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.Serializable; michael@0: import java.util.Calendar; michael@0: import java.util.Date; michael@0: import java.util.StringTokenizer; michael@0: import net.fortuna.ical4j.util.Dates; michael@0: michael@4: import org.apache.commons.lang3.builder.HashCodeBuilder; michael@0: michael@0: /** michael@0: * $Id$ michael@0: * michael@0: * Created on 20/06/2005 michael@0: * michael@0: * Represents a duration of time in iCalendar. Note that according to RFC2445 durations represented in weeks are michael@0: * mutually exclusive of other duration fields. michael@0: * michael@0: *
michael@0:  *  4.3.6   Duration
michael@0:  *  
michael@0:  *     Value Name: DURATION
michael@0:  *  
michael@0:  *     Purpose: This value type is used to identify properties that contain
michael@0:  *     a duration of time.
michael@0:  *  
michael@0:  *     Formal Definition: The value type is defined by the following
michael@0:  *     notation:
michael@0:  *  
michael@0:  *       dur-value  = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
michael@0:  *  
michael@0:  *       dur-date   = dur-day [dur-time]
michael@0:  *       dur-time   = "T" (dur-hour / dur-minute / dur-second)
michael@0:  *       dur-week   = 1*DIGIT "W"
michael@0:  *       dur-hour   = 1*DIGIT "H" [dur-minute]
michael@0:  *       dur-minute = 1*DIGIT "M" [dur-second]
michael@0:  *       dur-second = 1*DIGIT "S"
michael@0:  *       dur-day    = 1*DIGIT "D"
michael@0:  * 
michael@0: * michael@0: * @author Ben Fortuna michael@0: */ michael@0: public class Dur implements Comparable, Serializable { michael@0: michael@0: private static final long serialVersionUID = 5013232281547134583L; michael@0: michael@0: private static final int DAYS_PER_WEEK = 7; michael@0: michael@0: private static final int SECONDS_PER_MINUTE = 60; michael@0: michael@0: private static final int MINUTES_PER_HOUR = 60; michael@0: michael@0: private static final int HOURS_PER_DAY = 24; michael@0: michael@0: private static final int DAYS_PER_YEAR = 365; michael@0: michael@0: private boolean negative; michael@0: michael@0: private int weeks; michael@0: michael@0: private int days; michael@0: michael@0: private int hours; michael@0: michael@0: private int minutes; michael@0: michael@0: private int seconds; michael@0: michael@0: /** michael@0: * Constructs a new duration instance from a string representation. michael@0: * @param value a string representation of a duration michael@0: */ michael@0: public Dur(final String value) { michael@0: negative = false; michael@0: weeks = 0; michael@0: days = 0; michael@0: hours = 0; michael@0: minutes = 0; michael@0: seconds = 0; michael@0: michael@0: String token = null; michael@0: String prevToken = null; michael@0: michael@0: final StringTokenizer t = new StringTokenizer(value, "+-PWDTHMS", true); michael@0: while (t.hasMoreTokens()) { michael@0: prevToken = token; michael@0: token = t.nextToken(); michael@0: michael@0: if ("+".equals(token)) { michael@0: negative = false; michael@0: } michael@0: else if ("-".equals(token)) { michael@0: negative = true; michael@0: } michael@0: else if ("P".equals(token)) { michael@0: // does nothing.. michael@0: } michael@0: else if ("W".equals(token)) { michael@0: weeks = Integer.parseInt(prevToken); michael@0: } michael@0: else if ("D".equals(token)) { michael@0: days = Integer.parseInt(prevToken); michael@0: } michael@0: else if ("T".equals(token)) { michael@0: // does nothing.. michael@0: } michael@0: else if ("H".equals(token)) { michael@0: hours = Integer.parseInt(prevToken); michael@0: } michael@0: else if ("M".equals(token)) { michael@0: minutes = Integer.parseInt(prevToken); michael@0: } michael@0: else if ("S".equals(token)) { michael@0: seconds = Integer.parseInt(prevToken); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new duration from the specified weeks. michael@0: * @param weeks a duration in weeks. michael@0: */ michael@0: public Dur(final int weeks) { michael@0: this.weeks = Math.abs(weeks); michael@0: this.days = 0; michael@0: this.hours = 0; michael@0: this.minutes = 0; michael@0: this.seconds = 0; michael@0: this.negative = weeks < 0; michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new duration from the specified arguments. michael@0: * @param days duration in days michael@0: * @param hours duration in hours michael@0: * @param minutes duration in minutes michael@0: * @param seconds duration in seconds michael@0: */ michael@0: public Dur(final int days, final int hours, final int minutes, michael@0: final int seconds) { michael@0: michael@0: if (!(days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0) michael@0: && !(days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0)) { michael@0: michael@0: throw new IllegalArgumentException("Invalid duration representation"); michael@0: } michael@0: michael@0: this.weeks = 0; michael@0: this.days = Math.abs(days); michael@0: this.hours = Math.abs(hours); michael@0: this.minutes = Math.abs(minutes); michael@0: this.seconds = Math.abs(seconds); michael@0: michael@0: this.negative = days < 0 || hours < 0 || minutes < 0 || seconds < 0; michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new duration representing the time between the two specified dates. The end date may precede the michael@0: * start date in order to represent a negative duration. michael@0: * @param date1 the first date of the duration michael@0: * @param date2 the second date of the duration michael@0: */ michael@0: public Dur(final Date date1, final Date date2) { michael@0: michael@0: Date start = null; michael@0: Date end = null; michael@0: michael@0: // Negative range? (start occurs after end) michael@0: negative = date1.compareTo(date2) > 0; michael@0: if (negative) { michael@0: // Swap the dates (which eliminates the need to bother with michael@0: // negative after this!) michael@0: start = date2; michael@0: end = date1; michael@0: } michael@0: else { michael@0: start = date1; michael@0: end = date2; michael@0: } michael@0: michael@0: final Calendar startCal; michael@0: if (start instanceof net.fortuna.ical4j.model.Date) { michael@0: startCal = Dates.getCalendarInstance((net.fortuna.ical4j.model.Date)start); michael@0: } else { michael@0: startCal = Calendar.getInstance(); michael@0: } michael@0: startCal.setTime(start); michael@0: final Calendar endCal = Calendar.getInstance(startCal.getTimeZone()); michael@0: endCal.setTime(end); michael@0: michael@0: // Init our duration interval (which is in units that evolve as we michael@0: // compute, below) michael@0: int dur = 0; michael@0: michael@0: // Count days to get to the right year (loop in the very rare chance michael@0: // that a leap year causes us to come up short) michael@0: int nYears = endCal.get(Calendar.YEAR) - startCal.get(Calendar.YEAR); michael@0: while (nYears > 0) { michael@0: startCal.add(Calendar.DATE, DAYS_PER_YEAR * nYears); michael@0: dur += DAYS_PER_YEAR * nYears; michael@0: nYears = endCal.get(Calendar.YEAR) - startCal.get(Calendar.YEAR); michael@0: } michael@0: michael@0: // Count days to get to the right day michael@0: dur += endCal.get(Calendar.DAY_OF_YEAR) michael@0: - startCal.get(Calendar.DAY_OF_YEAR); michael@0: michael@0: // Count hours to get to right hour michael@0: dur *= HOURS_PER_DAY; // days -> hours michael@0: dur += endCal.get(Calendar.HOUR_OF_DAY) michael@0: - startCal.get(Calendar.HOUR_OF_DAY); michael@0: michael@0: // ... to the right minute michael@0: dur *= MINUTES_PER_HOUR; // hours -> minutes michael@0: dur += endCal.get(Calendar.MINUTE) - startCal.get(Calendar.MINUTE); michael@0: michael@0: // ... and second michael@0: dur *= SECONDS_PER_MINUTE; // minutes -> seconds michael@0: dur += endCal.get(Calendar.SECOND) - startCal.get(Calendar.SECOND); michael@0: michael@0: // Now unwind our units michael@0: seconds = dur % SECONDS_PER_MINUTE; michael@0: dur = dur / SECONDS_PER_MINUTE; // seconds -> minutes (drop remainder seconds) michael@0: minutes = dur % MINUTES_PER_HOUR; michael@0: dur /= MINUTES_PER_HOUR; // minutes -> hours (drop remainder minutes) michael@0: hours = dur % HOURS_PER_DAY; michael@0: dur /= HOURS_PER_DAY; // hours -> days (drop remainder hours) michael@0: days = dur; michael@0: weeks = 0; michael@0: michael@0: // Special case for week-only representation michael@0: if (seconds == 0 && minutes == 0 && hours == 0 michael@0: && (days % DAYS_PER_WEEK) == 0) { michael@0: weeks = days / DAYS_PER_WEEK; michael@0: days = 0; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns a date representing the end of this duration from the specified start date. michael@0: * @param start the date to start the duration michael@0: * @return the end of the duration as a date michael@0: */ michael@0: public final Date getTime(final Date start) { michael@0: final Calendar cal; michael@0: if (start instanceof net.fortuna.ical4j.model.Date) { michael@0: cal = Dates.getCalendarInstance((net.fortuna.ical4j.model.Date)start); michael@0: } else { michael@0: cal = Calendar.getInstance(); michael@0: } michael@0: michael@0: cal.setTime(start); michael@0: if (isNegative()) { michael@0: cal.add(Calendar.WEEK_OF_YEAR, -weeks); michael@0: cal.add(Calendar.DAY_OF_WEEK, -days); michael@0: cal.add(Calendar.HOUR_OF_DAY, -hours); michael@0: cal.add(Calendar.MINUTE, -minutes); michael@0: cal.add(Calendar.SECOND, -seconds); michael@0: } michael@0: else { michael@0: cal.add(Calendar.WEEK_OF_YEAR, weeks); michael@0: cal.add(Calendar.DAY_OF_WEEK, days); michael@0: cal.add(Calendar.HOUR_OF_DAY, hours); michael@0: cal.add(Calendar.MINUTE, minutes); michael@0: cal.add(Calendar.SECOND, seconds); michael@0: } michael@0: return cal.getTime(); michael@0: } michael@0: michael@0: /** michael@0: * Provides a negation of this instance. michael@0: * @return a Dur instance that represents a negation of this instance michael@0: */ michael@0: public final Dur negate() { michael@0: final Dur negated = new Dur(days, hours, minutes, seconds); michael@0: negated.weeks = weeks; michael@0: negated.negative = !negative; michael@0: return negated; michael@0: } michael@0: michael@0: /** michael@0: * Add two durations. Durations may only be added if they are both positive michael@0: * or both negative durations. michael@0: * @param duration the duration to add to this duration michael@0: * @return a new instance representing the sum of the two durations. michael@0: */ michael@0: public final Dur add(final Dur duration) { michael@0: if ((!isNegative() && duration.isNegative()) michael@0: || (isNegative() && !duration.isNegative())) { michael@0: michael@0: throw new IllegalArgumentException( michael@0: "Cannot add a negative and a positive duration"); michael@0: } michael@0: michael@0: Dur sum = null; michael@0: if (weeks > 0 && duration.weeks > 0) { michael@0: sum = new Dur(weeks + duration.weeks); michael@0: } michael@0: else { michael@0: int daySum = (weeks > 0) ? weeks * DAYS_PER_WEEK + days : days; michael@0: int hourSum = hours; michael@0: int minuteSum = minutes; michael@0: int secondSum = seconds; michael@0: michael@0: if ((secondSum + duration.seconds) / SECONDS_PER_MINUTE > 0) { michael@0: minuteSum += (secondSum + duration.seconds) / SECONDS_PER_MINUTE; michael@0: secondSum = (secondSum + duration.seconds) % SECONDS_PER_MINUTE; michael@0: } michael@0: else { michael@0: secondSum += duration.seconds; michael@0: } michael@0: michael@0: if ((minuteSum + duration.minutes) / MINUTES_PER_HOUR > 0) { michael@0: hourSum += (minuteSum + duration.minutes) / MINUTES_PER_HOUR; michael@0: minuteSum = (minuteSum + duration.minutes) % MINUTES_PER_HOUR; michael@0: } michael@0: else { michael@0: minuteSum += duration.minutes; michael@0: } michael@0: michael@0: if ((hourSum + duration.hours) / HOURS_PER_DAY > 0) { michael@0: daySum += (hourSum + duration.hours) / HOURS_PER_DAY; michael@0: hourSum = (hourSum + duration.hours) % HOURS_PER_DAY; michael@0: } michael@0: else { michael@0: hourSum += duration.hours; michael@0: } michael@0: michael@0: daySum += (duration.weeks > 0) ? duration.weeks * DAYS_PER_WEEK michael@0: + duration.days : duration.days; michael@0: michael@0: sum = new Dur(daySum, hourSum, minuteSum, secondSum); michael@0: } michael@0: sum.negative = negative; michael@0: return sum; michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public final String toString() { michael@0: final StringBuffer b = new StringBuffer(); michael@0: if (negative) { michael@0: b.append('-'); michael@0: } michael@0: b.append('P'); michael@0: if (weeks > 0) { michael@0: b.append(weeks); michael@0: b.append('W'); michael@0: } michael@0: else { michael@0: if (days > 0) { michael@0: b.append(days); michael@0: b.append('D'); michael@0: } michael@0: if (hours > 0 || minutes > 0 || seconds > 0) { michael@0: b.append('T'); michael@0: if (hours > 0) { michael@0: b.append(hours); michael@0: b.append('H'); michael@0: } michael@0: if (minutes > 0) { michael@0: b.append(minutes); michael@0: b.append('M'); michael@0: } michael@0: if (seconds > 0) { michael@0: b.append(seconds); michael@0: b.append('S'); michael@0: } michael@0: } michael@0: // handle case of zero length duration michael@0: if ((hours + minutes + seconds + days + weeks) == 0) { michael@0: b.append("T0S"); michael@0: } michael@0: } michael@0: return b.toString(); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public final int compareTo(final Object arg0) { michael@0: return compareTo((Dur) arg0); michael@0: } michael@0: michael@0: /** michael@0: * Compares this duration with another, acording to their length. michael@0: * @param arg0 another duration instance michael@0: * @return a postive value if this duration is longer, zero if the duration michael@0: * lengths are equal, otherwise a negative value michael@0: */ michael@0: public final int compareTo(final Dur arg0) { michael@0: int result; michael@0: if (isNegative() != arg0.isNegative()) { michael@0: // return Boolean.valueOf(isNegative()).compareTo(Boolean.valueOf(arg0.isNegative())); michael@0: // for pre-java 1.5 compatibility.. michael@0: if (isNegative()) { michael@0: return Integer.MIN_VALUE; michael@0: } michael@0: else { michael@0: return Integer.MAX_VALUE; michael@0: } michael@0: } michael@0: else if (getWeeks() != arg0.getWeeks()) { michael@0: result = getWeeks() - arg0.getWeeks(); michael@0: } michael@0: else if (getDays() != arg0.getDays()) { michael@0: result = getDays() - arg0.getDays(); michael@0: } michael@0: else if (getHours() != arg0.getHours()) { michael@0: result = getHours() - arg0.getHours(); michael@0: } michael@0: else if (getMinutes() != arg0.getMinutes()) { michael@0: result = getMinutes() - arg0.getMinutes(); michael@0: } michael@0: else { michael@0: result = getSeconds() - arg0.getSeconds(); michael@0: } michael@0: // invert sense of all tests if both durations are negative michael@0: if (isNegative()) { michael@0: return -result; michael@0: } michael@0: else { michael@0: return result; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public boolean equals(final Object obj) { michael@0: if (obj instanceof Dur) { michael@0: return ((Dur) obj).compareTo(this) == 0; michael@0: } michael@0: return super.equals(obj); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public int hashCode() { michael@0: return new HashCodeBuilder().append(weeks).append(days).append( michael@0: hours).append(minutes).append(seconds).append(negative).toHashCode(); michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the days. michael@0: */ michael@0: public final int getDays() { michael@0: return days; michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the hours. michael@0: */ michael@0: public final int getHours() { michael@0: return hours; michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the minutes. michael@0: */ michael@0: public final int getMinutes() { michael@0: return minutes; michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the negative. michael@0: */ michael@0: public final boolean isNegative() { michael@0: return negative; michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the seconds. michael@0: */ michael@0: public final int getSeconds() { michael@0: return seconds; michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the weeks. michael@0: */ michael@0: public final int getWeeks() { michael@0: return weeks; michael@0: } michael@0: michael@0: /** michael@0: * @param stream michael@0: * @throws IOException michael@0: * @throws ClassNotFoundException michael@0: */ michael@0: private void readObject(final java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { michael@0: stream.defaultReadObject(); michael@0: } michael@0: }