Java
Date Ranges
Overlapping Intervals
Programming
Code Logic

Combining Overlapping Date Ranges - Java

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Combining overlapping date ranges is the standard interval-merging problem applied to LocalDate values. The correct solution is to sort the ranges by start date, then scan once from left to right, merging whenever the current range overlaps the previous merged range.

Define the Range Clearly

Before writing code, decide whether ranges are inclusive or exclusive at the end. Most business-style date ranges are inclusive, meaning a range from 2026-03-01 to 2026-03-07 includes both dates.

The overlap rule for inclusive ranges is:

  • range A overlaps range B if A.start <= B.end and B.start <= A.end

If you also want adjacent ranges to merge, such as one range ending on 2026-03-07 and the next starting on 2026-03-08, then adjacency becomes part of your business rule.

A Simple LocalDate Model

Use a small value type to keep the code explicit.

java
1import java.time.LocalDate;
2
3public record DateRange(LocalDate start, LocalDate end) {
4    public DateRange {
5        if (start.isAfter(end)) {
6            throw new IllegalArgumentException("start must not be after end");
7        }
8    }
9}

This guards against invalid input before the merge logic starts.

Merge by Sorting Then Scanning

Here is a complete example that merges overlapping inclusive ranges.

java
1import java.time.LocalDate;
2import java.util.ArrayList;
3import java.util.Comparator;
4import java.util.List;
5
6public class MergeRanges {
7    public static List<DateRange> merge(List<DateRange> ranges) {
8        if (ranges.isEmpty()) {
9            return List.of();
10        }
11
12        List<DateRange> sorted = new ArrayList<>(ranges);
13        sorted.sort(Comparator.comparing(DateRange::start));
14
15        List<DateRange> merged = new ArrayList<>();
16        DateRange current = sorted.get(0);
17
18        for (int i = 1; i < sorted.size(); i++) {
19            DateRange next = sorted.get(i);
20
21            if (!next.start().isAfter(current.end())) {
22                LocalDate newEnd = current.end().isAfter(next.end()) ? current.end() : next.end();
23                current = new DateRange(current.start(), newEnd);
24            } else {
25                merged.add(current);
26                current = next;
27            }
28        }
29
30        merged.add(current);
31        return merged;
32    }
33
34    public static void main(String[] args) {
35        List<DateRange> input = List.of(
36            new DateRange(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 3, 5)),
37            new DateRange(LocalDate.of(2026, 3, 4), LocalDate.of(2026, 3, 10)),
38            new DateRange(LocalDate.of(2026, 3, 12), LocalDate.of(2026, 3, 15))
39        );
40
41        for (DateRange range : merge(input)) {
42            System.out.println(range);
43        }
44    }
45}

After sorting, each range only needs to be compared with the last merged range. That is why the scan is linear after the initial sort.

Merging Adjacent Ranges

Some systems want adjacent date blocks merged as one continuous period. If so, change the overlap test to treat current.end().plusDays(1) as still mergeable.

java
1if (!next.start().isAfter(current.end().plusDays(1))) {
2    LocalDate newEnd = current.end().isAfter(next.end()) ? current.end() : next.end();
3    current = new DateRange(current.start(), newEnd);
4}

That is a business rule, not a universal truth. Make it explicit because it changes reports and billing logic.

Why Sorting Is Necessary

Without sorting, you can miss merges or end up merging inefficiently. A naive approach that compares every range to every other range works for tiny inputs, but it becomes slower and harder to reason about.

The standard complexity is:

  • 'O(n log n) for sorting'
  • 'O(n) for the merge scan'

That is the right baseline for almost every application-level use case.

When LocalDateTime or Time Zones Matter

If the real data includes times or zones, do not flatten it into LocalDate unless that loss of precision is intentional.

Use:

  • 'LocalDateTime for date and time without zone'
  • 'ZonedDateTime or OffsetDateTime when time-zone correctness matters'

The merge pattern is the same, but the comparison semantics change with the type.

Common Pitfalls

The most common mistake is not deciding whether end dates are inclusive. Two developers can write two valid-looking merge functions and still disagree because they assumed different interval semantics.

Another mistake is skipping the initial sort. The one-pass merge logic depends on ranges being ordered by start date.

Developers also forget input validation. A range whose start is after its end should be rejected immediately, not allowed to create strange merge results later.

Finally, adjacency is easy to handle incorrectly. Only merge touching ranges if the business meaning says those periods should be combined.

Summary

  • Merge overlapping date ranges by sorting on start date and scanning once.
  • Define whether the range end is inclusive and whether adjacent ranges should merge.
  • Validate each range before merging so bad input fails fast.
  • 'LocalDate is appropriate only when you truly mean date-only ranges.'
  • The standard solution is O(n log n) because sorting is the dominant cost.

Course illustration
Course illustration

All Rights Reserved.