8

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;
    }
}
JodaStephen
  • 60,927
  • 15
  • 95
  • 117
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
  • possible duplicate of [Calculate number of weekdays between two dates in Java](http://stackoverflow.com/questions/4600034/calculate-number-of-weekdays-between-two-dates-in-java). And [this](http://stackoverflow.com/q/2751302/642706). And [this](http://stackoverflow.com/q/15622413/642706). And [this](http://stackoverflow.com/q/12143194/642706). And [this](http://stackoverflow.com/q/8171968/642706). – Basil Bourque Sep 12 '14 at 04:44
  • 1
    @BasilBourque this one is using the new Java time api. – assylias Sep 12 '14 at 06:24
  • Could you please provide some input, output and expected output? – Tapas Bose Sep 12 '14 at 08:28
  • 3
    @BasilBourque This is not a duplicate. I want to take advantage of Java 8's new Date library... – Mr. Polywhirl Sep 12 '14 at 10:13
  • @TapasBose I added some addition info. The example in my code above should give `2248` for `9/7/2014 -> 6/13/2025` excluding Sun, Wed, & Fri. – Mr. Polywhirl Sep 12 '14 at 11:07
  • @Mr.Polywhirl I am getting 2247 days from my code also I have checked my result against this site http://www.timeanddate.com/date/duration.html – Tapas Bose Sep 12 '14 at 11:59

2 Answers2

14

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Although streaming with map reduce and filter looks fancy, it is ~41.65% as slower than just looping over the days. See my performance test code here: http://pastebin.com/YaaPczwL – Mr. Polywhirl Sep 13 '14 at 03:20
  • Java 8 lambdas and map-reduce is great, but it is not very "performance favorable". When I mentioned Java 8, I really meant using the actual Date objects as opposed to jdk5,6,7 Date and Calendar functions. I do appreciate your response, thanks. – Mr. Polywhirl Sep 13 '14 at 03:24
  • 1
    Besides the fact that your testing method is questionabe as test runs which do not use the result values are potential subject of dead code elimination, speed differences below factor two are not worth discussing. They may look quite different with the next jre update. Or when changing between client and server jvm. Or using a different amount of max RAM. Or running it together with other (e.g. real life) code. With such low differences, the more readable solution is preferable. – Holger Sep 13 '14 at 10:53
  • OK, I am going to give you the benefit of the doubt. I do prefer this method and I am sure that as the language improves, this method will be more preferred. Thanks. – Mr. Polywhirl Sep 14 '14 at 17:50
  • 2
    @assylias finally, I added a high-performance alternative for those, who really need it. Though I continue recommending the readable version, unless that last bit of speed is really needed… – Holger Aug 15 '19 at 14:13
4

You you are using wrong Excel formula. See the section "Using SUM and INT function to count the number of workdays" of the site that you have provided. It is stating the formula as:

=SUM(INT((WEEKDAY(A2-{2,3,4,5,6})+B2-A2)/7))

In Excel, Sunday is 1 and Saturday is 7. The numbers inside the curly braces indicates the day-of-weeks to be included. So for your case the formula will be:

=SUM(INT((WEEKDAY(A2-{2,3,5,7})+B2-A2)/7))

Please see the attached screenshot:

enter image description here

It is returning 2247 as the following code returns:

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;


public class SO25798876 {

    public static void main(String[] args) {
        String strStartDate = "09/07/2014";
        String strEndDate = "06/13/2025";
        String pattern = "MM/dd/yyyy";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
        LocalDate startDate = LocalDate.parse(strStartDate, formatter);
        LocalDate endDate = LocalDate.parse(strEndDate, formatter);

        int count = 0;

        while(startDate.isBefore(endDate) || startDate.isEqual(endDate)) {  // you may want to not to use the isEqual method        
            DayOfWeek dayOfWeek = startDate.getDayOfWeek();         

            if(!(dayOfWeek == DayOfWeek.SUNDAY || dayOfWeek == DayOfWeek.WEDNESDAY || dayOfWeek == DayOfWeek.FRIDAY)) {
                count++;
            }           

            startDate = startDate.plusDays(1);
        }

        System.out.println(count);  
    }
}

You also have mentioned your doubt that the java.time may be not considering leap year, which is wrong, if you add the following piece of code

long year = startDate.getYear();

if(Year.isLeap(year)) {
    Month month = startDate.getMonth();

    if(month == Month.FEBRUARY && startDate.getDayOfMonth() == 29) {
        System.out.println("Calculated 29th Feb for the year: " + year);
    }
}

You will see that it is printing:

Calculated 29th Feb for the year: 2016
Calculated 29th Feb for the year: 2020
Calculated 29th Feb for the year: 2024

Lastly the count will be 2247 which matches the Excel result.

Happy coding.

-Tapas

Tapas Bose
  • 28,796
  • 74
  • 215
  • 331
  • Your answer is the same as my second. All you did was add `|| curr.isEqual(end)` to while condition. -_- It still uses a while-loop. Can this not be achieved using arithmetic? – Mr. Polywhirl Sep 13 '14 at 00:59