Difference between two dates, including only business days (i.e. excluding weekends and holidays)

sp00m picture sp00m · Aug 27, 2012 · Viewed 9.8k times · Source

How can I get the number of business days between two java.util.Date, i.e. excluding weekends and holidays? By holidays, I mean legally recognized holidays. It must be depending on the country, because of the holidays that are different from a country to another.

For example, 2012-08-27 - 2012-08-24 should return 1 instead of 3, because of the weekend inbetween.

I already had a look on Jollyday and ObjectLab-Kit, but I can't make them meet my need. I mean, both of them have a lot of interesting methods, but can't find something like getBusinessDaysCount(Date d1, Date d2)...

Answer

sp00m picture sp00m · Aug 27, 2012

Here is finally a solution based on the Calendar API:

/**
 * Handles holidays by country.
 */
public enum Holidays {

  /**
   * See <a href="http://www.wikiwand.com/en/Public_holidays_in_France">http://www.wikiwand.com/en/Public_holidays_in_France</a>.
   */
  FRANCE {

    @Override
    protected void addFixedHolidays(Set<Holiday> holidays) {
      holidays.add(new Holiday(Calendar.JANUARY, 1));
      holidays.add(new Holiday(Calendar.MAY, 1));
      holidays.add(new Holiday(Calendar.MAY, 8));
      holidays.add(new Holiday(Calendar.JULY, 14));
      holidays.add(new Holiday(Calendar.AUGUST, 15));
      holidays.add(new Holiday(Calendar.NOVEMBER, 1));
      holidays.add(new Holiday(Calendar.NOVEMBER, 11));
      holidays.add(new Holiday(Calendar.DECEMBER, 25));
    }

    @Override
    protected void addVariableHolidays(int year, Set<Holiday> holidays) {
      Date easterSunday = getEasterSunday(year);
      holidays.add(new Holiday(getEasterMonday(easterSunday)));
      holidays.add(new Holiday(getAscensionThursday(easterSunday)));
      holidays.add(new Holiday(getPentecostMonday(easterSunday)));
    }

  },

  /**
   * See <a href="http://www.wikiwand.com/en/Public_holidays_in_the_United_Kingdom">http://www.wikiwand.com/en/Public_holidays_in_the_United_Kingdom</a>.
   */
  ENGLAND {

    @Override
    protected void addFixedHolidays(Set<Holiday> holidays) {
      holidays.add(new Holiday(Calendar.JANUARY, 1));
      holidays.add(new Holiday(Calendar.DECEMBER, 25));
      holidays.add(new Holiday(Calendar.DECEMBER, 26));
    }

    @Override
    protected void addVariableHolidays(int year, Set<Holiday> holidays) {
      Date easterSunday = getEasterSunday(year);
      holidays.add(new Holiday(getGoodFriday(easterSunday)));
      holidays.add(new Holiday(getEasterMonday(easterSunday)));
      holidays.add(new Holiday(get(WeekdayIndex.FIRST, Calendar.MONDAY, Calendar.MAY, year)));
      holidays.add(new Holiday(get(WeekdayIndex.LAST, Calendar.MONDAY, Calendar.MAY, year)));
      holidays.add(new Holiday(get(WeekdayIndex.LAST, Calendar.MONDAY, Calendar.AUGUST, year)));
      Holiday christmasDay = new Holiday(Calendar.DECEMBER, 25);
      if (christmasDay.isWeekend(year)) {
        holidays.add(new Holiday(Calendar.DECEMBER, 27));
      }
      Holiday boxingDay = new Holiday(Calendar.DECEMBER, 26);
      if (boxingDay.isWeekend(year)) {
        holidays.add(new Holiday(Calendar.DECEMBER, 28));
      }
    }

  };

  public class HolidayException extends Exception {

    private static final long serialVersionUID = 1L;

    private HolidayException(String message) {
      super(message);
    }

  }

  /**
   * A holiday is defined by a {@link Calendar#MONTH} and a {@link Calendar#DAY_OF_MONTH}.
   */
  private class Holiday {

    private final int day;
    private final int month;

    public Holiday(Date date) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(date);
      month = calendar.get(Calendar.MONTH);
      day = calendar.get(Calendar.DAY_OF_MONTH);
    }

    public Holiday(int month, int day) {
      this.month = month;
      this.day = day;
    }

    public Date toDate(int year) {
      Calendar calendar = Calendar.getInstance();
      calendar.set(year, month, day);
      return calendar.getTime();
    }

    public boolean isWeekend(int year) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(toDate(year));
      int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
      return dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      } else if (!(obj instanceof Holiday)) {
        return false;
      } else {
        Holiday holiday = (Holiday) obj;
        return holiday.month == month && holiday.day == day;
      }
    }

    @Override
    public int hashCode() {
      return Arrays.hashCode(new int[] { month, day });
    }

  }

  /**
   * Use with {@link Holidays#get(WeekdayIndex, int, int, int)}.<br />
   * <br />
   * Example: <code>Holidays.get(WeekdayIndex.FIRST, Calendar.MONDAY, Calendar.MAY, 2000)</code>.
   */
  public enum WeekdayIndex {

    FIRST(1), SECOND(2), THIRD(3), FOURTH(4), LAST(null);

    private final Integer index;

    private WeekdayIndex(Integer index) {
      this.index = index;
    }

    private boolean is(int count) {
      return index != null && index == count;
    }

  }

  private final Set<Holiday> fixedHolidays = new HashSet<Holiday>();

  private final Map<Integer, Set<Holiday>> variableHolidays = new HashMap<Integer, Set<Holiday>>();

  private Holidays() {
    addFixedHolidays(fixedHolidays);
  }

  protected abstract void addFixedHolidays(Set<Holiday> holidays);

  protected abstract void addVariableHolidays(int year, Set<Holiday> holidays);

  /**
   * Returns the number of business days between two dates.
   * 
   * @param d1
   *          The first date.
   * @param d2
   *          The second date.
   * @return The number of business days between the two provided dates.
   * @throws HolidayException
   *           If <code>d1</code> or <code>d2</code> is not a business day.
   */
  public int getBusinessDayCount(Date d1, Date d2) throws HolidayException {
    Calendar calendar = Calendar.getInstance();
    DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
    try {
      d1 = formatter.parse(formatter.format(d1));
      d2 = formatter.parse(formatter.format(d2));
    } catch (ParseException ignore) {
      // cannot happen
    }
    if (!isBusinessDay(d1) || !isBusinessDay(d2)) {
      throw new HolidayException("Input dates must be business days");
    }
    int businessDayCount = 0;
    Date min = d1.before(d2) ? d1 : d2;
    Date max = min.equals(d2) ? d1 : d2;
    calendar.setTime(min);
    while (calendar.getTime().before(max)) {
      calendar.add(Calendar.DAY_OF_MONTH, 1);
      if (isBusinessDay(calendar.getTime())) {
        businessDayCount++;
      }
    }
    return businessDayCount;
  }

  /**
   * Returns whether a date is a business day.
   * 
   * @param date
   *          The date.
   * @return <code>true</code> if the <code>date</code> is a business day, <code>false</code> otherwise.
   */
  public boolean isBusinessDay(Date date) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
    if (dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) {
      return false;
    } else if (isFixedHoliday(date)) {
      return false;
    } else if (isVariableHoliday(date)) {
      return false;
    }
    return true;
  }

  private boolean isFixedHoliday(Date date) {
    return fixedHolidays.contains(new Holiday(date));
  }

  private boolean isVariableHoliday(Date date) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    int year = calendar.get(Calendar.YEAR);
    Set<Holiday> yearHolidays;
    if (!variableHolidays.containsKey(year)) {
      // variable holidays have not been calculated for this year yet
      yearHolidays = new HashSet<Holiday>();
      addVariableHolidays(year, yearHolidays);
      variableHolidays.put(year, yearHolidays);
    } else {
      yearHolidays = variableHolidays.get(year);
    }
    return yearHolidays.contains(new Holiday(date));
  }

  public static Date getEasterSunday(int year) {
    // credits: https://www.wikiwand.com/en/Computus#/Anonymous_Gregorian_algorithm
    Calendar calendar = Calendar.getInstance();
    int initialYear = year;
    if (year < 1900) {
      year += 1900;
    }
    int a = year % 19;
    int b = year / 100;
    int c = year % 100;
    int d = b / 4;
    int e = b % 4;
    int f = (b + 8) / 25;
    int g = (b - f + 1) / 3;
    int h = (19 * a + b - d - g + 15) % 30;
    int i = c / 4;
    int j = c % 4;
    int k = (32 + 2 * e + 2 * i - h - j) % 7;
    int l = (a + 11 * h + 22 * k) / 451;
    int m = (h + k - 7 * l + 114) % 31;
    int month = (h + k - 7 * l + 114) / 31 - 1;
    int day = m + 1;
    calendar.set(initialYear, month, day);
    return calendar.getTime();
  }

  public static Date getGoodFriday(Date easterSunday) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(easterSunday);
    calendar.add(Calendar.DAY_OF_MONTH, -2);
    return calendar.getTime();
  }

  public static Date getEasterMonday(Date easterSunday) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(easterSunday);
    calendar.add(Calendar.DAY_OF_MONTH, 1);
    return calendar.getTime();
  }

  public static Date getAscensionThursday(Date easterSunday) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(easterSunday);
    calendar.add(Calendar.DAY_OF_MONTH, 39);
    return calendar.getTime();
  }

  public static Date getPentecostMonday(Date easterSunday) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(easterSunday);
    calendar.add(Calendar.DAY_OF_MONTH, 50);
    return calendar.getTime();
  }

  public static Date get(WeekdayIndex weekdayIndex, int dayOfWeek, int month, int year) {
    Calendar calendar = Calendar.getInstance();
    calendar.set(year, month, 1);
    int count = 0;
    Date last = null;
    do {
      if (calendar.get(Calendar.DAY_OF_WEEK) == dayOfWeek) {
        count++;
        last = calendar.getTime();
        if (weekdayIndex.is(count)) {
          return last;
        }
      }
      calendar.add(Calendar.DAY_OF_MONTH, 1);
    } while (calendar.get(Calendar.MONTH) == month);
    if (weekdayIndex.equals(WeekdayIndex.LAST)) {
      return last;
    }
    return null;
  }

}

Calling with:

Holidays.FRANCE.getBusinessDayCount(d1, d2);