Tuesday, February 19, 2013

Java Find Next / Previous / Nth Business / Working Day

Below is a Utility class for calculating business days, holidays, moths'  nth weekdays (e.g. second Monday) etc. Most credits must go to http://stackoverflow.com/users/66125/jnt30 though I did add some code for previous business day, nth business day calculation and Greek Orthodox Easter Algorithm. Haven't fully tested it yet so feel free to comment if you find any issues with this code:
(dependencies: http://commons.apache.org/lang/ , http://logging.apache.org/log4j/1.2/ )

---------------------------------------------------------------------------------------------
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;

public class Holidays {
    private static Logger log = Logger.getLogger(Holidays.class);
    private static transient Map> computedDates = new HashMap>();

    /**
     * This method will calculate the next business day
     * after the one input.  This means that if the next
     * day falls on a weekend or one of the following
     * holidays then it will try the next day.
     *
     *
     **/
    public static boolean isBusinessDay(Date dateToCheck)
    {
        //Setup the calendar to have the start date truncated
        Calendar baseCal = Calendar.getInstance();
        baseCal.setTime(DateUtils.truncate(dateToCheck, Calendar.DATE));

        List offlimitDates;

        //Grab the list of dates for the year.  These SHOULD NOT be modified.
        synchronized (computedDates)
        {
                int year = baseCal.get(Calendar.YEAR);

                //If the map doesn't already have the dates computed, create them.
                if (!computedDates.containsKey(year))
                        computedDates.put(year, getOfflimitDates(year));
                offlimitDates = computedDates.get(year);
        }

        //Determine if the date is on a weekend.
        int dayOfWeek = baseCal.get(Calendar.DAY_OF_WEEK);
        boolean onWeekend =  dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY;

        //If it's on a holiday, increment and test again
        //If it's on a weekend, increment necessary amount and test again
        //System.out.println(offlimitDates);

        if (offlimitDates.contains(baseCal.getTime()) || onWeekend)
                return false;
        else
                return true;
    }

    
    /**
     *
     * This method will calculate the previous business day
     * before the one input.  This leverages the isBusinessDay
     * heavily, so look at that documentation for further information.
     *
     * @param startDate the Date of which you need the previous business day.
     * @return The previous business day.  I.E. it doesn't fall on a weekend,
     * a holiday or the official observance of that holiday if it fell
     * on a weekend.
     *
     */
    public static Date getPreviousBusinessDay(Date startDate)
    {
        //Decrement the Date object by a Day and clear out hour/min/sec information
        Date previousDay = DateUtils.truncate(addDays(startDate, -1), Calendar.DATE);
        //If yesterday is a valid business day, return it
        if (isBusinessDay(previousDay))
                return previousDay;
        //Else we recursively call our function until we find one.
        else
                return getPreviousBusinessDay(previousDay);
    }

    /**
     *
     * This method will calculate the last business day of the month of the 
     * input date.  This leverages the  getPreviousBusinessDay
     * heavily, so look at that documentation for further information.
     *
     * @param startDate the current Date, from which we will derive a month.
     * @return last business day of the month of the date given 
     * @see #getPreviousBusinessDay(java.util.Date) 
     **/
    public static Date getMonthsLastBusinessDay(Date startDate)
    {
        Date result=null;
        Calendar cal = new GregorianCalendar();
        cal.setTime(startDate);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = cal.getActualMaximum(Calendar.DAY_OF_MONTH); //cal.get(Calendar.DAY_OF_MONTH);
        result = new GregorianCalendar(year,month,day).getTime();
        if (!isBusinessDay(result))
            result = getPreviousBusinessDay(result);
        return result;
    }
    
    /**
     *
     * This method will calculate the Nth business day of the month of the 
     * input date.  This leverages the isBusinessDay
     * heavily, so look at that documentation for further information.
     *
     * @param startDate the current Date, from which we will derive a month.
     * @param n Number of days
     * @return last business day of the month of the date given 
     * @see #isBusinessDay(java.util.Date) 
     **/
    public static Date getNthBusinessDayOfMonth(Date startDate, int n)
    {
        Date result=null;
        Calendar cal = new GregorianCalendar();
        cal.setTime(startDate);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = 1; //cal.get(Calendar.DAY_OF_MONTH);
        result = new GregorianCalendar(year,month,day).getTime();
        int i=0;
        do{
         if (isBusinessDay(result)){
             i++;
             if (i>=n) break;
         }
         result = addDays(result, 1);
        }while(i getOfflimitDates(int year)
    {
        List offlimitDates = new ArrayList();

        Calendar baseCalendar = GregorianCalendar.getInstance();
        baseCalendar.clear();

        //Add in the static dates for the year.
        //New years day
        baseCalendar.set(year, Calendar.JANUARY, 1);
        offlimitDates.add(baseCalendar.getTime());

        //Christmas
        baseCalendar.set(year, Calendar.DECEMBER, 25);
        offlimitDates.add(baseCalendar.getTime());
        baseCalendar.set(year, Calendar.DECEMBER, 26);
        offlimitDates.add(baseCalendar.getTime());
        
        //Now deal with floating holidays.
        //Martin Luther King Day
        //offlimitDates.add(calculateFloatingHoliday(3, Calendar.MONDAY, year, Calendar.JANUARY));

        //Thanksgiving Day and Thanksgiving Friday
        //Date thanksgiving = calculateFloatingHoliday(4, Calendar.THURSDAY, year, Calendar.NOVEMBER);
        //offlimitDates.add(thanksgiving);
        //offlimitDates.add(addDays(thanksgiving, 1));
       
        //Greece Specific
        Date pasxa = getOrthodoxEaster(year);
        offlimitDates.add(pasxa);
        offlimitDates.add(addDays(pasxa,-3));
        offlimitDates.add(addDays(pasxa,1));
        //25 March 
        baseCalendar.set(year, Calendar.MARCH, 25);
        offlimitDates.add(baseCalendar.getTime());
        //28 October
        baseCalendar.set(year, Calendar.OCTOBER, 28);
        offlimitDates.add(baseCalendar.getTime());
        //Fwta
        baseCalendar.set(year, Calendar.JANUARY, 6);
        offlimitDates.add(baseCalendar.getTime());
        //May Day
        baseCalendar.set(year, Calendar.MAY, 1);
        offlimitDates.add(baseCalendar.getTime());
        // 15 August
        baseCalendar.set(year, Calendar.AUGUST, 15);
        offlimitDates.add(baseCalendar.getTime());
        
        return offlimitDates;
    }


    /**
     * This method will take in the various parameters and return a Date objet
     * that represents that value.
     *
     * Ex. To get Martin Luther Kings BDay, which is the 3rd Monday of January,
     * the method call woudl be:
     *
     * calculateFloatingHoliday(3, Calendar.MONDAY, year, Calendar.JANUARY);
     *
     * Reference material can be found at:
     * http://michaelthompson.org/technikos/holidays.php#MemorialDay
     *
     * @param nth 0 for Last, 1 for 1st, 2 for 2nd, etc.
     * @param dayOfWeek Use Calendar.MODAY, Calendar.TUESDAY, etc.
     * @param year
     * @param month Use Calendar.JANUARY, etc.
     * @return
     */
    private static Date calculateFloatingHoliday(int nth, int dayOfWeek, int year, int month)
    {
        Calendar baseCal = Calendar.getInstance();
        baseCal.clear();

        //Determine what the very earliest day this could occur.
        //If the value was 0 for the nth parameter, incriment to the following
        //month so that it can be subtracted alter.
        baseCal.set(year, month + ((nth <= 0) ? 1 : 0), 1);
        Date baseDate = baseCal.getTime();

        //Figure out which day of the week that this "earliest" could occur on
        //and then determine what the offset is for our day that we actually need.
        int baseDayOfWeek = baseCal.get(Calendar.DAY_OF_WEEK);
        int fwd = dayOfWeek - baseDayOfWeek;

        //Based on the offset and the nth parameter, we are able to determine the offset of days and then
        //adjust our base date.
        return addDays(baseDate, (fwd + (nth - (fwd >= 0 ? 1 : 0)) * 7));
    }

    /**
     * If the given date falls on a weekend, the
     * method will adjust to the closest weekday.
     * I.E. If the date is on a Saturday, then the Friday
     * will be returned, if it's a Sunday, then Monday
     * is returned.
     **/
    private static Date offsetForWeekend(Calendar baseCal)
    {
        Date returnDate = baseCal.getTime();
        if (baseCal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY)
        {
                if (log.isDebugEnabled())
                        log.debug("Offsetting the Saturday by -1: " + returnDate);
                return addDays(returnDate, -1);
        }
        else if (baseCal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)
        {
                if (log.isDebugEnabled())
                        log.debug("Offsetting the Sunday by +1: " + returnDate);
                return addDays(returnDate, 1);
        }
        else
                return returnDate;
    }

    /**
     * Private method simply adds
     * @param dateToAdd
     * @param numberOfDay
     * @return
     */
    private static Date addDays(Date dateToAdd, int numberOfDay)
    {
        if (dateToAdd == null)
                throw new IllegalArgumentException("Date can't be null!");
        Calendar tempCal = Calendar.getInstance();
        tempCal.setTime(dateToAdd);
        tempCal.add(Calendar.DATE, numberOfDay);
        return tempCal.getTime();
    }
    /**
     *  Compute the day of the year that Orthodox Easter falls on.
     *  Based on Gausean Algorithm.
     *   
     *  @param the_year Year 
     *  @return Orthodox Easter Sunday
     */
    public static final Date getOrthodoxEaster(int the_year){
    //Gaus Algorithm
    Date easter;
    int res = (19*(the_year%19)+16)%30+(2*(the_year%4)+4*(the_year%7)+6*(((19*(the_year%19))+16)%30))%7+3;
    if (res < 31)  
    easter = (new GregorianCalendar(the_year,Calendar.APRIL, res)).getTime(); // 4-1  //  pasxa = res+"/"+"04"+"/"+the_year;
    else 
    easter = (new GregorianCalendar(the_year,Calendar.MAY, res-30)).getTime(); // 5-1  // pasxa =  (res-30)+"/"+"05"+"/"+the_year;  
    return easter;
    } 
    
 /**
   * Compute the day of the year that Easter falls on. Step names E1 E2 etc.,
   * are direct references to Knuth, Vol 1, p 155. @exception
   * IllegalArgumentexception If the year is before 1582 (since the algorithm
   * only works on the Gregorian calendar).
   * 
   * @param year Year
   * @return Catholic Easter
   **/
  public static final Date findHolyDay(int year) {
    if (year <= 1582) {
      throw new IllegalArgumentException(
          "Algorithm invalid before April 1583");
    }
    int golden, century, x, z, d, epact, n;

    golden = (year % 19) + 1; // E1: metonic cycle 
    century = (year / 100) + 1; // E2: e.g. 1984 was in 20th C 
    x = (3 * century / 4) - 12; // E3: leap year correction 
    z = ((8 * century + 5) / 25) - 5; // E3: sync with moon's orbit 
    d = (5 * year / 4) - x - 10;
    epact = (11 * golden + 20 + z - x) % 30; // E5: epact 
    if ((epact == 25 && golden > 11) || epact == 24)
      epact++;
    n = 44 - epact;
    n += 30 * (n < 21 ? 1 : 0); /* E6: */
    n += 7 - ((d + n) % 7);
    if (n > 31) /* E7: */
      return (new GregorianCalendar(year, 4 - 1, n - 31)).getTime(); // April 
    else
      return (new GregorianCalendar(year, 3 - 1, n)).getTime(); // March 
  }
  
    public static void main(String [] args){
        SimpleDateFormat sdf  = new SimpleDateFormat("MM-dd-yyyy HH:mm");

        java.util.Date dd = sdf.parse("09-01-2011 13:00", new ParsePosition(0));
        dd = new java.util.Date(dd.getTime() - 24L*3600L*1000L);
        for(int j=0; j<7; j++){
            dd = Holidays.getNextBusinessDay(dd);
          //  System.out.println(j + ": " + dd);
        }

      System.out.println(dd);
      int thisYear = new GregorianCalendar().get(Calendar.YEAR);
      Date c = Holidays.findHolyDay(thisYear);
      System.out.println("catholic "+ c);
      System.out.println("orthodosx" + Holidays.getOrthodoxEaster(thisYear));
      System.out.println(Holidays.isBusinessDay(new GregorianCalendar(2013, 5 - 1, 5).getTime()));
      System.out.println(Holidays.isBusinessDay(new GregorianCalendar(2013, 10 - 1, 28).getTime()));
      System.out.println(Holidays.getMonthsLastBusinessDay(new GregorianCalendar(2013, Calendar.AUGUST, 1).getTime()));
      System.out.println(Holidays.getNthBusinessDayOfMonth(new Date(),7));
    }

}



---------------------------------------------------------------------------------------------