/** * Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.basics.date; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; import java.io.Serializable; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import org.joda.beans.PropertyDefinition; import org.joda.convert.FromString; import org.joda.convert.ToString; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.opengamma.strata.basics.ReferenceData; import com.opengamma.strata.basics.ReferenceDataId; import com.opengamma.strata.basics.ReferenceDataNotFoundException; import com.opengamma.strata.basics.Resolvable; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.Messages; import com.opengamma.strata.collect.named.Named; /** * An identifier for a holiday calendar. * <p> * This identifier is used to obtain a {@link HolidayCalendar} from {@link ReferenceData}. * The holiday calendar itself is used to determine whether a day is a business day or not. * <p> * Identifiers for common holiday calendars are provided in {@link HolidayCalendarIds}. */ public final class HolidayCalendarId implements ReferenceDataId<HolidayCalendar>, Resolvable<HolidayCalendar>, Named, Serializable { /** Serialization version. */ private static final long serialVersionUID = 1L; /** Instance cache. */ private static final ConcurrentHashMap<String, HolidayCalendarId> CACHE = new ConcurrentHashMap<>(); /** * The identifier, expressed as a normalized unique name. */ @PropertyDefinition(validate = "notNull") private final String name; /** * The hash code. */ private final transient int hashCode; /** * The resolver function. * Implementations of this function must only call {@link ReferenceData#queryValueOrNull(ReferenceDataId)}. */ private final transient BiFunction<HolidayCalendarId, ReferenceData, HolidayCalendar> resolver; //------------------------------------------------------------------------- /** * Obtains an instance from the specified unique name. * <p> * The name uniquely identifies the calendar. * The {@link HolidayCalendar} is resolved from {@link ReferenceData} 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. * The resulting identifier will have the individual identifiers normalized into alphabetical order. * * @param uniqueName the unique name * @return the identifier */ @FromString public static HolidayCalendarId of(String uniqueName) { HolidayCalendarId id = CACHE.get(uniqueName); return id != null ? id : create(uniqueName); } // create a new instance atomically, broken out to aid inlining private static HolidayCalendarId create(String name) { if (!name.contains("+")) { return CACHE.computeIfAbsent(name, n -> new HolidayCalendarId(name)); } // parse + separated names once and build resolver function to aid performance // name BBB+CCC+AAA changed to sorted form of AAA+BBB+CCC // dedicated resolver function created List<HolidayCalendarId> ids = Splitter.on('+').splitToList(name).stream() .filter(n -> !n.equals(HolidayCalendarIds.NO_HOLIDAYS.getName())) .map(n -> HolidayCalendarId.of(n)) .distinct() .sorted(comparing(HolidayCalendarId::getName)) .collect(toList()); String normalizedName = Joiner.on('+').join(ids); BiFunction<HolidayCalendarId, ReferenceData, HolidayCalendar> resolver = (id, refData) -> { HolidayCalendar cal = refData.queryValueOrNull(id); if (cal != null) { return cal; } cal = HolidayCalendars.NO_HOLIDAYS; for (HolidayCalendarId splitId : ids) { HolidayCalendar splitCal = refData.queryValueOrNull(splitId); if (splitCal == null) { throw new ReferenceDataNotFoundException(Messages.format( "Reference data not found for '{}' of type 'HolidayCalendarId' when finding '{}'", splitId, id)); } cal = cal.combinedWith(splitCal); } return cal; }; // cache under the normalized and non-normalized names HolidayCalendarId id = CACHE.computeIfAbsent(normalizedName, n -> new HolidayCalendarId(normalizedName, resolver)); CACHE.putIfAbsent(name, id); return id; } //------------------------------------------------------------------------- // creates an identifier for a single calendar private HolidayCalendarId(String normalizedName) { this.name = normalizedName; this.hashCode = normalizedName.hashCode(); this.resolver = (id, refData) -> refData.queryValueOrNull(this); } // creates an identifier for a combined calendar private HolidayCalendarId( String normalizedName, BiFunction<HolidayCalendarId, ReferenceData, HolidayCalendar> resolver) { this.name = normalizedName; this.hashCode = normalizedName.hashCode(); this.resolver = resolver; } // resolve after deserialization private Object readResolve() { return of(name); } //------------------------------------------------------------------------- /** * 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() { return name; } /** * Gets the type of data this identifier refers to. * <p> * A {@code HolidayCalendarId} refers to a {@code HolidayCalendar}. * * @return the type of the reference data this identifier refers to */ @Override public Class<HolidayCalendar> getReferenceDataType() { return HolidayCalendar.class; } //------------------------------------------------------------------------- /** * Resolves this identifier to a holiday calendar using the specified reference data. * <p> * This returns an instance of {@link HolidayCalendar} that can perform calculations. * <p> * Resolved objects may be bound to data that changes over time, such as holiday calendars. * If the data changes, such as the addition of a new holiday, the resolved form will not be updated. * Care must be taken when placing the resolved form in a cache or persistence layer. * * @param refData the reference data, used to resolve the reference * @return the resolved holiday calendar * @throws ReferenceDataNotFoundException if the identifier is not found */ @Override public HolidayCalendar resolve(ReferenceData refData) { return refData.getValue(this); } @Override public HolidayCalendar queryValueOrNull(ReferenceData refData) { return resolver.apply(this, refData); } //------------------------------------------------------------------------- /** * Combines this holiday calendar identifier 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 identifier * @return the combined holiday calendar identifier */ public HolidayCalendarId combinedWith(HolidayCalendarId other) { if (this == other) { return this; } if (this == HolidayCalendarIds.NO_HOLIDAYS) { return ArgChecker.notNull(other, "other"); } if (other == HolidayCalendarIds.NO_HOLIDAYS) { return this; } return HolidayCalendarId.of(name + '+' + other.name); } //------------------------------------------------------------------------- /** * Checks if this identifier equals another identifier. * <p> * The comparison checks the name. * * @param obj the other identifier, null returns false * @return true if equal */ @Override public boolean equals(Object obj) { // could use (obj == this), but this code seems to be a little faster if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } HolidayCalendarId that = (HolidayCalendarId) obj; return name.equals(that.name); } /** * Returns a suitable hash code for the identifier. * * @return the hash code */ @Override public int hashCode() { return hashCode; } /** * Returns the name of the identifier. * * @return the name */ @Override public String toString() { return name; } }