How to skip weekends while adding days to LocalDate in Java 8?
Asked Answered
M

6

30

Other answers here refer to Joda API. I want to do it using java.time.

Suppose today's date is 26th Nov 2015-Thursday, when I add 2 business days to it, I want the result as Monday 30th Nov 2015.

I am working on my own implementation but it would be great if something already exists!

EDIT:

Is there a way to do it apart from looping over?

I was trying to derive a function like:

Y = f(X1,X2) where
Y is actual number of days to add,
X1 is number of business days to add, 
X2 is day of the week (1-Monday to 7-Sunday)

Then given X1 and X2 (derived from day of week of the date), we can find Y and then use plusDays() method of LocalDate.

I have not been able to derive it so far, its not consistent. Can anyone confirm that looping over until desired number of workdays are added is the only way?

Moniquemonism answered 26/11, 2015 at 15:48 Comment(1)
R
43

The following method adds days one by one, skipping weekends, for positive values of workdays:

public LocalDate add(LocalDate date, int workdays) {
    if (workdays < 1) {
        return date;
    }

    LocalDate result = date;
    int addedDays = 0;
    while (addedDays < workdays) {
        result = result.plusDays(1);
        if (!(result.getDayOfWeek() == DayOfWeek.SATURDAY ||
              result.getDayOfWeek() == DayOfWeek.SUNDAY)) {
            ++addedDays;
        }
    }

    return result;
}

After some fiddling around, I came up with an algorithm to calculate the number of workdays to add or subtract.

/**
 * @param dayOfWeek
 *            The day of week of the start day. The values are numbered
 *            following the ISO-8601 standard, from 1 (Monday) to 7
 *            (Sunday).
 * @param businessDays
 *            The number of business days to count from the day of week. A
 *            negative number will count days in the past.
 * 
 * @return The absolute (positive) number of days including weekends.
 */
public long getAllDays(int dayOfWeek, long businessDays) {
    long result = 0;
    if (businessDays != 0) {
        boolean isStartOnWorkday = dayOfWeek < 6;
        long absBusinessDays = Math.abs(businessDays);

        if (isStartOnWorkday) {
            // if negative businessDays: count backwards by shifting weekday
            int shiftedWorkday = businessDays > 0 ? dayOfWeek : 6 - dayOfWeek;
            result = absBusinessDays + (absBusinessDays + shiftedWorkday - 1) / 5 * 2;
        } else { // start on weekend
            // if negative businessDays: count backwards by shifting weekday
            int shiftedWeekend = businessDays > 0 ? dayOfWeek : 13 - dayOfWeek;
            result = absBusinessDays + (absBusinessDays - 1) / 5 * 2 + (7 - shiftedWeekend);
        }
    }
    return result;
}

Usage Example:

LocalDate startDate = LocalDate.of(2015, 11, 26);
int businessDays = 2;
LocalDate endDate = startDate.plusDays(getAllDays(startDate.getDayOfWeek().getValue(), businessDays));

System.out.println(startDate + (businessDays > 0 ? " plus " : " minus ") + Math.abs(businessDays)
        + " business days: " + endDate);

businessDays = -6;
endDate = startDate.minusDays(getAllDays(startDate.getDayOfWeek().getValue(), businessDays));

System.out.println(startDate + (businessDays > 0 ? " plus " : " minus ") + Math.abs(businessDays)
        + " business days: " + endDate);

Example Output:

2015-11-26 plus 2 business days: 2015-11-30

2015-11-26 minus 6 business days: 2015-11-18

Rattlebox answered 26/11, 2015 at 16:46 Comment(8)
This looks great! I've tested it by adding up to 15 work days to each day from Monday to Sunday and its working as required. How far did you test it? I was also working on similar lines but couldn't get through it!Moniquemonism
I did a similar test (added an increasing amount of workdays to each day of the week), followed by generating thousands of random dates and workdays and testing the results against the simple loop solution.Rattlebox
Great! I wonder why most of the solutions refer to looping over. This solutions would be much better in terms of performance, right? Especially when large number of workdays need to be addedMoniquemonism
Might be because often a solution is needed that skips weekends and holidays. If it wasn't performace critical in my application, I would probably use the simple loop myself because it is easier to understand.Rattlebox
Right. Realized about holidays, it will get complex with the algorithm version.Moniquemonism
I reworked the example to allow calculation using negative business day values as well.Rattlebox
@bowmore The implementation should work for all locales since it uses the ISO-8601 standard. As long as the correct day of week is given, it should work.Rattlebox
Well, that was my point really. A lot of countries don't follow the ISO standard. And I'm certain that customers in those country won't care about the fact that your algorithm works for the ISO standard : It doesn't help them. In Isreal, two working days after a Wednesday is a Monday, not a Friday, because Friday is a weekend day (en.wikipedia.org/wiki/Workweek_and_weekend)Sanguinaria
E
14

Here is a version which supports both positive and negative number of days and exposes the operation as a TemporalAdjuster. That allows you to write:

LocalDate datePlus2WorkingDays = date.with(addWorkingDays(2));

Code:

/**
 * Returns the working day adjuster, which adjusts the date to the n-th following
 * working day (i.e. excluding Saturdays and Sundays).
 * <p>
 * If the argument is 0, the same date is returned if it is a working day otherwise the
 * next working day is returned.
 *
 * @param workingDays the number of working days to add to the date, may be negative
 *
 * @return the working day adjuster, not null
 */
public static TemporalAdjuster addWorkingDays(long workingDays) {
  return TemporalAdjusters.ofDateAdjuster(d -> addWorkingDays(d, workingDays));
}

private static LocalDate addWorkingDays(LocalDate startingDate, long workingDays) {
  if (workingDays == 0) return nextOrSameWorkingDay(startingDate);

  LocalDate result = startingDate;
  int step = Long.signum(workingDays); //are we going forward or backward?

  for (long i = 0; i < Math.abs(workingDays); i++) {
    result = nextWorkingDay(result, step);
  }

  return result;
}

private static LocalDate nextOrSameWorkingDay(LocalDate date) {
  return isWeekEnd(date) ? nextWorkingDay(date, 1) : date;
}

private static LocalDate nextWorkingDay(LocalDate date, int step) {
  do {
    date = date.plusDays(step);
  } while (isWeekEnd(date));
  return date;
}

private static boolean isWeekEnd(LocalDate date) {
  DayOfWeek dow = date.getDayOfWeek();
  return dow == SATURDAY || dow == SUNDAY;
}
Etherealize answered 17/12, 2015 at 18:31 Comment(1)
extremely nice! I needed close to that and slightly adjusted, but boy.. this is quite sexy. thank uLuminal
T
6

Determining business days is fundamentally a question of looping over dates, checking if each is a weekend or holiday.

The Strata project from OpenGamma (I am a committer) has an implementation of a holiday calendar. The API covers the case of finding the date 2 business days later. The implementation has an optimized bitmap design that performs better than day by day looping. It may be of interest here.

Tumor answered 26/11, 2015 at 23:49 Comment(2)
Thanks JodaStephen! Do you have any comments on Katja Christiansen's solution to this question?Moniquemonism
Algorithms can work to avoid Sat/Sun, but once you take into account holidays, you have to loop unless you use bitmaps.Tumor
V
2

This is a way to add business days using java.time Classes, some functional interfaces & lambda...

IntFunction<TemporalAdjuster> addBusinessDays = days -> TemporalAdjusters.ofDateAdjuster(
    date -> {
      LocalDate baseDate =
          days > 0 ? date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
              : days < 0 ? date.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY)) : date;
      int businessDays = days + Math.min(Math.max(baseDate.until(date).getDays(), -4), 4);
      return baseDate.plusWeeks(businessDays / 5).plusDays(businessDays % 5);
    });

LocalDate.of(2018, 1, 5).with(addBusinessDays.apply(2));
//Friday   Jan 5, 2018 -> Tuesday Jan  9, 2018

LocalDate.of(2018, 1, 6).with(addBusinessDays.apply(15));
//Saturday Jan 6, 2018 -> Friday  Jan 26, 2018

LocalDate.of(2018, 1, 7).with(addBusinessDays.apply(-10));
//Sunday   Jan 7, 2018 -> Monday  Dec 25, 2017

Supports negative values and from any week day!

Viglione answered 6/3, 2018 at 23:45 Comment(0)
O
1

This is a method which is adding or subtracting workdays to a given calendar object:

/**
 * This method adds workdays (MONDAY - FRIDAY) to a given calendar object.
 * If the number of days is negative than this method subtracts the working
 * days from the calendar object.
 * 
 * 
 * @param cal
 * @param days
 * @return new calendar instance
 */
public static Calendar addWorkDays(final Calendar baseDate, final int days) {
    Calendar resultDate = null;
    Calendar workCal = Calendar.getInstance();
    workCal.setTime(baseDate.getTime());

    int currentWorkDay = workCal.get(Calendar.DAY_OF_WEEK);

    // test if SATURDAY ?
    if (currentWorkDay == Calendar.SATURDAY) {
        // move to next FRIDAY
        workCal.add(Calendar.DAY_OF_MONTH, (days < 0 ? -1 : +2));
        currentWorkDay = workCal.get(Calendar.DAY_OF_WEEK);
    }
    // test if SUNDAY ?
    if (currentWorkDay == Calendar.SUNDAY) {
        // move to next FRIDAY
        workCal.add(Calendar.DAY_OF_MONTH, (days < 0 ? -2 : +1));
        currentWorkDay = workCal.get(Calendar.DAY_OF_WEEK);
    }

    // test if we are in a working week (should be so!)
    if (currentWorkDay >= Calendar.MONDAY && currentWorkDay <= Calendar.FRIDAY) {
        boolean inCurrentWeek = false;
        if (days > 0)
            inCurrentWeek = (currentWorkDay + days < 7);
        else
            inCurrentWeek = (currentWorkDay + days > 1);

        if (inCurrentWeek) {
            workCal.add(Calendar.DAY_OF_MONTH, days);
            resultDate = workCal;
        } else {
            int totalDays = 0;
            int daysInCurrentWeek = 0;

            // fill up current week.
            if (days > 0) {
                daysInCurrentWeek = Calendar.SATURDAY - currentWorkDay;
                totalDays = daysInCurrentWeek + 2;
            } else {
                daysInCurrentWeek = -(currentWorkDay - Calendar.SUNDAY);
                totalDays = daysInCurrentWeek - 2;
            }

            int restTotalDays = days - daysInCurrentWeek;
            // next working week... add 2 days for each week.
            int x = restTotalDays / 5;
            totalDays += restTotalDays + (x * 2);

            workCal.add(Calendar.DAY_OF_MONTH, totalDays);
            resultDate = workCal;

        }
    }   
    return resultDate;
}
Oldenburg answered 19/3, 2016 at 11:41 Comment(0)
S
0

Example:

Calculate the total number of days from the date I started working except Saturday and Sunday.

public class App {
    public static void main(String[] args) throws Exception {
        /** I write the code when 2019-8-15 */
        LocalDate now = LocalDate.now();
        LocalDate startWork = LocalDate.parse("2019-06-17");
        /** get all days */
        long allDays = Duration.between(startWork.atStartOfDay(), now.atStartOfDay()).toDays() + 1;
        System.out.println("This is the " + allDays + "th day you enter the company.");
        /** variable to store day except sunday and saturday */
        long workDays = allDays;

        for (int i = 0; i < allDays; i++) {

            if (startWork.getDayOfWeek() == DayOfWeek.SATURDAY || startWork.getDayOfWeek() == DayOfWeek.SUNDAY) {
                workDays--;
            }

            startWork = startWork.plusDays(1);
        }

        System.out.println("You actually work for a total of " + workDays + " days.");

    }
}
/**
This is the 60th day you enter the company.
You actually work for a total of 44 days.
*/
  • Hope that can help you.
Snifter answered 15/8, 2019 at 9:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.