/** * 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 java.io.Serializable; import java.time.DayOfWeek; import java.time.LocalDate; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedSet; import org.joda.beans.Bean; import org.joda.beans.BeanBuilder; import org.joda.beans.BeanDefinition; import org.joda.beans.ImmutableBean; import org.joda.beans.ImmutableConstructor; 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.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.opengamma.strata.basics.ReferenceData; import com.opengamma.strata.collect.ArgChecker; /** * A holiday calendar implementation based on an immutable set of holiday dates and weekends. * <p> * A standard immutable implementation of {@link HolidayCalendar} that stores all * dates that are holidays, plus a list of weekend days. * <p> * Internally, the class uses a range to determine the range of known holiday dates. * Beyond the range of known holiday dates, weekend days are used to determine business days. * Dates may be queried from year zero to year 10,000. * <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()}. */ @BeanDefinition(builderScope = "private") public final class ImmutableHolidayCalendar implements HolidayCalendar, ImmutableBean, Serializable { // optimized implementation of HolidayCalendar // uses an int array where each int represents a month // each bit within the int represents a date, where 0 is a holiday and 1 is a business day // (most logic involves finding business days, finding 1 is easier than finding 0 // when using Integer.numberOfTrailingZeros and Integer.numberOfLeadingZeros) // benchmarking showed nextOrSame() and previousOrSame() do not need to be overridden // out-of-range and weekend-only (used in testing) are handled using exceptions to fast-path the common case /** * The identifier, such as 'GBLO'. */ @PropertyDefinition(validate = "notNull", overrideGet = true) private final HolidayCalendarId id; /** * The set of holiday dates. * <p> * Each date in this set is not a business day. */ @PropertyDefinition(validate = "notNull") private final ImmutableSortedSet<LocalDate> holidays; /** * The set of weekend days. * <p> * Each date that has a day-of-week matching one of these days is not a business day. */ @PropertyDefinition(validate = "notNull") private final ImmutableSet<DayOfWeek> weekendDays; /** * The start year. * Used as the base year for the lookup table. */ private final transient int startYear; // not a property /** * The lookup table, where each item represents a month from January of startYear onwards. * Bits 0 to 31 are used for each day-of-month, where 0 is a holiday and 1 is a business day. * Trailing bits are set to 0 so they act as holidays, avoiding month length logic. */ private final transient int[] lookup; // not a property //------------------------------------------------------------------------- /** * Obtains an instance from a set of holiday dates and weekend days. * <p> * The holiday dates will be extracted into a set with duplicates ignored. * The minimum supported date for query is the start of the year of the earliest holiday. * The maximum supported date for query is the end of the year of the latest holiday. * <p> * The weekend days may both be the same. * * @param id the identifier * @param holidays the set of holiday dates * @param firstWeekendDay the first weekend day * @param secondWeekendDay the second weekend day, may be same as first * @return the holiday calendar */ public static ImmutableHolidayCalendar of( HolidayCalendarId id, Iterable<LocalDate> holidays, DayOfWeek firstWeekendDay, DayOfWeek secondWeekendDay) { ImmutableSet<DayOfWeek> weekendDays = Sets.immutableEnumSet(firstWeekendDay, secondWeekendDay); return new ImmutableHolidayCalendar(id, ImmutableSortedSet.copyOf(holidays), weekendDays); } /** * Obtains an instance from a set of holiday dates and weekend days. * <p> * The holiday dates will be extracted into a set with duplicates ignored. * The minimum supported date for query is the start of the year of the earliest holiday. * The maximum supported date for query is the end of the year of the latest holiday. * <p> * The weekend days may be empty, in which case the holiday dates should contain any weekends. * * @param id the identifier * @param holidays the set of holiday dates * @param weekendDays the days that define the weekend, if empty then weekends are treated as business days * @return the holiday calendar */ public static ImmutableHolidayCalendar of( HolidayCalendarId id, Iterable<LocalDate> holidays, Iterable<DayOfWeek> weekendDays) { return new ImmutableHolidayCalendar(id, ImmutableSortedSet.copyOf(holidays), Sets.immutableEnumSet(weekendDays)); } /** * Obtains a combined holiday calendar instance. * <p> * This combines the two input calendars. * It is intended for up-front occasional use rather than continuous use, as it is relatively slow. * * @param cal1 the first calendar * @param cal2 the second calendar * @return the combined calendar */ public static ImmutableHolidayCalendar combined(ImmutableHolidayCalendar cal1, ImmutableHolidayCalendar cal2) { // do not override combinedWith(), as this is too slow if (cal1 == cal2) { return ArgChecker.notNull(cal1, "cal1"); } ImmutableSortedSet<LocalDate> newHolidays = ImmutableSortedSet.copyOf(Iterables.concat(cal1.holidays, cal2.holidays)); ImmutableSet<DayOfWeek> newWeekends = ImmutableSet.copyOf(Iterables.concat(cal1.weekendDays, cal2.weekendDays)); return new ImmutableHolidayCalendar(cal1.id.combinedWith(cal2.id), newHolidays, newWeekends); } //------------------------------------------------------------------------- /** * Creates an instance calculating the supported range. * * @param name the calendar name * @param holidays the set of holidays, validated non-null * @param weekendDays the set of weekend days, validated non-null */ @ImmutableConstructor private ImmutableHolidayCalendar(HolidayCalendarId id, SortedSet<LocalDate> holidays, Set<DayOfWeek> weekendDays) { ArgChecker.notNull(id, "id"); ArgChecker.notNull(holidays, "holidays"); ArgChecker.notNull(weekendDays, "weekendDays"); this.id = id; this.holidays = ImmutableSortedSet.copyOfSorted(holidays); this.weekendDays = Sets.immutableEnumSet(weekendDays); if (holidays.isEmpty()) { // special case where no holiday dates are specified this.startYear = 0; this.lookup = new int[0]; } else { // normal case where holidays are specified this.startYear = holidays.first().getYear(); int endYearExclusive = holidays.last().getYear() + 1; this.lookup = buildLookupArray(holidays, weekendDays, startYear, endYearExclusive); } } // create and populate the int[] lookup // use 1 for business days and 0 for holidays private static int[] buildLookupArray( SortedSet<LocalDate> holidays, Set<DayOfWeek> weekendDays, int startYear, int endYearExclusive) { // array that has one entry for each month int[] array = new int[(endYearExclusive - startYear) * 12]; // loop through all months to handle end-of-month and weekends LocalDate firstOfMonth = LocalDate.of(startYear, 1, 1); for (int i = 0; i < array.length; i++) { int monthLen = firstOfMonth.lengthOfMonth(); // set each valid day-of-month to be a business day // the bits for days beyond the end-of-month will be unset and thus treated as non-business days // the minus one part converts a single set bit into each lower bit being set array[i] = (1 << monthLen) - 1; // unset the bits associated with a weekend // can unset across whole month using repeating pattern of 7 bits // just need to find the offset between the weekend and the day-of-week of the 1st of the month for (DayOfWeek weekendDow : weekendDays) { int daysDiff = weekendDow.getValue() - firstOfMonth.getDayOfWeek().getValue(); int offset = (daysDiff < 0 ? daysDiff + 7 : daysDiff); array[i] &= ~(0b10000001000000100000010000001 << offset); } firstOfMonth = firstOfMonth.plusMonths(1); } // unset the bit associated with each holiday date for (LocalDate date : holidays) { int index = (date.getYear() - startYear) * 12 + date.getMonthValue() - 1; array[index] &= ~(1 << (date.getDayOfMonth() - 1)); } return array; } // ensure standard constructor is invoked private Object readResolve() { return new ImmutableHolidayCalendar(id, holidays, weekendDays); } //------------------------------------------------------------------------- @Override public boolean isHoliday(LocalDate date) { try { // find data for month int index = (date.getYear() - startYear) * 12 + date.getMonthValue() - 1; // check if bit is 1 at zero-based day-of-month return (lookup[index] & (1 << (date.getDayOfMonth() - 1))) == 0; } catch (ArrayIndexOutOfBoundsException ex) { return isHolidayOutOfRange(date); } } // pulled out to aid hotspot inlining private boolean isHolidayOutOfRange(LocalDate date) { if (date.getYear() >= 0 && date.getYear() < 10000) { return weekendDays.contains(date.getDayOfWeek()); } throw new IllegalArgumentException("Date is outside the accepted range (year 0000 to 10,000): " + date); } //------------------------------------------------------------------------- @Override public LocalDate shift(LocalDate date, int amount) { try { if (amount > 0) { // day-of-month: minus one for zero-based day-of-month, plus one to start from next day return shiftNext(date.getYear(), date.getMonthValue(), date.getDayOfMonth(), amount); } else if (amount < 0) { // day-of-month: minus one to start from previous day return shiftPrev(date.getYear(), date.getMonthValue(), date.getDayOfMonth() - 1, amount); } return date; } catch (ArrayIndexOutOfBoundsException ex) { return shiftOutOfRange(date, amount); } } // pulled out to aid hotspot inlining private LocalDate shiftOutOfRange(LocalDate date, int amount) { if (date.getYear() >= 0 && date.getYear() < 10000) { return HolidayCalendar.super.shift(date, amount); } throw new IllegalArgumentException("Date is outside the accepted range (year 0000 to 10,000): " + date); } //------------------------------------------------------------------------- @Override public LocalDate next(LocalDate date) { try { // day-of-month: minus one for zero-based day-of-month, plus one to start from next day return shiftNext(date.getYear(), date.getMonthValue(), date.getDayOfMonth(), 1); } catch (ArrayIndexOutOfBoundsException ex) { return HolidayCalendar.super.next(date); } } // shift to a later working day, following nextOrSame semantics // input day-of-month is zero-based private LocalDate shiftNext(int baseYear, int baseMonth, int baseDom0, int amount) { // find data for month int index = (baseYear - startYear) * 12 + baseMonth - 1; int monthData = lookup[index]; // loop around amount, the number of days to shift by // use domOffset to keep track of day-of-month int domOffset = baseDom0; for (int amt = amount; amt > 0; amt--) { // shift to move the target day-of-month into bit-0, removing earlier days int shifted = monthData >> domOffset; // recurse to next month if no more business days in the month if (shifted == 0) { return baseMonth == 12 ? shiftNext(baseYear + 1, 1, 0, amt) : shiftNext(baseYear, baseMonth + 1, 0, amt); } // find least significant bit, which is next business day // use JDK numberOfTrailingZeros() method which is mapped to a fast intrinsic domOffset += (Integer.numberOfTrailingZeros(shifted) + 1); } return LocalDate.of(baseYear, baseMonth, domOffset); } //------------------------------------------------------------------------- @Override public LocalDate previous(LocalDate date) { try { // day-of-month: minus one to start from previous day return shiftPrev(date.getYear(), date.getMonthValue(), date.getDayOfMonth() - 1, -1); } catch (ArrayIndexOutOfBoundsException ex) { return previousOutOfRange(date); } } // shift to an earlier working day, following previousOrSame semantics // input day-of-month is one-based and may be zero or negative private LocalDate shiftPrev(int baseYear, int baseMonth, int baseDom, int amount) { // find data for month int index = (baseYear - startYear) * 12 + baseMonth - 1; int monthData = lookup[index]; // loop around amount, the number of days to shift by // use domOffset to keep track of day-of-month int domOffset = baseDom; for (int amt = amount; amt < 0; amt++) { // shift to move the target day-of-month into bit-31, removing later days int shifted = (monthData << (32 - domOffset)); // recurse to previous month if no more business days in the month if (shifted == 0 || domOffset <= 0) { return baseMonth == 1 ? shiftPrev(baseYear - 1, 12, 31, amt) : shiftPrev(baseYear, baseMonth - 1, 31, amt); } // find most significant bit, which is previous business day // use JDK numberOfLeadingZeros() method which is mapped to a fast intrinsic domOffset -= (Integer.numberOfLeadingZeros(shifted) + 1); } return LocalDate.of(baseYear, baseMonth, domOffset + 1); } // pulled out to aid hotspot inlining private LocalDate previousOutOfRange(LocalDate date) { if (date.getYear() >= 0 && date.getYear() < 10000) { return HolidayCalendar.super.previous(date); } throw new IllegalArgumentException("Date is outside the accepted range (year 0000 to 10,000): " + date); } //------------------------------------------------------------------------- @Override public LocalDate nextSameOrLastInMonth(LocalDate date) { try { // day-of-month: no alteration as method is one-based and same is valid return shiftNextSameLast(date); } catch (ArrayIndexOutOfBoundsException ex) { return HolidayCalendar.super.nextSameOrLastInMonth(date); } } // shift to a later working day, following nextOrSame semantics // falling back to the last business day-of-month to avoid crossing a month boundary // input day-of-month is one-based private LocalDate shiftNextSameLast(LocalDate baseDate) { int baseYear = baseDate.getYear(); int baseMonth = baseDate.getMonthValue(); int baseDom = baseDate.getDayOfMonth(); // find data for month int index = (baseYear - startYear) * 12 + baseMonth - 1; int monthData = lookup[index]; // shift to move the target day-of-month into bit-0, removing earlier days int shifted = monthData >> (baseDom - 1); // return last business day-of-month if no more business days in the month int dom; if (shifted == 0) { // need to find the most significant bit, which is the last business day // use JDK numberOfLeadingZeros() method which is mapped to a fast intrinsic int leading = Integer.numberOfLeadingZeros(monthData); dom = 32 - leading; } else { // find least significant bit, which is the next/same business day // use JDK numberOfTrailingZeros() method which is mapped to a fast intrinsic dom = baseDom + Integer.numberOfTrailingZeros(shifted); } // only one call to LocalDate to aid inlining return baseDate.withDayOfMonth(dom); } //------------------------------------------------------------------------- @Override public boolean isLastBusinessDayOfMonth(LocalDate date) { try { // find data for month int index = (date.getYear() - startYear) * 12 + date.getMonthValue() - 1; // shift right, leaving the input date as bit-0 and filling with 0 on the left // if the result is 1, which is all zeroes and a final 1 (...0001) then it is last business day of month return (lookup[index] >>> (date.getDayOfMonth() - 1)) == 1; } catch (ArrayIndexOutOfBoundsException ex) { return isLastBusinessDayOfMonthOutOfRange(date); } } // pulled out to aid hotspot inlining private boolean isLastBusinessDayOfMonthOutOfRange(LocalDate date) { if (date.getYear() >= 0 && date.getYear() < 10000) { return HolidayCalendar.super.isLastBusinessDayOfMonth(date); } throw new IllegalArgumentException("Date is outside the accepted range (year 0000 to 10,000): " + date); } //------------------------------------------------------------------------- @Override public LocalDate lastBusinessDayOfMonth(LocalDate date) { try { // find data for month int index = (date.getYear() - startYear) * 12 + date.getMonthValue() - 1; // need to find the most significant bit, which is the last business day // use JDK numberOfLeadingZeros() method which is mapped to a fast intrinsic int leading = Integer.numberOfLeadingZeros(lookup[index]); return date.withDayOfMonth(32 - leading); } catch (ArrayIndexOutOfBoundsException ex) { return lastBusinessDayOfMonthOutOfRange(date); } } // pulled out to aid hotspot inlining private LocalDate lastBusinessDayOfMonthOutOfRange(LocalDate date) { if (date.getYear() >= 0 && date.getYear() < 10000) { return HolidayCalendar.super.lastBusinessDayOfMonth(date); } throw new IllegalArgumentException("Date is outside the accepted range (year 0000 to 10,000): " + date); } //------------------------------------------------------------------------- @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof ImmutableHolidayCalendar) { return id.equals(((ImmutableHolidayCalendar) obj).id); } return false; } @Override public int hashCode() { return id.hashCode(); } //------------------------------------------------------------------------- /** * Returns the name of the calendar. * * @return the descriptive string */ @Override public String toString() { return "HolidayCalendar[" + getName() + ']'; } //------------------------- AUTOGENERATED START ------------------------- ///CLOVER:OFF /** * The meta-bean for {@code ImmutableHolidayCalendar}. * @return the meta-bean, not null */ public static ImmutableHolidayCalendar.Meta meta() { return ImmutableHolidayCalendar.Meta.INSTANCE; } static { JodaBeanUtils.registerMetaBean(ImmutableHolidayCalendar.Meta.INSTANCE); } /** * The serialization version id. */ private static final long serialVersionUID = 1L; @Override public ImmutableHolidayCalendar.Meta metaBean() { return ImmutableHolidayCalendar.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 identifier, such as 'GBLO'. * @return the value of the property, not null */ @Override public HolidayCalendarId getId() { return id; } //----------------------------------------------------------------------- /** * Gets the set of holiday dates. * <p> * Each date in this set is not a business day. * @return the value of the property, not null */ public ImmutableSortedSet<LocalDate> getHolidays() { return holidays; } //----------------------------------------------------------------------- /** * Gets the set of weekend days. * <p> * Each date that has a day-of-week matching one of these days is not a business day. * @return the value of the property, not null */ public ImmutableSet<DayOfWeek> getWeekendDays() { return weekendDays; } //----------------------------------------------------------------------- /** * The meta-bean for {@code ImmutableHolidayCalendar}. */ 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 id} property. */ private final MetaProperty<HolidayCalendarId> id = DirectMetaProperty.ofImmutable( this, "id", ImmutableHolidayCalendar.class, HolidayCalendarId.class); /** * The meta-property for the {@code holidays} property. */ @SuppressWarnings({"unchecked", "rawtypes" }) private final MetaProperty<ImmutableSortedSet<LocalDate>> holidays = DirectMetaProperty.ofImmutable( this, "holidays", ImmutableHolidayCalendar.class, (Class) ImmutableSortedSet.class); /** * The meta-property for the {@code weekendDays} property. */ @SuppressWarnings({"unchecked", "rawtypes" }) private final MetaProperty<ImmutableSet<DayOfWeek>> weekendDays = DirectMetaProperty.ofImmutable( this, "weekendDays", ImmutableHolidayCalendar.class, (Class) ImmutableSet.class); /** * The meta-properties. */ private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap( this, null, "id", "holidays", "weekendDays"); /** * Restricted constructor. */ private Meta() { } @Override protected MetaProperty<?> metaPropertyGet(String propertyName) { switch (propertyName.hashCode()) { case 3355: // id return id; case -510663909: // holidays return holidays; case 563236190: // weekendDays return weekendDays; } return super.metaPropertyGet(propertyName); } @Override public BeanBuilder<? extends ImmutableHolidayCalendar> builder() { return new ImmutableHolidayCalendar.Builder(); } @Override public Class<? extends ImmutableHolidayCalendar> beanType() { return ImmutableHolidayCalendar.class; } @Override public Map<String, MetaProperty<?>> metaPropertyMap() { return metaPropertyMap$; } //----------------------------------------------------------------------- /** * The meta-property for the {@code id} property. * @return the meta-property, not null */ public MetaProperty<HolidayCalendarId> id() { return id; } /** * The meta-property for the {@code holidays} property. * @return the meta-property, not null */ public MetaProperty<ImmutableSortedSet<LocalDate>> holidays() { return holidays; } /** * The meta-property for the {@code weekendDays} property. * @return the meta-property, not null */ public MetaProperty<ImmutableSet<DayOfWeek>> weekendDays() { return weekendDays; } //----------------------------------------------------------------------- @Override protected Object propertyGet(Bean bean, String propertyName, boolean quiet) { switch (propertyName.hashCode()) { case 3355: // id return ((ImmutableHolidayCalendar) bean).getId(); case -510663909: // holidays return ((ImmutableHolidayCalendar) bean).getHolidays(); case 563236190: // weekendDays return ((ImmutableHolidayCalendar) bean).getWeekendDays(); } 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 ImmutableHolidayCalendar}. */ private static final class Builder extends DirectFieldsBeanBuilder<ImmutableHolidayCalendar> { private HolidayCalendarId id; private SortedSet<LocalDate> holidays = ImmutableSortedSet.of(); private Set<DayOfWeek> weekendDays = ImmutableSet.of(); /** * Restricted constructor. */ private Builder() { } //----------------------------------------------------------------------- @Override public Object get(String propertyName) { switch (propertyName.hashCode()) { case 3355: // id return id; case -510663909: // holidays return holidays; case 563236190: // weekendDays return weekendDays; default: throw new NoSuchElementException("Unknown property: " + propertyName); } } @SuppressWarnings("unchecked") @Override public Builder set(String propertyName, Object newValue) { switch (propertyName.hashCode()) { case 3355: // id this.id = (HolidayCalendarId) newValue; break; case -510663909: // holidays this.holidays = (SortedSet<LocalDate>) newValue; break; case 563236190: // weekendDays this.weekendDays = (Set<DayOfWeek>) 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 ImmutableHolidayCalendar build() { return new ImmutableHolidayCalendar( id, holidays, weekendDays); } //----------------------------------------------------------------------- @Override public String toString() { StringBuilder buf = new StringBuilder(128); buf.append("ImmutableHolidayCalendar.Builder{"); buf.append("id").append('=').append(JodaBeanUtils.toString(id)).append(',').append(' '); buf.append("holidays").append('=').append(JodaBeanUtils.toString(holidays)).append(',').append(' '); buf.append("weekendDays").append('=').append(JodaBeanUtils.toString(weekendDays)); buf.append('}'); return buf.toString(); } } ///CLOVER:ON //-------------------------- AUTOGENERATED END -------------------------- }