-3

How would I go about getting the number of weekdays (Monday though Friday) in a month with LocalDate? I've never used java.time before so I don't know all of its workings. I've been looking on this site to no avail along with searching for an answer. I also do not want to use any external libraries.

Example: As of this month, April of 2018, there are 21 weekdays. And next month there is 23 weekdays.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
Kyu Vulpes
  • 79
  • 11
  • Can you give an example of expected input/output? – Idos Apr 29 '18 at 05:04
  • @idos Does the example help? – Kyu Vulpes Apr 29 '18 at 05:12
  • 3
    I don't know of an elegant solution, but the bruteforce is obvious: Create a [LocalDate](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html) for the first day of the respective month. Loop until the end of the month using ld.plusDays(1) and use ld.getDayOfWeek() to see which [DayOfWeek](https://docs.oracle.com/javase/8/docs/api/java/time/DayOfWeek.html) you're dealing with. Then conditionally add to your businessday counter. – Dreamspace President Apr 29 '18 at 05:17
  • *"I don't know all of its workings"* Then you should **read the documentation** so you can **learn**, e.g. the javadoc of [`LocalDate`](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html). – Andreas Apr 29 '18 at 05:51
  • @Andreas I am reading it, but I'm also coming here for understanding and to ask if somethings possible. That's the whole point of this site. – Kyu Vulpes Apr 29 '18 at 05:57
  • 2
    By 'business days', do you mean how many Monday-Fridays or do you want to exclude public holidays? The later cannot be done with internal libraries alone since it is dependent on country and varies with time. – DHa Apr 29 '18 at 06:01
  • @DHa By my knowledge, here in the US, a business day is usually Monday-Friday. And now since I'm thinking about it, saying weekday would have probably been a better title. – Kyu Vulpes Apr 29 '18 at 06:05
  • @TechNGamer Then you should edit the question and correct the title, to clarify it – Andreas Apr 29 '18 at 06:40
  • 1
    In case you’re interested, I compared the results from the two methods by @Andreas and the method from your own answer on all months from 1905 through 2070. They agreed. So seem to be correct all of them. – Ole V.V. Apr 30 '18 at 03:35

2 Answers2

5

If desired, see optimized brute-force solution at the end

Here is a non-brute-force implementation to calculate week days (Mon-Fri) in a month.

It uses YearMonth instead of LocalDate, since the day-of-month value is meaningless to the calculation.

public static int weekDaysInMonth(YearMonth yearMonth) {
    int len = yearMonth.lengthOfMonth(); // 28-31, supporting leap year
    int dow = yearMonth.atDay(1).getDayOfWeek().getValue(); // 1=Mon, 7=Sun
    return (dow <= 5 ? Math.min(len - 8, 26 - dow) : Math.max(len + dow - 16, 20));
}

Here is an overload taking a LocalDate, so it's easy to call if that's what you have.

public static int weekDaysInMonth(LocalDate date) {
    return weekDaysInMonth(YearMonth.from(date));
}

Test

System.out.println(weekDaysInMonth(LocalDate.parse("2018-04-15"))); // April 15, 2018
System.out.println(weekDaysInMonth(YearMonth.of(2018, 5)));         // May 2018

Output

21
23

Explanation of Formula

The formula in the return statement was created by examining the expected return value for every combination of len (number of days in month, 28 - 31) and dow (day-of-week of first day of month, 1=Mon - 7=Sun):

   |  1   2   3   4   5    6   7
   | Mo  Tu  We  Th  Fr   Sa  Su
---+----------------------------
28 | 20  20  20  20  20   20  20
29 | 21  21  21  21  21   20  20
30 | 22  22  22  22  21   20  21
31 | 23  23  23  22  21   21  22

Explanation for dow <= 5 (Mon-Fri)

Initially there are len - 8 weekdays, i.e. we subtract the 4 weekends that always exist in a month.

As we get to Thursday and Friday, we need to cap that for the 1 or 2 weekend days we lose. If you look at the 31-day row, we cap it at 26 - dow, i.e. for Friday (dow=5) we cap at 21, and for Thursday (dow=4) we cap at 22. For Monday-Wednesday, we also cap, but cap is equal to or higher than initial calculation, so it doesn't matter.

Capping is done using min(xxx, cap) method, so we get:

min(len - 8, 26 - dow)

Explanation for dow >= 6 (Sat-Sun)

You can see a small triangle in the lower-right corner. If we extend that pattern, we get:

   |  4   5   6   7
---+---------------
28 | 16  17  18  19
29 | 17  18  19  20
30 | 18  19  20  21
31 | 19  20  21  22

As a formula, that is len + dow - 16.

Comparing that to original grid, numbers bottom out at 20, which is done using max(xxx, bottom) method, so we get:

max(len + dow - 16, 20)

Conclusion

Finally we combine the two using ternary conditional operator:

dow <= 5  ?  min(len - 8, 26 - dow)  :  max(len + dow - 16, 20)

Full Java statement is then:

return (dow <= 5 ? Math.min(len - 8, 26 - dow) : Math.max(len + dow - 16, 20));

Brute-Force Solution

If you prefer a brute-force solution, you can ease it by skipping the first 4 weeks that always exists in a month:

public static int weekDaysInMonth(LocalDate refDate) {
    LocalDate firstOfMonth = refDate.withDayOfMonth(1);
    LocalDate nextMonth = firstOfMonth.plusMonths(1);
    int days = 20;
    for (LocalDate date = firstOfMonth.plusDays(28); date.isBefore(nextMonth); date = date.plusDays(1))
        if (date.getDayOfWeek().getValue() <= 5) // 1=Mon - 5=Fri, i.e. not 6=Sat and 7=Sun
            days++;
    return days;
}
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • Few years later noone will be able to decypher the `return` formula. – lexicore Apr 29 '18 at 07:03
  • @lexicore That's why I added the basis for the formula to the end of the answer, to at least have some reference to how I came up with that one. – Andreas Apr 29 '18 at 07:05
  • Yes, I have no doubts it is correct. :) However I personally would opt to the brute-force method just because it's easier to understand. – lexicore Apr 29 '18 at 07:09
  • It does look nicer and has fewer lines of code than the brute force one I thought of. However, I am not able to understand the return statement. Can you explain it in more detail? – Kyu Vulpes Apr 29 '18 at 07:18
  • @lexicore Few years later (unless someone bothers to change the Gregorian calender) it will still work efficiently so no one will care to look at it :) Personally I would comment the code with a link to this question :) – DHa Apr 29 '18 at 10:49
3

So, thanks to Dreamspace President for helping me find a solution (even though it is a brute force way) when he said this:

Create a LocalDate for the first day of the respective month. Loop until the end of the month using ld.plusDays(1) and use ld.getDayOfWeek() to see which DayOfWeek you're dealing with.

This is what I've found to work:

public static int businessDaysInMonth(final LocalDate ld) {

    int weekDays = 0;
    LocalDate date = ld.withDayOfMonth(1);
    final int intendedMonthValue = ld.getMonthValue();
    do {
        final DayOfWeek dayOfWeek = date.getDayOfWeek();

        if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY) {
            weekDays++;
        }

        date = date.plusDays(1);
    } while (date.getMonthValue() == intendedMonthValue);

    return weekDays;
}
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
Kyu Vulpes
  • 79
  • 11
  • You forget about hollidays and other not working days! – Oleg Cherednik Apr 29 '18 at 06:14
  • Andreas' solution is quite sexy (though I didn't check if the results are correct in all cases, but I assume so), e.g. it deals with leap years without even having to mention them. In your brute force implementation, you obviously should use more variables to reduce method calls, also you can compare Enum values directly (with ==), you don't have to use .ordinal() or (like you did) .getValue() //////// Actually, regarding leap years: It should be entirely sufficient that you keep adding 1 day until the ld.getMonthValue() changes. – Dreamspace President Apr 29 '18 at 06:51
  • Correction: It IS entirely sufficient to check if the month changes. https://i.imgur.com/bjN0im1.png – Dreamspace President Apr 29 '18 at 07:01
  • 2
    `LocalDate.of(ld.getYear(), intendedMonthValue, 1)` is better written as `ld.withDayOfMonth(1)` – Andreas Apr 29 '18 at 07:46
  • It’s certainly more readable than the code in Andreas’ answer (I have upvoted both, though). – Ole V.V. Apr 29 '18 at 07:48
  • 1
    The loop would be more obvious as a `for` loop, i.e. `for (LocalDate date = ld.withDayOfMonth(1); date.getMonthValue() == intendedMonthValue; date = date.plusDays(1))` – Andreas Apr 29 '18 at 07:49
  • @DreamspacePresident I think that replacing `daysInMonth` with the very generic `ret` is bad purification. – Andreas Apr 29 '18 at 07:50
  • 1
    @Andreas I think a `for` loop would make it a bit harder to read since it's not a collection. A `for` loop just seems better suited for collections while a `while` and `do-while` loops seem better suited for these types of calculations. Also, I changed ret to weekDays, however, me being tired didn't properly changed it. – Kyu Vulpes Apr 29 '18 at 07:56
  • @TechNGamer You're thinking of the enhanced-for loop, `for (E value : collection)`. I'm referring to a normal counting loop, such as `for (int i = 0; i < 10; i++)`, i.e. set first value, loop while condition is good, increment to next value, keeping all the looping logic together. It shows that this is a simple incrementing loop, for a particular range of values, i.e. all the days of the given month. – Andreas Apr 29 '18 at 07:59
  • @Andreas You're right about withDayOfMonth(1) and the for loop (more compact/expressive). I disagree about "ret": It expresses intention. Everything in a method (that returns a value) gravitates around it, so I assume a variable with a more speaking name to be serving the needs of the return value, not to be the return value itself. The method name kinda is the true name of ret. But I agree that if you're surfing your code, having a ret with a speaking name may be more helpful, because you don't have to make yourself aware first of where exactly you are. – Dreamspace President Apr 29 '18 at 08:04
  • @Andreas A for-each (or as Java calls it an enhanced-for loop) in my mind is a for loop where position doesn't matter while a regular for loop, position matters. Either way, a for loop just seems like it's more for collections of objects rather than anything else. A while and do-while seem better for things that aren't collections, but I do see your point. A for loop counts and has everything on one line for its statement. – Kyu Vulpes Apr 29 '18 at 08:04
  • 1
    @TechNGamer An enhanced for loop would be for **C**ollections, a standard for loop for **c**ollections, and your sequence of days of the one month certainly is a **c**ollection. But I think we're in matter-of-taste territory (same for the "ret" name question). – Dreamspace President Apr 29 '18 at 08:07