1. Introduction

In various applications such as appointments, bookings, or project timelines, avoiding scheduling conflicts is paramount. Overlapping dates can lead to inconsistencies and errors. In this tutorial, we’ll explore different scenarios of date range overlap and dive into various approaches and formulas to check for overlaps.

2. Understanding Overlapping Date Ranges

Before we dive into the implementation, let’s ensure a clear understanding of what it means for two date ranges to overlap. Understanding the different scenarios that constitute date range overlap is crucial when implementing an efficient and accurate overlap checker. Let’s break down the various scenarios that need to be considered.

2.1. Partial Overlap

Partial overlap occurs when one date range partially overlaps with another. This happens when two ranges share some portion of their timeframe but don’t entirely encompass each other. For example, if Project A spans from January 1st to February 10th and Project B runs from February 5th to March 1st, they partially overlap:

A                            B
|-----------------------------|
                C                                D
                |--------------------------------|

2.2. Full Overlap

Full overlap happens when one date range entirely encompasses another. This occurs when one date range completely falls within the boundaries of another. For example, if Booking A covers July 1st to July 31st and Booking B covers July 15th to July 25th, Booking B completely overlaps with Booking A:

A                                               B
|------------------------------------------------|
                C                      D
                |----------------------|

2.3. Consecutive Ranges

Two ranges are considered adjacent if one ends immediately before the other begins. This is common in project timelines, where Project X spans from March 1st to March 31st, and Project Y runs from April 1st to April 30th. These ranges are adjacent but not overlapping:

A                    B
|---------------------|
                       C                    D
                       |--------------------|

2.4. Zero-Range Duration and Consecutive Ranges

Zero-range duration refers to a scenario where the start and end dates of a range are the same, resulting in a duration of zero. For example, we may encounter an event scheduled for exactly one day with no duration. Although seemingly a special case, handling this scenario explicitly is essential to ensure the correctness of our date range overlap logic when considering consecutive ranges:

A                    B
|---------------------|
                      CD
                      |

3. Overlapping Formulas

Various mathematical formulas encapsulate the logic for determining date range overlap. Here, we’ll explore three commonly used formulas and explain their underlying principles.

Let’s first define the variables used in the formulas:

  • A: Start date of the first date range
  • B: End date of the first date range
  • C: Start date of the second date range
  • D: End date of the second date range

3.1. Calculating the Overlap Duration

This formula calculates the overlap duration by subtracting the earlier ending date from the later starting date:

(min(B, D) - max(A, C)) >= 0

If this duration is negative, there’s no overlap. If it’s zero, the ranges might be touching, or one might be a single point in time. The application should consider whether to treat this scenario as an overlap or not. Moreover, a positive duration tells us there’s an overlap.

3.2. Checking for Non-Overlap Conditions

This formula checks for non-overlapping conditions between two date ranges. If either condition fails, then there’s an overlap:

!(B <= C || A >= D)

Take note that if we change the condition to use strict inequality “<” and “>”, it means the ranges must not touch at all. There should be no common point between the two ranges. Once again, the application should decide whether to treat this scenario as an overlap or not.

3.3. Finding Minimum Overlap

This implementation calculates the overlap duration in days by considering the minimum of the four calculations. If the minimum overlap duration is zero or negative, it implies that there is no overlap, as the ranges do not intersect in a non-zero duration. A positive minimum overlap duration indicates an actual overlap, suggesting that the ranges share a duration greater than zero:

min((B - A), (B - C), (D - A), (D - C)) >= 0

However, if we change it to just greater than zero, the condition becomes stricter. This means there must be more than just a touch at a single point and must be a non-zero duration of overlap.

4. Implementation

Now that we’ve explored the different overlapping scenarios and formulas, let’s dive into implementing code to accurately detect these overlaps using various approaches.

4.1. Using Calendar

We’ll leverage the Calendar class to manage date ranges and check for overlaps. The Calendar class is part of the java.util package and provides a way to work with dates and times. The getTimeInMillis() method is used to obtain the time in milliseconds for each Calendar instance.

In this example, we’ll leverage the Math.min() and Math.max() methods to calculate the overlap duration by subtracting the earlier ending date from the later starting date:

boolean isOverlapUsingCalendarAndDuration(Calendar start1, Calendar end1, Calendar start2, Calendar end2) {
    long overlap = Math.min(end1.getTimeInMillis(), end2.getTimeInMillis()) -
      Math.max(start1.getTimeInMillis(), start2.getTimeInMillis());
    return overlap > 0;
}

To implement the non-overlap conditions checking, we’ll utilize the before() and after() methods provided by the Calendar class. These methods assess the chronological relationship between date instances and are essential for determining overlap. If either condition is true, it indicates an overlap:

boolean isOverlapUsingCalendarAndCondition(Calendar start1, Calendar end1, Calendar start2, Calendar end2) {
    return !(end1.before(start2) || start1.after(end2));
}

Now, let’s implement the logic using the find minimum formula. This method calculates the minimum overlap duration among different scenarios:

boolean isOverlapUsingCalendarAndFindMin(Calendar start1, Calendar end1, Calendar start2, Calendar end2) {
    long overlap1 = Math.min(end1.getTimeInMillis() - start1.getTimeInMillis(), 
      end1.getTimeInMillis() - start2.getTimeInMillis());
    long overlap2 = Math.min(end2.getTimeInMillis() - start2.getTimeInMillis(), 
      end2.getTimeInMillis() - start1.getTimeInMillis());

   return Math.min(overlap1, overlap2) / (24 * 60 * 60 * 1000) >= 0;
}

To ensure the correctness of our implementation, we can set up test data using Calendar instances representing different date ranges.

First, let’s create the start date range:

Calendar start1 = Calendar.getInstance().set(2024, 11, 15); 
Calendar end1 = Calendar.getInstance().set(2024, 11, 20);

Subsequently, we can set the end date range to partially overlap:

Calendar start2 = Calendar.getInstance()set(2024, 11, 18);
Calendar end2 = Calendar.getInstance().set(2024, 11, 22);

assertTrue(isOverlapUsingCalendarAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndFindMin(start1, end1, start2, end2));

Here’s the corresponding unit test code to see if the test data is fully overlapping:

Calendar start2 = Calendar.getInstance()set(2024, 11, 16);
Calendar end2 = Calendar.getInstance().set(2024, 11, 18);

assertTrue(isOverlapUsingCalendarAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndFindMin(start1, end1, start2, end2));

Finally, we set the end date range to consecutive ranges, and we should expect no overlapping:

Calendar start2 = Calendar.getInstance()set(2024, 11, 21);
Calendar end2 = Calendar.getInstance().set(2024, 11, 24);

assertFalse(isOverlapUsingCalendarAndDuration(start1, end1, start2, end2)); 
assertFalse(isOverlapUsingCalendarAndCondition(start1, end1, start2, end2)); 
assertFalse(isOverlapUsingCalendarAndFindMin(start1, end1, start2, end2));

4.2. Using Java 8’s LocalDate

Java 8 introduced the java.time.LocalDate class, which provides a more modern and convenient way to handle dates. We can leverage this class to simplify our date range overlap check. In the calculation of the overlap duration, we utilize the toEpochDay() method provided by the LocalDate class. This method is used to obtain the number of days from the epoch day for a given LocalDate:

boolean isOverlapUsingLocalDateAndDuration(LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2) {
    long overlap = Math.min(end1.toEpochDay(), end2.toEpochDay()) -
      Math.max(start1.toEpochDay(), start2.toEpochDay());

    return overlap >= 0;
}

The LocalDate class provides two essential methods, isBefore() and isAfter(), for assessing the chronological relationship between LocalDate instances. We’ll utilize both methods to determine the non-overlap conditions:

boolean isOverlapUsingLocalDateAndCondition(LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2) {
    return !(end1.isBefore(start2) || start1.isAfter(end2));
}

Next, we’ll explore the use of LocalDate to find the minimum overlap between two date ranges:

boolean isOverlapUsingLocalDateAndFindMin(LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2) {
    long overlap1 = Math.min(end1.toEpochDay() - start1.toEpochDay(), 
      end1.toEpochDay() - start2.toEpochDay());
    long overlap2 = Math.min(end2.toEpochDay() - start2.toEpochDay(), 
      end2.toEpochDay() - start1.toEpochDay());

    return Math.min(overlap1, overlap2) >= 0;
}

Now, let’s set up test data using LocaleDate instances representing different date ranges.

First, let’s create the start date range:

LocalDate start1 = LocalDate.of(2024, 11, 15); 
LocalDate end1 = LocalDate.of(2024, 11, 20);

Next, let’s set the end date range to partially overlap:

LocalDate start1 = LocalDate.of(2024, 11, 15); 
LocalDate end1 = LocalDate.of(2024, 11, 20);

assertTrue(isOverlapUsingLocalDateAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndFindMin(start1, end1, start2, end2));

We’ll follow this by setting the end date range to fully overlap:

LocalDate start1 = LocalDate.of(2024, 11, 16); 
LocalDate end1 = LocalDate.of(2024, 11, 18);

assertTrue(isOverlapUsingLocalDateAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndFindMin(start1, end1, start2, end2));

Finally, when we set the end date range to consecutive ranges, we should expect no overlapping:

LocalDate start1 = LocalDate.of(2024, 11, 21); 
LocalDate end1 = LocalDate.of(2024, 11, 24);

assertFalse(isOverlapUsingLocalDateAndDuration(start1, end1, start2, end2)); 
assertFalse(isOverlapUsingLocalDateAndCondition(start1, end1, start2, end2)); 
assertFalse(isOverlapUsingLocalDateAndFindMin(start1, end1, start2, end2));

4.3. Using Joda-Time

Joda-Time, a widely adopted library for date and time operations in Java, simplifies complex temporal calculations and offers a convenient method called overlaps() to determine if two intervals overlap.

The overlaps() method takes two Interval objects as parameters and returns true if there is any intersection between the two intervals. In other words, it identifies whether there is any shared duration between the specified date ranges.

However, it is important to note that Joda-Time considers two intervals with the same start and end points as non-overlapping. This behavior is especially relevant when dealing with scenarios where precise boundaries matter and even a point in time is not considered an overlap.

Let’s see an implementation using Joda-Time:

boolean isOverlapUsingJodaTime(DateTime start1, DateTime end1, DateTime start2, DateTime end2) {
    Interval interval1 = new Interval(start1, end1);
    Interval interval2 = new Interval(start2, end2);

    return interval1.overlaps(interval2);
}

Now, let’s set up test data using Interval instances representing different date ranges. We begin with the partial overlapping:

DateTime startJT1 = new DateTime(2024, 12, 15, 0, 0);
DateTime endJT1 = new DateTime(2024, 12, 20, 0, 0);
DateTime startJT2 = new DateTime(2024, 12, 18, 0, 0);
DateTime endJT2 = new DateTime(2024, 12, 22, 0, 0);

assertTrue(isOverlapUsingJodaTime(startJT1, endJT1, startJT2, endJT2));

Let’s change the end date range to fully overlap:

DateTime startJT2 = new DateTime(2024, 12, 16, 0, 0);
DateTime endJT2 = new DateTime(2024, 12, 18, 0, 0);

assertTrue(isOverlapUsingJodaTime(startJT1, endJT1, startJT2, endJT2));

Finally, we change the end date range to a consecutive range, and again, it should return no overlapping:

DateTime startJT2 = new DateTime(2024, 12, 21, 0, 0);
DateTime endJT2 = new DateTime(2024, 12, 24, 0, 0);

assertFalse(isOverlapUsingJodaTime(startJT1, endJT1, startJT2, endJT2));

5. Conclusion

In this article, we explored various scenarios, mathematical formulas, and approaches to check if two date ranges overlap in Java. Understanding partial overlap, full overlap, consecutive ranges, and zero-range duration gave us a solid foundation for our exploration.

As always, the code is available over on GitHub.