/** * 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.time.LocalDate; import java.time.temporal.Temporal; import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; import com.opengamma.strata.basics.ReferenceData; import com.opengamma.strata.collect.named.Named; /** * 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> * Applications should refer to holidays using {@link HolidayCalendarId}. * The identifier must be {@linkplain HolidayCalendarId#resolve(ReferenceData) resolved} * to a {@link HolidayCalendar} before the holiday data methods can be accessed. * See {@link HolidayCalendarIds} for a standard set of identifiers available in {@link ReferenceData#standard()}. * <p> * All implementations of this interface must be immutable and thread-safe. * * @see ImmutableHolidayCalendar */ public interface HolidayCalendar extends Named { /** * 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 zero, the input date is returned. * 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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 Math.toIntExact(LocalDateUtils.stream(startInclusive, endExclusive) .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 combinedWith(HolidayCalendar other) { if (this.equals(other)) { return this; } if (other == HolidayCalendars.NO_HOLIDAYS) { return this; } return new CombinedHolidayCalendar(this, other); } //------------------------------------------------------------------------- /** * Gets the identifier for the calendar. * <p> * This identifier is used to locate the index in {@link ReferenceData}. * * @return the identifier */ public abstract HolidayCalendarId getId(); //------------------------------------------------------------------------- /** * Gets the name that identifies this calendar. * <p> * This is the name associated with the {@linkplain HolidayCalendarId identifier}. * * @return the name */ @Override public default String getName() { return getId().getName(); } }