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.
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.
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.
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.
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));
}
}
LocalDate
or LocalDateTime
.With a solid date range model in place, your backend is well-prepared to handle date selection, comparisons, and further business logic reliably.
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.
Closed Range: Both the start and end points are included. For example, [start, end]
means the date range includes the start date and the end date.
Open Range: Both the start and end points are excluded. For example, (start, end)
means the range includes only dates strictly after the start and strictly before the end.
Half-Open Range: Either the start or the end is inclusive, while the other is exclusive. Commonly used variants:
[start, end)
includes the start date but excludes the end.(start, end]
excludes the start but includes the end.In Java date/time APIs, half-open intervals are often preferred for clarity and consistency.
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");
}
}
}
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)
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
}
}
[start, end)
are common in scheduling to avoid overlaps.Understanding and implementing these range types correctly is critical for building reliable date range handling in any scheduling or booking backend.
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.
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.
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
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
}
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
}
}
overlaps()
method helps detect conflicts effectively.By effectively managing overlapping ranges, you ensure robust booking systems that prevent conflicts and improve user trust.