/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.collect.range; import java.io.Serializable; import java.time.LocalDate; import java.time.temporal.TemporalAdjuster; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.joda.beans.Bean; import org.joda.beans.BeanBuilder; import org.joda.beans.BeanDefinition; import org.joda.beans.ImmutableBean; import org.joda.beans.ImmutableValidator; import org.joda.beans.JodaBeanUtils; import org.joda.beans.MetaProperty; import org.joda.beans.Property; import org.joda.beans.PropertyDefinition; import org.joda.beans.impl.direct.DirectFieldsBeanBuilder; import org.joda.beans.impl.direct.DirectMetaBean; import org.joda.beans.impl.direct.DirectMetaProperty; import org.joda.beans.impl.direct.DirectMetaPropertyMap; import com.opengamma.collect.ArgChecker; /** * A range of local dates. * <p> * Provides a mechanism to represent a range of dates. * Instances can be constructed from either a half-open or a closed range of dates. * Internally, both are unified to a single representation. * <p> * The constants {@link LocalDate#MIN} and {@link LocalDate#MAX} can be used * to indicate an unbounded far-past or far-future. Note that there is no difference * between a half-open and a closed range when the end is {@link LocalDate#MAX}. * <p> * This class is immutable and thread-safe. */ @BeanDefinition(builderScope = "private") public final class LocalDateRange implements ImmutableBean, Serializable { /** * A range over the whole time-line. */ public static final LocalDateRange ALL = new LocalDateRange(LocalDate.MIN, LocalDate.MAX); /** * The start date, inclusive. */ @PropertyDefinition(validate = "notNull", get = "manual") private final LocalDate start; /** * The end date, exclusive. */ @PropertyDefinition(validate = "notNull", get = "manual") private final LocalDate endExclusive; //------------------------------------------------------------------------- /** * Obtains a half-open range of dates, including the start and excluding the end. * <p> * The range includes the start date and excludes the end date, unless the end * is {@link LocalDate#MAX}. * The end date must be equal to or after the start date. * This definition permits an empty range located at a specific date. * * @param startInclusive the inclusive start date, MIN_DATE treated as unbounded * @param endExclusive the exclusive end date, MAX_DATE treated as unbounded * @return the half-open range * @throws IllegalArgumentException if the end date is before the start date */ public static LocalDateRange of(LocalDate startInclusive, LocalDate endExclusive) { ArgChecker.notNull(startInclusive, "startDate"); ArgChecker.notNull(endExclusive, "endExclusive"); return new LocalDateRange(startInclusive, endExclusive); } /** * Obtains a closed range of dates, including the start and end. * <p> * The range includes the start date and the end date. * The end date must be equal to or after the start date. * * @param startInclusive the inclusive start date, MIN_DATE treated as unbounded * @param endInclusive the inclusive end date, MAX_DATE treated as unbounded * @return the closed range * @throws IllegalArgumentException if the end date is before the start date */ public static LocalDateRange ofClosed(LocalDate startInclusive, LocalDate endInclusive) { ArgChecker.notNull(startInclusive, "startDate"); ArgChecker.notNull(endInclusive, "endExclusive"); if (endInclusive.isBefore(startInclusive)) { throw new IllegalArgumentException("End date must not be before start date"); } LocalDate endExclusive = (endInclusive.equals(LocalDate.MAX) ? LocalDate.MAX : endInclusive.plusDays(1)); return new LocalDateRange(startInclusive, endExclusive); } //------------------------------------------------------------------------- /** * Validates that the end is not before the start. */ @ImmutableValidator private void validate() { if (endExclusive.isBefore(start)) { throw new IllegalArgumentException("End date must not be before start date"); } } //------------------------------------------------------------------------- /** * Gets the start date, inclusive. * <p> * This will return {@link LocalDate#MIN} if the range is unbounded at the start. * In this case, the range includes all dates into the far-past. * * @return the start date */ public LocalDate getStart() { return start; } /** * Gets the end date, exclusive. * <p> * This will return {@link LocalDate#MAX} if the range is unbounded at the end. * In this case, the range includes all dates into the far-future. * * @return the end date, exclusive */ public LocalDate getEndExclusive() { return endExclusive; } /** * Gets the end date, inclusive. * <p> * This will return {@link LocalDate#MAX} if the range is unbounded at the end. * In this case, the range includes all dates into the far-future. * * @return the end date, inclusive */ public LocalDate getEndInclusive() { if (isUnboundedEnd()) { return LocalDate.MAX; } return endExclusive.minusDays(1); } //------------------------------------------------------------------------- /** * Checks if the range is empty. * * @return true if the range is empty */ public boolean isEmpty() { return start.equals(endExclusive); } /** * Checks if the start date is unbounded. * * @return true if start is unbounded */ public boolean isUnboundedStart() { return start.equals(LocalDate.MIN); } /** * Checks if the end date is unbounded. * * @return true if end is unbounded */ public boolean isUnboundedEnd() { return endExclusive.equals(LocalDate.MAX); } //------------------------------------------------------------------------- /** * Returns a copy of this range with the start date adjusted. * <p> * This returns a new instance with the start date altered. * Since {@code LocalDate} implements {@code TemporalAdjuster} any * local date can simply be passed in. * <p> * For example, to adjust the start to one week earlier: * <pre> * range = range.withStart(date -> date.minus(1, ChronoUnit.WEEKS)); * </pre> * * @param adjuster the adjuster to use * @return a copy of this range with the start date adjusted * @throws IllegalArgumentException if the new start date is after the current end date */ public LocalDateRange withStart(TemporalAdjuster adjuster) { ArgChecker.notNull(adjuster, "adjuster"); return LocalDateRange.of(start.with(adjuster), endExclusive); } /** * Returns a copy of this range with the end date adjusted. * <p> * This returns a new instance with the end date altered. * Since {@code LocalDate} implements {@code TemporalAdjuster} any * local date can simply be passed in. * <p> * For example, to adjust the end to one week later: * <pre> * range = range.withEndExclusive(date -> date.plus(1, ChronoUnit.WEEKS)); * </pre> * * @param adjuster the adjuster to use * @return a copy of this range with the end date adjusted * @throws IllegalArgumentException if the new end date is before the current start date */ public LocalDateRange withEndExclusive(TemporalAdjuster adjuster) { ArgChecker.notNull(adjuster, "adjuster"); return LocalDateRange.of(start, endExclusive.with(adjuster)); } //------------------------------------------------------------------------- /** * Checks if this range contains the specified date. * <p> * If this range has an unbounded start then {@code contains(LocalDate#MIN)} returns true. * If this range has an unbounded end then {@code contains(LocalDate#MAX)} returns true. * If this range is empty then this method always returns false. * * @param date the date to check for * @return true if this range contains the date */ public boolean contains(LocalDate date) { ArgChecker.notNull(date, "date"); // start <= date && date < endExclusive return start.compareTo(date) <= 0 && (date.compareTo(endExclusive) < 0 || isUnboundedEnd()); } /** * Checks if this range contains all dates in the specified range. * <p> * This checks that the start date of this range is before or equal the specified start date, * and the end date of this range is after or equal the specified end date. * If this range is empty then it only encloses an equal range. * * @param other the other range to check for * @return true if this range contains all dates in the other range */ public boolean encloses(LocalDateRange other) { ArgChecker.notNull(other, "other"); // start <= other.start && endExclusive >= other.endExclusive return start.compareTo(other.start) <= 0 && endExclusive.compareTo(other.endExclusive) >= 0; } /** * Checks if this range overlaps any dates in the specified range. * <p> * This checks that the two ranges overlap. * An empty range overlaps at dates where any date is in common. * Thus [2014-06-20,2014-06-25) overlaps both [2014-06-20,2014-06-20) and [2014-06-25,2014-06-25). * * @param other the other range to check for * @return true if this range contains all dates in the other range */ public boolean overlaps(LocalDateRange other) { ArgChecker.notNull(other, "other"); // start <= other.endExclusive && endExclusive >= other.start return start.compareTo(other.endExclusive) <= 0 && endExclusive.compareTo(other.start) >= 0; } /** * Calculates the range that is the intersection of this range and the specified range. * <p> * This finds the intersection of two ranges. * This returns an exception if the two ranges do not {@linkplain #overlaps(LocalDateRange) overlap}. * <p> * If the two ranges are adjacent but have no whole dates in common, an empty range is returned. * Thus the intersection of [2014-06-20,2014-06-25) and [2014-06-25,2014-06-30) is [2014-06-25,2014-06-25). * * @param other the other range to check for * @return the range that is the intersection of the two ranges * @throws IllegalArgumentException if the ranges do not overlap */ public LocalDateRange intersection(LocalDateRange other) { ArgChecker.notNull(other, "other"); if (overlaps(other) == false) { throw new IllegalArgumentException("Ranges do not overlap: " + this + " and " + other); } int cmpStart = start.compareTo(other.start); int cmpEnd = endExclusive.compareTo(other.endExclusive); if (cmpStart >= 0 && cmpEnd <= 0) { return this; } else if (cmpStart <= 0 && cmpEnd >= 0) { return other; } else { LocalDate newStart = (cmpStart >= 0 ? start : other.start); LocalDate newEnd = (cmpEnd <= 0 ? endExclusive : other.endExclusive); return LocalDateRange.of(newStart, newEnd); } } /** * Calculates the range that is the union of this range and the specified range. * <p> * This finds the union of two ranges. * This returns an exception if the two ranges do not {@linkplain #overlaps(LocalDateRange) overlap}. * <p> * If the two ranges are adjacent but have no whole dates in common, the union is still returned. * Thus the union of [2014-06-20,2014-06-25) and [2014-06-25,2014-06-30) is [2014-06-20,2014-06-30). * * @param other the other range to check for * @return the range that is the union of the two ranges * @throws IllegalArgumentException if the ranges do not overlap */ public LocalDateRange union(LocalDateRange other) { ArgChecker.notNull(other, "other"); if (overlaps(other) == false) { throw new IllegalArgumentException("Ranges do not overlap: " + this + " and " + other); } int cmpStart = start.compareTo(other.start); int cmpEnd = endExclusive.compareTo(other.endExclusive); if (cmpStart >= 0 && cmpEnd <= 0) { return other; } else if (cmpStart <= 0 && cmpEnd >= 0) { return this; } else { LocalDate newStart = (cmpStart >= 0 ? other.start : start); LocalDate newEnd = (cmpEnd <= 0 ? other.endExclusive : endExclusive); return LocalDateRange.of(newStart, newEnd); } } //------------------------------------------------------------------------- /** * Streams the set of dates included in the range. * <p> * This returns a stream consisting of each date in the range. * The stream is ordered. * * @return the stream of dates from the start to the end */ public Stream<LocalDate> stream() { Iterator<LocalDate> it = new Iterator<LocalDate>() { private LocalDate current = start; @Override public LocalDate next() { LocalDate result = current; current = current.plusDays(1); return result; } @Override public boolean hasNext() { return current.isBefore(endExclusive); } }; long count = endExclusive.toEpochDay() - start.toEpochDay() + 1; Spliterator<LocalDate> spliterator = Spliterators.spliterator(it, count, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.SORTED | Spliterator.SIZED | Spliterator.SUBSIZED); return StreamSupport.stream(spliterator, false); } //------------------------------------------------------------------------- /** * Checks if this range is entirely before the specified range. * * @param other the other range to check for * @return true if every date in this range is before every date in the other range */ public boolean isBefore(LocalDateRange other) { ArgChecker.notNull(other, "other"); if (isEmpty() && this.equals(other)) { return false; } // endExclusive <= other.start return endExclusive.compareTo(other.start) <= 0; } /** * Checks if this range is entirely after the specified range. * * @param other the other range to check for * @return true if every date in this range is after every date in the other range */ public boolean isAfter(LocalDateRange other) { ArgChecker.notNull(other, "other"); if (isEmpty() && this.equals(other)) { return false; } // start >= other.endExclusive return start.compareTo(other.endExclusive) >= 0; } //------------------------------------------------------------------------- /** * Checks if this range equals another. * * @param obj the other object * @return true if equal */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof LocalDateRange) { LocalDateRange other = (LocalDateRange) obj; return start.equals(other.start) && endExclusive.equals(other.endExclusive); } return false; } /** * Returns a suitable hash code. * * @return the hash code */ @Override public int hashCode() { return start.hashCode() ^ endExclusive.hashCode(); } /** * Returns this range as a string, such as {@code [2009-12-03,2014-06-30)}. * <p> * The string will be one of these formats:<br /> * {@code [2009-12-03,2014-06-30)}<br /> * {@code [2009-12-03,+INFINITY]} - if the end is unbounded<br /> * {@code [-INFINITY,2014-06-30)} - if the start is unbounded<br /> * {@code [-INFINITY,+INFINITY]} - if the start and end are unbounded<br /> * * @return the standard string */ @Override public String toString() { StringBuilder buf = new StringBuilder(23); if (isUnboundedStart()) { buf.append("[-INFINITY,"); } else { buf.append('[').append(start).append(','); } if (isUnboundedEnd()) { buf.append("+INFINITY]"); } else { buf.append(endExclusive).append(')'); } return buf.toString(); } //------------------------- AUTOGENERATED START ------------------------- ///CLOVER:OFF /** * The meta-bean for {@code LocalDateRange}. * @return the meta-bean, not null */ public static LocalDateRange.Meta meta() { return LocalDateRange.Meta.INSTANCE; } static { JodaBeanUtils.registerMetaBean(LocalDateRange.Meta.INSTANCE); } /** * The serialization version id. */ private static final long serialVersionUID = 1L; private LocalDateRange( LocalDate start, LocalDate endExclusive) { JodaBeanUtils.notNull(start, "start"); JodaBeanUtils.notNull(endExclusive, "endExclusive"); this.start = start; this.endExclusive = endExclusive; validate(); } @Override public LocalDateRange.Meta metaBean() { return LocalDateRange.Meta.INSTANCE; } @Override public <R> Property<R> property(String propertyName) { return metaBean().<R>metaProperty(propertyName).createProperty(this); } @Override public Set<String> propertyNames() { return metaBean().metaPropertyMap().keySet(); } //----------------------------------------------------------------------- /** * The meta-bean for {@code LocalDateRange}. */ public static final class Meta extends DirectMetaBean { /** * The singleton instance of the meta-bean. */ static final Meta INSTANCE = new Meta(); /** * The meta-property for the {@code start} property. */ private final MetaProperty<LocalDate> start = DirectMetaProperty.ofImmutable( this, "start", LocalDateRange.class, LocalDate.class); /** * The meta-property for the {@code endExclusive} property. */ private final MetaProperty<LocalDate> endExclusive = DirectMetaProperty.ofImmutable( this, "endExclusive", LocalDateRange.class, LocalDate.class); /** * The meta-properties. */ private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap( this, null, "start", "endExclusive"); /** * Restricted constructor. */ private Meta() { } @Override protected MetaProperty<?> metaPropertyGet(String propertyName) { switch (propertyName.hashCode()) { case 109757538: // start return start; case 1275403267: // endExclusive return endExclusive; } return super.metaPropertyGet(propertyName); } @Override public BeanBuilder<? extends LocalDateRange> builder() { return new LocalDateRange.Builder(); } @Override public Class<? extends LocalDateRange> beanType() { return LocalDateRange.class; } @Override public Map<String, MetaProperty<?>> metaPropertyMap() { return metaPropertyMap$; } //----------------------------------------------------------------------- /** * The meta-property for the {@code start} property. * @return the meta-property, not null */ public MetaProperty<LocalDate> start() { return start; } /** * The meta-property for the {@code endExclusive} property. * @return the meta-property, not null */ public MetaProperty<LocalDate> endExclusive() { return endExclusive; } //----------------------------------------------------------------------- @Override protected Object propertyGet(Bean bean, String propertyName, boolean quiet) { switch (propertyName.hashCode()) { case 109757538: // start return ((LocalDateRange) bean).getStart(); case 1275403267: // endExclusive return ((LocalDateRange) bean).getEndExclusive(); } return super.propertyGet(bean, propertyName, quiet); } @Override protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) { metaProperty(propertyName); if (quiet) { return; } throw new UnsupportedOperationException("Property cannot be written: " + propertyName); } } //----------------------------------------------------------------------- /** * The bean-builder for {@code LocalDateRange}. */ private static final class Builder extends DirectFieldsBeanBuilder<LocalDateRange> { private LocalDate start; private LocalDate endExclusive; /** * Restricted constructor. */ private Builder() { } //----------------------------------------------------------------------- @Override public Object get(String propertyName) { switch (propertyName.hashCode()) { case 109757538: // start return start; case 1275403267: // endExclusive return endExclusive; default: throw new NoSuchElementException("Unknown property: " + propertyName); } } @Override public Builder set(String propertyName, Object newValue) { switch (propertyName.hashCode()) { case 109757538: // start this.start = (LocalDate) newValue; break; case 1275403267: // endExclusive this.endExclusive = (LocalDate) newValue; break; default: throw new NoSuchElementException("Unknown property: " + propertyName); } return this; } @Override public Builder set(MetaProperty<?> property, Object value) { super.set(property, value); return this; } @Override public Builder setString(String propertyName, String value) { setString(meta().metaProperty(propertyName), value); return this; } @Override public Builder setString(MetaProperty<?> property, String value) { super.setString(property, value); return this; } @Override public Builder setAll(Map<String, ? extends Object> propertyValueMap) { super.setAll(propertyValueMap); return this; } @Override public LocalDateRange build() { return new LocalDateRange( start, endExclusive); } //----------------------------------------------------------------------- @Override public String toString() { StringBuilder buf = new StringBuilder(96); buf.append("LocalDateRange.Builder{"); buf.append("start").append('=').append(JodaBeanUtils.toString(start)).append(',').append(' '); buf.append("endExclusive").append('=').append(JodaBeanUtils.toString(endExclusive)); buf.append('}'); return buf.toString(); } } ///CLOVER:ON //-------------------------- AUTOGENERATED END -------------------------- }