Calculating the Number of Specific Weekdays Between Two Dates

Posted on Wed 27 January 2021 in Algorithms

When modeling recurring events, e.g. business days for a shop, we can encounter the problem of calculating how many times the event occurs between two specific dates. In the business days example, the question may be "How many business days are there betweeen May 2nd 2020 and December 31th 2020?" or "How many Sundays are there between two dates?".

In this article I'll derive a formula for the calculation and share sample implementations in Java and Scala.

Problem Definition

Given two specific dates and a set of target weekdays, compute the total number of occurences of the target weekdays on and between the two dates.

Assumptions, Definitions & Notation

  1. Holidays are not taken into account (potential for improvement here).
  2. Date intervals are inclusive, i.e. "from May 2nd" includes May 2nd as the first day of the interval and "to December 31st" includes December 31st as the last day of the interval.
  3. A week is merely a sequence of 7 consecutive days and doesn't have to start on any specific weekday.
  4. denotes the cardinality of the set , i.e. the number of elements in it.
  5. For brevity, we will refer to an occurrence of a target weekday within the date interval as a target day.

Input Parameters

  1. The start date. We include this day when counting occurrences.
  2. The end date. We include this day when counting occurrences.
  3. The subset of possible weekdays (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday) we are interested in.

Sample Problem

  1. Start date: 2021-01-01
  2. End date: 2021-03-31
  3. Target weekdays: Monday, Tuesday, Wednesday, Thursday, Friday

We think of the interval between the two dates in chunks of 7 days (weeks).

FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE TH
FR SA SU MO TU WE

Notice how all weeks but the last one display the same pattern? We will use this observation, along with some simple arithmetics and set operations to determine the number of target days (i.e. occurrences of the target weekdays).

The Algorithm

To construct the algorithm, lets start with a special case and generalize from there. The simplified problem is based on two restrictions.

  1. We are counting Mondays, Tuesdays, Wednesdays, Thursdays and Fridays rather than any combination of weekdays.
  2. The interval length is a multiple of 7 days.

With these restrictions the formula for the number of target days is, quite obviously:

where is the number of weeks and 5 is the size (cardinality) of the set of possible weekdays we are interested in. This formula would cover a problem similar to the sample one but with the last (incomplete) week missing, i.e. with the interval ending on 2021/03/25.

If we look at the problem definition however, we see that the interval is not defined in terms of weeks but in terms of days. Since we know (restriction #1) that this number is a multiple of 7, we can express the formula as

with being the interval length in days.

Now, let's lift the first restriction to allow for any set of weekdays, including Saturday and Sunday, while still only considering whole weeks. It's easy to see that in this case the total number of target days is

with being the set of weekdays we are counting. For the special case of Monday-Friday its cardinality is 5 and we get the first formula.

To lift the second restriction, we'll take advantage of the obvious fact that any interval of days contains a number of complete weeks (which can be 0) and possibly exactly one incomplete week. Note that the incomplete week starts on whichever weekday the interval itself starts on. E.g. if the interval starts on a Wednesday, the last complete week (if there are any) will end on a Tuesday and the incomplete week (if there is any) will start on a Wednesday.

The number of complete weeks is given by

and the number of target days contained in the complete weeks

The total number of target days between any two dates has the form

where is the number of targeted weekdays in the incomplete week. Since we already know how to calculate , once we have a formula for we have solved the whole problem.

How do we determine the number of target days in the incomplete week?

We simply count the number of weekdays from the incomplete week (6 at a most) that are contained in the target set. We can also take the opposite approach and count the number of weekdays from the target set which are contained in the incomplete week to arrive at the same result. Either way, we are computing the intersection of two sets, then determining its cardinality. Thus, the number of target days in the incomplete week is given by

with being the set of weekdays found in the incomplete week. Note that this set is the same as the set of days starting with the first day of the whole interval and going on for days, a fact that we can and will use for the implementation.

By combining the last three equations we arrive at the complete formula for the total number of target days in the interval:

Since the cardinalities of the two sets are limited regardless of interval length, we can see that the algorithm will complete in constant time.

Sample Code

Both of these implementations will return zero if the start date is later than the end date.

Java

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.LongStream;

// ...

public long countWeekDays(LocalDate startDate, LocalDate endDate, Set<DayOfWeek> daysOfWeek) {

    long periodLength = Math.max(0, ChronoUnit.DAYS.between(startDate, endDate) + 1);
    long fullWeeks = periodLength / 7;
    long residualWeekLength = periodLength % 7;

    Set<DayOfWeek> residualWeekDays = LongStream.range(0, residualWeekLength)
        .mapToObj(offset -> startDate.plusDays(offset).getDayOfWeek())
        .collect(Collectors.toSet());
    residualWeekDays.retainAll(daysOfWeek);

    return fullWeeks * daysOfWeek.size() + residualWeekDays.size();

}

Scala

import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.ChronoUnit.DAYS

// ...

def countWeekDays(start: LocalDate, end: LocalDate, weekdays: Set[DayOfWeek]) = {

    val periodLength = Math.max(0, DAYS.between(start, end) + 1)
    val numberOfCompleteWeeks = periodLength / 7
    val incompleteWeekLength = periodLength % 7

    val residualWeekDays = (
            for (offset <- 0L to incompleteWeekLength - 1)
            yield start.plusDays(offset).getDayOfWeek
        )
        .toSet
        .intersect(weekdays)

    numberOfCompleteWeeks * weekdays.size + residualWeekDays.size

}