/**
* Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.basics.date;
import static com.opengamma.strata.basics.date.LocalDateUtils.plusDays;
import java.io.Serializable;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeParseException;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.util.List;
import org.joda.convert.FromString;
import org.joda.convert.ToString;
import com.opengamma.strata.collect.ArgChecker;
/**
* A tenor indicating how long it will take for a financial instrument to reach maturity.
* <p>
* A tenor is allowed to be any non-negative non-zero period of days, weeks, month or years.
* This class provides constants for common tenors which are best used by static import.
* <p>
* Each tenor is based on a {@link Period}. The months and years of the period are not normalized,
* thus it is possible to have a tenor of 12 months and a different one of 1 year.
* When used, standard date addition rules apply, thus there is no difference between them.
* Call {@link #normalized()} to apply normalization.
*
* <h4>Usage</h4>
* {@code Tenor} implements {@code TemporalAmount} allowing it to be directly added to a date:
* <pre>
* LocalDate later = baseDate.plus(tenor);
* </pre>
*/
public final class Tenor
implements Comparable<Tenor>, TemporalAmount, Serializable {
/**
* Serialization version.
*/
private static final long serialVersionUID = 1;
/**
* A tenor of one day.
*/
public static final Tenor TENOR_1D = ofDays(1);
/**
* A tenor of two days.
*/
public static final Tenor TENOR_2D = ofDays(2);
/**
* A tenor of three days.
*/
public static final Tenor TENOR_3D = ofDays(3);
/**
* A tenor of 1 week.
*/
public static final Tenor TENOR_1W = ofWeeks(1);
/**
* A tenor of 2 weeks.
*/
public static final Tenor TENOR_2W = ofWeeks(2);
/**
* A tenor of 3 weeks.
*/
public static final Tenor TENOR_3W = ofWeeks(3);
/**
* A tenor of 4 weeks.
*/
public static final Tenor TENOR_4W = ofWeeks(4);
/**
* A tenor of 6 weeks.
*/
public static final Tenor TENOR_6W = ofWeeks(6);
/**
* A tenor of 1 month.
*/
public static final Tenor TENOR_1M = ofMonths(1);
/**
* A tenor of 2 months.
*/
public static final Tenor TENOR_2M = ofMonths(2);
/**
* A tenor of 3 months.
*/
public static final Tenor TENOR_3M = ofMonths(3);
/**
* A tenor of 4 months.
*/
public static final Tenor TENOR_4M = ofMonths(4);
/**
* A tenor of 5 months.
*/
public static final Tenor TENOR_5M = ofMonths(5);
/**
* A tenor of 6 months.
*/
public static final Tenor TENOR_6M = ofMonths(6);
/**
* A tenor of 7 months.
*/
public static final Tenor TENOR_7M = ofMonths(7);
/**
* A tenor of 8 months.
*/
public static final Tenor TENOR_8M = ofMonths(8);
/**
* A tenor of 9 months.
*/
public static final Tenor TENOR_9M = ofMonths(9);
/**
* A tenor of 10 months.
*/
public static final Tenor TENOR_10M = ofMonths(10);
/**
* A tenor of 11 months.
*/
public static final Tenor TENOR_11M = ofMonths(11);
/**
* A tenor of 12 months.
*/
public static final Tenor TENOR_12M = ofMonths(12);
/**
* A tenor of 18 months.
*/
public static final Tenor TENOR_18M = ofMonths(18);
/**
* A tenor of 1 year.
*/
public static final Tenor TENOR_1Y = ofYears(1);
/**
* A tenor of 2 years.
*/
public static final Tenor TENOR_2Y = ofYears(2);
/**
* A tenor of 3 years.
*/
public static final Tenor TENOR_3Y = ofYears(3);
/**
* A tenor of 4 years.
*/
public static final Tenor TENOR_4Y = ofYears(4);
/**
* A tenor of 5 years.
*/
public static final Tenor TENOR_5Y = ofYears(5);
/**
* A tenor of 6 years.
*/
public static final Tenor TENOR_6Y = ofYears(6);
/**
* A tenor of 7 years.
*/
public static final Tenor TENOR_7Y = ofYears(7);
/**
* A tenor of 8 years.
*/
public static final Tenor TENOR_8Y = ofYears(8);
/**
* A tenor of 9 years.
*/
public static final Tenor TENOR_9Y = ofYears(9);
/**
* A tenor of 10 years.
*/
public static final Tenor TENOR_10Y = ofYears(10);
/**
* A tenor of 12 years.
*/
public static final Tenor TENOR_12Y = ofYears(12);
/**
* A tenor of 15 years.
*/
public static final Tenor TENOR_15Y = ofYears(15);
/**
* A tenor of 20 years.
*/
public static final Tenor TENOR_20Y = ofYears(20);
/**
* A tenor of 25 years.
*/
public static final Tenor TENOR_25Y = ofYears(25);
/**
* A tenor of 30 years.
*/
public static final Tenor TENOR_30Y = ofYears(30);
/**
* The period of the tenor.
*/
private final Period period;
/**
* The name of the tenor.
*/
private final String name;
//-------------------------------------------------------------------------
/**
* Obtains an instance from a {@code Period}.
* <p>
* The period normally consists of either days and weeks, or months and years.
* It must also be positive and non-zero.
* <p>
* If the number of days is an exact multiple of 7 it will be converted to weeks.
* Months are not normalized into years.
*
* @param period the period to convert to a tenor
* @return the tenor
* @throws IllegalArgumentException if the period is negative or zero
*/
public static Tenor of(Period period) {
int days = period.getDays();
long months = period.toTotalMonths();
if (months == 0 && days != 0) {
return ofDays(days);
}
return new Tenor(period, period.toString().substring(1));
}
/**
* Obtains an instance backed by a period of days.
* <p>
* If the number of days is an exact multiple of 7 it will be converted to weeks.
*
* @param days the number of days
* @return the tenor
* @throws IllegalArgumentException if days is negative or zero
*/
public static Tenor ofDays(int days) {
if (days % 7 == 0) {
return ofWeeks(days / 7);
}
return new Tenor(Period.ofDays(days), days + "D");
}
/**
* Obtains an instance backed by a period of weeks.
*
* @param weeks the number of weeks
* @return the tenor
* @throws IllegalArgumentException if weeks is negative or zero
*/
public static Tenor ofWeeks(int weeks) {
return new Tenor(Period.ofWeeks(weeks), weeks + "W");
}
/**
* Obtains an instance backed by a period of months.
* <p>
* Months are not normalized into years.
*
* @param months the number of months
* @return the tenor
* @throws IllegalArgumentException if months is negative or zero
*/
public static Tenor ofMonths(int months) {
return new Tenor(Period.ofMonths(months), months + "M");
}
/**
* Obtains an instance backed by a period of years.
*
* @param years the number of years
* @return the tenor
* @throws IllegalArgumentException if years is negative or zero
*/
public static Tenor ofYears(int years) {
return new Tenor(Period.ofYears(years), years + "Y");
}
//-------------------------------------------------------------------------
/**
* Parses a formatted string representing the tenor.
* <p>
* The format can either be based on ISO-8601, such as 'P3M'
* or without the 'P' prefix e.g. '2W'.
*
* @param toParse the string representing the tenor
* @return the tenor
* @throws IllegalArgumentException if the tenor cannot be parsed
*/
@FromString
public static Tenor parse(String toParse) {
String prefixed = toParse.startsWith("P") ? toParse : "P" + toParse;
try {
return Tenor.of(Period.parse(prefixed));
} catch (DateTimeParseException ex) {
throw new IllegalArgumentException(ex);
}
}
//-------------------------------------------------------------------------
/**
* Creates a tenor.
*
* @param period the period to represent
* @param name the name
*/
private Tenor(Period period, String name) {
ArgChecker.notNull(period, "period");
ArgChecker.isFalse(period.isZero(), "Period must not be zero");
ArgChecker.isFalse(period.isNegative(), "Period must not be negative");
this.period = period;
this.name = name;
}
// safe deserialization
private Object readResolve() {
return new Tenor(period, name);
}
//-------------------------------------------------------------------------
/**
* Gets the underlying period of the tenor.
*
* @return the period
*/
public Period getPeriod() {
return period;
}
//-------------------------------------------------------------------------
/**
* Normalizes the months and years of this tenor.
* <p>
* This method returns a normalized tenor of an equivalent length.
* If the period is exactly 1 year then the result will be expressed as 12 months.
* Otherwise, the result will be expressed using {@link Period#normalized()}.
*
* @return the normalized tenor
*/
public Tenor normalized() {
if (period.getDays() == 0 && period.toTotalMonths() == 12) {
return TENOR_12M;
}
Period norm = period.normalized();
return (norm != period ? Tenor.of(norm) : this);
}
//-------------------------------------------------------------------------
/**
* Checks if the tenor is week-based.
* <p>
* A week-based tenor consists of an integral number of weeks.
* There must be no day, month or year element.
*
* @return true if this is week-based
*/
public boolean isWeekBased() {
return period.toTotalMonths() == 0 && period.getDays() % 7 == 0;
}
/**
* Checks if the tenor is month-based.
* <p>
* A month-based tenor consists of an integral number of months.
* Any year-based tenor is also counted as month-based.
* There must be no day or week element.
*
* @return true if this is month-based
*/
public boolean isMonthBased() {
return period.toTotalMonths() > 0 && period.getDays() == 0;
}
//-------------------------------------------------------------------------
/**
* Gets the value of the specified unit.
* <p>
* This will return a value for the years, months and days units.
* Note that weeks are not included.
* All other units throw an exception.
* <p>
* This method implements {@link TemporalAmount}.
* It is not intended to be called directly.
*
* @param unit the unit to query
* @return the value of the unit
* @throws UnsupportedTemporalTypeException if the unit is not supported
*/
@Override
public long get(TemporalUnit unit) {
return period.get(unit);
}
/**
* Gets the units supported by a tenor.
* <p>
* This returns a list containing years, months and days.
* Note that weeks are not included.
* <p>
* This method implements {@link TemporalAmount}.
* It is not intended to be called directly.
*
* @return a list containing the years, months and days units
*/
@Override
public List<TemporalUnit> getUnits() {
return period.getUnits();
}
/**
* Adds this tenor to the specified date.
* <p>
* This method implements {@link TemporalAmount}.
* It is not intended to be called directly.
* Use {@link LocalDate#plus(TemporalAmount)} instead.
*
* @param temporal the temporal object to add to
* @return the result with this tenor added
* @throws DateTimeException if unable to add
* @throws ArithmeticException if numeric overflow occurs
*/
@Override
public Temporal addTo(Temporal temporal) {
// special case for performance
if (temporal instanceof LocalDate) {
LocalDate date = (LocalDate) temporal;
return plusDays(date.plusMonths(period.toTotalMonths()), period.getDays());
}
return period.addTo(temporal);
}
/**
* Subtracts this tenor from the specified date.
* <p>
* This method implements {@link TemporalAmount}.
* It is not intended to be called directly.
* Use {@link LocalDate#minus(TemporalAmount)} instead.
*
* @param temporal the temporal object to subtract from
* @return the result with this tenor subtracted
* @throws DateTimeException if unable to subtract
* @throws ArithmeticException if numeric overflow occurs
*/
@Override
public Temporal subtractFrom(Temporal temporal) {
// special case for performance
if (temporal instanceof LocalDate) {
LocalDate date = (LocalDate) temporal;
return plusDays(date.minusMonths(period.toTotalMonths()), -period.getDays());
}
return period.subtractFrom(temporal);
}
//-------------------------------------------------------------------------
/**
* Compares this tenor to another tenor.
* <p>
* Comparing tenors is a hard problem in general, but for commonly used tenors the outcome is as expected.
* If the two tenors are both based on days, then comparison is easy.
* If the two tenors are both based on months/years, then comparison is easy.
* Otherwise, months are converted to days to form an estimated length in days which is compared.
* The conversion from months to days divides by 12 and then multiplies by 365.25.
* <p>
* The resulting order places:
* <ul>
* <li>a 1 month tenor between 30 and 31 days
* <li>a 2 month tenor between 60 and 61 days
* <li>a 3 month tenor between 91 and 92 days
* <li>a 6 month tenor between 182 and 183 days
* <li>a 1 year tenor between 365 and 366 days
* </ul>
*
* @param other the other tenor
* @return negative if this is less than the other, zero if equal and positive if greater
*/
@Override
public int compareTo(Tenor other) {
int thisDays = this.getPeriod().getDays();
long thisMonths = this.getPeriod().toTotalMonths();
int otherDays = other.getPeriod().getDays();
long otherMonths = other.getPeriod().toTotalMonths();
// both day-only
if (thisMonths == 0 && otherMonths == 0) {
return Integer.compare(thisDays, otherDays);
}
// both month-only
if (thisDays == 0 && otherDays == 0) {
return Long.compare(thisMonths, otherMonths);
}
// complex
double thisMonthsInDays = (thisMonths / 12d) * 365.25d;
double otherMonthsInDays = (otherMonths / 12d) * 365.25d;
return Double.compare(thisDays + thisMonthsInDays, otherDays + otherMonthsInDays);
}
/**
* Checks if this tenor equals another tenor.
* <p>
* The comparison checks the tenor period.
*
* @param obj the other tenor, null returns false
* @return true if equal
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Tenor other = (Tenor) obj;
return period.equals(other.period);
}
/**
* Returns a suitable hash code for the tenor.
*
* @return the hash code
*/
@Override
public int hashCode() {
return period.hashCode();
}
/**
* Returns a formatted string representing the tenor.
* <p>
* The format is a combination of the quantity and unit, such as 1D, 2W, 3M, 4Y.
*
* @return the formatted tenor
*/
@ToString
@Override
public String toString() {
return name;
}
}