Index

Implementing a Date Range Picker Backend

Java Date and Time

17.1 Representing and Validating Date Ranges

When implementing a date range picker backend, a fundamental step is modeling date ranges in a clear, robust, and maintainable way. Java’s java.time package offers powerful types like LocalDate and LocalDateTime that are ideal for this purpose.

Modeling Date Ranges with LocalDate or LocalDateTime

A date range typically consists of a start and an end point. Depending on your application needs, these points might represent just dates (LocalDate) or full timestamps (LocalDateTime). For example:

public class DateRange {
    private final LocalDate start;
    private final LocalDate end;

    public DateRange(LocalDate start, LocalDate end) {
        // Validation logic to be added here
        this.start = start;
        this.end = end;
    }

    public LocalDate getStart() {
        return start;
    }

    public LocalDate getEnd() {
        return end;
    }
}

This class encapsulates the date range as immutable fields, ensuring that once created, the range cannot be changed.

Validating Date Ranges

Proper validation is critical to prevent invalid or nonsensical ranges that could cause bugs downstream:

Here is how you can enforce these rules in the constructor:

public DateRange(LocalDate start, LocalDate end) {
    if (start == null || end == null) {
        throw new IllegalArgumentException("Start and end dates must not be null");
    }
    if (start.isAfter(end)) {
        throw new IllegalArgumentException("Start date must be before or equal to end date");
    }
    this.start = start;
    this.end = end;
}

Using isAfter() and isBefore() from LocalDate ensures the logic is clear and expressive.

Why Immutability and Encapsulation Matter

You can also add convenience methods to improve usability, such as:

public boolean contains(LocalDate date) {
    return !date.isBefore(start) && !date.isAfter(end);
}

This method checks if a given date falls within the range, simplifying downstream checks.

Click to view full runnable Code

import java.time.LocalDate;

public class DateRange {
    private final LocalDate start;
    private final LocalDate end;

    public DateRange(LocalDate start, LocalDate end) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("Start and end dates must not be null");
        }
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start date must be before or equal to end date");
        }
        this.start = start;
        this.end = end;
    }

    public LocalDate getStart() {
        return start;
    }

    public LocalDate getEnd() {
        return end;
    }

    public boolean contains(LocalDate date) {
        if (date == null) {
            throw new IllegalArgumentException("Date to check must not be null");
        }
        return !date.isBefore(start) && !date.isAfter(end);
    }

    public static void main(String[] args) {
        // Valid range
        DateRange range = new DateRange(LocalDate.of(2025, 1, 1), LocalDate.of(2025, 12, 31));
        System.out.println("Range start: " + range.getStart());
        System.out.println("Range end: " + range.getEnd());

        LocalDate testDate1 = LocalDate.of(2025, 6, 15);
        LocalDate testDate2 = LocalDate.of(2026, 1, 1);

        System.out.println("Contains " + testDate1 + "? " + range.contains(testDate1));
        System.out.println("Contains " + testDate2 + "? " + range.contains(testDate2));

        // Uncommenting below line will throw IllegalArgumentException due to invalid range
        // DateRange invalidRange = new DateRange(LocalDate.of(2025, 12, 31), LocalDate.of(2025, 1, 1));
    }
}

Summary

With a solid date range model in place, your backend is well-prepared to handle date selection, comparisons, and further business logic reliably.

Index

17.2 Handling Open and Closed Ranges

When working with date ranges, it’s important to understand the concepts of open, closed, and half-open intervals. These distinctions affect how you check whether a specific date or date-time falls within a range and have practical implications for applications like booking systems.

Understanding Open, Closed, and Half-Open Ranges

In Java date/time APIs, half-open intervals are often preferred for clarity and consistency.

Examples: Checking If a Date Is Inside Various Ranges

Let’s extend the DateRange class to support these interval types and check containment accordingly.

public enum RangeType {
    CLOSED,       // [start, end]
    OPEN,         // (start, end)
    HALF_OPEN_LEFT,  // (start, end]
    HALF_OPEN_RIGHT  // [start, end)
}

public class DateRange {
    private final LocalDate start;
    private final LocalDate end;
    private final RangeType rangeType;

    public DateRange(LocalDate start, LocalDate end, RangeType rangeType) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("Start and end dates cannot be null");
        }
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start must not be after end");
        }
        this.start = start;
        this.end = end;
        this.rangeType = rangeType;
    }

    public boolean contains(LocalDate date) {
        switch (rangeType) {
            case CLOSED:
                return !date.isBefore(start) && !date.isAfter(end);
            case OPEN:
                return date.isAfter(start) && date.isBefore(end);
            case HALF_OPEN_LEFT:
                return date.isAfter(start) && !date.isAfter(end);
            case HALF_OPEN_RIGHT:
                return !date.isBefore(start) && date.isBefore(end);
            default:
                throw new IllegalStateException("Unknown RangeType");
        }
    }
}

Real-World Implications: Hotel Check-In and Check-Out Times

In hospitality or rental systems, half-open ranges are often used to avoid double bookings:

This means the room is available for new guests on the check-out date, preventing overlap.

Example:

DateRange booking = new DateRange(LocalDate.of(2025, 6, 1), LocalDate.of(2025, 6, 5), RangeType.HALF_OPEN_RIGHT);

System.out.println(booking.contains(LocalDate.of(2025, 6, 1))); // true (check-in day)
System.out.println(booking.contains(LocalDate.of(2025, 6, 5))); // false (check-out day)
Click to view full runnable Code

import java.time.LocalDate;

enum RangeType {
    CLOSED,        // [start, end]
    OPEN,          // (start, end)
    HALF_OPEN_LEFT,  // (start, end]
    HALF_OPEN_RIGHT  // [start, end)
}

public class DateRange {
    private final LocalDate start;
    private final LocalDate end;
    private final RangeType rangeType;

    public DateRange(LocalDate start, LocalDate end, RangeType rangeType) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("Start and end dates cannot be null");
        }
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start must not be after end");
        }
        this.start = start;
        this.end = end;
        this.rangeType = rangeType;
    }

    public boolean contains(LocalDate date) {
        if (date == null) {
            throw new IllegalArgumentException("Date to check must not be null");
        }
        switch (rangeType) {
            case CLOSED:
                return !date.isBefore(start) && !date.isAfter(end);
            case OPEN:
                return date.isAfter(start) && date.isBefore(end);
            case HALF_OPEN_LEFT:
                return date.isAfter(start) && !date.isAfter(end);
            case HALF_OPEN_RIGHT:
                return !date.isBefore(start) && date.isBefore(end);
            default:
                throw new IllegalStateException("Unknown RangeType");
        }
    }

    public static void main(String[] args) {
        DateRange booking = new DateRange(LocalDate.of(2025, 6, 1), LocalDate.of(2025, 6, 5), RangeType.HALF_OPEN_RIGHT);

        System.out.println("Booking contains 2025-06-01 (check-in day): " + booking.contains(LocalDate.of(2025, 6, 1))); // true
        System.out.println("Booking contains 2025-06-05 (check-out day): " + booking.contains(LocalDate.of(2025, 6, 5))); // false

        // Additional tests
        DateRange closedRange = new DateRange(LocalDate.of(2025, 1, 1), LocalDate.of(2025, 1, 10), RangeType.CLOSED);
        System.out.println("Closed range contains 2025-01-01: " + closedRange.contains(LocalDate.of(2025, 1, 1))); // true
        System.out.println("Closed range contains 2025-01-10: " + closedRange.contains(LocalDate.of(2025, 1, 10))); // true
        System.out.println("Closed range contains 2024-12-31: " + closedRange.contains(LocalDate.of(2024, 12, 31))); // false
    }
}

Summary

Understanding and implementing these range types correctly is critical for building reliable date range handling in any scheduling or booking backend.

Index

17.3 Overlapping Ranges and Booking Conflicts

Detecting overlapping date ranges is a fundamental requirement in scheduling systems, especially to prevent booking conflicts or double reservations. In this section, we will explore how to identify overlaps between date ranges, demonstrate practical code examples, and discuss performance considerations when handling large datasets.

Understanding Overlapping Date Ranges

Two date ranges overlap if there is at least one date that exists in both ranges. Formally, given two ranges [start1, end1) and [start2, end2), they overlap if:

start1 < end2 && start2 < end1

This logic assumes half-open intervals where the start date is inclusive and the end date is exclusive, a common convention in booking systems.

Example: Detecting Overlap Between Two Date Ranges

Let’s extend our DateRange class with an overlaps() method that uses the above logic:

public class DateRange {
    private final LocalDate start;
    private final LocalDate end;

    public DateRange(LocalDate start, LocalDate end) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("Start and end dates cannot be null");
        }
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start must not be after end");
        }
        this.start = start;
        this.end = end;
    }

    // Check if this range overlaps with another
    public boolean overlaps(DateRange other) {
        return this.start.isBefore(other.end) && other.start.isBefore(this.end);
    }
}

Usage example:

DateRange booking1 = new DateRange(LocalDate.of(2025, 6, 1), LocalDate.of(2025, 6, 5));
DateRange booking2 = new DateRange(LocalDate.of(2025, 6, 4), LocalDate.of(2025, 6, 8));

System.out.println(booking1.overlaps(booking2)); // true, they overlap on June 4

Preventing Booking Conflicts

When adding new bookings, you should check for overlaps against all existing bookings to ensure no double reservations occur:

public boolean canBook(DateRange newBooking, List<DateRange> existingBookings) {
    for (DateRange existing : existingBookings) {
        if (newBooking.overlaps(existing)) {
            return false;  // Conflict found
        }
    }
    return true;  // No conflicts
}
Click to view full runnable Code

import java.time.LocalDate;
import java.util.List;
import java.util.ArrayList;

public class DateRange {
    private final LocalDate start;
    private final LocalDate end;

    public DateRange(LocalDate start, LocalDate end) {
        if (start == null || end == null) {
            throw new IllegalArgumentException("Start and end dates cannot be null");
        }
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start must not be after end");
        }
        this.start = start;
        this.end = end;
    }

    public boolean overlaps(DateRange other) {
        // Overlaps if start is before other's end and other's start is before this end
        return this.start.isBefore(other.end) && other.start.isBefore(this.end);
    }

    public static boolean canBook(DateRange newBooking, List<DateRange> existingBookings) {
        for (DateRange existing : existingBookings) {
            if (newBooking.overlaps(existing)) {
                return false; // Conflict found
            }
        }
        return true; // No conflicts
    }

    public LocalDate getStart() {
        return start;
    }

    public LocalDate getEnd() {
        return end;
    }

    public static void main(String[] args) {
        DateRange booking1 = new DateRange(LocalDate.of(2025, 6, 1), LocalDate.of(2025, 6, 5));
        DateRange booking2 = new DateRange(LocalDate.of(2025, 6, 4), LocalDate.of(2025, 6, 8));
        DateRange booking3 = new DateRange(LocalDate.of(2025, 6, 6), LocalDate.of(2025, 6, 10));

        System.out.println("booking1 overlaps booking2? " + booking1.overlaps(booking2)); // true
        System.out.println("booking1 overlaps booking3? " + booking1.overlaps(booking3)); // false

        List<DateRange> existingBookings = new ArrayList<>();
        existingBookings.add(booking1);
        existingBookings.add(booking3);

        DateRange newBooking = new DateRange(LocalDate.of(2025, 6, 4), LocalDate.of(2025, 6, 6));
        System.out.println("Can book newBooking? " + canBook(newBooking, existingBookings)); // false due to overlap with booking1

        DateRange newBooking2 = new DateRange(LocalDate.of(2025, 6, 10), LocalDate.of(2025, 6, 12));
        System.out.println("Can book newBooking2? " + canBook(newBooking2, existingBookings)); // true
    }
}

Performance Considerations

Summary

By effectively managing overlapping ranges, you ensure robust booking systems that prevent conflicts and improve user trust.

Index