/** * 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 java.time.temporal.ChronoUnit.DAYS; import static java.time.temporal.ChronoUnit.MONTHS; import static java.time.temporal.ChronoUnit.YEARS; import java.io.Serializable; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; 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 java.util.Locale; import org.joda.convert.FromString; import org.joda.convert.ToString; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.Messages; /** * A periodic frequency used by financial products that have a specific event every so often. * <p> * Frequency is primarily intended to be used to subdivide events within a year. * <p> * A frequency is allowed to be any non-negative period of days, weeks, month or years. * This class provides constants for common frequencies which are best used by static import. * <p> * A special value, 'Term', is provided for when there are no subdivisions of the entire term. * This is also know as 'zero-coupon' or 'once'. It is represented using the period 10,000 years, * which allows addition/subtraction to work, producing a date after the end of the term. * <p> * Each frequency is based on a {@link Period}. The months and years of the period are not normalized, * thus it is possible to have a frequency 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. * <p> * The periodic frequency is often expressed as a number of events per year. * The {@link #eventsPerYear()} method can be used to obtain this for common frequencies. * * <h4>Usage</h4> * {@code Frequency} implements {@code TemporalAmount} allowing it to be directly added to a date: * <pre> * LocalDate later = baseDate.plus(frequency); * </pre> */ public final class Frequency implements TemporalAmount, Serializable { /** * Serialization version. */ private static final long serialVersionUID = 1; /** * The artificial maximum length of a normal tenor in years. */ private static final int MAX_YEARS = 1_000; /** * The artificial maximum length of a normal tenor in months. */ private static final int MAX_MONTHS = MAX_YEARS * 12; /** * The artificial length in years of the 'Term' frequency. */ private static final int TERM_YEARS = 10_000; /** * A periodic frequency of one day. * Also known as daily. * There are considered to be 364 events per year with this frequency. */ public static final Frequency P1D = ofDays(1); /** * A periodic frequency of 1 week (7 days). * Also known as weekly. * There are considered to be 52 events per year with this frequency. */ public static final Frequency P1W = new Frequency(Period.ofWeeks(1), "P1W"); /** * A periodic frequency of 2 weeks (14 days). * Also known as bi-weekly. * There are considered to be 26 events per year with this frequency. */ public static final Frequency P2W = new Frequency(Period.ofWeeks(2), "P2W"); /** * A periodic frequency of 4 weeks (28 days). * Also known as lunar. * There are considered to be 13 events per year with this frequency. */ public static final Frequency P4W = new Frequency(Period.ofWeeks(4), "P4W"); /** * A periodic frequency of 13 weeks (91 days). * There are considered to be 4 events per year with this frequency. */ public static final Frequency P13W = new Frequency(Period.ofWeeks(13), "P13W"); /** * A periodic frequency of 26 weeks (182 days). * There are considered to be 2 events per year with this frequency. */ public static final Frequency P26W = new Frequency(Period.ofWeeks(26), "P26W"); /** * A periodic frequency of 52 weeks (364 days). * There is considered to be 1 event per year with this frequency. */ public static final Frequency P52W = new Frequency(Period.ofWeeks(52), "P52W"); /** * A periodic frequency of 1 month. * Also known as monthly. * There are 12 events per year with this frequency. */ public static final Frequency P1M = new Frequency(Period.ofMonths(1)); /** * A periodic frequency of 2 months. * Also known as bi-monthly. * There are 6 events per year with this frequency. */ public static final Frequency P2M = new Frequency(Period.ofMonths(2)); /** * A periodic frequency of 3 months. * Also known as quarterly. * There are 4 events per year with this frequency. */ public static final Frequency P3M = new Frequency(Period.ofMonths(3)); /** * A periodic frequency of 4 months. * There are 3 events per year with this frequency. */ public static final Frequency P4M = new Frequency(Period.ofMonths(4)); /** * A periodic frequency of 6 months. * Also known as semi-annual. * There are 2 events per year with this frequency. */ public static final Frequency P6M = new Frequency(Period.ofMonths(6)); /** * A periodic frequency of 12 months (1 year). * Also known as annual. * There is 1 event per year with this frequency. */ public static final Frequency P12M = new Frequency(Period.ofMonths(12)); /** * A periodic frequency matching the term. * Also known as zero-coupon. * This is represented using the period 10,000 years. * There are no events per year with this frequency. */ public static final Frequency TERM = new Frequency(Period.ofYears(TERM_YEARS), "Term"); /** * The period of the frequency. */ private final Period period; /** * The name of the frequency. */ private final String name; /** * The number of events per year. */ private final transient int eventsPerYear; /** * The number of events per year. */ private final transient double eventsPerYearEstimate; //------------------------------------------------------------------------- /** * 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. * <p> * The maximum tenor length is 1,000 years. * * @param period the period to convert to a periodic frequency * @return the periodic frequency * @throws IllegalArgumentException if the period is negative, zero or too large */ public static Frequency of(Period period) { ArgChecker.notNull(period, "period"); int days = period.getDays(); long months = period.toTotalMonths(); if (months == 0 && days != 0) { return ofDays(days); } if (months > MAX_MONTHS) { throw new IllegalArgumentException("Period must not exceed 1000 years"); } return new Frequency(period); } /** * 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 periodic frequency * @throws IllegalArgumentException if days is negative or zero */ public static Frequency ofDays(int days) { if (days % 7 == 0) { return ofWeeks(days / 7); } return new Frequency(Period.ofDays(days)); } /** * Obtains an instance backed by a period of weeks. * * @param weeks the number of weeks * @return the periodic frequency * @throws IllegalArgumentException if weeks is negative or zero */ public static Frequency ofWeeks(int weeks) { switch (weeks) { case 1: return P1W; case 2: return P2W; case 4: return P4W; case 13: return P13W; case 26: return P26W; case 52: return P52W; default: return new Frequency(Period.ofWeeks(weeks), "P" + 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 periodic frequency * @throws IllegalArgumentException if months is negative, zero or over 12,000 */ public static Frequency ofMonths(int months) { switch (months) { case 1: return P1M; case 2: return P2M; case 3: return P3M; case 4: return P4M; case 6: return P6M; case 12: return P12M; default: if (months > MAX_MONTHS) { throw new IllegalArgumentException(maxMonthMsg()); } return new Frequency(Period.ofMonths(months)); } } // extracted to aid inlining private static String maxMonthMsg() { DecimalFormat formatter = new DecimalFormat("#,###", new DecimalFormatSymbols(Locale.ENGLISH)); return "Months must not exceed " + formatter.format(MAX_MONTHS); } /** * Obtains an instance backed by a period of years. * * @param years the number of years * @return the periodic frequency * @throws IllegalArgumentException if years is negative, zero or over 1,000 */ public static Frequency ofYears(int years) { if (years > MAX_YEARS) { throw new IllegalArgumentException(maxYearMsg()); } return new Frequency(Period.ofYears(years)); } // extracted to aid inlining private static String maxYearMsg() { DecimalFormat formatter = new DecimalFormat("#,###", new DecimalFormatSymbols(Locale.ENGLISH)); return "Years must not exceed " + formatter.format(MAX_YEARS); } //------------------------------------------------------------------------- /** * Parses a formatted string representing the frequency. * <p> * The format can either be based on ISO-8601, such as 'P3M' * or without the 'P' prefix e.g. '2W'. * <p> * The period must be positive and non-zero. * * @param toParse the string representing the frequency * @return the frequency * @throws IllegalArgumentException if the frequency cannot be parsed */ @FromString public static Frequency parse(String toParse) { ArgChecker.notNull(toParse, "toParse"); if (toParse.equalsIgnoreCase("Term")) { return TERM; } String prefixed = toParse.startsWith("P") ? toParse : "P" + toParse; try { return Frequency.of(Period.parse(prefixed)); } catch (DateTimeParseException ex) { throw new IllegalArgumentException(ex); } } //------------------------------------------------------------------------- /** * Creates a periodic frequency. * * @param period the period to represent */ private Frequency(Period period) { this(period, period.toString()); } /** * Creates a periodic frequency. * * @param period the period to represent * @param name the name */ private Frequency(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; // calculate events per year long monthsLong = period.toTotalMonths(); if (monthsLong > MAX_MONTHS) { eventsPerYear = 0; eventsPerYearEstimate = 0; } else { int months = (int) monthsLong; int days = period.getDays(); if (months > 0 && days == 0) { eventsPerYear = (12 % months == 0) ? 12 / months : -1; eventsPerYearEstimate = 12d / months; } else if (days > 0 && months == 0) { eventsPerYear = (364 % days == 0) ? 364 / days : -1; eventsPerYearEstimate = 364d / days; } else { eventsPerYear = -1; double estimatedSecs = months * MONTHS.getDuration().getSeconds() + days * DAYS.getDuration().getSeconds(); eventsPerYearEstimate = YEARS.getDuration().getSeconds() / estimatedSecs; } } } // safe deserialization private Object readResolve() { if (this.equals(TERM)) { return TERM; } return of(period); } //------------------------------------------------------------------------- /** * Gets the underlying period of the frequency. * * @return the period */ public Period getPeriod() { return period; } /** * Checks if the periodic frequency is the 'Term' instance. * <p> * The term instance corresponds to there being no subdivisions of the entire term. * * @return true if this is the 'Term' instance */ public boolean isTerm() { return this == TERM; } //------------------------------------------------------------------------- /** * Normalizes the months and years of this tenor. * <p> * This method returns a tenor of an equivalent length but with any number * of months greater than 12 normalized into a combination of months and years. * * @return the normalized tenor */ public Frequency normalized() { Period norm = period.normalized(); return (norm != period ? Frequency.of(norm) : this); } //------------------------------------------------------------------------- /** * Checks if the periodic frequency is week-based. * <p> * A week-based frequency 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 periodic frequency is month-based. * <p> * A month-based frequency consists of an integral number of months. * Any year-based frequency 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 && isTerm() == false; } /** * Checks if the periodic frequency is annual. * <p> * An annual frequency consists of 12 months. * There must be no day or week element. * * @return true if this is annual */ public boolean isAnnual() { return period.toTotalMonths() == 12 && period.getDays() == 0; } //------------------------------------------------------------------------- /** * Calculates the number of events that occur in a year. * <p> * The number of events per year is the number of times that the period occurs per year. * Not all periodic frequency instances can be converted to an integer events per year. * All constants declared on this class will return a result. * <p> * Month-based and year-based periodic frequencies are converted by dividing 12 by the number of months. * Only the following periodic frequencies return a value - P1M, P2M, P3M, P4M, P6M, P1Y. * <p> * Day-based and week-based periodic frequencies are converted by dividing 364 by the number of days. * Only the following periodic frequencies return a value - P1D, P2D, P4D, P1W, P2W, P4W, P13W, P26W, P52W. * <p> * The 'Term' periodic frequency returns zero. * * @return the number of events per year * @throws IllegalArgumentException if unable to calculate the number of events per year */ public int eventsPerYear() { if (eventsPerYear == -1) { throw new IllegalArgumentException("Unable to calculate events per year: " + this); } return eventsPerYear; } /** * Estimates the number of events that occur in a year. * <p> * The number of events per year is the number of times that the period occurs per year. * This method returns an estimate without throwing an exception. * The exact number of events is returned by {@link #eventsPerYear()}. * <p> * The 'Term' periodic frequency returns zero. * Month-based and year-based periodic frequencies return 12 divided by the number of months. * Day-based and week-based periodic frequencies return 364 divided by the number of days. * Other frequencies are calculated using estimated durations, dividing the year by the period. * * @return the estimated number of events per year */ public double eventsPerYearEstimate() { return eventsPerYearEstimate; } //------------------------------------------------------------------------- /** * Exactly divides this frequency by another. * <p> * This calculates the integer division of this frequency by the specified frequency. * If the result is not an integer, an exception is thrown. * <p> * Month-based and year-based periodic frequencies are calculated by dividing the total number of months. * For example, P6M divided by P3M results in 2, and P2Y divided by P6M returns 4. * <p> * Day-based and week-based periodic frequencies are calculated by dividing the total number of days. * For example, P26W divided by P13W results in 2, and P2W divided by P1D returns 14. * <p> * The 'Term' frequency throws an exception. * * @param other the other frequency to divide into this one * @return this frequency divided by the other frequency * @throws IllegalArgumentException if the frequency does not exactly divide into this one */ public int exactDivide(Frequency other) { ArgChecker.notNull(other, "other"); if (isMonthBased() && other.isMonthBased()) { long paymentMonths = getPeriod().toTotalMonths(); long accrualMonths = other.getPeriod().toTotalMonths(); if ((paymentMonths % accrualMonths) == 0) { return Math.toIntExact(paymentMonths / accrualMonths); } } else if (period.toTotalMonths() == 0 && other.period.toTotalMonths() == 0) { long paymentDays = getPeriod().getDays(); long accrualDays = other.getPeriod().getDays(); if ((paymentDays % accrualDays) == 0) { return Math.toIntExact(paymentDays / accrualDays); } } throw new IllegalArgumentException(Messages.format( "Frequency '{}' is not a multiple of '{}'", this, other)); } //------------------------------------------------------------------------- /** * 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> * The 'Term' period is returned as a period of 10,000 years. * <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 unit of this periodic frequency. * <p> * This returns a list containing years, months and days. * Note that weeks are not included. * <p> * The 'Term' period is returned as a period of 10,000 years. * <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 the period of this frequency 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 frequency 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 date.plusMonths(period.toTotalMonths()).plusDays(period.getDays()); } return period.addTo(temporal); } /** * Subtracts the period of this frequency 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 frequency 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 date.minusMonths(period.toTotalMonths()).minusDays(period.getDays()); } return period.subtractFrom(temporal); } //------------------------------------------------------------------------- /** * Checks if this periodic frequency equals another periodic frequency. * <p> * The comparison checks the frequency period. * * @param obj the other frequency, 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; } Frequency other = (Frequency) obj; return period.equals(other.period); } /** * Returns a suitable hash code for the periodic frequency. * * @return the hash code */ @Override public int hashCode() { return period.hashCode(); } /** * Returns a formatted string representing the periodic frequency. * <p> * The format is a combination of the quantity and unit, such as P1D, P2W, P3M, P4Y. * The 'Term' amount is returned as 'Term'. * * @return the formatted frequency */ @ToString @Override public String toString() { return name; } }