3

I have requirement to calculate number of business days between two given dates
I have the list of holidays as an Array list provided by the user.
So I can investigate each and every day between the dates and check if its weekday and not federal holiday like the code I provided below (which is working fine)

But this is very expensive, lets say 12 federal holidays and each day I will have to check its not a weekend,
so if I need to count between 5 years it will take 365 * 5 * 12 its 21,000 iterations! its crazy (not even including the calculation for business day)
Is there a better way?

package test;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;

import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang3.time.DateUtils;

public class TestDiff {

    public static void main(String[] args) throws ParseException {
        DateFormat formatter = new SimpleDateFormat("MM/dd/yy");
        // add 4 years as an example
        Date fromDate = formatter.parse("11/06/2017"),toDate = formatter.parse("11/29/2017");// DateUtils.addDays(fromDate,365 * 4);
        int numberOfDaysCount=0;
        int daysBetween  = daysBetween(fromDate,toDate);
        Date caurDate = fromDate;

        for(int i=0;i<=daysBetween ; i++ ) {
            if(isWeekDay(caurDate) && !isFederalHoliday(caurDate) )
                numberOfDaysCount++;
            caurDate = DateUtils.addDays(caurDate,1); // add one day
        }
        System.out.println("number of business days between "+fromDate+" and "+toDate+" is: "+numberOfDaysCount);
    }

   private static boolean isWeekDay(Date caurDate) {
             Calendar c = Calendar.getInstance();
             c.setTime(caurDate);
              int dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
              return dayOfWeek!= Calendar.SATURDAY && dayOfWeek!= Calendar.SUNDAY ;
    }

        private static boolean isFederalHoliday(Date caurDate) throws ParseException {
            DateFormat formatter = new SimpleDateFormat("MM/dd/yy");    //list will come from dao.getFederalHoliday();
                List<Date> federalHolidays =  Arrays.asList(formatter.parse("01/02/2017"),formatter.parse("01/16/2017"),formatter.parse("02/20/2017"),formatter.parse("05/29/2017"),formatter.parse("07/04/2017"),formatter.parse("09/04/2017"),formatter.parse("10/09/2017"),formatter.parse("07/04/2017"),formatter.parse("11/10/2017"),formatter.parse("11/23/2017"),formatter.parse("12/25/2017"));
                for (Date holiday : federalHolidays) {
                    if(DateUtils.isSameDay(caurDate,holiday)) //using Apache commons-lang 
                        return true;
                }
                return false;
    }

        public static int daysBetween(Date d1, Date d2){
             return (int)( (d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
     }

}
JavaSheriff
  • 7,074
  • 20
  • 89
  • 159
  • 1
    This may be helpful to you, as it uses no loop, and thus doesn't exponentially get higher in runtime i don't think: https://stackoverflow.com/a/4600534/1932789 – Zachary Craig Nov 15 '17 at 18:03
  • 1
    Similar questions have been asked a number of times. Did you have a look? I’m not saying I am sure this is an exact duplicate, but I think you can find some inspiration if you search. – Ole V.V. Nov 15 '17 at 18:03
  • 1
    Take a look at [How to calculate the working(excluding weekends) days in between two different dates in JAVA?](https://stackoverflow.com/questions/32758569/how-to-calculate-the-workingexcluding-weekends-days-in-between-two-different-d); [how to calculate number of days between two dates excluding weekend java](https://stackoverflow.com/questions/18354727/how-to-calculate-number-of-days-between-two-dates-excluding-weekend-java); [Calculate number of weekdays between two dates in Java](https://stackoverflow.com/questions/4600034/calculate-number-of-weekdays-between-two-dates-in-java). – Ole V.V. Nov 15 '17 at 18:05
  • 1
    First off, Calendar has constants for days of the week, you're using literal values `6` and `7` which are wrong at least in a north american context. Second, you're re-parsing the strings into dates every time isFederalHoliday is called, which will probably be the slowest thing in that code. Finally, store them once in a sorted array and do a binary search https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html#binarySearch(java.lang.Object[],%20java.lang.Object) – Taylor Nov 15 '17 at 18:06
  • correct!! i fixed to use Calendar.SATURDAY Calendar.SUNDAY ; – JavaSheriff Nov 15 '17 at 18:40

5 Answers5

7

Here's an answer implemented in Java 8 using java.time.*.

public class TestSo47314277 {

  /**
   * A set of federal holidays. Compared to iteration, using a
   * hash-based container provides a faster access for reading
   * element via hash code. Using {@link Set} avoids duplicates.
   * <p>
   * Add more dates if needed.
   */
  private static final Set<LocalDate> HOLIDAYS;

  static {
    List<LocalDate> dates = Arrays.asList(
        LocalDate.of(2017, 1, 2),
        LocalDate.of(2017, 1, 16),
        LocalDate.of(2017, 2, 20),
        LocalDate.of(2017, 5, 29),
        LocalDate.of(2017, 7, 4),
        LocalDate.of(2017, 9, 4),
        LocalDate.of(2017, 10, 9),
        LocalDate.of(2017, 11, 10),
        LocalDate.of(2017, 11, 23),
        LocalDate.of(2017, 12, 25)
    );
    HOLIDAYS = Collections.unmodifiableSet(new HashSet<>(dates));
  }

  public int getBusinessDays(LocalDate startInclusive, LocalDate endExclusive) {
    if (startInclusive.isAfter(endExclusive)) {
      String msg = "Start date " + startInclusive
          + " must be earlier than end date " + endExclusive;
      throw new IllegalArgumentException(msg);
    }
    int businessDays = 0;
    LocalDate d = startInclusive;
    while (d.isBefore(endExclusive)) {
      DayOfWeek dw = d.getDayOfWeek();
      if (!HOLIDAYS.contains(d)
          && dw != DayOfWeek.SATURDAY
          && dw != DayOfWeek.SUNDAY) {
        businessDays++;
      }
      d = d.plusDays(1);
    }
    return businessDays;
  }
}
Mincong Huang
  • 5,284
  • 8
  • 39
  • 62
  • Thanks, do you know what is the complexity of HOLIDAYS.contains? – JavaSheriff Nov 20 '17 at 14:07
  • 1
    `HashSet#contains` is O(1) here, because the search is based on hash code of `LocalDate` and not based on iteration. See this post for more detail: https://stackoverflow.com/questions/25247854 – Mincong Huang Nov 20 '17 at 14:43
2

There are many example already given in the comments to calculate how many weekdays are between the two dates.

As far as subtracting the the federal holidays goes. Instead of looping over all the days in your fromdate-todate range, why don't you loop over all the entries in your federalHoliday array once per year in your fromDate-toDate range.

Excuse the pseudo code:

int workdays = getWeekdayCount();
for(int i = 0, count = getYearsBetween(); i < count; ++i)
{
    startIndex = (i==0?getIndexFirstHoliday():0);
    endIndex   = (i==(count-1)?getIndexLastHoliday():11);
    for(; startIndex <= endIndex; ++startIndex)
    {
        if(!federalHolidays[startIndex].IsWeekday(count))
            workdays--;
    }
}
  • getWeekdayCount: gets you all of the weekdays in the range.
  • getIndexFirstHoliday: loops through your federalHolidays array and returns the first index where the date is bigger than the fromDate
  • getIndexLastHoliday: loops through your federalHolidays array (backwards) and returns the last index where the date is smaller than the toDate.
  • isWeekday: determines if the date is a weekday in the year you're looping through (if it is, it's already been discarded in getWeekdayCount so we don't need to subtract!)

This way, you're looping max 12 times per year, plus another 2 * 12 to get the first and the last index.

user1178830
  • 436
  • 3
  • 11
1

Instead of iterating over each day and checking whether it is a week day and not a holiday, you could instead do the following:

  • get the number of days between your 2 dates
  • calculate the number of full weeks and either calculate the number of potential business days (multiply with 5) or the number of weekends to subtract (multiply with 2)
  • calculate the number of week days to adjust for partial weeks
  • get the number of non-weekend holidays in the required range and subtract those (for n holidays this would just be up to n checks for each year - and that could be optimized even further)

Putting everything together, it could look like this:

//start is inclusive, end is exclusive
public long getBusinessDays(LocalDate start, LocalDate end) {
  long days = ChronoUnit.DAYS.between(start, end);

  long fullWeeks = days/7;

  //day of week value ranges from 1 (Monday) to 7 (Sunday)
  //clamp Sunday to Saturday (so treat them as one day) - range is now 1 to 6
  //business days is the difference in days if dow(start) < dow(end)
  //if start and end are on a weekend we'll get 6-6 = 0
  long partialWeekAdjustment = Math.min(6, end.getDayOfWeek().getValue()) -  Math.min(6, start.getDayOfWeek().getValue() );

  //if the result is negative, we have dow(start) > dow(end) so add 5 business days
  //ex.: thu (4) to wed (3) will be 3-4 = -1, so adding 5 will result in 4 (thu, fri, mon, tue)
  if( partialWeekAdjustment < 0 ) {
    partialWeekAdjustment += 5;         
  }

  //get the number of non-weekend holidays between the 2 dates       
  long numNonWeekendHolidays = getNonWeekendHolidays(start, end);

  long businessDays = fullWeeks * 5 + partialWeekAdjustment - numNonWeekendHolidays;
  return businessDays;
}

private long getNonWeekendHolidays(LocalDate start, LocalDate end) {
   //get this from somewhere, could also be something else
   SortedSet<LocalDate> holidays = ...;

   //get holidays between the 2 dates, filter for non-weekend and count
   return holidays.subSet(start, end).stream()
                  .filter(d -> d.getDayOfWeek().compareTo(DayOfWeek.SATURDAY) < 0)
                  .count();

   //if weekend and non-weekend holidays are kept separate this could be:
   // SortedSet<LocalDate> nonWeekendHolidays = ...;
   // return nonWeekendHolidays.subSet(start, end).size();
}
Thomas
  • 87,414
  • 12
  • 119
  • 157
1
long numberOfDaysBetween = ChronoUnit.DAYS.between(fromDate, toDate);
                ArrayList<LocalDate> dates = new ArrayList<>();
                for(int i=0; i<=numberOfDaysBetween; i++) {
                    LocalDate l = fromDate.plus(i, ChronoUnit.DAYS);
                    if(l.getDayOfWeek().getValue() != 6 && l.getDayOfWeek().getValue() != 7) {
                        dates.add(l);
                    }
                }
  • 1
    I would fine `! l.getDayOfWeek().equals(DayOfWeek.SATURDAY)` clearer to read and understand (similar for Sunday). – Ole V.V. Oct 05 '22 at 11:17
0

The below code is a complete project that I've written in java 8 and will solve your problem.

    import java.time.DayOfWeek;
    import java.time.LocalDate;
    import java.time.temporal.ChronoUnit;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;
    import java.util.function.Predicate;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    public class App {
    
        public static void main(String[] args) {
    
            System.out.println("Hello World!");
            LocalDate today = LocalDate.of(2020, 5, 5);
    
            // Add one holiday for testing
            List<LocalDate> holidays = new ArrayList<>();
            holidays.add(LocalDate.of(2020, 5, 11));
            holidays.add(LocalDate.of(2020, 5, 1));
            for (LocalDate x : BusinessDaysBetween(today, today.plusDays(20), Optional.empty()))
            {
                System.out.println(x.toString());
            }
        }
        private static List<LocalDate> BusinessDaysBetween(LocalDate startDate, LocalDate endDate,
                                                Optional<List<LocalDate>> holidays)
        {
            if (startDate == null || endDate == null || !holidays.isPresent()) {
                throw new IllegalArgumentException("Invalid method argument(s) to countBusinessDaysBetween(" + startDate
                        + "," + endDate + "," + holidays + ")");
            }
    
            Predicate<LocalDate> isHoliday = date -> holidays.map(localDates -> localDates.contains(date)).orElse(false);
    
            Predicate<LocalDate> isWeekend = date -> date.getDayOfWeek() == DayOfWeek.SATURDAY
                    || date.getDayOfWeek() == DayOfWeek.SUNDAY;
    
            long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
    
            return Stream.iterate(startDate, date -> date.plusDays(1)).limit(daysBetween)
                    .filter(isHoliday.or(isWeekend).negate()).collect(Collectors.toList());
        }
    }
SternK
  • 11,649
  • 22
  • 32
  • 46
Mohsen Bahaloo
  • 257
  • 2
  • 2