/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.basics.date; import static com.opengamma.basics.date.LocalDateUtils.plusDays; import java.time.LocalDate; import java.time.temporal.Temporal; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; import org.joda.convert.FromString; import org.joda.convert.ToString; import com.opengamma.collect.ArgChecker; import com.opengamma.collect.named.ExtendedEnum; import com.opengamma.collect.named.Named; import com.opengamma.collect.range.LocalDateRange; /** * A holiday calendar, classifying dates as holidays or business days. * <p> * Many calculations in finance require knowledge of whether a date is a business day or not. * This class encapsulates that knowledge, with each day treated as a holiday or a business day. * Weekends are effectively treated as a special kind of holiday. * <p> * The most common implementations are provided in {@link HolidayCalendars}. * Additional implementations may be added using {@link ImmutableHolidayCalendar}, * or by directly implementing this interface. * <p> * All implementations of this interface must be immutable and thread-safe. */ public interface HolidayCalendar extends Named { /** * Obtains a {@code HolidayCalendar} from a unique name. * <p> * The unique name identifies a calendar in an underlying source of calendars. * The calendar itself is looked up on demand when required. * <p> * It is possible to combine two or more calendars using the '+' symbol. * For example, 'GBLO+USNY' will combine the separate 'GBLO' and 'USNY' calendars. * * @param uniqueName the unique name of the calendar * @return the holiday calendar * @throws IllegalArgumentException if the name is not known */ @FromString public static HolidayCalendar of(String uniqueName) { ArgChecker.notNull(uniqueName, "uniqueName"); return HolidayCalendars.of(uniqueName); } /** * Gets the extended enum helper. * <p> * This helper allows instances of {@code HolidayCalendar} to be lookup up. * It also provides the complete set of available instances. * * @return the extended enum helper */ public static ExtendedEnum<HolidayCalendar> extendedEnum() { return HolidayCalendars.ENUM_LOOKUP; } //------------------------------------------------------------------------- /** * Checks if the specified date is a holiday. * <p> * This is the opposite of {@link #isBusinessDay(LocalDate)}. * A weekend is treated as a holiday. * * @param date the date to check * @return true if the specified date is a holiday * @throws IllegalArgumentException if the date is outside the supported range */ public abstract boolean isHoliday(LocalDate date); /** * Checks if the specified date is a business day. * <p> * This is the opposite of {@link #isHoliday(LocalDate)}. * A weekend is treated as a holiday. * * @param date the date to check * @return true if the specified date is a business day * @throws IllegalArgumentException if the date is outside the supported range */ public default boolean isBusinessDay(LocalDate date) { return !isHoliday(date); } //------------------------------------------------------------------------- /** * Returns an adjuster that changes the date. * <p> * The adjuster is intended to be used with the method {@link Temporal#with(TemporalAdjuster)}. * For example: * <pre> * threeDaysLater = date.with(businessDays.adjustBy(3)); * twoDaysEarlier = date.with(businessDays.adjustBy(-2)); * </pre> * * @param amount the number of business days to adjust by * @return the first business day after this one * @throws IllegalArgumentException if the calculation is outside the supported range */ public default TemporalAdjuster adjustBy(int amount) { return TemporalAdjusters.ofDateAdjuster(date -> shift(date, amount)); } //------------------------------------------------------------------------- /** * Shifts the date by the specified number of business days. * <p> * If the amount is positive, later business days are chosen. * If the amount is negative, earlier business days are chosen. * * @param date the date to adjust * @param amount the number of business days to adjust by * @return the shifted date * @throws IllegalArgumentException if the calculation is outside the supported range */ public default LocalDate shift(LocalDate date, int amount) { ArgChecker.notNull(date, "date"); LocalDate adjusted = date; if (amount > 0) { for (int i = 0; i < amount; i++) { adjusted = next(adjusted); } } else if (amount < 0) { for (int i = 0; i > amount; i--) { adjusted = previous(adjusted); } } return adjusted; } /** * Finds the next business day, always returning a later date. * <p> * Given a date, this method returns the next business day. * * @param date the date to adjust * @return the first business day after the input date * @throws IllegalArgumentException if the calculation is outside the supported range */ public default LocalDate next(LocalDate date) { ArgChecker.notNull(date, "date"); LocalDate next = plusDays(date, 1); return isHoliday(next) ? next(next) : next; } /** * Finds the next business day, returning the input date if it is a business day. * <p> * Given a date, this method returns a business day. * If the input date is a business day, it is returned. * Otherwise, the next business day is returned. * * @param date the date to adjust * @return the input date if it is a business day, or the next business day * @throws IllegalArgumentException if the calculation is outside the supported range */ public default LocalDate nextOrSame(LocalDate date) { ArgChecker.notNull(date, "date"); return isHoliday(date) ? next(date) : date; } //------------------------------------------------------------------------- /** * Finds the previous business day, always returning an earlier date. * <p> * Given a date, this method returns the previous business day. * * @param date the date to adjust * @return the first business day before the input date * @throws IllegalArgumentException if the calculation is outside the supported range */ public default LocalDate previous(LocalDate date) { ArgChecker.notNull(date, "date"); LocalDate previous = plusDays(date, -1); return isHoliday(previous) ? previous(previous) : previous; } /** * Finds the previous business day, returning the input date if it is a business day. * <p> * Given a date, this method returns a business day. * If the input date is a business day, it is returned. * Otherwise, the previous business day is returned. * * @param date the date to adjust * @return the input date if it is a business day, or the previous business day * @throws IllegalArgumentException if the calculation is outside the supported range */ public default LocalDate previousOrSame(LocalDate date) { ArgChecker.notNull(date, "date"); return isHoliday(date) ? previous(date) : date; } //------------------------------------------------------------------------- /** * Finds the next business day within the month, returning the input date if it is a business day, * or the last business day of the month if the next business day is in a different month. * <p> * Given a date, this method returns a business day. * If the input date is a business day, it is returned. * If the next business day is within the same month, it is returned. * Otherwise, the last business day of the month is returned. * <p> * Note that the result of this method may be earlier than the input date. * <p> * This corresponds to the {@linkplain BusinessDayConventions#MODIFIED_FOLLOWING modified following} * business day convention. * * @param date the date to adjust * @return the input date if it is a business day, the next business day if within the same month * or the last business day of the month * @throws IllegalArgumentException if the calculation is outside the supported range */ public default LocalDate nextSameOrLastInMonth(LocalDate date) { ArgChecker.notNull(date, "date"); LocalDate nextOrSame = nextOrSame(date); return (nextOrSame.getMonthValue() != date.getMonthValue() ? previous(date) : nextOrSame); } //------------------------------------------------------------------------- /** * Checks if the specified date is the last business day of the month. * <p> * This returns true if the date specified is the last valid business day of the month. * * @param date the date to check * @return true if the specified date is the last business day of the month * @throws IllegalArgumentException if the date is outside the supported range */ public default boolean isLastBusinessDayOfMonth(LocalDate date) { ArgChecker.notNull(date, "date"); return isBusinessDay(date) && next(date).getMonthValue() != date.getMonthValue(); } /** * Calculates the last business day of the month. * <p> * Given a date, this method returns the date of the last business day of the month. * * @param date the date to check * @return true if the specified date is the last business day of the month * @throws IllegalArgumentException if the date is outside the supported range */ public default LocalDate lastBusinessDayOfMonth(LocalDate date) { ArgChecker.notNull(date, "date"); return previousOrSame(date.withDayOfMonth(date.lengthOfMonth())); } //------------------------------------------------------------------------- /** * Calculates the number of business days between two dates. * <p> * This calculates the number of business days within the range. * If the dates are equal, zero is returned. * If the end is before the start, an exception is thrown. * * @param startInclusive the start date * @param endExclusive the end date * @return the total number of business days between the start and end date * @throws IllegalArgumentException if the calculation is outside the supported range */ public default int daysBetween(LocalDate startInclusive, LocalDate endExclusive) { return daysBetween(LocalDateRange.of(startInclusive, endExclusive)); } /** * Calculates the number of business days in a date range. * <p> * This calculates the number of business days within the range. * * @param dateRange the date range to calculate business days for * @return the total number of business days between the start and end date * @throws IllegalArgumentException if the calculation is outside the supported range */ public default int daysBetween(LocalDateRange dateRange) { ArgChecker.notNull(dateRange, "dateRange"); return Math.toIntExact(dateRange.stream() .filter(this::isBusinessDay) .count()); } //------------------------------------------------------------------------- /** * Combines this holiday calendar with another. * <p> * The resulting calendar will declare a day as a business day if it is a * business day in both source calendars. * * @param other the other holiday calendar * @return the combined calendar * @throws IllegalArgumentException if unable to combine the calendars */ public default HolidayCalendar combineWith(HolidayCalendar other) { ArgChecker.notNull(other, "other"); if (this.equals(other)) { return this; } if (other == HolidayCalendars.NO_HOLIDAYS) { return this; } return new HolidayCalendars.Combined(this, other); } //------------------------------------------------------------------------- /** * Gets the name that uniquely identifies this calendar. * <p> * This name is used in serialization and can be parsed using {@link #of(String)}. * * @return the unique name */ @ToString @Override public String getName(); }