Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Count days between two dates with Java 8 while ignoring certain days of week

Below I have 3 methods. The first is very simple. It just counts the total number of days. The second, however, will not only count the days, but will ignore the days of the week that are passed in to the method.

My problem is that the third method is not always correct. It should match the second method. I am guessing it has something to do with leap years, because the difference is usually +=3|4 when it is incorrect.

Additional Info

I am attempting to mock Excel's weekday(serial_number,[return_type]) formula in a way.

serial_number = startDate:Date - daysOfWeekToInclude:Array<Integer>

Example

  | A       | B                                                  | C
  +---------+----------------------------------------------------+-----------
1 | Start   | =DATE(2014,9,7)                                    | 9/7/2014                 
2 | End     | =DATE(2025,6,13)                                   | 6/13/2025                    
3 | Include | ={1,2,4,6} (Mon, Tue, Thu, & Sat)                  | <Disp Only>
4 | Days    | =SUM(INT((WEEKDAY($B$1-{1,2,4,6},1)+$B$2-$B$1)/7)) | 2248 

There is more information on this function here: How to count / calculate the number of days between two dates in Excel?

Raw Image

enter image description here

Methods

  1. Simply count the number of days between two dates.

    public static int simpleDaysBetween(final LocalDate start,
            final LocalDate end) {
        return (int) ChronoUnit.DAYS.between(start, end);
    }
    
  2. Count number of days, ignoring certain days of week, using a loop.

    public static int betterDaysBetween(final LocalDate start,
            final LocalDate end, final List<DayOfWeek> ignore) {
        int count = 0;
        LocalDate curr = start.plusDays(0);
    
        while (curr.isBefore(end)) {
            if (!ignore.contains(curr.getDayOfWeek())) {
                count++;
            }
            curr = curr.plusDays(1); // Increment by a day.
        }
    
        return count;
    }
    
  3. Count number of days. again but without a loop.

    public static int bestDaysBetween(final LocalDate start,
            final LocalDate end, final List<DayOfWeek> ignore) {
        int days = simpleDaysBetween(start, end);
    
        if (days == 0) {
            return 0;
        }
    
        if (!ignore.isEmpty()) {
            int weeks = days / 7;
            int startDay = start.getDayOfWeek().getValue();
            int endDay = end.getDayOfWeek().getValue();
            int diff = weeks * ignore.size();
    
            for (DayOfWeek day : ignore) {
                int currDay = day.getValue();
                if (startDay <= currDay) {
                    diff++;
                }
                if (endDay > currDay) {
                    diff++;
                }
            }
    
            if (endDay > startDay) {
                diff -= endDay - startDay;
            }
    
            return days - diff;
        }
    
        return days;
    }
    

Full code

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;

public class DayCounter {
    public static void main(String[] args) {
        final LocalDate start = LocalDate.of(2014, 9, 7);
        final LocalDate end = LocalDate.of(2025, 6, 13);
        List<DayOfWeek> ignore = Arrays.asList(DayOfWeek.SUNDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY);

        print(start);
        print(end);

        System.out.println(simpleDaysBetween(start, end));
        System.out.println(betterDaysBetween(start, end, ignore));
        System.out.println(bestDaysBetween(start, end, ignore));
    }

    public static void print(LocalDate date) {
        System.out.printf("%s -> %s%n", date, date.getDayOfWeek());
    }

    public static int simpleDaysBetween(final LocalDate start,
            final LocalDate end) {
        return (int) ChronoUnit.DAYS.between(start, end);
    }

    public static int betterDaysBetween(final LocalDate start,
            final LocalDate end, final List<DayOfWeek> ignore) {
        int count = 0;
        LocalDate curr = start.plusDays(0);

        while (curr.isBefore(end)) {
            if (!ignore.contains(curr.getDayOfWeek())) {
                count++;
            }
            curr = curr.plusDays(1); // Increment by a day.
        }

        return count;
    }

    public static int bestDaysBetween(final LocalDate start,
            final LocalDate end, final List<DayOfWeek> ignore) {
        int days = simpleDaysBetween(start, end);

        if (days == 0) {
            return 0;
        }

        if (!ignore.isEmpty()) {
            int weeks = days / 7;
            int startDay = start.getDayOfWeek().getValue();
            int endDay = end.getDayOfWeek().getValue();
            int diff = weeks * ignore.size();

            for (DayOfWeek day : ignore) {
                int currDay = day.getValue();
                if (startDay <= currDay) {
                    diff++;
                }
                if (endDay > currDay) {
                    diff++;
                }
            }

            if (endDay > startDay) {
                diff -= endDay - startDay;
            }

            return days - diff;
        }

        return days;
    }
}
like image 582
Mr. Polywhirl Avatar asked Sep 12 '14 00:09

Mr. Polywhirl


People also ask

How do I calculate the number of days between two dates in Java 8?

In Java 8, we can use ChronoUnit. DAYS. between(from, to) to calculate days between two dates.

How do I get the number of days between two dates in Java?

The Period class has a between() method - just as the previously discussed ChronoUnit . This method takes in two LocalDate objects, one representing the starting date, and the second being the end date. It returns a Period consisting of the number of years, months, and days between two dates.

How do you check if a date falls on WeekEnd or holiday with Java 8?

Checking a Weekend using LocalDateget(ChronoField. DAY_OF_WEEK) method returns an integer value in the range of 1 to 7. Each integer value represents a different weekday. 1 represents Monday, and so on 6 represents Saturday and 7 represents Sunday.


1 Answers

If we talk about a Java 8 API, why not use the Java 8 features consequently…

static long daysBetween(LocalDate start, LocalDate end, List<DayOfWeek> ignore) {
    return Stream.iterate(start, d->d.plusDays(1))
                 .limit(start.until(end, ChronoUnit.DAYS))
                 .filter(d->!ignore.contains(d.getDayOfWeek()))
                 .count();
}

Starting with Java 9, we can use the even simpler

static long daysBetween(LocalDate start, LocalDate end, List<DayOfWeek> ignore) {
    return start.datesUntil(end)
        .filter(d->!ignore.contains(d.getDayOfWeek()))
        .count();
}

Though, it might be worth using a Set with a better-than-linear lookup rather than the List:

static long daysBetween(LocalDate start, LocalDate end, List<DayOfWeek> ignore) {
    if(ignore.isEmpty()) return start.until(end, ChronoUnit.DAYS);
    EnumSet<DayOfWeek> set = EnumSet.copyOf(ignore);
    return start.datesUntil(end)
        .filter(d->!ignore.contains(d.getDayOfWeek()))
        .count();
}

You may consider changing the parameter to Set<DayOfWeek>, as it is not only more efficient but better suited to the actual use cases. Instead of Arrays.asList(DayOfWeek.SUNDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY), you can pass EnumSet.of(DayOfWeek.SUNDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY), but you can also use constructs like EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), to denote the typical working days.

You can avoid iterating over all days, but it requires special care about corner cases and hence, thorough testing. And will only pay off for really large ranges. For completeness, this is the optimized variant:

static long daysBetween(LocalDate start, LocalDate end, Set<DayOfWeek> ignore) {
    long d1 = start.toEpochDay(), d2 = end.toEpochDay();
    if(d1 > d2) throw new IllegalArgumentException();
    if(ignore.isEmpty()) return d2 - d1;
    int incompleteWeek = 0;
    DayOfWeek startDoW = start.getDayOfWeek(), endDoW = end.getDayOfWeek();
    if(startDoW != endDoW) {
        for(int v1 = startDoW.getValue(), v2 = endDoW.getValue();
            v1 != v2 && d1 < d2; v1 = v1%7+1, d1++) {
                if(!ignore.contains(DayOfWeek.of(v1))) incompleteWeek++;
        }
    }
    return incompleteWeek + (d2 - d1) * (7 - ignore.size()) / 7;
}

Here, the performance of the ignore set’s lookup doesn’t matter, as we only look up at most six values, however, enforcing a Set, i.e. no duplicates, allows us to use the set’s size to calculate the number of days contained in complete weeks of the range. Complete weeks have the same day of week for the start and (exclusive) end date. So the code only needs to iterate the days, until the start and end day of week match.

like image 89
Holger Avatar answered Nov 15 '22 21:11

Holger