/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.basics.schedule; import static com.opengamma.strata.collect.Guavate.toImmutableList; import java.io.Serializable; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import org.joda.beans.Bean; import org.joda.beans.BeanDefinition; import org.joda.beans.ImmutableBean; 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.google.common.collect.ImmutableList; import com.opengamma.strata.basics.date.DateAdjuster; import com.opengamma.strata.basics.date.DayCount.ScheduleInfo; import com.opengamma.strata.collect.ArgChecker; /** * A complete schedule of periods (date ranges), with both unadjusted and adjusted dates. * <p> * The schedule consists of one or more adjacent periods (date ranges). * This is typically used as the basis for financial calculations, such as accrual of interest. * <p> * It is recommended to create a {@link Schedule} using a {@link PeriodicSchedule}. */ @BeanDefinition public final class Schedule implements ScheduleInfo, ImmutableBean, Serializable { /** * The schedule periods. * <p> * There will be at least one period. * The periods are ordered from earliest to latest. * It is intended that each period is adjacent to the next one, however each * period is independent and non-adjacent periods are allowed. */ @PropertyDefinition(validate = "notEmpty") private final ImmutableList<SchedulePeriod> periods; /** * The periodic frequency used when building the schedule. * <p> * If the schedule was not built from a regular periodic frequency, * then the frequency should be a suitable estimate. */ @PropertyDefinition(validate = "notNull", overrideGet = true) private final Frequency frequency; /** * The roll convention used when building the schedule. * <p> * If the schedule was not built from a regular periodic frequency, then the convention should be 'None'. */ @PropertyDefinition(validate = "notNull") private final RollConvention rollConvention; //------------------------------------------------------------------------- /** * Obtains a 'Term' instance based on a single period. * <p> * A 'Term' schedule has one period with a frequency of 'Term'. * * @param period the single period * @return the merged 'Term' schedule */ public static Schedule ofTerm(SchedulePeriod period) { ArgChecker.notNull(period, "period"); return Schedule.builder() .periods(ImmutableList.of(period)) .frequency(Frequency.TERM) .rollConvention(RollConventions.NONE) .build(); } //------------------------------------------------------------------------- /** * Gets the number of periods in the schedule. * <p> * This returns the number of periods, which will be at least one. * * @return the number of periods */ public int size() { return periods.size(); } /** * Checks if this schedule represents a single 'Term' period. * <p> * A 'Term' schedule has one period. * * @return true if this is a 'Term' schedule */ public boolean isTerm() { return size() == 1; } //------------------------------------------------------------------------- /** * Gets a schedule period by index. * <p> * This returns a period using a zero-based index. * * @param index the zero-based period index * @return the schedule period * @throws IndexOutOfBoundsException if the index is invalid */ public SchedulePeriod getPeriod(int index) { return periods.get(index); } /** * Gets the first schedule period. * * @return the first schedule period */ public SchedulePeriod getFirstPeriod() { return periods.get(0); } /** * Gets the last schedule period. * * @return the last schedule period */ public SchedulePeriod getLastPeriod() { return periods.get(periods.size() - 1); } //------------------------------------------------------------------------- /** * Gets the start date of the schedule. * <p> * The first date in the schedule, typically treated as inclusive. * If the schedule adjusts for business days, then this is the adjusted date. * * @return the schedule start date */ @Override public LocalDate getStartDate() { return getFirstPeriod().getStartDate(); } /** * Gets the end date of the schedule. * <p> * The last date in the schedule, typically treated as exclusive. * If the schedule adjusts for business days, then this is the adjusted date. * * @return the schedule end date */ @Override public LocalDate getEndDate() { return getLastPeriod().getEndDate(); } //------------------------------------------------------------------------- /** * Gets the initial stub if it exists. * <p> * There is an initial stub if there is more than one period and the first * period is a stub. * * @return the initial stub, empty if no initial stub */ public Optional<SchedulePeriod> getInitialStub() { return (isInitialStub() ? Optional.of(getFirstPeriod()) : Optional.empty()); } // checks if there is an initial stub private boolean isInitialStub() { return !isTerm() && !getFirstPeriod().isRegular(frequency, rollConvention); } /** * Gets the final stub if it exists. * <p> * There is a final stub if there is more than one period and the last * period is a stub. * * @return the final stub, empty if no final stub */ public Optional<SchedulePeriod> getFinalStub() { return (isFinalStub() ? Optional.of(getLastPeriod()) : Optional.empty()); } // checks if there is a final stub private boolean isFinalStub() { return !isTerm() && !getLastPeriod().isRegular(frequency, rollConvention); } /** * Gets the regular schedule periods. * <p> * The regular periods exclude any initial or final stub. * In most cases, the periods returned will be regular, corresponding to the periodic * frequency and roll convention, however there are cases when this is not true. * See {@link SchedulePeriod#isRegular(Frequency, RollConvention)}. * * @return the non-stub schedule periods */ public ImmutableList<SchedulePeriod> getRegularPeriods() { if (isTerm()) { return periods; } int startStub = isInitialStub() ? 1 : 0; int endStub = isFinalStub() ? 1 : 0; return (startStub == 0 && endStub == 0 ? periods : periods.subList(startStub, periods.size() - endStub)); } //------------------------------------------------------------------------- /** * Checks if the end of month convention is in use. * <p> * If true then when building a schedule, dates will be at the end-of-month if the * first date in the series is at the end-of-month. * * @return true if the end of month convention is in use */ @Override public boolean isEndOfMonthConvention() { return rollConvention == RollConventions.EOM; } /** * Finds the period end date given a date in the period. * <p> * The first matching period is returned. * The adjusted start and end dates of each period are used in the comparison. * The start date is included, the end date is excluded. * * @param date the date to find * @return the end date of the period that includes the specified date */ @Override public LocalDate getPeriodEndDate(LocalDate date) { return periods.stream() .filter(p -> p.contains(date)) .map(p -> p.getEndDate()) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Date is not contained in any period")); } //------------------------------------------------------------------------- /** * Merges this schedule to form a new schedule with a single 'Term' period. * <p> * The result will have one period of type 'Term', with dates matching this schedule. * * @return the merged 'Term' schedule */ public Schedule mergeToTerm() { if (isTerm()) { return this; } SchedulePeriod first = getFirstPeriod(); SchedulePeriod last = getLastPeriod(); return Schedule.ofTerm(SchedulePeriod.of( first.getStartDate(), last.getEndDate(), first.getUnadjustedStartDate(), last.getUnadjustedEndDate())); } /** * Merges this schedule to form a new schedule by combining the regular schedule periods. * <p> * This produces a schedule where some periods are merged together. * For example, this could be used to convert a 3 monthly schedule into a 6 monthly schedule. * <p> * The merging is controlled by the group size, which defines the number of periods * to merge together in the result. For example, to convert a 3 monthly schedule into * a 6 monthly schedule the group size would be 2 (6 divided by 3). * <p> * A group size of zero or less will throw an exception. * A group size of 1 will return this schedule. * A larger group size will return a schedule where each group of regular periods are merged. * The roll flag is used to determine the direction in which grouping occurs. * <p> * Any existing stub periods are considered to be special, and are not merged. * Even if the grouping results in an excess period, such as 10 periods with a group size * of 3, the excess period will not be merged with a stub. * <p> * If this period is a 'Term' period, this schedule is returned. * * @param groupSize the group size * @param rollForwards whether to roll forwards (true) or backwards (false) * @return the merged schedule * @throws IllegalArgumentException if the group size is zero or less */ public Schedule mergeRegular(int groupSize, boolean rollForwards) { ArgChecker.notNegativeOrZero(groupSize, "groupSize"); if (isTerm() || groupSize == 1) { return this; } List<SchedulePeriod> newSchedule = new ArrayList<>(); // retain initial stub Optional<SchedulePeriod> initialStub = getInitialStub(); if (initialStub.isPresent()) { newSchedule.add(initialStub.get()); } // merge regular, handling stubs via min/max ImmutableList<SchedulePeriod> regularPeriods = getRegularPeriods(); int regularSize = regularPeriods.size(); int remainder = regularSize % groupSize; int startIndex = (rollForwards || remainder == 0 ? 0 : -(groupSize - remainder)); for (int i = startIndex; i < regularSize; i += groupSize) { int from = Math.max(i, 0); int to = Math.min(i + groupSize, regularSize); newSchedule.add(createSchedulePeriod(regularPeriods.subList(from, to))); } // retain final stub Optional<SchedulePeriod> finalStub = getFinalStub(); if (finalStub.isPresent()) { newSchedule.add(finalStub.get()); } // build schedule return Schedule.builder() .periods(newSchedule) .frequency(Frequency.of(frequency.getPeriod().multipliedBy(groupSize))) .rollConvention(rollConvention) .build(); } // creates a schedule period private SchedulePeriod createSchedulePeriod(List<SchedulePeriod> accruals) { SchedulePeriod first = accruals.get(0); if (accruals.size() == 1) { return first; } SchedulePeriod last = accruals.get(accruals.size() - 1); return SchedulePeriod.of( first.getStartDate(), last.getEndDate(), first.getUnadjustedStartDate(), last.getUnadjustedEndDate()); } //------------------------------------------------------------------------- /** * Converts this schedule to a schedule where all the start and end dates are * adjusted using the specified adjuster. * <p> * The result will have the same number of periods, but each start date and * end date is replaced by the adjusted date as returned by the adjuster. * The unadjusted start date and unadjusted end date of each period will not be changed. * * @param adjuster the adjuster to use * @return the adjusted schedule */ public Schedule toAdjusted(DateAdjuster adjuster) { // implementation needs to return 'this' if unchanged to optimize downstream code boolean adjusted = false; ImmutableList.Builder<SchedulePeriod> builder = ImmutableList.builder(); for (SchedulePeriod period : periods) { SchedulePeriod adjPeriod = period.toAdjusted(adjuster); builder.add(adjPeriod); adjusted |= (adjPeriod != period); } return adjusted ? new Schedule(builder.build(), frequency, rollConvention) : this; } //------------------------------------------------------------------------- /** * Converts this schedule to a schedule where every adjusted date is reset * to the unadjusted equivalent. * <p> * The result will have the same number of periods, but each start date and * end date is replaced by the matching unadjusted start or end date. * * @return the equivalent unadjusted schedule */ public Schedule toUnadjusted() { return toBuilder() .periods(periods.stream() .map(p -> p.toUnadjusted()) .collect(toImmutableList())) .build(); } //------------------------- AUTOGENERATED START ------------------------- ///CLOVER:OFF /** * The meta-bean for {@code Schedule}. * @return the meta-bean, not null */ public static Schedule.Meta meta() { return Schedule.Meta.INSTANCE; } static { JodaBeanUtils.registerMetaBean(Schedule.Meta.INSTANCE); } /** * The serialization version id. */ private static final long serialVersionUID = 1L; /** * Returns a builder used to create an instance of the bean. * @return the builder, not null */ public static Schedule.Builder builder() { return new Schedule.Builder(); } private Schedule( List<SchedulePeriod> periods, Frequency frequency, RollConvention rollConvention) { JodaBeanUtils.notEmpty(periods, "periods"); JodaBeanUtils.notNull(frequency, "frequency"); JodaBeanUtils.notNull(rollConvention, "rollConvention"); this.periods = ImmutableList.copyOf(periods); this.frequency = frequency; this.rollConvention = rollConvention; } @Override public Schedule.Meta metaBean() { return Schedule.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(); } //----------------------------------------------------------------------- /** * Gets the schedule periods. * <p> * There will be at least one period. * The periods are ordered from earliest to latest. * It is intended that each period is adjacent to the next one, however each * period is independent and non-adjacent periods are allowed. * @return the value of the property, not empty */ public ImmutableList<SchedulePeriod> getPeriods() { return periods; } //----------------------------------------------------------------------- /** * Gets the periodic frequency used when building the schedule. * <p> * If the schedule was not built from a regular periodic frequency, * then the frequency should be a suitable estimate. * @return the value of the property, not null */ @Override public Frequency getFrequency() { return frequency; } //----------------------------------------------------------------------- /** * Gets the roll convention used when building the schedule. * <p> * If the schedule was not built from a regular periodic frequency, then the convention should be 'None'. * @return the value of the property, not null */ public RollConvention getRollConvention() { return rollConvention; } //----------------------------------------------------------------------- /** * Returns a builder that allows this bean to be mutated. * @return the mutable builder, not null */ public Builder toBuilder() { return new Builder(this); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj.getClass() == this.getClass()) { Schedule other = (Schedule) obj; return JodaBeanUtils.equal(periods, other.periods) && JodaBeanUtils.equal(frequency, other.frequency) && JodaBeanUtils.equal(rollConvention, other.rollConvention); } return false; } @Override public int hashCode() { int hash = getClass().hashCode(); hash = hash * 31 + JodaBeanUtils.hashCode(periods); hash = hash * 31 + JodaBeanUtils.hashCode(frequency); hash = hash * 31 + JodaBeanUtils.hashCode(rollConvention); return hash; } @Override public String toString() { StringBuilder buf = new StringBuilder(128); buf.append("Schedule{"); buf.append("periods").append('=').append(periods).append(',').append(' '); buf.append("frequency").append('=').append(frequency).append(',').append(' '); buf.append("rollConvention").append('=').append(JodaBeanUtils.toString(rollConvention)); buf.append('}'); return buf.toString(); } //----------------------------------------------------------------------- /** * The meta-bean for {@code Schedule}. */ 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 periods} property. */ @SuppressWarnings({"unchecked", "rawtypes" }) private final MetaProperty<ImmutableList<SchedulePeriod>> periods = DirectMetaProperty.ofImmutable( this, "periods", Schedule.class, (Class) ImmutableList.class); /** * The meta-property for the {@code frequency} property. */ private final MetaProperty<Frequency> frequency = DirectMetaProperty.ofImmutable( this, "frequency", Schedule.class, Frequency.class); /** * The meta-property for the {@code rollConvention} property. */ private final MetaProperty<RollConvention> rollConvention = DirectMetaProperty.ofImmutable( this, "rollConvention", Schedule.class, RollConvention.class); /** * The meta-properties. */ private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap( this, null, "periods", "frequency", "rollConvention"); /** * Restricted constructor. */ private Meta() { } @Override protected MetaProperty<?> metaPropertyGet(String propertyName) { switch (propertyName.hashCode()) { case -678739246: // periods return periods; case -70023844: // frequency return frequency; case -10223666: // rollConvention return rollConvention; } return super.metaPropertyGet(propertyName); } @Override public Schedule.Builder builder() { return new Schedule.Builder(); } @Override public Class<? extends Schedule> beanType() { return Schedule.class; } @Override public Map<String, MetaProperty<?>> metaPropertyMap() { return metaPropertyMap$; } //----------------------------------------------------------------------- /** * The meta-property for the {@code periods} property. * @return the meta-property, not null */ public MetaProperty<ImmutableList<SchedulePeriod>> periods() { return periods; } /** * The meta-property for the {@code frequency} property. * @return the meta-property, not null */ public MetaProperty<Frequency> frequency() { return frequency; } /** * The meta-property for the {@code rollConvention} property. * @return the meta-property, not null */ public MetaProperty<RollConvention> rollConvention() { return rollConvention; } //----------------------------------------------------------------------- @Override protected Object propertyGet(Bean bean, String propertyName, boolean quiet) { switch (propertyName.hashCode()) { case -678739246: // periods return ((Schedule) bean).getPeriods(); case -70023844: // frequency return ((Schedule) bean).getFrequency(); case -10223666: // rollConvention return ((Schedule) bean).getRollConvention(); } 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 Schedule}. */ public static final class Builder extends DirectFieldsBeanBuilder<Schedule> { private List<SchedulePeriod> periods = ImmutableList.of(); private Frequency frequency; private RollConvention rollConvention; /** * Restricted constructor. */ private Builder() { } /** * Restricted copy constructor. * @param beanToCopy the bean to copy from, not null */ private Builder(Schedule beanToCopy) { this.periods = beanToCopy.getPeriods(); this.frequency = beanToCopy.getFrequency(); this.rollConvention = beanToCopy.getRollConvention(); } //----------------------------------------------------------------------- @Override public Object get(String propertyName) { switch (propertyName.hashCode()) { case -678739246: // periods return periods; case -70023844: // frequency return frequency; case -10223666: // rollConvention return rollConvention; default: throw new NoSuchElementException("Unknown property: " + propertyName); } } @SuppressWarnings("unchecked") @Override public Builder set(String propertyName, Object newValue) { switch (propertyName.hashCode()) { case -678739246: // periods this.periods = (List<SchedulePeriod>) newValue; break; case -70023844: // frequency this.frequency = (Frequency) newValue; break; case -10223666: // rollConvention this.rollConvention = (RollConvention) 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 Schedule build() { return new Schedule( periods, frequency, rollConvention); } //----------------------------------------------------------------------- /** * Sets the schedule periods. * <p> * There will be at least one period. * The periods are ordered from earliest to latest. * It is intended that each period is adjacent to the next one, however each * period is independent and non-adjacent periods are allowed. * @param periods the new value, not empty * @return this, for chaining, not null */ public Builder periods(List<SchedulePeriod> periods) { JodaBeanUtils.notEmpty(periods, "periods"); this.periods = periods; return this; } /** * Sets the {@code periods} property in the builder * from an array of objects. * @param periods the new value, not empty * @return this, for chaining, not null */ public Builder periods(SchedulePeriod... periods) { return periods(ImmutableList.copyOf(periods)); } /** * Sets the periodic frequency used when building the schedule. * <p> * If the schedule was not built from a regular periodic frequency, * then the frequency should be a suitable estimate. * @param frequency the new value, not null * @return this, for chaining, not null */ public Builder frequency(Frequency frequency) { JodaBeanUtils.notNull(frequency, "frequency"); this.frequency = frequency; return this; } /** * Sets the roll convention used when building the schedule. * <p> * If the schedule was not built from a regular periodic frequency, then the convention should be 'None'. * @param rollConvention the new value, not null * @return this, for chaining, not null */ public Builder rollConvention(RollConvention rollConvention) { JodaBeanUtils.notNull(rollConvention, "rollConvention"); this.rollConvention = rollConvention; return this; } //----------------------------------------------------------------------- @Override public String toString() { StringBuilder buf = new StringBuilder(128); buf.append("Schedule.Builder{"); buf.append("periods").append('=').append(JodaBeanUtils.toString(periods)).append(',').append(' '); buf.append("frequency").append('=').append(JodaBeanUtils.toString(frequency)).append(',').append(' '); buf.append("rollConvention").append('=').append(JodaBeanUtils.toString(rollConvention)); buf.append('}'); return buf.toString(); } } ///CLOVER:ON //-------------------------- AUTOGENERATED END -------------------------- }