/**
  * Construct an array of dates according the a start date, an end date, the period between dates
  * and the conventions. The start date is not included in the array. The date are constructed
  * forward and the stub period, if any, is last. The end date is always included in the schedule.
  *
  * @param startDate The reference initial date for the construction.
  * @param endDate The end date. Usually unadjusted.
  * @param period The period between payments.
  * @param businessDayConvention The business day convention.
  * @param calendar The applicable calendar.
  * @param isEOM The end-of-month rule flag.
  * @param stubShort Flag indicating if the stub, if any, is short (true) or long (false).
  * @return The array of dates.
  */
 public static ZonedDateTime[] getAdjustedDateSchedule(
     final ZonedDateTime startDate,
     final ZonedDateTime endDate,
     final Period period,
     final BusinessDayConvention businessDayConvention,
     final Calendar calendar,
     final boolean isEOM,
     final boolean stubShort) {
   boolean eomApply = false;
   if (isEOM) {
     final BusinessDayConvention following = new FollowingBusinessDayConvention();
     eomApply =
         (following.adjustDate(calendar, startDate.plusDays(1)).getMonth()
             != startDate.getMonth());
   }
   // When the end-of-month rule applies and the start date is on month-end, the dates are the last
   // business day of the month.
   BusinessDayConvention actualBDC;
   final List<ZonedDateTime> adjustedDates = new ArrayList<>();
   ZonedDateTime date = startDate;
   if (eomApply) {
     actualBDC =
         new PrecedingBusinessDayConvention(); // To ensure that the date stays in the current
     // month.
     date = date.plus(period).with(TemporalAdjusters.lastDayOfMonth());
     while (date.isBefore(endDate)) { // date is strictly before endDate
       adjustedDates.add(actualBDC.adjustDate(calendar, date));
       date = date.plus(period).with(TemporalAdjusters.lastDayOfMonth());
     }
   } else {
     actualBDC = businessDayConvention;
     date = date.plus(period);
     while (date.isBefore(endDate)) { // date is strictly before endDate
       adjustedDates.add(businessDayConvention.adjustDate(calendar, date));
       date = date.plus(period);
     }
   }
   // For long stub the last date before end date, if any, is removed.
   if (!stubShort && adjustedDates.size() >= 1) {
     adjustedDates.remove(adjustedDates.size() - 1);
   }
   adjustedDates.add(actualBDC.adjustDate(calendar, endDate)); // the end date
   return adjustedDates.toArray(EMPTY_ARRAY);
 }
 /**
  * Compute the end date of a period from the start date, the tenor and the conventions.
  *
  * @param startDate The period start date.
  * @param tenor The period tenor.
  * @param convention The business day convention.
  * @param calendar The calendar.
  * @param endOfMonthRule True if end-of-month rule applies, false if it does not. The rule applies
  *     when the start date is the last business day of the month and the period is a number of
  *     months or years, not days or weeks. When the rule applies, the end date is the last
  *     business day of the month.
  * @return The end date.
  */
 public static ZonedDateTime getAdjustedDate(
     final ZonedDateTime startDate,
     final Period tenor,
     final BusinessDayConvention convention,
     final Calendar calendar,
     final boolean endOfMonthRule) {
   ArgumentChecker.notNull(startDate, "Start date");
   ArgumentChecker.notNull(convention, "Convention");
   ArgumentChecker.notNull(calendar, "Calendar");
   ArgumentChecker.notNull(tenor, "Tenor");
   final ZonedDateTime endDate = startDate.plus(tenor); // Unadjusted date.
   // Adjusted to month-end: when start date is last business day of the month, the end date is the
   // last business day of the month.
   final boolean isStartDateEOM =
       (startDate.getMonth() != getAdjustedDate(startDate, 1, calendar).getMonth());
   if ((tenor.getDays() == 0) & (endOfMonthRule) & (isStartDateEOM)) {
     final BusinessDayConvention preceding = new PrecedingBusinessDayConvention();
     return preceding.adjustDate(calendar, endDate.with(TemporalAdjusters.lastDayOfMonth()));
   }
   return convention.adjustDate(calendar, endDate); // Adjusted by Business day convention
 }
 /**
  * Adjust an array of date with a given convention and EOM flag.
  *
  * @param dates The array of unadjusted dates.
  * @param convention The business day convention.
  * @param calendar The calendar.
  * @param eomApply The flag indicating if the EOM apply, i.e. if the flag is true, the adjusted
  *     date is the last business day of the unadjusted date.
  * @return The adjusted dates.
  */
 public static ZonedDateTime[] getAdjustedDateSchedule(
     final ZonedDateTime[] dates,
     final BusinessDayConvention convention,
     final Calendar calendar,
     final boolean eomApply) {
   final ZonedDateTime[] result = new ZonedDateTime[dates.length];
   if (eomApply) {
     final BusinessDayConvention precedingDBC =
         new PrecedingBusinessDayConvention(); // To ensure that the date stays in the current
     // month.
     for (int loopdate = 0; loopdate < dates.length; loopdate++) {
       result[loopdate] =
           precedingDBC.adjustDate(
               calendar, dates[loopdate].with(TemporalAdjusters.lastDayOfMonth()));
     }
     return result;
   }
   for (int loopdate = 0; loopdate < dates.length; loopdate++) {
     result[loopdate] = convention.adjustDate(calendar, dates[loopdate]);
   }
   return result;
 }
/** Expiry calculator for gold future contracts. */
public final class GoldFutureExpiryCalculator implements ExchangeTradedInstrumentExpiryCalculator {

  /** Name of the calculator */
  public static final String NAME = "GoldFutureExpiryCalculator";
  /** Singleton. */
  private static final GoldFutureExpiryCalculator INSTANCE = new GoldFutureExpiryCalculator();
  /** Adjuster. */
  private static final TemporalAdjuster LAST_DAY_ADJUSTER = TemporalAdjusters.lastDayOfMonth();

  /**
   * Gets the singleton instance.
   *
   * @return the instance, not null
   */
  public static GoldFutureExpiryCalculator getInstance() {
    return INSTANCE;
  }

  /** Restricted constructor. */
  private GoldFutureExpiryCalculator() {}

  // -------------------------------------------------------------------------
  /**
   * Gets trading months (not static as depends on current date).
   *
   * @param now the date today, not null
   * @return the valid trading months, not null
   */
  private Month[] getTradingMonths(final LocalDate now) {
    // this may need improvements as the year end approaches
    Set<Month> ret = new TreeSet<>();
    ret.add(now.getMonth()); // this month
    ret.add(now.getMonth().plus(1)); // next month
    ret.add(now.getMonth().plus(2)); // next 2 months
    //  February, April, August, and October in next 23 months
    ret.add(Month.FEBRUARY);
    ret.add(Month.APRIL);
    ret.add(Month.AUGUST);
    ret.add(Month.OCTOBER);
    // June and December falling in next 72 month period
    ret.add(Month.JUNE);
    ret.add(Month.DECEMBER);
    // assuming this gives enough valid dates so dont go round to next 12 month period
    return ret.toArray(new Month[0]);
  }

  /**
   * Expiry date of Soybean Futures: The 3rd last business day of the month. See
   * http://www.cmegroup.com/trading/metals/precious/gold_contract_specifications.html
   *
   * @param n the n'th expiry date after today, greater than zero
   * @param today the valuation date, not null
   * @param holidayCalendar the holiday calendar, not null
   * @return the expiry date, not null
   */
  @Override
  public LocalDate getExpiryDate(
      final int n, final LocalDate today, final Calendar holidayCalendar) {
    ArgumentChecker.isTrue(n > 0, "n must be greater than zero; have {}", n);
    ArgumentChecker.notNull(today, "today");
    ArgumentChecker.notNull(holidayCalendar, "holiday calendar");

    LocalDate expiryDate = getExpiryMonth(n, today).with(LAST_DAY_ADJUSTER);
    int nBusinessDays = 3;
    if (holidayCalendar.isWorkingDay(expiryDate)) {
      nBusinessDays--;
    }
    // go back to 3 business days
    while (nBusinessDays > 0) {
      expiryDate = expiryDate.minusDays(1);
      if (holidayCalendar.isWorkingDay(expiryDate)) {
        nBusinessDays--;
      }
    }
    return expiryDate;
  }

  @Override
  public LocalDate getExpiryMonth(final int n, final LocalDate today) {
    ArgumentChecker.isTrue(n > 0, "n must be greater than zero");
    ArgumentChecker.notNull(today, "today");
    LocalDate expiryDate = today;
    Month[] validMonths = getTradingMonths(today);
    for (int m = n; m > 0; m--) {
      expiryDate = getNextExpiryMonth(validMonths, expiryDate);
    }
    return expiryDate;
  }

  private LocalDate getNextExpiryMonth(final Month[] validMonths, final LocalDate dtCurrent) {
    Month mthCurrent = dtCurrent.getMonth();
    int idx = Arrays.binarySearch(validMonths, mthCurrent);
    if (Math.abs(idx) >= (validMonths.length - 1)) {
      return LocalDate.of(dtCurrent.getYear() + 1, validMonths[0], dtCurrent.getDayOfMonth());
    } else if (idx >= 0) {
      return dtCurrent.with(validMonths[idx + 1]);
    } else {
      return dtCurrent.with(validMonths[-idx + 1]);
    }
  }

  @Override
  public String getName() {
    return NAME;
  }
}