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.text.ParseException; michael@0: import java.util.Date; michael@0: michael@0: import org.apache.commons.lang.builder.EqualsBuilder; michael@0: import org.apache.commons.lang.builder.HashCodeBuilder; michael@0: michael@0: /** michael@0: * $Id$ [Apr 14, 2004] michael@0: * michael@0: * Defines a period of time. A period may be specified as either a start date michael@0: * and end date, or a start date and duration. NOTE: End dates and durations are michael@0: * implicitly derived when not explicitly specified. This means that you cannot michael@0: * rely on the returned values from the getters to deduce whether a period has michael@0: * an explicit end date or duration. michael@0: * michael@0: * @author Ben Fortuna michael@0: */ michael@0: public class Period extends DateRange implements Comparable { michael@0: michael@0: private static final long serialVersionUID = 7321090422911676490L; michael@0: michael@0: private Dur duration; michael@0: michael@0: /** michael@0: * Constructor. michael@0: * michael@0: * @param aValue michael@0: * a string representation of a period michael@0: * @throws ParseException michael@0: * where the specified string is not a valid representation michael@0: */ michael@0: public Period(final String aValue) throws ParseException { michael@0: super(parseStartDate(aValue), parseEndDate(aValue, true)); michael@0: michael@0: // period may end in either a date-time or a duration.. michael@0: try { michael@0: parseEndDate(aValue, false); michael@0: } michael@0: catch (ParseException pe) { michael@0: // duration = DurationFormat.getInstance().parse(aValue); michael@0: duration = parseDuration(aValue); michael@0: } michael@0: normalise(); michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new period with the specied start and end date. michael@0: * michael@0: * @param start michael@0: * the start date of the period michael@0: * @param end michael@0: * the end date of the period michael@0: */ michael@0: public Period(final DateTime start, final DateTime end) { michael@0: super(start, end); michael@0: normalise(); michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new period with the specified start date and duration. michael@0: * michael@0: * @param start michael@0: * the start date of the period michael@0: * @param duration michael@0: * the duration of the period michael@0: */ michael@0: public Period(final DateTime start, final Dur duration) { michael@0: super(start, new DateTime(duration.getTime(start))); michael@0: this.duration = duration; michael@0: normalise(); michael@0: } michael@0: michael@0: private static DateTime parseStartDate(String value) throws ParseException { michael@0: return new DateTime(value.substring(0, value.indexOf('/'))); michael@0: } michael@0: michael@0: private static DateTime parseEndDate(String value, boolean resolve) throws ParseException { michael@0: DateTime end = null; michael@0: try { michael@0: end = new DateTime(value.substring(value.indexOf('/') + 1)); michael@0: } michael@0: catch (ParseException e) { michael@0: if (resolve) { michael@0: final Dur duration = parseDuration(value); michael@0: end = new DateTime(duration.getTime(parseStartDate(value))); michael@0: } michael@0: else { michael@0: throw e; michael@0: } michael@0: } michael@0: return end; michael@0: } michael@0: michael@0: private static Dur parseDuration(String value) { michael@0: return new Dur(value.substring(value.indexOf('/') + 1)); michael@0: } michael@0: michael@0: private void normalise() { michael@0: // ensure the end timezone is the same as the start.. michael@0: if (getStart().isUtc()) { michael@0: getEnd().setUtc(true); michael@0: } michael@0: else { michael@0: getEnd().setTimeZone(getStart().getTimeZone()); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns the duration of this period. If an explicit duration is not michael@0: * specified, the duration is derived from the end date. michael@0: * michael@0: * @return the duration of this period in milliseconds. michael@0: */ michael@0: public final Dur getDuration() { michael@0: if (duration == null) { michael@0: return new Dur(getStart(), getEnd()); michael@0: } michael@0: return duration; michael@0: } michael@0: michael@0: /** michael@0: * Returns the end date of this period. If an explicit end date is not michael@0: * specified, the end date is derived from the duration. michael@0: * michael@0: * @return the end date of this period. michael@0: */ michael@0: public final DateTime getEnd() { michael@0: return (DateTime) getRangeEnd(); michael@0: } michael@0: michael@0: /** michael@0: * @return Returns the start. michael@0: */ michael@0: public final DateTime getStart() { michael@0: return (DateTime) getRangeStart(); michael@0: } michael@0: michael@0: /** michael@0: * @param date a date to test for inclusion michael@0: * @param inclusive indicates if the start and end of the period are included in the test michael@0: * @return true if the specified date occurs within the current period michael@0: * @deprecated use {@link Period#includes(Date, int)} instead. michael@0: */ michael@0: public final boolean includes(final Date date, final boolean inclusive) { michael@0: if (inclusive) { michael@0: return includes(date, INCLUSIVE_START | INCLUSIVE_END); michael@0: } michael@0: else { michael@0: return includes(date, 0); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Creates a period that encompasses both this period and another one. If michael@0: * the other period is null, return a copy of this period. NOTE: Resulting michael@0: * periods are specified by explicitly setting a start date and end date michael@0: * (i.e. durations are implied). michael@0: * michael@0: * @param period michael@0: * the period to add to this one michael@0: * @return a period michael@0: */ michael@0: public final Period add(final Period period) { michael@0: DateTime newPeriodStart = null; michael@0: DateTime newPeriodEnd = null; michael@0: michael@0: if (period == null) { michael@0: newPeriodStart = getStart(); michael@0: newPeriodEnd = getEnd(); michael@0: } michael@0: else { michael@0: if (getStart().before(period.getStart())) { michael@0: newPeriodStart = getStart(); michael@0: } michael@0: else { michael@0: newPeriodStart = period.getStart(); michael@0: } michael@0: if (getEnd().after(period.getEnd())) { michael@0: newPeriodEnd = getEnd(); michael@0: } michael@0: else { michael@0: newPeriodEnd = period.getEnd(); michael@0: } michael@0: } michael@0: michael@0: return new Period(newPeriodStart, newPeriodEnd); michael@0: } michael@0: michael@0: /** michael@0: * Creates a set of periods resulting from the subtraction of the specified michael@0: * period from this one. If the specified period is completely contained michael@0: * in this period, the resulting list will contain two periods. Otherwise michael@0: * it will contain one. If the specified period does not interest this period michael@0: * a list containing this period is returned. If this period is completely michael@0: * contained within the specified period an empty period list is returned. michael@0: * @param period a period to subtract from this one michael@0: * @return a list containing zero, one or two periods. michael@0: */ michael@0: public final PeriodList subtract(final Period period) { michael@0: final PeriodList result = new PeriodList(); michael@0: michael@0: if (period.contains(this)) { michael@0: return result; michael@0: } michael@0: else if (!period.intersects(this)) { michael@0: result.add(this); michael@0: return result; michael@0: } michael@0: michael@0: DateTime newPeriodStart; michael@0: DateTime newPeriodEnd; michael@0: if (!period.getStart().after(getStart())) { michael@0: newPeriodStart = period.getEnd(); michael@0: newPeriodEnd = getEnd(); michael@0: } michael@0: else if (!period.getEnd().before(getEnd())) { michael@0: newPeriodStart = getStart(); michael@0: newPeriodEnd = period.getStart(); michael@0: } michael@0: else { michael@0: // subtraction consumed by this period.. michael@0: // initialise and add head period.. michael@0: newPeriodStart = getStart(); michael@0: newPeriodEnd = period.getStart(); michael@0: result.add(new Period(newPeriodStart, newPeriodEnd)); michael@0: // initialise tail period.. michael@0: newPeriodStart = period.getEnd(); michael@0: newPeriodEnd = getEnd(); michael@0: } michael@0: result.add(new Period(newPeriodStart, newPeriodEnd)); michael@0: return result; michael@0: } michael@0: michael@0: /** michael@0: * An empty period is one that consumes no time. michael@0: * @return true if this period consumes no time, otherwise false michael@0: */ michael@0: public final boolean isEmpty() { michael@0: return getStart().equals(getEnd()); michael@0: } michael@0: michael@0: /** michael@0: * Updates the start and (possible) end times of this period to reflect michael@0: * the specified UTC timezone status. michael@0: * @param utc indicates whether the period is in UTC time michael@0: */ michael@0: public void setUtc(final boolean utc) { michael@0: getStart().setUtc(utc); michael@0: getEnd().setUtc(utc); michael@0: } michael@0: michael@0: /** michael@0: * Updates the start and (possible) end times of this period to reflect michael@0: * the specified timezone status. michael@0: * @param timezone a timezone for the period michael@0: */ michael@0: public final void setTimeZone(final TimeZone timezone) { michael@0: getStart().setUtc(false); michael@0: getStart().setTimeZone(timezone); michael@0: getEnd().setUtc(false); michael@0: getEnd().setTimeZone(timezone); 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: b.append(getStart()); michael@0: b.append('/'); michael@0: if (duration == null) { michael@0: b.append(getEnd()); michael@0: } michael@0: else { michael@0: // b.append(DurationFormat.getInstance().format(duration)); michael@0: b.append(duration); 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((Period) arg0); michael@0: } michael@0: michael@0: /** michael@0: * Compares the specified period with this period. michael@0: * michael@0: * @param arg0 a period to compare with this one michael@0: * @return a postive value if this period is greater, negative if the other is michael@0: * greater, or zero if they are equal michael@0: */ michael@0: public final int compareTo(final Period arg0) { michael@0: // Throws documented exception if type is wrong or parameter is null michael@0: if (arg0 == null) { michael@0: throw new ClassCastException("Cannot compare this object to null"); michael@0: } michael@0: final int startCompare = getStart().compareTo(arg0.getStart()); michael@0: if (startCompare != 0) { michael@0: return startCompare; michael@0: } michael@0: // start dates are equal, compare end dates.. michael@0: else if (duration == null) { michael@0: final int endCompare = getEnd().compareTo(arg0.getEnd()); michael@0: if (endCompare != 0) { michael@0: return endCompare; michael@0: } michael@0: } michael@0: // ..or durations michael@0: return getDuration().compareTo(arg0.getDuration()); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public final boolean equals(final Object o) { michael@0: if (this == o) { michael@0: return true; michael@0: } michael@0: if (!(o instanceof Period)) { michael@0: return false; michael@0: } michael@0: michael@0: final Period period = (Period) o; michael@0: return new EqualsBuilder().append(getStart(), period.getStart()) michael@0: .append(getEnd(), period.getEnd()).isEquals(); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public final int hashCode() { michael@0: return new HashCodeBuilder().append(getStart()) michael@0: .append((duration == null) ? (Object) getEnd() : duration).toHashCode(); michael@0: } michael@0: }